Compare commits

...
Author SHA1 Message Date
Brendan Allan ab8f417aed chore: merge v2 into type-count-labels 2026-09-03 07:23:26 +08:00
Brendan Allan d5d74103ee fix(session-ui): count timeline tool types 2026-09-02 14:49:28 +08:00
9 changed files with 56 additions and 29 deletions
@@ -12,7 +12,7 @@ story("merges follow-up patches into one stack with a distinct file count", asyn
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 /)
).toHaveText("1 Shell, 2 Patch")
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: "Used 1 Shell, 1 Read, 2 Agents", exact: true })).toBeVisible()
})
for (const grouped of [true, false]) {
@@ -19,7 +19,7 @@ for (const tool of ["shell", "execute", "subagent"]) {
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 /)
).toHaveText(`1 Read, 1 ${tool === "subagent" ? "Agent" : tool[0]!.toUpperCase() + tool.slice(1)}`)
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)
@@ -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(/^Used 1 .+, 1 /)
await expect(
group.locator('[data-component="context-tool-group-trigger"] [data-slot="basic-tool-tool-title"]'),
).toHaveText(/^2 /)
).toHaveText(/^1 .+, 1 /)
await expect(page.locator("html")).toHaveAttribute("lang", locale)
})
}
@@ -44,7 +44,7 @@ story("renders every tool error outcome without leaking hidden tools", async ({
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} `))
).toHaveText("1 Shell, 1 Edit, 1 Write, 1 Patch, 1 Webfetch, 1 Web Search, 1 Agent, 1 Skill, 1 mcp_probe")
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"]')
@@ -70,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 3 Parallel Web Search, Exa Web Search, Web Search" }).click()
await timeline.getByRole("button", { name: "Used 1 Parallel Web Search, 1 Exa Web Search, 1 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()
@@ -102,11 +102,11 @@ story("labels read tools from their path input", async ({ mount }) => {
// Moved from packages/app/e2e/regression/session-timeline-tool-projection.spec.ts
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")
const group = timeline.locator('[data-timeline-part-ids="tool_skill_id,tool_skill_name,tool_skill_agent"]')
await expect(group.getByRole("button")).toHaveAccessibleName("Used 2 Skills, 1 Agent")
await expect(
group.locator('[data-component="context-tool-group-trigger"] [data-slot="basic-tool-tool-title"]'),
).toHaveText("2 Skill")
).toHaveText("2 Skills, 1 Agent")
await group.getByRole("button").click()
const loaded = group.locator('[data-component="tool-loaded-item"]')
await expect(loaded).toHaveCount(1)
@@ -128,10 +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 4 Glob, Grep, Shell, List")
await expect(group.getByRole("button")).toHaveAccessibleName("Used 1 Glob, 1 Grep, 1 Shell, 1 List")
await expect(
group.locator('[data-component="context-tool-group-trigger"] [data-slot="basic-tool-tool-title"]'),
).toHaveText("4 Glob, Grep, Shell, List")
).toHaveText("1 Glob, 1 Grep, 1 Shell, 1 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: /^Used 1 Shell, \d+ Patch$/ })
await expect(trigger).toHaveAccessibleName("Used 1 Shell, 1 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(`1 Shell, ${count - 1} Patch`)
await expect(trigger).toHaveAccessibleName(`Used 1 Shell, ${count - 1} 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: /^Used \d+ Read, \d+ Skills?$/ })
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("Used 1 Read, 3 Skills")
await expect(
group.locator('[data-component="context-tool-group-trigger"] [data-slot="basic-tool-tool-title"]'),
).toHaveText("1 Read, 3 Skill")
).toHaveText("1 Read, 3 Skills")
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("Used 2 Read, 3 Skills")
await expect(
group.locator('[data-component="context-tool-group-trigger"] [data-slot="basic-tool-tool-title"]'),
).toHaveText("2 Read, 3 Skill")
).toHaveText("2 Read, 3 Skills")
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: "Used 1 Shell, 1 Read, 2 Agents", 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("1 Shell, 1 Read, 2 Agents")
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: "Used 1 Shell, 1 Read, 2 Agents", 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("1 Shell, 1 Read, 2 Agents")
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) {
@@ -193,8 +193,22 @@ const LoadingSpecializedSkills = {
title="Loading specialized skills"
description="Active and completed skills display their identifier or resolved name."
document={storyDocument([
storyTool("tool_skill_id", "skill", "running", { id: "frontend-design" }),
storyTool("tool_skill_id", "skill", "completed", { id: "frontend-design" }),
storyTool("tool_skill_name", "skill", "completed", { id: "opencode" }, { metadata: { name: "OpenCode" } }),
storyTool(
"tool_skill_agent",
"subagent",
"completed",
{
description: "Review skill usage",
agent: "explore",
prompt: "Review the loaded skills.",
},
{
output: "The loaded skills are ready.",
metadata: { sessionID: "session_child_review", status: "completed" },
},
),
])}
/>
),
@@ -507,11 +507,20 @@ export function CurrentContextToolGroup(props: {
: tool.name === "subagent"
? i18n.t("ui.tool.agent.default")
: getToolInfo(tool.name, input, currentToolMetadata(tool)).title
counts.set(name, (counts.get(name) ?? 0) + 1)
const current = counts.get(name)
counts.set(name, {
count: (current?.count ?? 0) + 1,
plural:
tool.name === "skill"
? "ui.messagePart.tools.skill"
: tool.name === "subagent"
? "ui.messagePart.tools.agent"
: undefined,
})
return counts
}, new Map<string, number>()),
}, new Map<string, { count: number; plural?: "ui.messagePart.tools.skill" | "ui.messagePart.tools.agent" }>()),
]
.map(([name, count]) => `${count} ${name}`)
.map(([name, item]) => (item.plural ? i18n.plural(item.plural, item.count) : `${item.count} ${name}`))
.join(", "),
)
const label = createMemo(() => {
+4
View File
@@ -111,6 +111,10 @@ const source = {
"ui.messagePart.context.match.one": "({{count}} match)",
"ui.messagePart.context.match.other": "({{count}} matches)",
"ui.messagePart.tools.used": "Used {{tools}}",
"ui.messagePart.tools.skill.one": "{{count}} Skill",
"ui.messagePart.tools.skill.other": "{{count}} Skills",
"ui.messagePart.tools.agent.one": "{{count}} Agent",
"ui.messagePart.tools.agent.other": "{{count}} Agents",
"ui.list.loading": "Loading",
"ui.list.empty": "No results",