@tangle-network/ui 11.6.0 → 11.8.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 (59) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +43 -0
  3. package/dist/chat.d.ts +18 -8
  4. package/dist/chat.js +9 -7
  5. package/dist/chunk-4CLN43XT.js +45 -0
  6. package/dist/{chunk-AZWDI2JG.js → chunk-565EATZZ.js} +1 -1
  7. package/dist/{chunk-XROQW5J7.js → chunk-BWOUSEHM.js} +1 -1
  8. package/dist/{chunk-J2G7KCYH.js → chunk-KNYSKNZX.js} +113 -69
  9. package/dist/chunk-L57WRIND.js +59 -0
  10. package/dist/{chunk-ULDNFLIM.js → chunk-ND3ZFXQK.js} +11 -58
  11. package/dist/{chunk-AAUNOHVL.js → chunk-NWECTSOH.js} +1 -1
  12. package/dist/{chunk-R5GVQDL3.js → chunk-OXZRCXJK.js} +9 -7
  13. package/dist/{chunk-XPYPZMYT.js → chunk-Q7ZR6LSD.js} +1 -1
  14. package/dist/{chunk-RKC3GXHE.js → chunk-X53RZSX6.js} +99 -104
  15. package/dist/{chunk-PLWRAS5Z.js → chunk-XGOWUDDW.js} +270 -106
  16. package/dist/{chunk-ZRVH3WCA.js → chunk-XU3HB5QL.js} +21 -1
  17. package/dist/chunk-YTO5V6QS.js +19 -0
  18. package/dist/files.d.ts +41 -12
  19. package/dist/files.js +13 -5
  20. package/dist/hooks.js +9 -7
  21. package/dist/index.d.ts +2 -2
  22. package/dist/index.js +37 -24
  23. package/dist/primitives.d.ts +15 -1
  24. package/dist/primitives.js +5 -1
  25. package/dist/run.d.ts +22 -17
  26. package/dist/run.js +11 -8
  27. package/dist/sdk-hooks.js +9 -7
  28. package/dist/tool-previews.js +2 -2
  29. package/dist/utils.d.ts +6 -0
  30. package/dist/utils.js +6 -4
  31. package/package.json +1 -1
  32. package/src/chat/agent-timeline.stories.tsx +163 -10
  33. package/src/chat/agent-timeline.test.tsx +114 -7
  34. package/src/chat/agent-timeline.tsx +120 -158
  35. package/src/chat/chat-container.test.tsx +59 -0
  36. package/src/chat/user-message.test.tsx +33 -0
  37. package/src/chat/user-message.tsx +13 -8
  38. package/src/files/file-artifact-pane.stories.tsx +149 -0
  39. package/src/files/file-artifact-pane.test.tsx +192 -0
  40. package/src/files/file-artifact-pane.tsx +6 -1
  41. package/src/files/file-format.ts +42 -4
  42. package/src/files/file-preview-kind.test.ts +109 -0
  43. package/src/files/file-preview.tsx +315 -132
  44. package/src/files/index.ts +8 -1
  45. package/src/primitives/index.ts +3 -0
  46. package/src/primitives/text-shimmer.test.tsx +34 -0
  47. package/src/primitives/text-shimmer.tsx +82 -0
  48. package/src/run/inline-thinking-item.stories.tsx +2 -2
  49. package/src/run/inline-thinking-item.tsx +2 -2
  50. package/src/run/inline-tool-item.test.tsx +76 -0
  51. package/src/run/inline-tool-item.tsx +9 -7
  52. package/src/run/run-group.tsx +4 -3
  53. package/src/run/run-row-shell.test.tsx +74 -4
  54. package/src/run/run-row-shell.tsx +81 -53
  55. package/src/run/tool-call-step.tsx +9 -7
  56. package/src/utils/tool-display.test.ts +53 -0
  57. package/src/utils/tool-display.ts +16 -15
  58. package/dist/chunk-LQS34IGP.js +0 -0
  59. /package/dist/{chunk-EJ5NBW7V.js → chunk-LDLOJKPC.js} +0 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @tangle-network/ui
2
2
 
3
+ ## 11.8.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 3416c80: `FileArtifactPane` (and `FilePreview`, its body) now previews every file kind a sandbox produces, so a product no longer builds its own image, PDF, CSV, video, audio, or spreadsheet viewers. Pass `blobUrl` (an object URL or remote URL) for binary content and the pane renders an image fit to the pane on a checker ground (click for natural size), a PDF at full pane height, or a video/audio player; pass `content` for CSV and the pane parses quoted commas and line breaks (RFC 4180) into a sticky-header table capped at `CSV_PREVIEW_ROW_LIMIT` (500) rows with a "Showing N of M rows" line; an `.xlsx`/`.xls` file renders a download card with the filename, its size when the new optional `size` prop (bytes) is set, and a Download button wired to `onDownload`. A file with no `blobUrl` and no text content renders one sentence, "This file needs a download link to preview.", with the same Download button. Text, code, JSON, YAML, and markdown render as before. `@tangle-network/ui/files` also exports the pure `resolveFilePreviewKind(filename, mimeType)` helper (`FilePreviewKind`: MIME type wins over extension; anything unmatched is `binary`), `parseCsv`, and `CSV_PREVIEW_ROW_LIMIT`; `detectFileFormat` gains the `video` and `audio` formats and the `avif` image extension.
8
+
9
+ ## 11.7.0
10
+
11
+ ### Minor Changes
12
+
13
+ - 0982a63: Quiet transcript rows: the agent transcript (`AgentTimeline`, `ChatContainer`'s `timeline` presentation) now reads as one column of prose with tool calls as quiet one-liners between the paragraphs. `RunRowShell` (and so `InlineToolItem` / `InlineThinkingItem`) rests with no card chrome — transparent border and fill, a bare 14px glyph, title and description at `text-sm` in `text-muted-foreground` that brighten on hover — and takes the bordered `--md3-surface-container` card only while open; the expand chevron replaces the glyph on hover and open, the success dot is gone (success is silence), the running spinner and live-ticking duration are gone (a running row shimmers its title through the new `TextShimmer` primitive, exported from `/primitives`), and the completed duration shows on hover only. `RunRowStatusDot` still exists and now draws only the error dot; `RunRowShellProps.startTime` is accepted and ignored (deprecated). `getToolDisplayMetadata` titles are the bare verb (`Read`, `Edit`, `Write`, `Shell`, `Search`, `Find`, `Web search`, `Fetch`, `Task`) and the path, command, pattern or query lives in `description` only, so a row no longer prints its subject twice; `targetPath` / `commandSnippet` are unchanged. `AgentTimeline` drops the connector spine, the uppercase "Tool activity" header above grouped calls (the group keeps that name as its `aria-label`), and the per-block timestamp on assistant prose; prose renders at 15px/1.5 with a muted streaming caret, and vertical rhythm now follows the pair of neighbours (`mt-4` at a user turn, `mt-1` inside tool sequences and between prose and tools). `UserMessage` loses its border and shows its timestamp beside the bubble on hover. Consumers that composed `RunRowShell` with a shimmer title of their own get the intended result now that the shell draws no spinner beside it.
14
+
3
15
  ## 11.6.0
4
16
 
5
17
  ### Minor Changes
package/README.md CHANGED
@@ -26,6 +26,49 @@ export function App() {
26
26
  }
27
27
  ```
28
28
 
29
+ ## Styling
30
+
31
+ This package ships no CSS. Its components are Tailwind v4 class names in source, and the build emits JavaScript only, so two pieces of consumer setup carry real weight.
32
+
33
+ ### Scan this package
34
+
35
+ Your Tailwind or UnoCSS build has to read this package's files. Neither scans `node_modules` on its own, so a class that only this package names is never generated. Nothing fails loudly when one is missing: the element still renders, still occupies space and still takes clicks, it just looks inert. A `Switch` with no track colour in either state, a `Tabs` where the selected tab is indistinguishable from the rest, and a `Textarea` collapsed to one row all come out of this.
36
+
37
+ Tailwind v4, in the stylesheet that imports `tailwindcss`. `@source` paths resolve relative to that stylesheet, so adjust the prefix to wherever yours lives:
38
+
39
+ ```css
40
+ /* from src/styles/app.css, with node_modules two levels up */
41
+ @source "../../node_modules/@tangle-network/ui/src/**/*.tsx";
42
+ @source "../../node_modules/@tangle-network/ui/src/**/*.ts";
43
+ ```
44
+
45
+ `sandbox-ui` runs exactly these two globs in `src/styles/globals.css`, alongside `@import "tailwindcss" source(none)` so the scan is the whole of what the bundle compiles from.
46
+
47
+ UnoCSS, in `uno.config.ts`:
48
+
49
+ ```ts
50
+ content: {
51
+ filesystem: ["node_modules/@tangle-network/ui/dist/**/*.js"],
52
+ }
53
+ ```
54
+
55
+ Under pnpm that plain glob can match zero files. `node_modules/@tangle-network/ui` is a symlink into `node_modules/.pnpm`, and UnoCSS's globber does not follow it, which leaves you exactly where you started with no error. Pre-expand the glob with something that does follow symlinks, resolve each hit through `realpath`, and hand UnoCSS concrete paths. `tangle-network/blueprint-agent`'s `apps/web/uno.config.ts` carries a worked version that walks the import graph of the two entries it uses rather than the whole `dist`.
56
+
57
+ Two things to check after wiring this up, because the failure is silent either way. Read the generated CSS for a class this package alone names, rather than eyeballing the app. And assert the scan resolved a non-zero number of files: a scan that silently matches nothing looks identical to no scan at all.
58
+
59
+ One consequence worth knowing. This is a scan, not a manifest, so a consumer emits only the classes the version it installed actually names. Upgrading this package can introduce a class your build has never generated, and it will go missing the same silent way.
60
+
61
+ ### Import the brand stylesheet
62
+
63
+ `@tangle-network/brand` is a peer dependency. It holds the tokens every colour here resolves through, and a few real classes that some components reference by name instead of composing out of utilities. `Badge` with `dot` is the one to know about: it emits `status-dot status-dot-running` and its siblings, which are defined in `@tangle-network/brand/styles/globals.css` and nowhere else. Without that import the dots have no size and no colour.
64
+
65
+ ```css
66
+ @import "@tangle-network/brand/styles";
67
+ @import "tailwindcss";
68
+ ```
69
+
70
+ See [`@tangle-network/brand`'s README](../brand/README.md) for the token-level breakdown, the finer-grained imports, and the fonts, which brand deliberately does not bundle.
71
+
29
72
  ## Subpaths
30
73
 
31
74
  Sixteen named exports: `primitives`, `chat`, `run`, `openui`, `files`, `editor`, `markdown`, `auth`, `hooks`, `sdk-hooks`, `stores`, `types`, `utils`, `tool-previews`, `nav`, `redaction`.
package/dist/chat.d.ts CHANGED
@@ -87,9 +87,11 @@ interface UserMessageProps {
87
87
  actions?: ReactNode;
88
88
  }
89
89
  /**
90
- * The single user message bubble — a quiet, right-aligned bordered bubble on the
91
- * muted surface (no loud fill, no uppercase label). Used by both the run/message
92
- * list (session-model `parts`) and AgentTimeline (direct `content`/`timestamp`).
90
+ * The single user message bubble — a quiet, right-aligned fill on the muted
91
+ * surface (no border, no loud fill, no uppercase label). The timestamp sits
92
+ * beside the bubble and appears on hover, so it costs no height. Used by both
93
+ * the run/message list (session-model `parts`) and AgentTimeline (direct
94
+ * `content`/`timestamp`).
93
95
  */
94
96
  declare const UserMessage: React.MemoExoticComponent<({ message: _message, parts, content, timestamp, actions }: UserMessageProps) => react_jsx_runtime.JSX.Element | null>;
95
97
 
@@ -126,6 +128,8 @@ interface AgentTimelineMessageItem {
126
128
  content: string;
127
129
  toolCalls?: ReactNode;
128
130
  isStreaming?: boolean;
131
+ /** Shown beside a user bubble on hover. Assistant prose carries no
132
+ * timestamp of its own — the turn's user message dates the exchange. */
129
133
  timestamp?: Date;
130
134
  after?: ReactNode;
131
135
  }
@@ -140,6 +144,7 @@ interface AgentTimelineToolItem {
140
144
  interface AgentTimelineToolGroupItem {
141
145
  id: string;
142
146
  kind: "tool_group";
147
+ /** Accessible name of the group. Not drawn — the rows are the label. */
143
148
  title?: string;
144
149
  calls: ToolCallData[];
145
150
  /** Source tool parts, parallel to `calls`. */
@@ -166,6 +171,9 @@ interface AgentTimelineArtifactItem {
166
171
  interface AgentTimelineCustomItem {
167
172
  id: string;
168
173
  kind: "custom";
174
+ /** Laid out as a run row: it takes the same horizontal bleed as tool rows,
175
+ * so a `RunRowShell`-based row (reasoning) aligns its glyph with the prose
176
+ * edge. */
169
177
  content: ReactNode;
170
178
  }
171
179
  type AgentTimelineItem = AgentTimelineMessageItem | AgentTimelineToolItem | AgentTimelineToolGroupItem | AgentTimelineStatusItem | AgentTimelineArtifactItem | AgentTimelineCustomItem;
@@ -177,14 +185,16 @@ interface AgentTimelineProps {
177
185
  /** Optional actions rendered beside each tool item (e.g. "open in artifacts").
178
186
  * Receives the source tool part carried on the item. */
179
187
  renderToolActions?: (part: ToolPart) => ReactNode;
180
- /** When set, collapse the timeline to the first N spine rows behind a
181
- * "Show N more steps" toggle. Omit to always show every row. */
188
+ /** When set, collapse the timeline to the first N step rows (every item
189
+ * except user messages) behind a "Show N more steps" toggle. Omit to always
190
+ * show every row. */
182
191
  collapseAfter?: number;
183
192
  }
184
193
  /**
185
- * AgentTimeline — unified mixed-content timeline for agent-backed sandbox
186
- * sessions. Renders messages, tool steps, status cards, and artifact handoffs in
187
- * a single execution narrative.
194
+ * AgentTimeline — unified mixed-content transcript for agent-backed sandbox
195
+ * sessions. Renders messages, tool rows, status cards, and artifact handoffs
196
+ * as one plain column: prose at the reading size, tool calls as quiet
197
+ * one-liners between the paragraphs, no connector chrome.
188
198
  */
189
199
  declare function AgentTimeline({ items, isThinking, emptyState, className, renderToolActions, collapseAfter, }: AgentTimelineProps): react_jsx_runtime.JSX.Element | null;
190
200
 
package/dist/chat.js CHANGED
@@ -5,16 +5,18 @@ import {
5
5
  MessageList,
6
6
  ThinkingIndicator,
7
7
  UserMessage
8
- } from "./chunk-RKC3GXHE.js";
9
- import "./chunk-AZWDI2JG.js";
10
- import "./chunk-R5GVQDL3.js";
11
- import "./chunk-J2G7KCYH.js";
12
- import "./chunk-ULDNFLIM.js";
13
- import "./chunk-AAUNOHVL.js";
8
+ } from "./chunk-X53RZSX6.js";
9
+ import "./chunk-565EATZZ.js";
10
+ import "./chunk-OXZRCXJK.js";
11
+ import "./chunk-KNYSKNZX.js";
12
+ import "./chunk-ND3ZFXQK.js";
13
+ import "./chunk-NWECTSOH.js";
14
+ import "./chunk-L57WRIND.js";
14
15
  import "./chunk-3M7BPSSW.js";
15
16
  import "./chunk-GYPQXTJU.js";
17
+ import "./chunk-4CLN43XT.js";
18
+ import "./chunk-XU3HB5QL.js";
16
19
  import "./chunk-MKTSMWVD.js";
17
- import "./chunk-ZRVH3WCA.js";
18
20
  import "./chunk-P5F43A65.js";
19
21
  import "./chunk-WUQDUBJG.js";
20
22
  import "./chunk-RQHJBTEU.js";
@@ -0,0 +1,45 @@
1
+ // src/utils/format.ts
2
+ function formatDuration(ms) {
3
+ if (ms < 1e3) return "<1s";
4
+ const seconds = Math.floor(ms / 1e3);
5
+ if (seconds < 60) return `${seconds}s`;
6
+ const minutes = Math.floor(seconds / 60);
7
+ const remaining = seconds % 60;
8
+ return remaining > 0 ? `${minutes}m ${remaining}s` : `${minutes}m`;
9
+ }
10
+ function truncateText(text, max) {
11
+ const cleaned = text.replace(/\s+/g, " ").trim();
12
+ if (cleaned.length <= max) return cleaned;
13
+ return cleaned.slice(0, max).trim() + "...";
14
+ }
15
+ function formatUptime(ms) {
16
+ if (!Number.isFinite(ms) || ms < 0) return "\u2014";
17
+ const totalSeconds = Math.floor(ms / 1e3);
18
+ if (totalSeconds < 60) return `${totalSeconds}s`;
19
+ const minutes = Math.floor(totalSeconds / 60);
20
+ const seconds = totalSeconds % 60;
21
+ if (minutes < 60) return `${minutes}m ${seconds}s`;
22
+ const hours = Math.floor(minutes / 60);
23
+ const remMinutes = minutes % 60;
24
+ if (hours < 24) return `${hours}h ${remMinutes}m`;
25
+ const days = Math.floor(hours / 24);
26
+ const remHours = hours % 24;
27
+ return `${days}d ${remHours}h`;
28
+ }
29
+ function formatBytes(bytes) {
30
+ if (!Number.isFinite(bytes) || bytes < 0) return "\u2014";
31
+ if (bytes < 1024) return `${Math.round(bytes)} B`;
32
+ const kb = bytes / 1024;
33
+ if (kb < 1024) return `${kb < 10 ? kb.toFixed(1) : Math.round(kb)} KB`;
34
+ const mb = kb / 1024;
35
+ if (mb < 1024) return `${mb < 10 ? mb.toFixed(1) : Math.round(mb)} MB`;
36
+ const gb = mb / 1024;
37
+ return `${gb < 10 ? gb.toFixed(2) : gb.toFixed(1)} GB`;
38
+ }
39
+
40
+ export {
41
+ formatDuration,
42
+ truncateText,
43
+ formatUptime,
44
+ formatBytes
45
+ };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  getToolCategory
3
- } from "./chunk-ULDNFLIM.js";
3
+ } from "./chunk-ND3ZFXQK.js";
4
4
 
5
5
  // src/hooks/use-run-groups.ts
6
6
  import { useMemo } from "react";
@@ -11,7 +11,7 @@ import {
11
11
  } from "./chunk-OEX7NZE3.js";
12
12
  import {
13
13
  parseToolEvent
14
- } from "./chunk-XPYPZMYT.js";
14
+ } from "./chunk-Q7ZR6LSD.js";
15
15
 
16
16
  // src/hooks/use-dropdown-menu.ts
17
17
  import { useEffect, useRef, useState } from "react";
@@ -1,9 +1,8 @@
1
1
  import {
2
- formatDuration,
3
2
  getToolCategory,
4
3
  getToolDisplayMetadata,
5
4
  getToolErrorText
6
- } from "./chunk-ULDNFLIM.js";
5
+ } from "./chunk-ND3ZFXQK.js";
7
6
  import {
8
7
  CommandPreview,
9
8
  DiffPreview,
@@ -12,7 +11,13 @@ import {
12
11
  QuestionPreview,
13
12
  WebSearchPreview,
14
13
  WriteFilePreview
15
- } from "./chunk-AAUNOHVL.js";
14
+ } from "./chunk-NWECTSOH.js";
15
+ import {
16
+ TextShimmer
17
+ } from "./chunk-L57WRIND.js";
18
+ import {
19
+ formatDuration
20
+ } from "./chunk-4CLN43XT.js";
16
21
  import {
17
22
  CodeBlock
18
23
  } from "./chunk-WUQDUBJG.js";
@@ -204,34 +209,20 @@ var ExpandedToolDetail = memo(({ part }) => {
204
209
  });
205
210
  ExpandedToolDetail.displayName = "ExpandedToolDetail";
206
211
 
207
- // src/run/run-item-primitives.tsx
208
- import { useEffect as useEffect2, useState as useState2 } from "react";
209
- import { jsx as jsx3 } from "react/jsx-runtime";
210
- function LiveDuration({ startTime }) {
211
- const [elapsed, setElapsed] = useState2(Date.now() - startTime);
212
- useEffect2(() => {
213
- const id = setInterval(() => setElapsed(Date.now() - startTime), 100);
214
- return () => clearInterval(id);
215
- }, [startTime]);
216
- return /* @__PURE__ */ jsx3("span", { className: "text-xs font-mono text-neutral-400 dark:text-neutral-500 tabular-nums", children: formatDuration(elapsed) });
217
- }
218
-
219
212
  // src/run/run-row-shell.tsx
220
- import { useState as useState3 } from "react";
213
+ import { useState as useState2 } from "react";
221
214
  import * as Collapsible from "@radix-ui/react-collapsible";
222
- import { Loader2 as Loader22, ChevronRight } from "lucide-react";
223
- import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
215
+ import { ChevronRight } from "lucide-react";
216
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
224
217
  function RunRowStatusDot({ status }) {
225
- if (status === "running") {
226
- return /* @__PURE__ */ jsx4(Loader22, { className: "h-3 w-3 shrink-0 animate-spin text-[var(--accent-text)]" });
227
- }
228
- if (status === "success") {
229
- return /* @__PURE__ */ jsx4("span", { className: "h-1.5 w-1.5 shrink-0 rounded-full bg-[var(--surface-success-text)]" });
230
- }
231
- if (status === "error") {
232
- return /* @__PURE__ */ jsx4("span", { className: "h-1.5 w-1.5 shrink-0 rounded-full bg-[var(--surface-danger-text)]" });
233
- }
234
- return null;
218
+ if (status !== "error") return null;
219
+ return /* @__PURE__ */ jsx3(
220
+ "span",
221
+ {
222
+ "data-run-row-status": "error",
223
+ className: "h-1.5 w-1.5 shrink-0 rounded-full bg-[var(--surface-danger-text)]"
224
+ }
225
+ );
235
226
  }
236
227
  var SHAPE_CLASS = {
237
228
  single: "rounded-[var(--radius-lg)]",
@@ -239,13 +230,14 @@ var SHAPE_CLASS = {
239
230
  middle: "rounded-[var(--radius-sm)]",
240
231
  last: "rounded-t-[var(--radius-sm)] rounded-b-[var(--radius-lg)]"
241
232
  };
233
+ var HEADER_INSET = "px-2 py-1";
234
+ var ERROR_INDENT = "pl-[2.375rem]";
242
235
  function RunRowShell({
243
236
  icon,
244
237
  title,
245
238
  description,
246
239
  descriptionMono = false,
247
240
  status = "idle",
248
- startTime,
249
241
  durationMs,
250
242
  groupPosition = "single",
251
243
  collapsedError,
@@ -257,7 +249,7 @@ function RunRowShell({
257
249
  contentClassName,
258
250
  children
259
251
  }) {
260
- const [openState, setOpenState] = useState3(defaultOpen);
252
+ const [openState, setOpenState] = useState2(defaultOpen);
261
253
  const isControlled = openProp !== void 0;
262
254
  const open = isControlled ? openProp : openState;
263
255
  const setOpen = (next) => {
@@ -266,62 +258,110 @@ function RunRowShell({
266
258
  };
267
259
  const isRunning = status === "running";
268
260
  const expandable = children != null;
269
- return /* @__PURE__ */ jsx4(Collapsible.Root, { open, onOpenChange: setOpen, children: /* @__PURE__ */ jsxs3("div", { className: "flex items-start gap-2", children: [
261
+ return /* @__PURE__ */ jsx3(Collapsible.Root, { open, onOpenChange: setOpen, children: /* @__PURE__ */ jsxs3("div", { className: "flex items-start gap-2", children: [
270
262
  /* @__PURE__ */ jsxs3(
271
263
  "div",
272
264
  {
265
+ "data-run-row": "",
266
+ "data-state": open ? "open" : "closed",
273
267
  className: cn(
274
- "min-w-0 flex-1 overflow-hidden border transition-colors",
275
- "border-[var(--border-subtle)] bg-[var(--md3-surface-container)]",
276
- expandable && "hover:border-border",
277
- open && "border-border",
268
+ "group min-w-0 flex-1 overflow-hidden border transition-colors",
269
+ open ? "border-border bg-[var(--md3-surface-container)]" : "border-transparent bg-transparent",
270
+ expandable && !open && "hover:bg-[var(--md3-surface-container)]",
278
271
  SHAPE_CLASS[groupPosition],
279
272
  className
280
273
  ),
281
274
  children: [
282
- /* @__PURE__ */ jsx4(Collapsible.Trigger, { asChild: true, disabled: !expandable, children: /* @__PURE__ */ jsxs3(
275
+ /* @__PURE__ */ jsx3(Collapsible.Trigger, { asChild: true, disabled: !expandable, children: /* @__PURE__ */ jsxs3(
283
276
  "button",
284
277
  {
285
278
  type: "button",
286
279
  className: cn(
287
- "flex w-full items-center gap-2.5 px-3 py-2 text-left",
280
+ "flex w-full items-center gap-2 text-left",
281
+ HEADER_INSET,
288
282
  expandable ? "cursor-pointer" : "cursor-default"
289
283
  ),
290
284
  children: [
291
- /* @__PURE__ */ jsx4("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", children: icon }),
292
- /* @__PURE__ */ jsx4("span", { className: "shrink-0 whitespace-nowrap text-xs font-medium text-foreground", children: title }),
293
- description ? /* @__PURE__ */ jsx4(
285
+ /* @__PURE__ */ jsxs3(
286
+ "span",
287
+ {
288
+ "aria-hidden": true,
289
+ className: "flex h-[1.375rem] w-[1.375rem] shrink-0 items-center justify-center text-muted-foreground",
290
+ children: [
291
+ /* @__PURE__ */ jsx3(
292
+ "span",
293
+ {
294
+ "data-run-row-glyph": "",
295
+ className: cn(
296
+ "flex items-center justify-center",
297
+ expandable && "group-hover:hidden",
298
+ open && "hidden"
299
+ ),
300
+ children: icon
301
+ }
302
+ ),
303
+ expandable ? /* @__PURE__ */ jsx3(
304
+ ChevronRight,
305
+ {
306
+ "data-run-row-chevron": "",
307
+ className: cn(
308
+ "hidden h-3.5 w-3.5 transition-transform group-hover:block",
309
+ open && "block rotate-90"
310
+ )
311
+ }
312
+ ) : null
313
+ ]
314
+ }
315
+ ),
316
+ /* @__PURE__ */ jsx3(
294
317
  "span",
295
318
  {
296
319
  className: cn(
297
- "hidden min-w-0 flex-1 truncate text-xs text-muted-foreground sm:inline",
320
+ "shrink-0 whitespace-nowrap text-sm font-normal text-muted-foreground transition-colors group-hover:text-foreground",
321
+ open && "text-foreground"
322
+ ),
323
+ children: title
324
+ }
325
+ ),
326
+ description ? /* @__PURE__ */ jsx3(
327
+ "span",
328
+ {
329
+ className: cn(
330
+ "hidden min-w-0 flex-1 truncate text-sm text-muted-foreground transition-colors group-hover:text-foreground sm:inline",
298
331
  descriptionMono && "font-mono"
299
332
  ),
300
333
  children: description
301
334
  }
302
335
  ) : null,
303
- /* @__PURE__ */ jsxs3("div", { className: "ml-auto flex shrink-0 items-center gap-1.5", children: [
304
- isRunning && startTime != null ? /* @__PURE__ */ jsx4(LiveDuration, { startTime }) : durationMs != null ? /* @__PURE__ */ jsx4("span", { className: "text-[10px] font-mono tabular-nums text-muted-foreground", children: formatDuration(durationMs) }) : null,
305
- /* @__PURE__ */ jsx4(RunRowStatusDot, { status }),
306
- expandable ? /* @__PURE__ */ jsx4(
307
- ChevronRight,
336
+ /* @__PURE__ */ jsxs3("span", { className: "ml-auto flex shrink-0 items-center gap-1.5", children: [
337
+ !isRunning && durationMs != null ? /* @__PURE__ */ jsx3(
338
+ "span",
308
339
  {
309
- className: cn(
310
- "h-3 w-3 shrink-0 text-muted-foreground transition-transform",
311
- open && "rotate-90"
312
- )
340
+ "data-run-row-duration": "",
341
+ className: "font-mono text-[10px] tabular-nums text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100",
342
+ children: formatDuration(durationMs)
313
343
  }
314
- ) : null
344
+ ) : null,
345
+ /* @__PURE__ */ jsx3(RunRowStatusDot, { status })
315
346
  ] })
316
347
  ]
317
348
  }
318
349
  ) }),
319
- collapsedError && !open ? /* @__PURE__ */ jsx4("div", { className: "border-t border-border px-3 py-2 text-xs text-[var(--surface-danger-text)]", children: collapsedError }) : null,
320
- expandable ? /* @__PURE__ */ jsx4(Collapsible.Content, { className: "overflow-hidden data-[state=open]:animate-slideDown data-[state=closed]:animate-slideUp", children: /* @__PURE__ */ jsx4("div", { className: cn("border-t border-border", contentClassName), children }) }) : null
350
+ collapsedError && !open ? /* @__PURE__ */ jsx3(
351
+ "div",
352
+ {
353
+ className: cn(
354
+ "pb-1.5 pr-2 text-xs leading-snug text-[var(--surface-danger-text)]",
355
+ ERROR_INDENT
356
+ ),
357
+ children: collapsedError
358
+ }
359
+ ) : null,
360
+ expandable ? /* @__PURE__ */ jsx3(Collapsible.Content, { className: "overflow-hidden data-[state=open]:animate-slideDown data-[state=closed]:animate-slideUp", children: /* @__PURE__ */ jsx3("div", { className: cn("border-t border-border", contentClassName), children }) }) : null
321
361
  ]
322
362
  }
323
363
  ),
324
- actions ? /* @__PURE__ */ jsx4(
364
+ actions ? /* @__PURE__ */ jsx3(
325
365
  "div",
326
366
  {
327
367
  className: "flex shrink-0 flex-wrap items-center justify-end gap-1.5 pt-1",
@@ -345,7 +385,7 @@ import {
345
385
  ClipboardList,
346
386
  Settings
347
387
  } from "lucide-react";
348
- import { jsx as jsx5 } from "react/jsx-runtime";
388
+ import { jsx as jsx4 } from "react/jsx-runtime";
349
389
  var TOOL_CATEGORY_ICON_MAP = {
350
390
  command: Terminal,
351
391
  write: FileEdit,
@@ -382,22 +422,22 @@ var InlineToolItem = memo2(
382
422
  const durationMs = startTime != null && endTime != null ? endTime - startTime : void 0;
383
423
  const category = getToolCategory(part.tool);
384
424
  const DefaultIcon = TOOL_CATEGORY_ICON_MAP[category] ?? Settings;
385
- return /* @__PURE__ */ jsx5(
425
+ const status = toRowStatus(part.state.status);
426
+ return /* @__PURE__ */ jsx4(
386
427
  RunRowShell,
387
428
  {
388
- icon: /* @__PURE__ */ jsx5(DefaultIcon, { className: "h-3.5 w-3.5" }),
389
- title,
429
+ icon: /* @__PURE__ */ jsx4(DefaultIcon, { className: "h-3.5 w-3.5" }),
430
+ title: status === "running" ? /* @__PURE__ */ jsx4(TextShimmer, { children: title }) : title,
390
431
  description,
391
432
  descriptionMono: true,
392
- status: toRowStatus(part.state.status),
393
- startTime,
433
+ status,
394
434
  durationMs,
395
435
  groupPosition,
396
436
  collapsedError: errorText ?? void 0,
397
437
  actions,
398
438
  className,
399
439
  contentClassName: cn("p-2", contentClassName),
400
- children: renderToolDetail?.(part) ?? /* @__PURE__ */ jsx5(ExpandedToolDetail, { part })
440
+ children: renderToolDetail?.(part) ?? /* @__PURE__ */ jsx4(ExpandedToolDetail, { part })
401
441
  }
402
442
  );
403
443
  }
@@ -405,7 +445,7 @@ var InlineToolItem = memo2(
405
445
  InlineToolItem.displayName = "InlineToolItem";
406
446
 
407
447
  // src/run/tool-call-step.tsx
408
- import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
448
+ import { jsx as jsx5 } from "react/jsx-runtime";
409
449
  var EXT_LANGUAGE = {
410
450
  ts: "typescript",
411
451
  tsx: "typescript",
@@ -468,7 +508,7 @@ function ToolCallStep({
468
508
  }
469
509
  };
470
510
  const lang = inferLanguage(detail, language);
471
- return /* @__PURE__ */ jsx6(
511
+ return /* @__PURE__ */ jsx5(
472
512
  InlineToolItem,
473
513
  {
474
514
  part: resolvedPart,
@@ -476,7 +516,7 @@ function ToolCallStep({
476
516
  description: detail,
477
517
  className,
478
518
  actions,
479
- renderToolDetail: part ? void 0 : output ? () => /* @__PURE__ */ jsx6(
519
+ renderToolDetail: part ? void 0 : output ? () => /* @__PURE__ */ jsx5(
480
520
  CodeBlock,
481
521
  {
482
522
  code: output,
@@ -488,16 +528,20 @@ function ToolCallStep({
488
528
  );
489
529
  }
490
530
  function ToolCallGroup({ title, children, className }) {
491
- return /* @__PURE__ */ jsxs4("div", { className: ["my-2 space-y-2", className].filter(Boolean).join(" "), children: [
492
- title && /* @__PURE__ */ jsx6("div", { className: "mb-1 px-1 text-xs font-medium uppercase tracking-wider text-muted-foreground", children: title }),
493
- children
494
- ] });
531
+ return /* @__PURE__ */ jsx5(
532
+ "div",
533
+ {
534
+ role: "group",
535
+ "aria-label": title,
536
+ className: cn("flex flex-col gap-1", className),
537
+ children
538
+ }
539
+ );
495
540
  }
496
541
 
497
542
  export {
498
543
  ExpandableContent,
499
544
  ExpandedToolDetail,
500
- LiveDuration,
501
545
  RunRowStatusDot,
502
546
  RunRowShell,
503
547
  InlineToolItem,
@@ -0,0 +1,59 @@
1
+ import {
2
+ cn
3
+ } from "./chunk-RQHJBTEU.js";
4
+
5
+ // src/primitives/text-shimmer.tsx
6
+ import { useEffect, useLayoutEffect } from "react";
7
+ import { jsx } from "react/jsx-runtime";
8
+ var CLASS_NAME = "tangle-text-shimmer";
9
+ var STYLE_ID = "tangle-text-shimmer-styles";
10
+ var SHIMMER_CSS = `
11
+ @keyframes ${CLASS_NAME} {
12
+ from { -webkit-mask-position: 150% center; mask-position: 150% center; }
13
+ to { -webkit-mask-position: -50% center; mask-position: -50% center; }
14
+ }
15
+ .${CLASS_NAME} {
16
+ -webkit-mask-image: linear-gradient(90deg, rgba(0,0,0,0.72) 0%, #000 45%, rgba(0,0,0,0.72) 60%);
17
+ mask-image: linear-gradient(90deg, rgba(0,0,0,0.72) 0%, #000 45%, rgba(0,0,0,0.72) 60%);
18
+ -webkit-mask-size: 250% 100%;
19
+ mask-size: 250% 100%;
20
+ animation: ${CLASS_NAME} 1.4s linear infinite;
21
+ }
22
+ @media (prefers-reduced-motion: reduce) {
23
+ .${CLASS_NAME} {
24
+ animation: none;
25
+ -webkit-mask-image: none;
26
+ mask-image: none;
27
+ text-decoration: underline dotted currentColor;
28
+ text-underline-offset: 3px;
29
+ }
30
+ }
31
+ `;
32
+ function ensureShimmerStyles() {
33
+ if (typeof document === "undefined") return;
34
+ if (document.getElementById(STYLE_ID)) return;
35
+ const style = document.createElement("style");
36
+ style.id = STYLE_ID;
37
+ style.textContent = SHIMMER_CSS;
38
+ document.head.appendChild(style);
39
+ }
40
+ var useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
41
+ function TextShimmer({
42
+ children,
43
+ className,
44
+ durationMs = 1400
45
+ }) {
46
+ useIsomorphicLayoutEffect(ensureShimmerStyles, []);
47
+ return /* @__PURE__ */ jsx(
48
+ "span",
49
+ {
50
+ className: cn(CLASS_NAME, "text-foreground", className),
51
+ style: { animationDuration: `${durationMs}ms` },
52
+ children
53
+ }
54
+ );
55
+ }
56
+
57
+ export {
58
+ TextShimmer
59
+ };