fix(session-ui): show one count before used (#48038)

This commit is contained in:
David Hill
2026-09-08 17:15:24 -06:00
committed by GitHub
parent f3128fa241
commit 8c5eca5bb2
22 changed files with 84 additions and 83 deletions
@@ -39,7 +39,7 @@ for (const width of [1400, 390]) {
reducedMotion: true,
viewport: { width, height: 900 },
})
await page.getByRole("button", { name: "Used 1 Patch", exact: true }).click()
await page.getByRole("button", { name: "1 used Patch", exact: true }).click()
const patch = page.locator('[data-component="apply-patch-tool"]')
const trigger = patch.getByRole("button", { name: /patch-border.ts/ })
await expect(trigger).toHaveAttribute("aria-expanded", "false")
@@ -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).toHaveText(/^Used\s*1 Read, 1 Grep$/)
await expect(tools.locator('[data-slot="basic-tool-tool-title"]')).toHaveText("1 Read, 1 Grep")
await expect(tools).toHaveText(/^2 used\s*Read, Grep$/)
await expect(tools.locator('[data-slot="basic-tool-tool-title"]')).toHaveText("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*1 Read, 1 Grep$/, /U2: Also check the retry path\./])
await expect.soft(tools.or(pending)).toHaveText([/^2 used\s*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*1 Read, 1 Grep$/,
/^2 used\s*Read, Grep$/,
/U2: Also check the retry path\./,
/A3: Now checking the retry path for U2\./,
])
@@ -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 1 Shell", exact: true })
const trigger = page.getByRole("button", { name: "1 used Shell", exact: true })
await expect
.poll(() => scroller.evaluate((element) => element.scrollHeight - element.clientHeight))
.toBeGreaterThan(300)
@@ -93,8 +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 \d+ Patch$/ })
await expect(summary).toHaveAccessibleName("Used 1 Patch")
const summary = group.getByRole("button", { name: /^\d+ used Patch$/ })
await expect(summary).toHaveAccessibleName("1 used Patch")
await summary.click()
await group.locator(`[data-timeline-part-id="${editPartID}"]`).evaluate((element) => {
element.setAttribute("data-disclosure-probe", "existing")
@@ -110,8 +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(summary).toHaveAccessibleName(`Used ${count} Patch`)
await expect(summary.locator('[data-slot="basic-tool-tool-title"]')).toHaveText(`${count} Patch`)
await expect(summary).toHaveAccessibleName(`${count} used Patch`)
await expect(summary.locator('[data-slot="basic-tool-tool-title"]')).toHaveText("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")
@@ -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 1 Read, 1 Glob, 1 Grep, 1 List")
await expect(context.getByRole("button")).toHaveAccessibleName("4 used 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 1 Read, 1 Glob, 1 Grep, 1 List")
await expect(context.getByRole("button")).toHaveAccessibleName("4 used Read, Glob, Grep, List")
await page.waitForTimeout(700)
const trace = await stopVisualProbe<keyof typeof regions>(page)
const labels = trace.samples
@@ -107,7 +107,7 @@ test.describe("regression: session timeline context group resize", () => {
]),
)
expect(labels).toEqual(["Used 1 Read, 1 Glob, 1 Grep, 1 List"])
expect(labels).toEqual(["4 used Read, Glob, Grep, List"])
expect(issues, JSON.stringify(trace.samples, null, 2)).toEqual([])
})
})
@@ -16,8 +16,8 @@ for (const locale of ["de", "ar"] as const) {
})
const group = page.locator(`[data-timeline-part-ids="${ids.join(",")}"]`)
const names = locale === "de" ? "1 Lesen, 1 Glob" : "1 \u0642\u0631\u0627\u0621\u0629, 1 Glob"
await expect(group.getByRole("button")).toHaveAccessibleName(`Used ${names}`)
const names = locale === "de" ? "Lesen, Glob" : "\u0642\u0631\u0627\u0621\u0629, Glob"
await expect(group.getByRole("button")).toHaveAccessibleName(`2 used ${names}`)
await expect(group.locator('[data-slot="basic-tool-tool-title"]')).toHaveText(names)
await expect(page.locator("html")).toHaveAttribute("lang", locale)
})
@@ -386,7 +386,7 @@ test("separates blocking and already-backgrounded work into two rows", async ({
const used = page
.locator('[data-timeline-part-ids="call_backgrounded,call_shell_backgrounded,call_blocking"]')
.locator(':scope > [data-component="collapsible"] > [data-slot="collapsible-trigger"]')
await expect(used).toHaveText(/^Used\s*2 Agent, 1 Shell$/)
await expect(used).toHaveText(/^3 used\s*Agent, Shell$/)
await expect(used).toHaveAttribute("aria-expanded", "false")
await used.click()
await expect(used).toHaveAttribute("aria-expanded", "true")
@@ -46,7 +46,7 @@ test("changes timeline presets and saves custom thinking details", async ({ page
.toEqual({ placement: "grouped", details: "collapsed" })
await settings.getByRole("button", { name: "Back to app", exact: true }).click()
await expect(settings).toBeHidden()
await page.getByRole("button", { name: "Used 1 Thought", exact: true }).click()
await page.getByRole("button", { name: "1 used Thought", exact: true }).click()
await expect(part.getByRole("button")).toHaveAttribute("aria-expanded", "false")
await part.getByRole("button").click()
await expect(part.getByText("The selected mode controls these details.", { exact: true })).toBeVisible()
@@ -45,10 +45,10 @@ 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 2 Shell, 1 Agent, 1 Patch", exact: true })
const summary = group.getByRole("button", { name: "4 used Shell, Agent, Patch", exact: true })
await expect(summary).toHaveAttribute("aria-expanded", "false")
await expect(summary).toHaveCSS("height", "28px")
await expect(summary.locator('[data-slot="basic-tool-tool-title"]')).toHaveText("2 Shell, 1 Agent, 1 Patch")
await expect(summary.locator('[data-slot="basic-tool-tool-title"]')).toHaveText("Shell, Agent, Patch")
await expect(summary.locator('[data-component="tag"]')).toHaveCount(0)
await summary.click()
await expect(summary).toHaveAttribute("aria-expanded", "true")
@@ -75,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 1 Patch, 1 Read", exact: true })).toBeVisible()
await expect(group.locator('[data-slot="basic-tool-tool-title"]')).toHaveText("1 Patch, 1 Read")
await expect(group.getByRole("button", { name: "2 used Patch, Read", exact: true })).toBeVisible()
await expect(group.locator('[data-slot="basic-tool-tool-title"]')).toHaveText("Patch, Read")
await expect(page.locator('[data-timeline-spacing="tool"]')).toHaveCSS("padding-top", "8px")
})
@@ -114,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 1 Shell, 1 Patch", exact: true }).click()
await group.getByRole("button", { name: "2 used Shell, Patch", exact: true }).click()
await expect(group.getByText("2 files", { exact: true })).toBeVisible()
await timeline.send(
partUpdated(
@@ -129,7 +129,7 @@ test("combines follow-up patches into one three-file stack inside Used", async (
),
),
)
await expect(group.getByRole("button", { name: "Used 1 Shell, 2 Patch", exact: true })).toHaveAttribute(
await expect(group.getByRole("button", { name: "3 used Shell, Patch", exact: true })).toHaveAttribute(
"aria-expanded",
"true",
)
@@ -162,7 +162,7 @@ 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-component="collapsed-tool-group"]')
const summary = group.getByRole("button", { name: "Used 1 Glob, 1 Grep", exact: true })
const summary = group.getByRole("button", { name: "2 used Glob, Grep", exact: true })
await expect(summary).toHaveAttribute("aria-expanded", "false")
await summary.click()
await expect(group.locator('[data-kind="tool-error-card"]')).toHaveCount(2)
@@ -181,7 +181,7 @@ for (const grouped of [false, true]) {
await expect(working).toHaveCount(0)
return
}
const trigger = group.getByRole("button", { name: "Used 2 Shell", exact: true, includeHidden: true })
const trigger = group.getByRole("button", { name: "2 used Shell", exact: true, includeHidden: true })
await expect(trigger).toHaveAttribute("aria-expanded", "false")
await expect(working).toBeVisible()
await trigger.click()
@@ -51,7 +51,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 1 Agent", exact: true }).click()
await page.getByRole("button", { name: "1 used 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([
@@ -76,7 +76,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 1 Agent", exact: true }).click()
await page.getByRole("button", { name: "1 used 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(
@@ -194,7 +194,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 1 Agent", exact: true }).click()
await page.getByRole("button", { name: "1 used Agent", exact: true }).click()
const card = page.locator(`a[href="${sessionHref(childID)}"]`)
await expect(card).toBeVisible()
@@ -7,7 +7,7 @@ for (const theme of ["light", "dark"]) {
globals: { theme },
})
const group = root.locator('[data-component="collapsed-tool-group"]').filter({ hasText: "Patch" })
const disclosure = group.getByRole("button", { name: /^Used .*Edit.*Write.*Patch$/ })
const disclosure = group.getByRole("button", { name: /^\d+ used .*Edit.*Write.*Patch$/ })
await disclosure.click()
for (const name of ["edit", "write", "patch"]) {
const tool = group.locator(`[data-timeline-part-id="tool_family_${name}"]`)
@@ -11,8 +11,8 @@ story("merges follow-up patches into one stack with a distinct file count", asyn
await expect(first).toHaveAttribute("aria-expanded", "true")
await root.getByRole("button", { name: "Start follow-up patch" }).click()
await expect(
group.locator('[data-component="context-tool-group-trigger"] [data-slot="basic-tool-tool-title"]'),
).toHaveText(/^3 /)
group.locator('[data-component="context-tool-group-trigger"] [data-slot="context-tool-group-prefix"]'),
).toHaveText("3 used")
await expect(patches).toHaveCount(1)
await expect(patches.getByText("2 files", { exact: true })).toBeVisible()
await root.getByRole("button", { name: "Finish follow-up patch" }).click()
@@ -9,7 +9,7 @@ const png = Buffer.from(
story.beforeEach(async ({ mount }) => {
const root = await mount("current-tool-group--mixed-tools")
await expect(root.getByRole("button", { name: "Used 4 Shell, Read, Agent", exact: true })).toBeVisible()
await expect(root.getByRole("button", { name: "4 used Shell, Read, Agent", exact: true })).toBeVisible()
})
for (const grouped of [true, false]) {
@@ -18,8 +18,8 @@ for (const tool of ["shell", "execute", "subagent"]) {
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="context-tool-group-trigger"] [data-slot="basic-tool-tool-title"]'),
).toHaveText(/^2 /)
group.locator('[data-component="context-tool-group-trigger"] [data-slot="context-tool-group-prefix"]'),
).toHaveText("2 used")
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)
@@ -35,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 1 Shell", exact: true })
: timeline.getByRole("button", { name: "1 used Shell", exact: true })
await expect(trigger).toHaveAttribute("aria-expanded", String(expanded))
await trigger.click()
await expect(trigger).toHaveAttribute("aria-expanded", String(!expanded))
@@ -83,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 1 Shell", exact: true })
const summary = timeline.getByRole("button", { name: "1 used Shell", exact: true })
await expect(summary).toHaveAttribute("aria-expanded", "false")
await summary.click()
await expect(subtitle).toHaveText("printf ready")
@@ -130,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 1 Shell", exact: true })
const used = group.getByRole("button", { name: "1 used Shell", exact: true })
await expect(used).toHaveAttribute("aria-expanded", "false")
await used.click()
await expect(used).toHaveAttribute("aria-expanded", "true")
@@ -141,7 +141,7 @@ for (const open of [false, true]) {
)
await expect(
group.locator('[data-component="context-tool-group-trigger"] [data-slot="basic-tool-tool-title"]'),
).toHaveText("1 Shell")
).toHaveText("Shell")
await expect(timeline.locator('[data-timeline-row="Thinking"]')).toHaveCount(0)
await expect(used).toHaveAttribute("aria-expanded", "true")
if (!open) await thought.click()
@@ -187,10 +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 2 /)
await expect(group.getByRole("button")).toHaveAccessibleName(/^2 used /)
await expect(
group.locator('[data-component="context-tool-group-trigger"] [data-slot="basic-tool-tool-title"]'),
).toHaveText(/^2 /)
).toHaveText(locale === "de" ? "Lesen, Glob" : "\u0642\u0631\u0627\u0621\u0629, Glob")
await expect(page.locator("html")).toHaveAttribute("lang", locale)
})
}
@@ -50,11 +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).toHaveText(/^Used\s*1 Skill$/)
await expect(trigger).toHaveText(/^1 used\s*Skill$/)
await expect(trigger).toHaveAttribute("aria-expanded", "false")
await expect(
group.locator('[data-component="context-tool-group-trigger"] [data-slot="basic-tool-tool-title"]'),
).toHaveText("1 Skill")
).toHaveText("Skill")
await expect(timeline.getByText("Inspecting stability", { exact: true })).toBeHidden()
await trigger.click()
await expect(trigger).toHaveAttribute("aria-expanded", "true")
@@ -53,7 +53,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 1 Shell", exact: true })
const trigger = timeline.getByRole("button", { name: "1 used Shell", exact: true })
await expect(trigger).toHaveAttribute("aria-expanded", "false")
await trigger.focus()
const before = await page.evaluate(() => window.scrollY)
@@ -45,8 +45,8 @@ story("renders every tool error outcome without leaking hidden tools", async ({
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="context-tool-group-trigger"] [data-slot="basic-tool-tool-title"]'),
).toHaveText(new RegExp(`^${names.length} `))
group.locator('[data-component="context-tool-group-trigger"] [data-slot="context-tool-group-prefix"]'),
).toHaveText(`${names.length} used`)
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"]')
@@ -72,7 +72,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 3 Parallel Web Search, Exa Web Search, Web Search" }).click()
await timeline.getByRole("button", { name: "3 used 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()
@@ -105,10 +105,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 2 Skill")
await expect(group.getByRole("button")).toHaveAccessibleName("2 used Skill")
await expect(
group.locator('[data-component="context-tool-group-trigger"] [data-slot="basic-tool-tool-title"]'),
).toHaveText("2 Skill")
).toHaveText("Skill")
await group.getByRole("button").click()
const loaded = group.locator('[data-component="tool-loaded-item"]')
await expect(loaded).toHaveCount(1)
@@ -130,10 +130,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 4 Glob, Grep, Shell, List")
await expect(group.getByRole("button")).toHaveAccessibleName("4 used 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")
).toHaveText("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")
@@ -6,8 +6,8 @@ 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"]')
const trigger = group.getByRole("button", { name: /^Used \d+ Shell, Patch$/ })
await expect(trigger).toHaveAccessibleName("Used 2 Shell, Patch")
const trigger = group.getByRole("button", { name: /^\d+ used Shell, Patch$/ })
await expect(trigger).toHaveAccessibleName("2 used 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) => {
@@ -33,8 +33,8 @@ for (const open of [true, false]) {
await root.getByRole("button", { name: "Append tool call", exact: true }).click()
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`)
).toHaveText("Shell, Patch")
await expect(trigger).toHaveAccessibleName(`${count} used Shell, Patch`)
await expect(diff).toBeVisible()
await root
.locator('[data-component="session-timeline"]')
@@ -6,14 +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 \d+ Read, 3 Skill$/ })
const used = group.getByRole("button", { name: /^\d+ used 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(used).toHaveAccessibleName("Used 1 Read, 3 Skill")
await expect(used).toHaveAccessibleName("4 used Read, Skill")
await expect(
group.locator('[data-component="context-tool-group-trigger"] [data-slot="basic-tool-tool-title"]'),
).toHaveText("1 Read, 3 Skill")
).toHaveText("Read, Skill")
await expect(group.locator('[data-slot="context-tool-group-item"]')).toHaveText([
/Read.*group\.ts/,
/Thought/,
@@ -34,10 +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(used).toHaveAccessibleName("Used 2 Read, 3 Skill")
await expect(used).toHaveAccessibleName("5 used Read, Skill")
await expect(
group.locator('[data-component="context-tool-group-trigger"] [data-slot="basic-tool-tool-title"]'),
).toHaveText("2 Read, 3 Skill")
).toHaveText("Read, Skill")
await expect(group.locator('[data-slot="context-tool-group-item"]')).toHaveText([
/Read.*group\.ts/,
/Thought/,
@@ -70,10 +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 1 Shell, 1 Read, 2 Agent", exact: true })).toBeVisible()
await expect(group.getByRole("button", { name: "4 used Shell, Read, Agent", exact: true })).toBeVisible()
await expect(
group.locator('[data-component="context-tool-group-trigger"] [data-slot="basic-tool-tool-title"]'),
).toHaveText("1 Shell, 1 Read, 2 Agent")
).toHaveText("Shell, Read, Agent")
const gap = await group.evaluate((element) => {
const title = element.querySelector('[data-component="context-tool-group-trigger"]')!.getBoundingClientRect()
const arrow = element.querySelector('[data-slot="collapsible-arrow-icon"]')!.getBoundingClientRect()
@@ -88,9 +88,9 @@ 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 1 Shell, 1 Read, 2 Agent", exact: true })
const trigger = group.getByRole("button", { name: "4 used 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("1 Shell, 1 Read, 2 Agent")
await expect(header.locator('[data-slot="basic-tool-tool-title"]')).toHaveText("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) {
+17 -17
View File
@@ -516,22 +516,21 @@ export function CurrentContextToolGroup(props: {
)
const names = createMemo(() =>
[
...props.parts.reduce((counts, part) => {
if (part.type !== "tool" && part.type !== "shell") return counts
const name =
part.type !== "tool"
? i18n.t("ui.tool.shell")
: part.name === "skill"
? i18n.t("ui.tool.skill")
: part.name === "subagent"
? i18n.t("ui.tool.agent.default")
: getToolInfo(part.name, currentToolInput(part), currentToolMetadata(part)).title
counts.set(name, (counts.get(name) ?? 0) + 1)
return counts
}, new Map<string, number>()),
]
.map(([name, count]) => `${count} ${name}`)
.join(", "),
...new Set(
props.parts.flatMap((part) => {
if (part.type !== "tool" && part.type !== "shell") return []
return [
part.type !== "tool"
? i18n.t("ui.tool.shell")
: part.name === "skill"
? i18n.t("ui.tool.skill")
: part.name === "subagent"
? i18n.t("ui.tool.agent.default")
: getToolInfo(part.name, currentToolInput(part), currentToolMetadata(part)).title,
]
}),
),
].join(", "),
)
const label = createMemo(() => {
const thoughts = props.parts.filter((part) => part.type === "reasoning").length
@@ -540,7 +539,8 @@ export function CurrentContextToolGroup(props: {
return { text: title, title, before: "", after: "" }
}
const title = names() || i18n.plural("ui.messagePart.context.thought", thoughts)
const text = i18n.t("ui.messagePart.tools.used", { tools: title })
const count = props.parts.filter((part) => part.type === "tool" || part.type === "shell").length || thoughts
const text = i18n.plural("ui.messagePart.tools.used", count, { tools: title })
const index = text.indexOf(title)
return { text, title, before: text.slice(0, index).trim(), after: text.slice(index + title.length).trim() }
})
+4 -3
View File
@@ -114,11 +114,12 @@ const source = {
"ui.messagePart.context.list.other": "{{count}} lists",
"ui.messagePart.context.notice.one": "{{count}} Notice",
"ui.messagePart.context.notice.other": "{{count}} Notices",
"ui.messagePart.context.thought.one": "{{count}} Thought",
"ui.messagePart.context.thought.other": "{{count}} Thoughts",
"ui.messagePart.context.thought.one": "Thought",
"ui.messagePart.context.thought.other": "Thoughts",
"ui.messagePart.context.match.one": "({{count}} match)",
"ui.messagePart.context.match.other": "({{count}} matches)",
"ui.messagePart.tools.used": "Used {{tools}}",
"ui.messagePart.tools.used.one": "{{count}} used {{tools}}",
"ui.messagePart.tools.used.other": "{{count}} used {{tools}}",
"ui.list.loading": "Loading",
"ui.list.empty": "No results",