Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions docs/chat-work-summary-disclosure/change.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,17 @@ Risk classification: standard change because it alters shared user-visible Rende
## Spec

- Shared `Working for …` and `Worked for …` disclosures are closed on first render and never open or close automatically when lifecycle status changes.
- A collapsed running summary shows the latest running Tool or streaming Thinking item in timeline order. Tool activity reuses its existing icon, localized action, and compact semantic object summary; Thinking uses only its localized label and icon.
- The running summary is a single truncating line and never includes raw reasoning, arguments, output, result, stderr, or error details.
- A running summary shows its duration on the first line and the latest Tool or Thinking activity on a dedicated second line.
- The latest activity remains visible after that Tool or Thinking completes and is replaced only when a later Tool or Thinking item arrives. It disappears when the turn changes from `Working for …` to `Worked for …`.
- Tool activity reuses its existing icon, localized action, and compact semantic object summary; Thinking uses only its localized label and icon. The activity line truncates responsively and never includes raw reasoning, arguments, output, result, stderr, or error details.
- Expanded work preserves timeline order and continues to render Thinking, Tool, Todo, and intermediate assistant messages.
- Tool and Thinking disclosures are also closed on first render regardless of status. User-controlled disclosure state and Tool tab state survive ordinary streaming rerenders.
- Existing duration timing, final-answer placement, notices/errors, scrolling, localization, keyboard/details semantics, focus styles, and reduced-motion behavior remain intact.
- Task intake continues to use the same shared `ChatView` path, so the behavior applies consistently to normal Chat, intake, and execution sessions.

## Plan

1. Add regression tests for closed active/completed work, active-item selection and switching, private-detail exclusion, closed active Tool/Thinking items, and preserved manual disclosure/tab state.
2. Replace lifecycle-driven disclosure state with stable user-controlled state initialized closed.
3. Add a small active-work selector and render the semantic activity inside the running work summary by reusing `toolPresentation` and `ReasoningIcon`.
4. Add narrowly scoped flex/ellipsis styles for a one-line responsive collapsed row.
5. Run focused Chat/Task tests, `pnpm check`, and `pnpm build`; review the diff for scope and accessibility regressions.
1. Add regression tests proving the activity occupies a dedicated line, remains after completion, and switches only when a later Tool or Thinking arrives.
2. Change the active-work selector to retain the latest Tool or Thinking item for the lifetime of a running turn.
3. Split the running summary into a duration heading row and a responsive, truncating activity row while reusing `toolPresentation` and `ReasoningIcon`.
4. Run focused Chat tests and `pnpm check`; review the diff for scope and accessibility regressions.
29 changes: 27 additions & 2 deletions src/features/chat/WorkSummary.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,15 @@ describe("WorkSummary", () => {

const details = screen.getByText("Working for 10s").closest("details");
expect(details).not.toHaveAttribute("open");
expect(details?.querySelector(".chat-work-active")).toBeNull();
expect(details?.querySelector(".chat-work-active-text")).toHaveTextContent(
"Read file · chat.tsx",
);
void act(() => vi.advanceTimersByTime(2_000));
expect(screen.getByText("Working for 12s")).toBeVisible();

rerender(<WorkSummary work={worked} />);
expect(details).not.toHaveAttribute("open");
expect(details?.querySelector(".chat-work-active")).toBeNull();
});

it("shows only the latest semantic active item and updates it in place", () => {
Expand Down Expand Up @@ -102,6 +105,10 @@ describe("WorkSummary", () => {
const summary = details.querySelector("summary")!;

expect(details).not.toHaveAttribute("open");
expect(
summary.querySelector(":scope > .chat-work-summary-heading"),
).not.toBeNull();
expect(summary.querySelector(":scope > .chat-work-active")).not.toBeNull();
expect(summary.querySelector(".chat-work-active-text")).toHaveTextContent(
"Reading file · very-long-file.tsx",
);
Expand All @@ -111,12 +118,30 @@ describe("WorkSummary", () => {
).toBeNull();
expect(within(summary).queryByText("private partial output")).toBeNull();

rerender(
<WorkSummary
work={{
...activeWork,
items: activeWork.items.map((item) =>
item.type === "tool" ? { ...item, status: "done" as const } : item,
),
}}
/>,
);
expect(summary.querySelector(".chat-work-active-text")).toHaveTextContent(
"Read file · very-long-file.tsx",
);

rerender(
<WorkSummary
work={{
...activeWork,
items: [
...activeWork.items,
...activeWork.items.map((item) =>
item.type === "tool"
? { ...item, status: "done" as const }
: item,
),
{
type: "thinking",
id: "thinking-latest",
Expand Down
28 changes: 17 additions & 11 deletions src/features/chat/WorkSummary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,7 @@ export function activeWorkItem(
.reverse()
.find(
(item): item is ActiveWorkItem =>
(item.type === "tool" && item.status === "running") ||
(item.type === "thinking" && item.status === "streaming"),
item.type === "tool" || item.type === "thinking",
);
}

Expand Down Expand Up @@ -111,32 +110,39 @@ export function WorkSummary({ work }: { work: ChatWorkDisplayItem }) {
undefined
}
>
<span className="chat-work-label" aria-live="off">
{label}
<span className="chat-work-summary-heading">
<span className="chat-work-label" aria-live="off">
{label}
</span>
<RiArrowRightSLine
className="chat-disclosure-icon"
aria-hidden="true"
/>
</span>
{active?.type === "thinking" && (
<span className="chat-work-active">
<ReasoningIcon running />
<ReasoningIcon running={active.status === "streaming"} />
<span className="chat-work-active-text">{t("chat.thinking")}</span>
</span>
)}
{active?.type === "tool" && activeTool && ActiveToolIcon && (
<span className="chat-work-active">
<ActiveToolIcon
className="chat-status-icon chat-process-icon-active"
className={`chat-status-icon${active.status === "running" ? " chat-process-icon-active" : ""}`}
data-process-icon={activeTool.iconKind}
aria-hidden="true"
/>
<span className="chat-work-active-text">
{t(activeTool.activeActionKey, { name: active.name })}
{t(
active.status === "running"
? activeTool.activeActionKey
: activeTool.actionKey,
{ name: active.name },
)}
{activeTool.summary ? ` · ${activeTool.summary}` : ""}
</span>
</span>
)}
<RiArrowRightSLine
className="chat-disclosure-icon"
aria-hidden="true"
/>
</summary>
<div className="chat-work-details">
{work.items.map((item) => {
Expand Down
21 changes: 14 additions & 7 deletions src/styles/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -3961,8 +3961,9 @@ details[open] > summary > .chat-disclosure-icon {
display: flex;
width: fit-content;
max-width: 100%;
align-items: center;
gap: 6px;
flex-direction: column;
align-items: flex-start;
gap: 2px;
color: var(--text-subtle);
font-size: 13px;
line-height: 22px;
Expand All @@ -3977,7 +3978,12 @@ details[open] > summary > .chat-disclosure-icon {
.chat-work-summary > summary:focus-visible {
color: var(--text-muted);
}
.chat-work-summary > summary > .chat-disclosure-icon {
.chat-work-summary-heading {
display: flex;
align-items: center;
gap: 6px;
}
.chat-work-summary-heading > .chat-disclosure-icon {
visibility: visible !important;
opacity: 1 !important;
transform: none;
Expand All @@ -4004,10 +4010,10 @@ details[open] > summary > .chat-disclosure-icon {
text-overflow: ellipsis;
white-space: nowrap;
}
.chat-work-summary[open] > summary .chat-work-active {
display: none;
}
.chat-work-summary[open] > summary > .chat-disclosure-icon {
.chat-work-summary[open]
> summary
> .chat-work-summary-heading
> .chat-disclosure-icon {
transform: rotate(90deg);
}
.chat-work-details {
Expand Down Expand Up @@ -4156,6 +4162,7 @@ details[open] > summary > .chat-disclosure-icon {
white-space: nowrap;
}
.chat-process-shimmer > span:not(.chat-work-active),
.chat-process-shimmer .chat-work-label,
.chat-process-shimmer .chat-work-active-text {
color: transparent !important;
background-image: linear-gradient(
Expand Down
Loading