Skip to content
Open
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
2 changes: 1 addition & 1 deletion packages/session-ui/component-tests/patch-group.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion packages/session-ui/component-tests/read-image.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]) {
Expand Down
6 changes: 3 additions & 3 deletions packages/session-ui/component-tests/session-lifecycle.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
})
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"]')
Expand All @@ -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()
Expand Down Expand Up @@ -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)
Expand All @@ -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")
Expand Down
8 changes: 4 additions & 4 deletions packages/session-ui/component-tests/tool-disclosure.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand All @@ -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"]')
Expand Down
18 changes: 9 additions & 9 deletions packages/session-ui/component-tests/tool-group.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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/,
Expand All @@ -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/,
Expand Down Expand Up @@ -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()
Expand All @@ -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) {
Expand Down
16 changes: 15 additions & 1 deletion packages/session-ui/src/timeline/research-and-agents.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
},
),
])}
/>
),
Expand Down
15 changes: 12 additions & 3 deletions packages/session-ui/src/tools/tool-renderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand Down
4 changes: 4 additions & 0 deletions packages/ui/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading