diff --git a/packages/app/e2e/regression/session-queue.spec.ts b/packages/app/e2e/regression/session-queue.spec.ts index 9e094a17fdb..450ddba3df4 100644 --- a/packages/app/e2e/regression/session-queue.spec.ts +++ b/packages/app/e2e/regression/session-queue.spec.ts @@ -308,8 +308,8 @@ for (const delivery of ["steer", "queue"] as const) { }) const tools = page.locator('[data-timeline-part-ids="tool_queue_read,tool_queue_grep"]') await expect(tools).toBeVisible() - await expect(tools).toContainText(/Used\s*Read, Grep/) - await expect(tools.locator('[data-component="tag"]')).toHaveText("2") + await expect(tools).toHaveText(/^Used\s*2 Read, Grep$/) + await expect(tools.locator('[data-slot="basic-tool-tool-title"]')).toHaveText("2 Read, Grep") await expect(thinking).toHaveCount(0) await expect(pending).toBeVisible() expect(mock.rows.map((row) => ({ id: row.id, delivery: row.delivery }))).toEqual([ @@ -318,7 +318,7 @@ for (const delivery of ["steer", "queue"] as const) { await transcript.screenshot({ path: testInfo.outputPath("pending-steer.png") }) // Soft assertions let delivery run too, even when the pending ordering regresses. - await expect.soft(tools.or(pending)).toHaveText([/Used\s*Read, Grep/, /U2: Also check the retry path\./]) + await expect.soft(tools.or(pending)).toHaveText([/^Used\s*2 Read, Grep$/, /U2: Also check the retry path\./]) await expect .soft(transcript.locator('[data-timeline-row="AssistantPart"]').filter({ has: tools })) .toHaveAttribute("data-message-id", userID) @@ -350,7 +350,7 @@ for (const delivery of ["steer", "queue"] as const) { await expect(response).toHaveAttribute("data-message-id", inboxID) await expect(thinking).toHaveCount(0) await expect(tools.or(pending).or(response)).toHaveText([ - /Used\s*Read, Grep/, + /^Used\s*2 Read, Grep$/, /U2: Also check the retry path\./, /A3: Now checking the retry path for U2\./, ]) diff --git a/packages/app/e2e/regression/session-timeline-accessibility.spec.ts b/packages/app/e2e/regression/session-timeline-accessibility.spec.ts index 3888c4a6e17..1c5eacad283 100644 --- a/packages/app/e2e/regression/session-timeline-accessibility.spec.ts +++ b/packages/app/e2e/regression/session-timeline-accessibility.spec.ts @@ -25,7 +25,7 @@ test("space activates a focused timeline button instead of scrolling", async ({ seedHistory: true, }) const scroller = page.locator(".scroll-view__viewport", { has: page.locator("[data-timeline-row]") }) - const trigger = page.getByRole("button", { name: "Used Shell" }) + const trigger = page.getByRole("button", { name: "Used 1 Shell", exact: true }) await expect .poll(() => scroller.evaluate((element) => element.scrollHeight - element.clientHeight)) .toBeGreaterThan(300) diff --git a/packages/app/e2e/regression/session-timeline-collapse-state.spec.ts b/packages/app/e2e/regression/session-timeline-collapse-state.spec.ts index 3bbc12b17f9..c7b4ef52044 100644 --- a/packages/app/e2e/regression/session-timeline-collapse-state.spec.ts +++ b/packages/app/e2e/regression/session-timeline-collapse-state.spec.ts @@ -93,7 +93,8 @@ test.describe("regression: session timeline local row state", () => { await expectSessionTitle(page, title) const group = page.locator('[data-component="collapsed-tool-group"]') - const summary = group.getByRole("button", { name: "Used Patch", exact: true }) + const summary = group.getByRole("button", { name: /^Used \d+ Patch$/ }) + await expect(summary).toHaveAccessibleName("Used 1 Patch") await summary.click() await group.locator(`[data-timeline-part-id="${editPartID}"]`).evaluate((element) => { element.setAttribute("data-disclosure-probe", "existing") @@ -109,7 +110,8 @@ test.describe("regression: session timeline local row state", () => { if (count === 3) await trigger.click() const id = `prt_patch_${count}` events.push(...toolEvents({ ...part, id, callID: id })) - await expect(group.locator('[data-component="tag"]')).toHaveText(String(count)) + await expect(summary).toHaveAccessibleName(`Used ${count} Patch`) + await expect(summary.locator('[data-slot="basic-tool-tool-title"]')).toHaveText(`${count} Patch`) await expect(group).toHaveAttribute("data-timeline-part-ids", new RegExp(`${id}$`)) await expect(trigger).toHaveAttribute("aria-expanded", String(count === 2)) await expect(summary).toHaveAttribute("aria-expanded", "true") diff --git a/packages/app/e2e/regression/session-timeline-context-resize.spec.ts b/packages/app/e2e/regression/session-timeline-context-resize.spec.ts index 1e36e77368a..56b0f81b7de 100644 --- a/packages/app/e2e/regression/session-timeline-context-resize.spec.ts +++ b/packages/app/e2e/regression/session-timeline-context-resize.spec.ts @@ -55,7 +55,7 @@ test.describe("regression: session timeline context group resize", () => { await devtools.send("Emulation.setCPUThrottlingRate", { rate: 4 }) const context = page.locator(`[data-timeline-part-ids="${contextIDs.join(",")}"]`).first() await expectAppVisible(context) - await expect(context.getByRole("button")).toHaveAccessibleName("Used Read, Glob, Grep, List") + await expect(context.getByRole("button")).toHaveAccessibleName("Used 4 Read, Glob, Grep, List") const contextSelector = `[data-timeline-part-ids="${contextIDs.join(",")}"]` const regions = defineVisualRegions({ @@ -88,7 +88,7 @@ test.describe("regression: session timeline context group resize", () => { await page.waitForTimeout(delay) } - await expect(context.getByRole("button")).toHaveAccessibleName("Used Read, Glob, Grep, List") + await expect(context.getByRole("button")).toHaveAccessibleName("Used 4 Read, Glob, Grep, List") await page.waitForTimeout(700) const trace = await stopVisualProbe(page) const labels = trace.samples @@ -107,7 +107,7 @@ test.describe("regression: session timeline context group resize", () => { ]), ) - expect(labels).toEqual(["Used Read, Glob, Grep, List"]) + expect(labels).toEqual(["Used 4 Read, Glob, Grep, List"]) expect(issues, JSON.stringify(trace.samples, null, 2)).toEqual([]) }) }) diff --git a/packages/app/e2e/regression/session-timeline-lifecycle-state.spec.ts b/packages/app/e2e/regression/session-timeline-lifecycle-state.spec.ts index 65e6d2c0810..93bad4196ee 100644 --- a/packages/app/e2e/regression/session-timeline-lifecycle-state.spec.ts +++ b/packages/app/e2e/regression/session-timeline-lifecycle-state.spec.ts @@ -28,7 +28,7 @@ for (const expanded of [false, true]) { }) const trigger = expanded ? page.locator(`[data-timeline-part-id="${id}"] [data-slot="collapsible-trigger"]`) - : page.getByRole("button", { name: "Used Shell" }) + : page.getByRole("button", { name: "Used 1 Shell", exact: true }) await expect(trigger).toHaveAttribute("aria-expanded", String(expanded)) await trigger.click() await expect(trigger).toHaveAttribute("aria-expanded", String(!expanded)) @@ -141,7 +141,7 @@ for (const open of [false, true]) { await timeline.send(partUpdated(shell(shellID, "completed", "done"))) await timeline.send(messageUpdated(completedAssistantInfo(assistant))) await timeline.send(status("idle")) - const used = group.getByRole("button", { name: "Used Shell", exact: true }) + const used = group.getByRole("button", { name: "Used 1 Shell", exact: true }) await expect(used).toHaveAttribute("aria-expanded", "false") await used.click() await expect(used).toHaveAttribute("aria-expanded", "true") @@ -150,7 +150,7 @@ for (const open of [false, true]) { "aria-expanded", String(open), ) - await expect(group.locator('[data-component="tag"]')).toHaveText("1") + await expect(used.locator('[data-slot="basic-tool-tool-title"]')).toHaveText("1 Shell") await expect(page.locator('[data-timeline-row="Thinking"]')).toHaveCount(0) await expect(used).toHaveAttribute("aria-expanded", "true") if (!open) await thought.click() diff --git a/packages/app/e2e/regression/session-timeline-locale-projection.spec.ts b/packages/app/e2e/regression/session-timeline-locale-projection.spec.ts index 30405aff356..6be176057e9 100644 --- a/packages/app/e2e/regression/session-timeline-locale-projection.spec.ts +++ b/packages/app/e2e/regression/session-timeline-locale-projection.spec.ts @@ -16,8 +16,9 @@ for (const locale of ["de", "ar"] as const) { }) const group = page.locator(`[data-timeline-part-ids="${ids.join(",")}"]`) - await expect(group.getByRole("button")).toHaveAccessibleName(/^Used /) - await expect(group.locator('[data-component="tag"]')).toHaveText("2") + const names = locale === "de" ? "Lesen, Glob" : "\u0642\u0631\u0627\u0621\u0629, Glob" + await expect(group.getByRole("button")).toHaveAccessibleName(`Used 2 ${names}`) + await expect(group.locator('[data-slot="basic-tool-tool-title"]')).toHaveText(`2 ${names}`) await expect(page.locator("html")).toHaveAttribute("lang", locale) }) } diff --git a/packages/app/e2e/regression/session-timeline-reasoning-projection.spec.ts b/packages/app/e2e/regression/session-timeline-reasoning-projection.spec.ts index 5b48540f77d..ab3cfed6d0b 100644 --- a/packages/app/e2e/regression/session-timeline-reasoning-projection.spec.ts +++ b/packages/app/e2e/regression/session-timeline-reasoning-projection.spec.ts @@ -105,10 +105,10 @@ for (const summaries of [false, true]) { if (profile === "tool") { const group = page.locator('[data-component="collapsed-tool-group"]') const used = group.locator(':scope > [data-component="collapsible"] > [data-slot="collapsible-trigger"]') - await expect(used).toContainText("UsedSkill") + await expect(used).toHaveText(/^Used\s*1 Skill$/) await expect(used).toHaveAttribute("aria-expanded", "false") await expect(page.getByText("Inspecting stability", { exact: true })).toBeHidden() - await expect(group.locator('[data-component="tag"]')).toHaveText("1") + await expect(used.locator('[data-slot="basic-tool-tool-title"]')).toHaveText("1 Skill") await used.click() await expect(used).toHaveAttribute("aria-expanded", "true") await expect(group.locator(`[data-timeline-part-id="prt_reasoning_tool_${summaries}"]`)).toBeVisible() diff --git a/packages/app/e2e/regression/session-timeline-reducer-projection.spec.ts b/packages/app/e2e/regression/session-timeline-reducer-projection.spec.ts index 189ff9ea2b5..b1fde4d4c03 100644 --- a/packages/app/e2e/regression/session-timeline-reducer-projection.spec.ts +++ b/packages/app/e2e/regression/session-timeline-reducer-projection.spec.ts @@ -45,10 +45,11 @@ test("expands a mixed collapsed tool stack without expanding its individual call const group = page.locator( '[data-timeline-part-ids="prt_stack_shell_1,prt_stack_explore,prt_stack_patch,prt_stack_shell_2"]', ) - const summary = group.getByRole("button", { name: "Used Shell, Agent, Patch" }) + const summary = group.getByRole("button", { name: "Used 4 Shell, Agent, Patch", exact: true }) await expect(summary).toHaveAttribute("aria-expanded", "false") await expect(summary).toHaveCSS("height", "28px") - await expect(group.locator('[data-component="tag"]')).toHaveText("4") + await expect(summary.locator('[data-slot="basic-tool-tool-title"]')).toHaveText("4 Shell, Agent, Patch") + await expect(summary.locator('[data-component="tag"]')).toHaveCount(0) await summary.click() await expect(summary).toHaveAttribute("aria-expanded", "true") await expect(group.locator('[data-slot="context-tool-group-item"]')).toHaveCount(4) @@ -74,8 +75,8 @@ test("leaves tools expanded by settings outside the collapsed stack", async ({ p await expect(page.locator('[data-timeline-part-id="prt_expanded_shell"]')).toBeVisible() const group = page.locator('[data-timeline-part-ids="prt_collapsed_patch,prt_collapsed_read"]') - await expect(group.getByRole("button", { name: "Used Patch, Read" })).toBeVisible() - await expect(group.locator('[data-component="tag"]')).toHaveText("2") + await expect(group.getByRole("button", { name: "Used 2 Patch, Read", exact: true })).toBeVisible() + await expect(group.locator('[data-slot="basic-tool-tool-title"]')).toHaveText("2 Patch, Read") await expect(page.locator('[data-timeline-spacing="tool"]')).toHaveCSS("padding-top", "8px") }) @@ -113,7 +114,7 @@ test("combines follow-up patches into one three-file stack inside Used", async ( ], }) const group = page.locator('[data-component="collapsed-tool-group"]') - await group.getByRole("button", { name: "Used Shell, Patch", exact: true }).click() + await group.getByRole("button", { name: "Used 2 Shell, Patch", exact: true }).click() await expect(group.getByText("2 files", { exact: true })).toBeVisible() await timeline.send( partUpdated( @@ -128,7 +129,10 @@ test("combines follow-up patches into one three-file stack inside Used", async ( ), ), ) - await expect(group.locator('[data-component="tag"]')).toHaveText("3") + await expect(group.getByRole("button", { name: "Used 3 Shell, Patch", exact: true })).toHaveAttribute( + "aria-expanded", + "true", + ) await expect(group.locator('[data-component="apply-patch-tool"]')).toHaveCount(1) await expect(group.getByText("3 files", { exact: true })).toBeVisible() await expect(group.locator('[data-slot="apply-patch-filename"]')).toHaveText(["a.ts", "b.ts", "c.ts"]) @@ -158,8 +162,8 @@ test("keeps failed search calls and their error cards inside the collapsed stack await setupTimeline(page, { messages: [userMessage(), assistantMessage(parts)] }) const group = page.locator('[data-timeline-part-ids="prt_error_glob,prt_error_grep"]') - const summary = group.getByRole("button", { name: "Used Glob, Grep" }) - await expect(group.locator('[data-component="tag"]')).toHaveText("2") + const summary = group.getByRole("button", { name: "Used 2 Glob, Grep", exact: true }) + await expect(summary.locator('[data-slot="basic-tool-tool-title"]')).toHaveText("2 Glob, Grep") await summary.click() await expect(group.locator('[data-kind="tool-error-card"]')).toHaveCount(2) const glob = group.locator('[data-timeline-part-id="prt_error_glob"]') diff --git a/packages/app/e2e/regression/subagent-child-navigation.spec.ts b/packages/app/e2e/regression/subagent-child-navigation.spec.ts index 956987156ba..a8daae33229 100644 --- a/packages/app/e2e/regression/subagent-child-navigation.spec.ts +++ b/packages/app/e2e/regression/subagent-child-navigation.spec.ts @@ -52,7 +52,7 @@ test("shows parent lineage while the child timeline loads", async ({ page }) => await page.goto(sessionHref(parentID)) await expectSessionTitle(page, parentTitle) - await page.getByRole("button", { name: "Used Agent" }).click() + await page.getByRole("button", { name: "Used 1 Agent", exact: true }).click() await page.locator(`a[href="${sessionHref(childID)}"]`).click() await Promise.all([requested.promise, expect(page).toHaveURL(sessionHref(childID))]) await Promise.all([ @@ -77,7 +77,7 @@ test("keeps the parent visible while the child session resolves", async ({ page await page.goto(sessionHref(parentID)) await expectSessionTitle(page, parentTitle) - await page.getByRole("button", { name: "Used Agent" }).click() + await page.getByRole("button", { name: "Used 1 Agent", exact: true }).click() await page.locator(`a[href="${sessionHref(childID)}"]`).click() await requested.promise await Promise.all([expect(page).toHaveURL(sessionHref(parentID)), expectSessionTitle(page, parentTitle)]).finally( @@ -195,7 +195,7 @@ async function setup(page: Page, events?: () => OpenCodeEvent[]) { async function openChildFromParent(page: Page) { await page.goto(sessionHref(parentID)) await expectSessionTitle(page, parentTitle) - await page.getByRole("button", { name: "Used Agent" }).click() + await page.getByRole("button", { name: "Used 1 Agent", exact: true }).click() const card = page.locator(`a[href="${sessionHref(childID)}"]`) await expect(card).toBeVisible() diff --git a/packages/session-ui/component-tests/patch-group.spec.ts b/packages/session-ui/component-tests/patch-group.spec.ts index 36ee769c830..b3c8cfd246f 100644 --- a/packages/session-ui/component-tests/patch-group.spec.ts +++ b/packages/session-ui/component-tests/patch-group.spec.ts @@ -10,7 +10,9 @@ story("merges follow-up patches into one stack with a distinct file count", asyn await first.click() await expect(first).toHaveAttribute("aria-expanded", "true") await root.getByRole("button", { name: "Start follow-up patch" }).click() - await expect(group.locator('[data-component="tag"]')).toHaveText("3") + await expect( + group.locator('[data-component="context-tool-group-trigger"] [data-slot="basic-tool-tool-title"]'), + ).toHaveText(/^3 /) await expect(patches).toHaveCount(1) await expect(patches.getByText("2 files", { exact: true })).toBeVisible() await root.getByRole("button", { name: "Finish follow-up patch" }).click() diff --git a/packages/session-ui/component-tests/session-lifecycle.spec.ts b/packages/session-ui/component-tests/session-lifecycle.spec.ts index 0b4b6e25b18..57d426b8272 100644 --- a/packages/session-ui/component-tests/session-lifecycle.spec.ts +++ b/packages/session-ui/component-tests/session-lifecycle.spec.ts @@ -17,7 +17,9 @@ for (const tool of ["shell", "execute", "subagent"]) { for (const action of [undefined, "Complete input", "Run command", "Complete command"]) { if (action) await timeline.getByRole("button", { name: action, exact: true }).click() await expect(group).toHaveAttribute("data-timeline-part-ids", "tool_context_lifecycle,tool_shell_lifecycle") - await expect(group.locator('[data-component="tag"]')).toHaveText("2") + await expect( + group.locator('[data-component="context-tool-group-trigger"] [data-slot="basic-tool-tool-title"]'), + ).toHaveText(/^2 /) await expect(timeline.locator('[data-timeline-row="AssistantPart"]')).toHaveCount(1) await expect(trigger).toHaveAttribute("aria-expanded", String(open)) expect(await original!.evaluate((node) => node.isConnected)).toBe(true) @@ -33,7 +35,7 @@ for (const expanded of [false, true]) { const timeline = await mount("current-session-terminal-work--terminal-commands", { args: { expanded } }) const trigger = expanded ? timeline.locator('[data-timeline-part-id="tool_shell_lifecycle"] [data-slot="collapsible-trigger"]') - : timeline.getByRole("button", { name: "Used Shell", exact: true }) + : timeline.getByRole("button", { name: "Used 1 Shell", exact: true }) await expect(trigger).toHaveAttribute("aria-expanded", String(expanded)) await trigger.click() await expect(trigger).toHaveAttribute("aria-expanded", String(!expanded)) @@ -81,7 +83,7 @@ story("transitions a streaming shell from writing through command execution", as await expect(subtitle).toHaveText("printf ready") await expect(tool).not.toContainText("Writing command...") await timeline.getByRole("button", { name: "Complete command" }).click() - const summary = timeline.getByRole("button", { name: "Used Shell", exact: true }) + const summary = timeline.getByRole("button", { name: "Used 1 Shell", exact: true }) await expect(summary).toHaveAttribute("aria-expanded", "false") await summary.click() await expect(subtitle).toHaveText("printf ready") @@ -128,7 +130,7 @@ for (const open of [false, true]) { await expect(thought).not.toContainText("Inspecting stability") await expect(thought).toHaveAttribute("aria-expanded", String(open)) await timeline.getByRole("button", { name: "Finish session" }).click() - const used = group.getByRole("button", { name: "Used Shell", exact: true }) + const used = group.getByRole("button", { name: "Used 1 Shell", exact: true }) await expect(used).toHaveAttribute("aria-expanded", "false") await used.click() await expect(used).toHaveAttribute("aria-expanded", "true") @@ -137,7 +139,9 @@ for (const open of [false, true]) { "aria-expanded", String(open), ) - await expect(group.locator('[data-component="tag"]')).toHaveText("1") + await expect( + group.locator('[data-component="context-tool-group-trigger"] [data-slot="basic-tool-tool-title"]'), + ).toHaveText("1 Shell") await expect(timeline.locator('[data-timeline-row="Thinking"]')).toHaveCount(0) await expect(used).toHaveAttribute("aria-expanded", "true") if (!open) await thought.click() @@ -183,8 +187,10 @@ for (const locale of ["de", "ar"] as const) { await timeline.getByRole("button", { name: "Complete read" }).click() await timeline.getByRole("button", { name: "Complete glob" }).click() const group = timeline.locator('[data-timeline-part-ids="tool_context_read,tool_context_glob"]') - await expect(group.getByRole("button")).toHaveAccessibleName(/^Used /) - await expect(group.locator('[data-component="tag"]')).toHaveText("2") + await expect(group.getByRole("button")).toHaveAccessibleName(/^Used 2 /) + await expect( + group.locator('[data-component="context-tool-group-trigger"] [data-slot="basic-tool-tool-title"]'), + ).toHaveText(/^2 /) await expect(page.locator("html")).toHaveAttribute("lang", locale) }) } diff --git a/packages/session-ui/component-tests/session-timeline-reasoning.spec.ts b/packages/session-ui/component-tests/session-timeline-reasoning.spec.ts index 79041e6db79..f248cf99edc 100644 --- a/packages/session-ui/component-tests/session-timeline-reasoning.spec.ts +++ b/packages/session-ui/component-tests/session-timeline-reasoning.spec.ts @@ -50,9 +50,11 @@ for (const mode of ["hidden", "compact", "full"] as const) { if (following === "tool") { const group = timeline.locator('[data-component="collapsed-tool-group"]') const trigger = group.locator(':scope > [data-component="collapsible"] > [data-slot="collapsible-trigger"]') - await expect(trigger).toContainText("UsedSkill") + await expect(trigger).toHaveText(/^Used\s*1 Skill$/) await expect(trigger).toHaveAttribute("aria-expanded", "false") - await expect(group.locator('[data-component="tag"]')).toHaveText("1") + await expect( + group.locator('[data-component="context-tool-group-trigger"] [data-slot="basic-tool-tool-title"]'), + ).toHaveText("1 Skill") await expect(timeline.getByText("Inspecting stability", { exact: true })).toBeHidden() await trigger.click() await expect(trigger).toHaveAttribute("aria-expanded", "true") diff --git a/packages/session-ui/component-tests/session-timeline.spec.ts b/packages/session-ui/component-tests/session-timeline.spec.ts index af73e519210..e7007d8830a 100644 --- a/packages/session-ui/component-tests/session-timeline.spec.ts +++ b/packages/session-ui/component-tests/session-timeline.spec.ts @@ -25,7 +25,7 @@ story("space activates a focused timeline button instead of scrolling", async ({ await page.setViewportSize({ width: 800, height: 240 }) const timeline = await mount("current-session-terminal-work--terminal-commands", { args: { scenario: "collapsed" } }) await expect.poll(() => page.evaluate(() => document.documentElement.scrollHeight - innerHeight)).toBeGreaterThan(0) - const trigger = timeline.getByRole("button", { name: "Used Shell", exact: true }) + const trigger = timeline.getByRole("button", { name: "Used 1 Shell", exact: true }) await expect(trigger).toHaveAttribute("aria-expanded", "false") await trigger.focus() const before = await page.evaluate(() => window.scrollY) diff --git a/packages/session-ui/component-tests/session-tool-projection.spec.ts b/packages/session-ui/component-tests/session-tool-projection.spec.ts index 3b5047ff777..5f36d1473a1 100644 --- a/packages/session-ui/component-tests/session-tool-projection.spec.ts +++ b/packages/session-ui/component-tests/session-tool-projection.spec.ts @@ -42,7 +42,9 @@ story("renders every tool error outcome without leaking hidden tools", async ({ const timeline = await mount("current-session-research-agents--agent-research", { args: { scenario: "failures" } }) const names = ["shell", "edit", "write", "patch", "webfetch", "websearch", "subagent", "skill", "mcp_probe"] const group = timeline.locator(`[data-timeline-part-ids="${names.map((name) => `tool_error_${name}`).join(",")}"]`) - await expect(group.locator('[data-component="tag"]')).toHaveText(String(names.length)) + await expect( + group.locator('[data-component="context-tool-group-trigger"] [data-slot="basic-tool-tool-title"]'), + ).toHaveText(new RegExp(`^${names.length} `)) await group.getByRole("button").click() await expect(timeline.locator('[data-kind="tool-error-card"]')).toHaveCount(names.length + 1) const dismissed = timeline.locator('[data-timeline-part-id="tool_error_question_dismissed"]') @@ -68,7 +70,7 @@ story("transitions shell and question through running error outcomes", async ({ // Moved from packages/app/e2e/regression/session-timeline-tool-projection.spec.ts story("labels all web search provider variants", async ({ mount }) => { const timeline = await mount("current-session-research-agents--agent-research", { args: { scenario: "providers" } }) - await timeline.getByRole("button", { name: "Used Parallel Web Search, Exa Web Search, Web Search" }).click() + await timeline.getByRole("button", { name: "Used 3 Parallel Web Search, Exa Web Search, Web Search" }).click() const tools = timeline.locator('[data-component="context-tool-group-list"]') await expect(tools.getByRole("button", { name: /Parallel Web Search/ })).toBeVisible() await expect(tools.getByRole("button", { name: /Exa Web Search/ })).toBeVisible() @@ -101,8 +103,10 @@ story("labels read tools from their path input", async ({ mount }) => { story("labels skill tools from IDs and result metadata", async ({ mount }) => { const timeline = await mount("current-session-research-agents--agent-research", { args: { scenario: "skills" } }) const group = timeline.locator('[data-timeline-part-ids="tool_skill_id,tool_skill_name"]') - await expect(group.getByRole("button")).toHaveAccessibleName("Used Skill") - await expect(group.locator('[data-component="tag"]')).toHaveText("2") + await expect(group.getByRole("button")).toHaveAccessibleName("Used 2 Skill") + await expect( + group.locator('[data-component="context-tool-group-trigger"] [data-slot="basic-tool-tool-title"]'), + ).toHaveText("2 Skill") await group.getByRole("button").click() const loaded = group.locator('[data-component="tool-loaded-item"]') await expect(loaded).toHaveCount(1) @@ -124,8 +128,10 @@ story("groups every collapsed tool until visible text separates the stack", asyn '[data-timeline-part-ids="tool_boundary_glob,tool_boundary_grep,tool_boundary_shell,tool_boundary_list"]', ) await expect(group).toBeVisible() - await expect(group.getByRole("button")).toHaveAccessibleName("Used Glob, Grep, Shell, List") - await expect(group.locator('[data-component="tag"]')).toHaveText("4") + await expect(group.getByRole("button")).toHaveAccessibleName("Used 4 Glob, Grep, Shell, List") + await expect( + group.locator('[data-component="context-tool-group-trigger"] [data-slot="basic-tool-tool-title"]'), + ).toHaveText("4 Glob, Grep, Shell, List") await expect(timeline.locator('[data-timeline-row="AssistantPart"]')).toHaveCount(3) await expect(timeline.locator('[data-timeline-spacing="content"]')).toHaveCount(2) await expect(timeline.locator('[data-timeline-spacing="content"]').nth(0)).toHaveCSS("padding-top", "16px") diff --git a/packages/session-ui/component-tests/tool-disclosure.spec.ts b/packages/session-ui/component-tests/tool-disclosure.spec.ts index 3b5816a9f83..8fcd1f8ca56 100644 --- a/packages/session-ui/component-tests/tool-disclosure.spec.ts +++ b/packages/session-ui/component-tests/tool-disclosure.spec.ts @@ -6,12 +6,14 @@ for (const open of [true, false]) { async ({ mount }, info) => { const root = await mount("current-session-file-changes--appending-tool-calls") const group = root.locator('[data-component="collapsed-tool-group"]') - await group.getByRole("button", { name: "Used Shell, Patch", exact: true }).click() + const trigger = group.getByRole("button", { name: /^Used \d+ Shell, Patch$/ }) + await expect(trigger).toHaveAccessibleName("Used 2 Shell, Patch") + await trigger.click() const shell = group.locator('[data-timeline-part-id="tool_shell_existing"] [data-slot="collapsible-trigger"]') - await group.locator('[data-timeline-part-id="tool_patch_existing"]').evaluate((element) => { - element.setAttribute("data-disclosure-probe", "existing") - }) - const patch = group.locator('[data-disclosure-probe="existing"]') + await group.locator('[data-timeline-part-id="tool_patch_existing"]').evaluate((element) => { + element.setAttribute("data-disclosure-probe", "existing") + }) + const patch = group.locator('[data-disclosure-probe="existing"]') const first = patch.locator('[data-scope="apply-patch"] button').filter({ hasText: "a.ts" }) const second = patch.locator('[data-scope="apply-patch"] button').filter({ hasText: "b.ts" }) const diff = patch.locator('[data-type="update"]').filter({ hasText: "b.ts" }).locator('[data-component="file"]') @@ -29,7 +31,10 @@ for (const open of [true, false]) { const original = await patch.elementHandle() for (const count of [3, 4]) { await root.getByRole("button", { name: "Append tool call", exact: true }).click() - await expect(group.locator('[data-component="tag"]')).toHaveText(String(count)) + await expect( + group.locator('[data-component="context-tool-group-trigger"] [data-slot="basic-tool-tool-title"]'), + ).toHaveText(`${count} Shell, Patch`) + await expect(trigger).toHaveAccessibleName(`Used ${count} Shell, Patch`) await expect(diff).toBeVisible() await root .locator('[data-component="session-timeline"]') @@ -38,10 +43,7 @@ for (const open of [true, false]) { await expect(first).toHaveAttribute("aria-expanded", String(open)) await expect(second).toHaveAttribute("aria-expanded", "true") expect(await original!.evaluate((node) => node.isConnected)).toBe(true) - await expect(group.getByRole("button", { name: "Used Shell, Patch", exact: true })).toHaveAttribute( - "aria-expanded", - "true", - ) + await expect(trigger).toHaveAttribute("aria-expanded", "true") } }, ) diff --git a/packages/session-ui/component-tests/tool-group.spec.ts b/packages/session-ui/component-tests/tool-group.spec.ts index e7a7a65ed0c..e924f34d1e2 100644 --- a/packages/session-ui/component-tests/tool-group.spec.ts +++ b/packages/session-ui/component-tests/tool-group.spec.ts @@ -6,11 +6,14 @@ for (const reasoningDefaultOpen of [false, true]) { async ({ mount }) => { const root = await mount("current-tool-group--mixed-reasoning", { args: { reasoningDefaultOpen } }) const group = root.locator('[data-component="collapsed-tool-group"]') - const used = group.getByRole("button", { name: "Used Read, Skill", exact: true }) + const used = group.getByRole("button", { name: /^Used \d+ Read, Skill$/ }) const first = group.locator('[data-timeline-part-id="reasoning_first"]') const second = group.locator('[data-timeline-part-id="reasoning_second"]') await expect(used).toHaveAttribute("aria-expanded", "true") - await expect(group.locator('[data-component="tag"]')).toHaveText("4") + await expect(used).toHaveAccessibleName("Used 4 Read, Skill") + await expect( + group.locator('[data-component="context-tool-group-trigger"] [data-slot="basic-tool-tool-title"]'), + ).toHaveText("4 Read, Skill") await expect(group.locator('[data-slot="context-tool-group-item"]')).toHaveText([ /Read.*group\.ts/, /Thought/, @@ -31,7 +34,10 @@ for (const reasoningDefaultOpen of [false, true]) { ) await first.getByRole("button", { name: "Thought", exact: true }).click() await root.getByRole("button", { name: "Append follow-up read", exact: true }).click() - await expect(group.locator('[data-component="tag"]')).toHaveText("5") + await expect(used).toHaveAccessibleName("Used 5 Read, Skill") + await expect( + group.locator('[data-component="context-tool-group-trigger"] [data-slot="basic-tool-tool-title"]'), + ).toHaveText("5 Read, Skill") await expect(group.locator('[data-slot="context-tool-group-item"]')).toHaveText([ /Read.*group\.ts/, /Thought/, @@ -64,8 +70,10 @@ for (const reasoningDefaultOpen of [false, true]) { story("summarizes subagents as Agent while retaining their card titles", async ({ mount }) => { const root = await mount("current-tool-group--mixed-tools") const group = root.locator('[data-component="collapsed-tool-group"]') - await expect(group.getByRole("button", { name: "Used Shell, Read, Agent", exact: true })).toBeVisible() - await expect(group.locator('[data-component="tag"]')).toHaveText("4") + await expect(group.getByRole("button", { name: "Used 4 Shell, Read, Agent", exact: true })).toBeVisible() + await expect( + group.locator('[data-component="context-tool-group-trigger"] [data-slot="basic-tool-tool-title"]'), + ).toHaveText("4 Shell, Read, Agent") await expect(group.locator('[data-component="task-tool-title"]')).toHaveText(["General", "Explore"]) }) @@ -74,6 +82,23 @@ for (const width of [840, 390]) { await page.setViewportSize({ width, height: 600 }) const root = await mount("current-tool-group--mixed-tools") const group = root.locator('[data-component="collapsed-tool-group"]') + const trigger = group.getByRole("button", { name: "Used 4 Shell, Read, Agent", exact: true }) + const header = group.locator('[data-component="context-tool-group-trigger"]') + await expect(header.locator('[data-slot="basic-tool-tool-title"]')).toHaveText("4 Shell, Read, Agent") + await expect(header.locator('[data-component="tag"]')).toHaveCount(0) + await expect(trigger).toHaveAttribute("aria-expanded", "true") + for (const action of ["click", "Enter", "Space"] as const) { + if (action === "click") await trigger.click() + if (action !== "click") await trigger.press(action) + await expect(trigger).toHaveAttribute("aria-expanded", "false") + await expect(group.locator('[data-component="context-tool-group-list"]')).toBeHidden() + await expect(trigger).toBeFocused() + if (action === "click") await trigger.click() + if (action !== "click") await trigger.press(action) + await expect(trigger).toHaveAttribute("aria-expanded", "true") + await expect(group.locator('[data-component="context-tool-group-list"]')).toBeVisible() + await expect(trigger).toBeFocused() + } const cards = group.locator('[data-component="task-tool-surface"]') await expect(cards).toHaveCount(2) await expect diff --git a/packages/session-ui/src/components/message-part.css b/packages/session-ui/src/components/message-part.css index 1d0cfa13083..03472c2e2e5 100644 --- a/packages/session-ui/src/components/message-part.css +++ b/packages/session-ui/src/components/message-part.css @@ -702,12 +702,6 @@ white-space: nowrap; } - [data-component="tag"] { - flex-shrink: 0; - border: 0; - background: var(--v2-background-bg-layer-03); - } - [data-slot="collapsible-arrow"] { color: var(--icon-weaker); cursor: pointer; diff --git a/packages/session-ui/src/tools/tool-renderer.tsx b/packages/session-ui/src/tools/tool-renderer.tsx index 7074a204c94..1334974410c 100644 --- a/packages/session-ui/src/tools/tool-renderer.tsx +++ b/packages/session-ui/src/tools/tool-renderer.tsx @@ -20,7 +20,6 @@ import { useFileComponent } from "@opencode-ai/ui/context/file" import { type UiI18n, useI18n } from "@opencode-ai/ui/context/i18n" import { BasicTool, GenericTool } from "../components/basic-tool" import { Accordion } from "@opencode-ai/ui/accordion" -import { Badge } from "@opencode-ai/ui/badge" import { StickyAccordionHeader } from "@opencode-ai/ui/sticky-accordion-header" import { Collapsible } from "@opencode-ai/ui/collapsible" import { FileIcon } from "@opencode-ai/ui/file-icon" @@ -503,10 +502,10 @@ export function CurrentContextToolGroup(props: { ].join(", "), ) const label = createMemo(() => { - const tools = names() - const text = i18n.t("ui.messagePart.tools.used", { tools }) - const index = text.indexOf(tools) - return { text, before: text.slice(0, index).trim(), after: text.slice(index + tools.length).trim() } + const title = `${tools().length} ${names()}` + const text = i18n.t("ui.messagePart.tools.used", { tools: title }) + const index = text.indexOf(title) + return { text, title, before: text.slice(0, index).trim(), after: text.slice(index + title.length).trim() } }) const items = createMemo(() => props.parts.reduce<(SessionMessageAssistantTool[] | (SessionMessageAssistantReasoning & { id: string }))[]>( @@ -564,11 +563,10 @@ export function CurrentContextToolGroup(props: { {(before) => {before()}} - {names()} + {label().title} {(after) => {after()}} - {tools().length} }