@tangle-network/ui 11.5.0 → 11.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +70 -2
  3. package/dist/chat.d.ts +18 -8
  4. package/dist/chat.js +6 -5
  5. package/dist/{chunk-XROQW5J7.js → chunk-4ENFLPWQ.js} +1 -1
  6. package/dist/{chunk-XPYPZMYT.js → chunk-6RIX7M27.js} +1 -1
  7. package/dist/{chunk-R5GVQDL3.js → chunk-CVX53NCC.js} +7 -5
  8. package/dist/{chunk-ULDNFLIM.js → chunk-EIUE5GV2.js} +10 -14
  9. package/dist/chunk-L57WRIND.js +59 -0
  10. package/dist/chunk-LFVF2HFB.js +19 -0
  11. package/dist/{chunk-7O22XQ4D.js → chunk-PLWRAS5Z.js} +1 -1
  12. package/dist/{chunk-NZLLDHEG.js → chunk-QGTIBEPX.js} +608 -420
  13. package/dist/{chunk-AZWDI2JG.js → chunk-UYTRNR7Z.js} +1 -1
  14. package/dist/{chunk-RKC3GXHE.js → chunk-VDCZHOOO.js} +99 -104
  15. package/dist/{chunk-J2G7KCYH.js → chunk-XT4JMYVB.js} +109 -67
  16. package/dist/{document-editor-pane-MX3FIZSE.js → document-editor-pane-A3JURPW5.js} +1 -1
  17. package/dist/{document-editor-pane-C3cYEDWZ.d.ts → document-editor-pane-IAEQn_Dl.d.ts} +8 -4
  18. package/dist/editor.d.ts +5 -4
  19. package/dist/editor.js +1 -1
  20. package/dist/files.d.ts +1 -1
  21. package/dist/files.js +1 -1
  22. package/dist/hooks.js +6 -5
  23. package/dist/index.d.ts +2 -2
  24. package/dist/index.js +16 -11
  25. package/dist/primitives.d.ts +15 -1
  26. package/dist/primitives.js +5 -1
  27. package/dist/run.d.ts +22 -17
  28. package/dist/run.js +8 -6
  29. package/dist/sdk-hooks.js +6 -5
  30. package/dist/utils.d.ts +6 -0
  31. package/dist/utils.js +1 -1
  32. package/package.json +1 -1
  33. package/src/chat/agent-timeline.stories.tsx +163 -10
  34. package/src/chat/agent-timeline.test.tsx +114 -7
  35. package/src/chat/agent-timeline.tsx +120 -158
  36. package/src/chat/chat-container.test.tsx +59 -0
  37. package/src/chat/user-message.test.tsx +33 -0
  38. package/src/chat/user-message.tsx +13 -8
  39. package/src/editor/document-editor-pane.test.tsx +58 -0
  40. package/src/editor/editor-lazy.test.tsx +102 -0
  41. package/src/editor/editor-lazy.ts +40 -0
  42. package/src/editor/editor-loading.tsx +25 -0
  43. package/src/editor/editor-peers.test.ts +250 -0
  44. package/src/editor/editor-peers.ts +272 -0
  45. package/src/editor/editor-provider.test.tsx +226 -0
  46. package/src/editor/editor-provider.tsx +301 -265
  47. package/src/editor/editor-toolbar.tsx +1 -1
  48. package/src/editor/markdown-document-editor.test.tsx +121 -0
  49. package/src/editor/markdown-document-editor.tsx +114 -82
  50. package/src/editor/tiptap-editor.test.tsx +228 -0
  51. package/src/editor/tiptap-editor.tsx +162 -130
  52. package/src/primitives/index.ts +3 -0
  53. package/src/primitives/text-shimmer.test.tsx +34 -0
  54. package/src/primitives/text-shimmer.tsx +82 -0
  55. package/src/run/inline-thinking-item.stories.tsx +2 -2
  56. package/src/run/inline-thinking-item.tsx +2 -2
  57. package/src/run/inline-tool-item.test.tsx +76 -0
  58. package/src/run/inline-tool-item.tsx +9 -7
  59. package/src/run/run-group.tsx +4 -3
  60. package/src/run/run-row-shell.test.tsx +74 -4
  61. package/src/run/run-row-shell.tsx +81 -53
  62. package/src/run/tool-call-step.tsx +9 -7
  63. package/src/utils/tool-display.test.ts +53 -0
  64. package/src/utils/tool-display.ts +16 -15
  65. package/dist/chunk-LQS34IGP.js +0 -0
  66. /package/dist/{chunk-EJ5NBW7V.js → chunk-LDLOJKPC.js} +0 -0
@@ -1,13 +1,18 @@
1
1
  import { render, screen } from "@testing-library/react";
2
2
  import { describe, expect, it } from "vitest";
3
+ import { formatDuration } from "../utils/format";
3
4
  import { RunRowShell } from "./run-row-shell";
4
5
 
5
- function renderShell(status: "running" | "success" | "error" | "idle") {
6
+ function renderShell(
7
+ status: "running" | "success" | "error" | "idle",
8
+ extra: { durationMs?: number; collapsedError?: string } = {},
9
+ ) {
6
10
  return render(
7
11
  <RunRowShell
8
12
  icon={<span data-testid="lead-icon">icon</span>}
9
13
  title="Ran command"
10
14
  status={status}
15
+ {...extra}
11
16
  >
12
17
  <div>body</div>
13
18
  </RunRowShell>,
@@ -24,15 +29,80 @@ describe("RunRowShell", () => {
24
29
  }
25
30
  });
26
31
 
27
- it("shows a spinner while running", () => {
32
+ it("draws no spinner while running — the running signal is the title", () => {
28
33
  const { container } = renderShell("running");
29
- expect(container.querySelector(".animate-spin")).not.toBeNull();
34
+ expect(container.querySelector(".animate-spin")).toBeNull();
35
+ expect(container.querySelector("[data-run-row-status]")).toBeNull();
36
+ });
37
+
38
+ it("renders no status dot on success (success is silence)", () => {
39
+ const { container } = renderShell("success", { durationMs: 1200 });
40
+ expect(container.querySelector("[data-run-row-status]")).toBeNull();
41
+ });
42
+
43
+ it("renders the red dot and the collapsed error on error", () => {
44
+ const { container } = renderShell("error", {
45
+ collapsedError: "exit code 1",
46
+ });
47
+ expect(
48
+ container.querySelector('[data-run-row-status="error"]'),
49
+ ).not.toBeNull();
50
+ expect(screen.getByText("exit code 1")).toBeInTheDocument();
51
+ });
52
+
53
+ it("rests with no card chrome: transparent border and fill, bare glyph", () => {
54
+ const { container } = renderShell("success");
55
+ const row = container.querySelector("[data-run-row]");
56
+ expect(row).not.toBeNull();
57
+ expect(row!.className).toContain("border-transparent");
58
+ expect(row!.className).toContain("bg-transparent");
59
+ // The border stays reserved so hover/open never shift layout.
60
+ expect(row!.className).toMatch(/(^|\s)border(\s|$)/);
61
+ // No bordered badge box around the glyph.
62
+ const glyph = container.querySelector("[data-run-row-glyph]");
63
+ expect(glyph!.className).not.toContain("border");
64
+ });
65
+
66
+ it("takes the card fill only while open", () => {
67
+ const { container } = render(
68
+ <RunRowShell icon={<span>i</span>} title="Ran command" open>
69
+ <div>body</div>
70
+ </RunRowShell>,
71
+ );
72
+ const row = container.querySelector("[data-run-row]");
73
+ expect(row!.className).toContain("bg-[var(--md3-surface-container)]");
74
+ expect(row!.className).toContain("border-border");
75
+ });
76
+
77
+ it("reveals the completed duration on hover only", () => {
78
+ const { container } = renderShell("success", { durationMs: 1840 });
79
+ const duration = container.querySelector("[data-run-row-duration]");
80
+ expect(duration).not.toBeNull();
81
+ expect(duration!.textContent).toBe(formatDuration(1840));
82
+ expect(duration!.className).toContain("opacity-0");
83
+ expect(duration!.className).toContain("group-hover:opacity-100");
84
+ });
85
+
86
+ it("shows no duration while running", () => {
87
+ const { container } = renderShell("running", { durationMs: 1840 });
88
+ expect(container.querySelector("[data-run-row-duration]")).toBeNull();
89
+ });
90
+
91
+ it("puts the chevron in the glyph slot for expandable rows, hover/open only", () => {
92
+ const { container } = renderShell("success");
93
+ const chevron = container.querySelector("[data-run-row-chevron]");
94
+ expect(chevron).not.toBeNull();
95
+ expect(chevron!.getAttribute("class")).toContain("hidden");
96
+ expect(chevron!.getAttribute("class")).toContain("group-hover:block");
30
97
  });
31
98
 
32
99
  it("is not expandable and has no chevron when no children are provided", () => {
33
- render(<RunRowShell icon={<span>i</span>} title="Reasoning" />);
100
+ const { container } = render(
101
+ <RunRowShell icon={<span>i</span>} title="Reasoning" />,
102
+ );
34
103
  // The trigger button is disabled when there is no expandable body.
35
104
  const button = screen.getByRole("button", { name: /reasoning/i });
36
105
  expect(button).toBeDisabled();
106
+ expect(container.querySelector("[data-run-row-chevron]")).toBeNull();
37
107
  });
38
108
  });
@@ -1,49 +1,44 @@
1
1
  import { useState, type ReactNode } from "react";
2
2
  import * as Collapsible from "@radix-ui/react-collapsible";
3
- import { Loader2, ChevronRight } from "lucide-react";
3
+ import { ChevronRight } from "lucide-react";
4
4
  import { cn } from "../lib/utils";
5
5
  import { formatDuration } from "../utils/format";
6
- import { LiveDuration } from "./run-item-primitives";
7
6
 
8
7
  export type RunRowStatus = "running" | "success" | "error" | "idle";
9
8
 
10
9
  /**
11
- * Trailing status indicator shared by every run row: a spinner while running,
12
- * a green/red dot on terminal states, nothing when idle. Status is shown here
13
- * — it never replaces the row's semantic lead icon.
10
+ * Trailing status indicator shared by every run row. Only `error` draws a
11
+ * mark (a red dot). Success is silence, and a running row carries its state in
12
+ * the title — a `TextShimmer` sweep — so neither renders anything here.
14
13
  */
15
14
  export function RunRowStatusDot({ status }: { status: RunRowStatus }) {
16
- if (status === "running") {
17
- return <Loader2 className="h-3 w-3 shrink-0 animate-spin text-[var(--accent-text)]" />;
18
- }
19
- if (status === "success") {
20
- return (
21
- <span className="h-1.5 w-1.5 shrink-0 rounded-full bg-[var(--surface-success-text)]" />
22
- );
23
- }
24
- if (status === "error") {
25
- return (
26
- <span className="h-1.5 w-1.5 shrink-0 rounded-full bg-[var(--surface-danger-text)]" />
27
- );
28
- }
29
- return null;
15
+ if (status !== "error") return null;
16
+ return (
17
+ <span
18
+ data-run-row-status="error"
19
+ className="h-1.5 w-1.5 shrink-0 rounded-full bg-[var(--surface-danger-text)]"
20
+ />
21
+ );
30
22
  }
31
23
 
32
24
  export interface RunRowShellProps {
33
- /** Semantic lead glyph — always visible; never replaced by status. */
25
+ /** Semantic lead glyph (14px). At rest it is the row's only ornament; on
26
+ * hover and while open the expand chevron takes its slot. */
34
27
  icon: ReactNode;
35
28
  /** Row title. A string in the common case; a node when the title carries its
36
- * own treatment (e.g. an active-state shimmer sweep) — it renders inline in
37
- * the shell's title slot either way. */
29
+ * own treatment — a running row passes `<TextShimmer>` here, which is the
30
+ * whole in-flight signal (the shell draws no spinner). */
38
31
  title: ReactNode;
39
32
  /** Secondary inline text (tool path/command, or a reasoning preview). */
40
33
  description?: string;
41
34
  /** Render the description in mono (tools) vs prose (reasoning). */
42
35
  descriptionMono?: boolean;
43
36
  status?: RunRowStatus;
44
- /** Live-ticks a duration while running. */
37
+ /** @deprecated The shell no longer ticks a live duration while running; the
38
+ * running state lives in the title (`TextShimmer`). Accepted and ignored so
39
+ * existing callers keep compiling. */
45
40
  startTime?: number;
46
- /** Static duration shown once the row is no longer running. */
41
+ /** Static duration, revealed on hover once the row is no longer running. */
47
42
  durationMs?: number;
48
43
  /** Radius shaping when rows are stacked into a group. */
49
44
  groupPosition?: "single" | "first" | "middle" | "last";
@@ -70,11 +65,17 @@ const SHAPE_CLASS: Record<
70
65
  last: "rounded-t-[var(--radius-sm)] rounded-b-[var(--radius-lg)]",
71
66
  };
72
67
 
68
+ // Header geometry: 8px inset, 22px glyph slot, 8px gap. The collapsed error
69
+ // indents by their sum so it starts under the title, not under the glyph.
70
+ const HEADER_INSET = "px-2 py-1";
71
+ const ERROR_INDENT = "pl-[2.375rem]";
72
+
73
73
  /**
74
- * Shared shell for the agent activity rows (reasoning + tool). Owns the
75
- * bordered container, the lead icon badge, title/description, the trailing
76
- * meta cluster (duration + status + chevron) and the collapsible body, so
77
- * both row kinds read as one family instead of two bespoke layouts.
74
+ * Shared shell for the agent activity rows (reasoning + tool). A quiet
75
+ * one-liner at rest — bare glyph, muted title and description, no fill, no
76
+ * border — that becomes a bordered card only while open. The border is always
77
+ * present (transparent at rest) so hover and expand never shift layout.
78
+ * Both row kinds render through it so they read as one family.
78
79
  */
79
80
  export function RunRowShell({
80
81
  icon,
@@ -82,7 +83,6 @@ export function RunRowShell({
82
83
  description,
83
84
  descriptionMono = false,
84
85
  status = "idle",
85
- startTime,
86
86
  durationMs,
87
87
  groupPosition = "single",
88
88
  collapsedError,
@@ -109,11 +109,14 @@ export function RunRowShell({
109
109
  <Collapsible.Root open={open} onOpenChange={setOpen}>
110
110
  <div className="flex items-start gap-2">
111
111
  <div
112
+ data-run-row=""
113
+ data-state={open ? "open" : "closed"}
112
114
  className={cn(
113
- "min-w-0 flex-1 overflow-hidden border transition-colors",
114
- "border-[var(--border-subtle)] bg-[var(--md3-surface-container)]",
115
- expandable && "hover:border-border",
116
- open && "border-border",
115
+ "group min-w-0 flex-1 overflow-hidden border transition-colors",
116
+ open
117
+ ? "border-border bg-[var(--md3-surface-container)]"
118
+ : "border-transparent bg-transparent",
119
+ expandable && !open && "hover:bg-[var(--md3-surface-container)]",
117
120
  SHAPE_CLASS[groupPosition],
118
121
  className,
119
122
  )}
@@ -122,21 +125,48 @@ export function RunRowShell({
122
125
  <button
123
126
  type="button"
124
127
  className={cn(
125
- "flex w-full items-center gap-2.5 px-3 py-2 text-left",
128
+ "flex w-full items-center gap-2 text-left",
129
+ HEADER_INSET,
126
130
  expandable ? "cursor-pointer" : "cursor-default",
127
131
  )}
128
132
  >
129
- <span className="flex h-6 w-6 shrink-0 items-center justify-center rounded-[var(--radius-sm)] border border-border bg-muted text-muted-foreground">
130
- {icon}
133
+ <span
134
+ aria-hidden
135
+ className="flex h-[1.375rem] w-[1.375rem] shrink-0 items-center justify-center text-muted-foreground"
136
+ >
137
+ <span
138
+ data-run-row-glyph=""
139
+ className={cn(
140
+ "flex items-center justify-center",
141
+ expandable && "group-hover:hidden",
142
+ open && "hidden",
143
+ )}
144
+ >
145
+ {icon}
146
+ </span>
147
+ {expandable ? (
148
+ <ChevronRight
149
+ data-run-row-chevron=""
150
+ className={cn(
151
+ "hidden h-3.5 w-3.5 transition-transform group-hover:block",
152
+ open && "block rotate-90",
153
+ )}
154
+ />
155
+ ) : null}
131
156
  </span>
132
157
 
133
- <span className="shrink-0 whitespace-nowrap text-xs font-medium text-foreground">
158
+ <span
159
+ className={cn(
160
+ "shrink-0 whitespace-nowrap text-sm font-normal text-muted-foreground transition-colors group-hover:text-foreground",
161
+ open && "text-foreground",
162
+ )}
163
+ >
134
164
  {title}
135
165
  </span>
136
166
  {description ? (
137
167
  <span
138
168
  className={cn(
139
- "hidden min-w-0 flex-1 truncate text-xs text-muted-foreground sm:inline",
169
+ "hidden min-w-0 flex-1 truncate text-sm text-muted-foreground transition-colors group-hover:text-foreground sm:inline",
140
170
  descriptionMono && "font-mono",
141
171
  )}
142
172
  >
@@ -144,29 +174,27 @@ export function RunRowShell({
144
174
  </span>
145
175
  ) : null}
146
176
 
147
- <div className="ml-auto flex shrink-0 items-center gap-1.5">
148
- {isRunning && startTime != null ? (
149
- <LiveDuration startTime={startTime} />
150
- ) : durationMs != null ? (
151
- <span className="text-[10px] font-mono tabular-nums text-muted-foreground">
177
+ <span className="ml-auto flex shrink-0 items-center gap-1.5">
178
+ {!isRunning && durationMs != null ? (
179
+ <span
180
+ data-run-row-duration=""
181
+ className="font-mono text-[10px] tabular-nums text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100"
182
+ >
152
183
  {formatDuration(durationMs)}
153
184
  </span>
154
185
  ) : null}
155
186
  <RunRowStatusDot status={status} />
156
- {expandable ? (
157
- <ChevronRight
158
- className={cn(
159
- "h-3 w-3 shrink-0 text-muted-foreground transition-transform",
160
- open && "rotate-90",
161
- )}
162
- />
163
- ) : null}
164
- </div>
187
+ </span>
165
188
  </button>
166
189
  </Collapsible.Trigger>
167
190
 
168
191
  {collapsedError && !open ? (
169
- <div className="border-t border-border px-3 py-2 text-xs text-[var(--surface-danger-text)]">
192
+ <div
193
+ className={cn(
194
+ "pb-1.5 pr-2 text-xs leading-snug text-[var(--surface-danger-text)]",
195
+ ERROR_INDENT,
196
+ )}
197
+ >
170
198
  {collapsedError}
171
199
  </div>
172
200
  ) : null}
@@ -7,6 +7,7 @@
7
7
  */
8
8
 
9
9
  import { type ReactNode } from "react";
10
+ import { cn } from "../lib/utils";
10
11
  import { CodeBlock } from "../markdown/code-block";
11
12
  import type { ToolPart, ToolStatus } from "../types/parts";
12
13
  import { InlineToolItem } from "./inline-tool-item";
@@ -129,7 +130,9 @@ export function ToolCallStep({
129
130
  }
130
131
 
131
132
  /**
132
- * ToolCallGroup — groups multiple tool calls under a heading.
133
+ * ToolCallGroup — a tight stack of consecutive tool rows. The title names the
134
+ * group for assistive tech only; the rows themselves are the visible label, so
135
+ * no heading is drawn above them.
133
136
  */
134
137
  export interface ToolCallGroupProps {
135
138
  title?: string;
@@ -139,12 +142,11 @@ export interface ToolCallGroupProps {
139
142
 
140
143
  export function ToolCallGroup({ title, children, className }: ToolCallGroupProps) {
141
144
  return (
142
- <div className={["my-2 space-y-2", className].filter(Boolean).join(" ")}>
143
- {title && (
144
- <div className="mb-1 px-1 text-xs font-medium uppercase tracking-wider text-muted-foreground">
145
- {title}
146
- </div>
147
- )}
145
+ <div
146
+ role="group"
147
+ aria-label={title}
148
+ className={cn("flex flex-col gap-1", className)}
149
+ >
148
150
  {children}
149
151
  </div>
150
152
  );
@@ -0,0 +1,53 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import type { ToolPart } from "../types/parts";
3
+ import { getToolDisplayMetadata } from "./tool-display";
4
+
5
+ function part(tool: string, input: unknown): ToolPart {
6
+ return {
7
+ type: "tool",
8
+ id: `id-${tool}`,
9
+ tool,
10
+ state: { status: "completed", input },
11
+ };
12
+ }
13
+
14
+ describe("getToolDisplayMetadata", () => {
15
+ it.each([
16
+ ["read", { file_path: "/home/u/project/src/app.ts" }, "Read", "/home/u/project/src/app.ts"],
17
+ ["edit", { path: "src/app.ts" }, "Edit", "src/app.ts"],
18
+ ["write", { file: "src/new.ts" }, "Write", "src/new.ts"],
19
+ ["bash", { command: "pnpm test --run" }, "Shell", "pnpm test --run"],
20
+ ["grep", { pattern: "useCallback" }, "Search", "useCallback"],
21
+ ["glob", { pattern: "src/**/*.ts" }, "Find", "src/**/*.ts"],
22
+ ["web_search", { query: "vitest mocking" }, "Web search", "vitest mocking"],
23
+ ["web_fetch", { url: "https://vitest.dev" }, "Fetch", "https://vitest.dev"],
24
+ ["task", { description: "audit routes" }, "Task", "audit routes"],
25
+ ])("%s → bare verb title, subject in the description only", (tool, input, title, description) => {
26
+ const meta = getToolDisplayMetadata(part(tool, input));
27
+ expect(meta.title).toBe(title);
28
+ expect(meta.description).toBe(description);
29
+ expect(meta.title).not.toContain(description);
30
+ });
31
+
32
+ it("keeps the untruncated path and command for consumers that need them", () => {
33
+ const edit = getToolDisplayMetadata(part("edit", { file_path: "/a/b/c/d/e.ts" }));
34
+ expect(edit.targetPath).toBe("/a/b/c/d/e.ts");
35
+ expect(edit.diffFilePath).toBe("/a/b/c/d/e.ts");
36
+
37
+ const long = "x".repeat(80);
38
+ const shell = getToolDisplayMetadata(part("bash", { command: long }));
39
+ expect(shell.commandSnippet).toBe(long);
40
+ expect(shell.description).toHaveLength(60);
41
+ });
42
+
43
+ it("falls back to the verb alone when the input carries no subject", () => {
44
+ expect(getToolDisplayMetadata(part("read", {})).title).toBe("Read");
45
+ expect(getToolDisplayMetadata(part("read", {})).description).toBeUndefined();
46
+ });
47
+
48
+ it("names unknown tools by their tool name", () => {
49
+ const meta = getToolDisplayMetadata(part("custom_probe", { pattern: "p" }));
50
+ expect(meta.title).toBe("custom_probe");
51
+ expect(meta.description).toBe("p");
52
+ });
53
+ });
@@ -43,11 +43,6 @@ function extractFilePath(input: unknown): string | undefined {
43
43
  );
44
44
  }
45
45
 
46
- function cleanPath(path: string): string {
47
- const parts = path.split("/");
48
- return parts.length > 3 ? ".../" + parts.slice(-2).join("/") : path;
49
- }
50
-
51
46
  // ---------------------------------------------------------------------------
52
47
  // Category icons (emoji shorthand used by compact views)
53
48
  // ---------------------------------------------------------------------------
@@ -116,6 +111,12 @@ export function getToolCategory(toolName: string): ToolCategory {
116
111
  // Main metadata resolver
117
112
  // ---------------------------------------------------------------------------
118
113
 
114
+ /**
115
+ * Title is the bare verb ("Read", "Edit", "Shell"); the path, command, pattern
116
+ * or query lives in `description` only, so a row prints its subject once.
117
+ * `targetPath` / `commandSnippet` carry the untruncated value for consumers
118
+ * that need it (diff headers, expanded detail).
119
+ */
119
120
  export function getToolDisplayMetadata(part: ToolPart): ToolDisplayMetadata {
120
121
  const name = normalizeToolName(part.tool);
121
122
  const input = part.state.status !== "pending" ? part.state.input : undefined;
@@ -128,7 +129,7 @@ export function getToolDisplayMetadata(part: ToolPart): ToolDisplayMetadata {
128
129
  case "command":
129
130
  case "execute":
130
131
  return {
131
- title: "Run command",
132
+ title: "Shell",
132
133
  description: command ? truncateCommand(command) : undefined,
133
134
  displayVariant: "command",
134
135
  commandSnippet: command,
@@ -138,7 +139,7 @@ export function getToolDisplayMetadata(part: ToolPart): ToolDisplayMetadata {
138
139
  case "write_file":
139
140
  case "create_file":
140
141
  return {
141
- title: filePath ? `Write ${cleanPath(filePath)}` : "Write file",
142
+ title: "Write",
142
143
  description: filePath,
143
144
  displayVariant: "write-file",
144
145
  targetPath: filePath,
@@ -147,7 +148,7 @@ export function getToolDisplayMetadata(part: ToolPart): ToolDisplayMetadata {
147
148
  case "edit":
148
149
  case "patch":
149
150
  return {
150
- title: filePath ? `Edit ${cleanPath(filePath)}` : "Edit file",
151
+ title: "Edit",
151
152
  description: filePath,
152
153
  hasDiffOutput: true,
153
154
  diffFilePath: filePath,
@@ -159,7 +160,7 @@ export function getToolDisplayMetadata(part: ToolPart): ToolDisplayMetadata {
159
160
  case "read_file":
160
161
  case "cat":
161
162
  return {
162
- title: filePath ? `Read ${cleanPath(filePath)}` : "Read file",
163
+ title: "Read",
163
164
  description: filePath,
164
165
  displayVariant: "read-file",
165
166
  targetPath: filePath,
@@ -170,7 +171,7 @@ export function getToolDisplayMetadata(part: ToolPart): ToolDisplayMetadata {
170
171
  case "rg": {
171
172
  const pattern = extractString(input, "pattern");
172
173
  return {
173
- title: pattern ? `Search: ${pattern}` : "Search",
174
+ title: "Search",
174
175
  description: pattern,
175
176
  displayVariant: "grep",
176
177
  };
@@ -181,7 +182,7 @@ export function getToolDisplayMetadata(part: ToolPart): ToolDisplayMetadata {
181
182
  case "ls": {
182
183
  const pattern = extractString(input, "pattern");
183
184
  return {
184
- title: pattern ? `Find: ${pattern}` : "Find files",
185
+ title: "Find",
185
186
  description: pattern,
186
187
  displayVariant: "glob",
187
188
  };
@@ -193,8 +194,8 @@ export function getToolDisplayMetadata(part: ToolPart): ToolDisplayMetadata {
193
194
  const query =
194
195
  extractString(input, "query") ?? extractString(input, "url");
195
196
  return {
196
- title: query ? `Web: ${truncateCommand(query)}` : "Web search",
197
- description: query,
197
+ title: name === "web_search" ? "Web search" : "Fetch",
198
+ description: query ? truncateCommand(query) : undefined,
198
199
  displayVariant: "web-search",
199
200
  };
200
201
  }
@@ -206,8 +207,8 @@ export function getToolDisplayMetadata(part: ToolPart): ToolDisplayMetadata {
206
207
  extractString(input, "description") ??
207
208
  extractString(input, "prompt");
208
209
  return {
209
- title: desc ? `Task: ${truncateCommand(desc)}` : "Agent task",
210
- description: desc,
210
+ title: "Task",
211
+ description: desc ? truncateCommand(desc) : undefined,
211
212
  };
212
213
  }
213
214
 
File without changes
File without changes