diff --git a/packages/app/e2e/regression/session-timeline-tool-projection.spec.ts b/packages/app/e2e/regression/session-timeline-tool-projection.spec.ts index 9fab74276b..85485acbfe 100644 --- a/packages/app/e2e/regression/session-timeline-tool-projection.spec.ts +++ b/packages/app/e2e/regression/session-timeline-tool-projection.spec.ts @@ -83,6 +83,26 @@ test("labels all web search provider variants", async ({ page }) => { await expect(page.getByRole("button", { name: /^Web Search/ })).toBeVisible() }) +test("labels completed searches with result counts", async ({ page }) => { + const glob = "prt_glob_count" + const grep = "prt_grep_count" + await setupTimeline(page, { + messages: [ + userMessage(), + assistantMessage([ + toolPart(glob, "glob", "completed", { path: ".", pattern: "**/*.ts" }, { metadata: { count: 1 } }), + toolPart(grep, "grep", "completed", { path: ".", pattern: "value" }, { metadata: { matches: 12 } }), + ]), + ], + }) + + const group = page.locator(`[data-timeline-part-ids="${glob},${grep}"]`) + await group.locator('[data-slot="collapsible-trigger"]').click() + const rows = group.locator('[data-component="tool-trigger"]') + await expect(rows.nth(0)).toContainText("(1 match)") + await expect(rows.nth(1)).toContainText("(12 matches)") +}) + test("labels V2 read tools from their path input", async ({ page }) => { const id = "prt_read_path" await setupTimeline(page, { diff --git a/packages/session-ui/src/components/message-part.tsx b/packages/session-ui/src/components/message-part.tsx index fd0db38441..4b1ef1460c 100644 --- a/packages/session-ui/src/components/message-part.tsx +++ b/packages/session-ui/src/components/message-part.tsx @@ -866,6 +866,12 @@ function contextToolTrigger(part: ToolPart, i18n: ReturnType) { const include = typeof input.include === "string" ? input.include : undefined const offset = typeof input.offset === "number" ? input.offset : undefined const limit = typeof input.limit === "number" ? input.limit : undefined + const metadata = "metadata" in part.state ? part.state.metadata : undefined + const count = part.tool === "glob" ? metadata?.count : part.tool === "grep" ? metadata?.matches : undefined + const matches = + typeof count === "number" && Number.isFinite(count) && count !== 0 + ? i18n.plural("ui.messagePart.context.match", count) + : undefined switch (part.tool) { case "read": { @@ -887,12 +893,13 @@ function contextToolTrigger(part: ToolPart, i18n: ReturnType) { return { title: i18n.t("ui.tool.glob"), subtitle: getDirectory(path), - args: pattern ? ["pattern=" + pattern] : [], + args: [...(pattern ? ["pattern=" + pattern] : []), ...(matches ? [matches] : [])], } case "grep": { const args: string[] = [] if (pattern) args.push("pattern=" + pattern) if (include) args.push("include=" + include) + if (matches) args.push(matches) return { title: i18n.t("ui.tool.grep"), subtitle: getDirectory(path), diff --git a/packages/ui/src/i18n/en.ts b/packages/ui/src/i18n/en.ts index abaaa059cc..c58054ac66 100644 --- a/packages/ui/src/i18n/en.ts +++ b/packages/ui/src/i18n/en.ts @@ -108,6 +108,8 @@ const source = { "ui.messagePart.context.search.other": "{{count}} searches", "ui.messagePart.context.list.one": "{{count}} list", "ui.messagePart.context.list.other": "{{count}} lists", + "ui.messagePart.context.match.one": "({{count}} match)", + "ui.messagePart.context.match.other": "({{count}} matches)", "ui.list.loading": "Loading", "ui.list.empty": "No results",