@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.
- package/CHANGELOG.md +22 -0
- package/README.md +70 -2
- package/dist/chat.d.ts +18 -8
- package/dist/chat.js +6 -5
- package/dist/{chunk-XROQW5J7.js → chunk-4ENFLPWQ.js} +1 -1
- package/dist/{chunk-XPYPZMYT.js → chunk-6RIX7M27.js} +1 -1
- package/dist/{chunk-R5GVQDL3.js → chunk-CVX53NCC.js} +7 -5
- package/dist/{chunk-ULDNFLIM.js → chunk-EIUE5GV2.js} +10 -14
- package/dist/chunk-L57WRIND.js +59 -0
- package/dist/chunk-LFVF2HFB.js +19 -0
- package/dist/{chunk-7O22XQ4D.js → chunk-PLWRAS5Z.js} +1 -1
- package/dist/{chunk-NZLLDHEG.js → chunk-QGTIBEPX.js} +608 -420
- package/dist/{chunk-AZWDI2JG.js → chunk-UYTRNR7Z.js} +1 -1
- package/dist/{chunk-RKC3GXHE.js → chunk-VDCZHOOO.js} +99 -104
- package/dist/{chunk-J2G7KCYH.js → chunk-XT4JMYVB.js} +109 -67
- package/dist/{document-editor-pane-MX3FIZSE.js → document-editor-pane-A3JURPW5.js} +1 -1
- package/dist/{document-editor-pane-C3cYEDWZ.d.ts → document-editor-pane-IAEQn_Dl.d.ts} +8 -4
- package/dist/editor.d.ts +5 -4
- package/dist/editor.js +1 -1
- package/dist/files.d.ts +1 -1
- package/dist/files.js +1 -1
- package/dist/hooks.js +6 -5
- package/dist/index.d.ts +2 -2
- package/dist/index.js +16 -11
- package/dist/primitives.d.ts +15 -1
- package/dist/primitives.js +5 -1
- package/dist/run.d.ts +22 -17
- package/dist/run.js +8 -6
- package/dist/sdk-hooks.js +6 -5
- package/dist/utils.d.ts +6 -0
- package/dist/utils.js +1 -1
- package/package.json +1 -1
- package/src/chat/agent-timeline.stories.tsx +163 -10
- package/src/chat/agent-timeline.test.tsx +114 -7
- package/src/chat/agent-timeline.tsx +120 -158
- package/src/chat/chat-container.test.tsx +59 -0
- package/src/chat/user-message.test.tsx +33 -0
- package/src/chat/user-message.tsx +13 -8
- package/src/editor/document-editor-pane.test.tsx +58 -0
- package/src/editor/editor-lazy.test.tsx +102 -0
- package/src/editor/editor-lazy.ts +40 -0
- package/src/editor/editor-loading.tsx +25 -0
- package/src/editor/editor-peers.test.ts +250 -0
- package/src/editor/editor-peers.ts +272 -0
- package/src/editor/editor-provider.test.tsx +226 -0
- package/src/editor/editor-provider.tsx +301 -265
- package/src/editor/editor-toolbar.tsx +1 -1
- package/src/editor/markdown-document-editor.test.tsx +121 -0
- package/src/editor/markdown-document-editor.tsx +114 -82
- package/src/editor/tiptap-editor.test.tsx +228 -0
- package/src/editor/tiptap-editor.tsx +162 -130
- package/src/primitives/index.ts +3 -0
- package/src/primitives/text-shimmer.test.tsx +34 -0
- package/src/primitives/text-shimmer.tsx +82 -0
- package/src/run/inline-thinking-item.stories.tsx +2 -2
- package/src/run/inline-thinking-item.tsx +2 -2
- package/src/run/inline-tool-item.test.tsx +76 -0
- package/src/run/inline-tool-item.tsx +9 -7
- package/src/run/run-group.tsx +4 -3
- package/src/run/run-row-shell.test.tsx +74 -4
- package/src/run/run-row-shell.tsx +81 -53
- package/src/run/tool-call-step.tsx +9 -7
- package/src/utils/tool-display.test.ts +53 -0
- package/src/utils/tool-display.ts +16 -15
- package/dist/chunk-LQS34IGP.js +0 -0
- /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(
|
|
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("
|
|
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")).
|
|
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
|
-
|
|
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 {
|
|
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
|
|
12
|
-
* a
|
|
13
|
-
* —
|
|
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
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
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
|
|
37
|
-
* the shell
|
|
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
|
-
/**
|
|
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
|
|
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).
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
|
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
|
|
130
|
-
|
|
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
|
|
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-
|
|
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
|
-
<
|
|
148
|
-
{isRunning &&
|
|
149
|
-
<
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
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
|
|
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 —
|
|
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
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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: "
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
210
|
-
description: desc,
|
|
210
|
+
title: "Task",
|
|
211
|
+
description: desc ? truncateCommand(desc) : undefined,
|
|
211
212
|
};
|
|
212
213
|
}
|
|
213
214
|
|
package/dist/chunk-LQS34IGP.js
DELETED
|
File without changes
|
|
File without changes
|