@tangle-network/ui 11.50.3 → 11.51.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 CHANGED
@@ -1,5 +1,15 @@
1
1
  # @tangle-network/ui
2
2
 
3
+ ## 11.51.0
4
+
5
+ ### Minor Changes
6
+
7
+ - ee52171: AgentTimeline folds a run of steps into one row: a `fold` item names what its tool calls did ("Read 6 files · Ran 2 commands", `foldSummary`) and opens to the rows; a failed call keeps the row in the failure ink with its error under it. A user bubble can fold a long message to its first `collapsedLines` lines, with a fade and a "Show more" inside the bubble. The bubble's hover time reads at 14px.
8
+
9
+ ### Patch Changes
10
+
11
+ - d5798a3: FileTabs tells two open files with one name apart by the nearest folder that differs ("CHECK.md · route-a-lead-2"), and an inactive tab keeps a tab's slot with a 1px divider. A SegmentedControl's count reads at 14px in tabular figures.
12
+
3
13
  ## 11.50.3
4
14
 
5
15
  ### Patch Changes
@@ -11,6 +11,9 @@ export interface AgentTimelineMessageItem {
11
11
  content: string;
12
12
  toolCalls?: ReactNode;
13
13
  isStreaming?: boolean;
14
+ /** A user bubble longer than this many lines shows them with a fade and a
15
+ * "Show more" inside the bubble; omit to show the whole message. */
16
+ collapsedLines?: number;
14
17
  /** Shown beside a user bubble on hover. Assistant prose carries no
15
18
  * timestamp of its own — the turn's user message dates the exchange. */
16
19
  timestamp?: Date;
@@ -39,6 +42,22 @@ export interface AgentTimelineToolGroupItem {
39
42
  /** Source tool parts, parallel to `calls`. */
40
43
  parts?: ToolPart[];
41
44
  }
45
+ /**
46
+ * A run of steps folded into one quiet row that names what they did ("Read 6
47
+ * files · Ran 2 commands") and opens to the rows themselves, as an agent app
48
+ * folds a settled multi-step turn. A failed call keeps the fold in the failure
49
+ * ink with its error under the row, so a failure never hides behind it.
50
+ */
51
+ export interface AgentTimelineFoldItem {
52
+ id: string;
53
+ kind: "fold";
54
+ /** The folded rows in order: tool calls, reasoning and other run rows. */
55
+ items: AgentTimelineItem[];
56
+ /** The row's words; by default what its tool calls did (`foldSummary`). */
57
+ summary?: string;
58
+ /** Start open, e.g. when a link points at one of its steps. */
59
+ defaultOpen?: boolean;
60
+ }
42
61
  export interface AgentTimelineStatusItem {
43
62
  id: string;
44
63
  kind: "status";
@@ -65,7 +84,7 @@ export interface AgentTimelineCustomItem {
65
84
  * edge. */
66
85
  content: ReactNode;
67
86
  }
68
- export type AgentTimelineItem = AgentTimelineMessageItem | AgentTimelineToolItem | AgentTimelineToolGroupItem | AgentTimelineStatusItem | AgentTimelineArtifactItem | AgentTimelineCustomItem;
87
+ export type AgentTimelineItem = AgentTimelineMessageItem | AgentTimelineToolItem | AgentTimelineToolGroupItem | AgentTimelineFoldItem | AgentTimelineStatusItem | AgentTimelineArtifactItem | AgentTimelineCustomItem;
69
88
  export interface AgentTimelineProps {
70
89
  items: AgentTimelineItem[];
71
90
  isThinking?: boolean;
@@ -93,6 +112,12 @@ export interface AgentTimelineProps {
93
112
  export type AgentTimelineStepKind = "user" | "prose" | "tool" | "card";
94
113
  /** What a row is, for the gap above the next one. */
95
114
  export declare function agentTimelineStepKind(item: AgentTimelineItem): AgentTimelineStepKind;
115
+ /**
116
+ * What a fold's tool calls did, by verb in the order they first ran:
117
+ * "Read 6 files · Ran 2 commands". A fold without tool calls reads by its
118
+ * row count.
119
+ */
120
+ export declare function foldSummary(items: readonly AgentTimelineItem[]): string;
96
121
  /** The transcript column every timeline sits in: centred, at the reading width. */
97
122
  export declare function AgentTimelineColumn({ className, children }: {
98
123
  className?: string;
@@ -1,6 +1,8 @@
1
1
  import { focusRing } from "../lib/focus.js";
2
2
  import { cn } from "../lib/utils.js";
3
+ import { TextShimmer } from "../primitives/text-shimmer.js";
3
4
  import { statusMeta } from "../run/status-meta.js";
5
+ import { RunRowShell } from "../run/run-row-shell.js";
4
6
  import { Markdown } from "../markdown/markdown.js";
5
7
  import "./transcript-text.js";
6
8
  import { MessageAuthor } from "./message-author.js";
@@ -9,7 +11,7 @@ import { ThinkingIndicator } from "./thinking-indicator.js";
9
11
  import { ToolCallGroup, ToolCallStep } from "../run/tool-call-step.js";
10
12
  import { useState } from "react";
11
13
  import { jsx, jsxs } from "react/jsx-runtime";
12
- import { AlertTriangle, CircleDot, FileText, Info } from "lucide-react";
14
+ import { AlertTriangle, CircleDot, FileText, Info, Layers } from "lucide-react";
13
15
  // src/chat/agent-timeline.tsx
14
16
  const TONE_STYLES = {
15
17
  default: {
@@ -153,6 +155,71 @@ function ArtifactCard({ item }) {
153
155
  children: content
154
156
  });
155
157
  }
158
+ const FOLD_VERBS = {
159
+ read: (n) => `Read ${n} ${n === 1 ? "file" : "files"}`,
160
+ write: (n) => `Wrote ${n} ${n === 1 ? "file" : "files"}`,
161
+ edit: (n) => `Edited ${n} ${n === 1 ? "file" : "files"}`,
162
+ bash: (n) => `Ran ${n} ${n === 1 ? "command" : "commands"}`,
163
+ grep: (n) => `Searched ${n} ${n === 1 ? "time" : "times"}`,
164
+ glob: (n) => `Matched ${n} ${n === 1 ? "pattern" : "patterns"}`,
165
+ list: (n) => `Listed ${n} ${n === 1 ? "folder" : "folders"}`,
166
+ download: (n) => `Downloaded ${n} ${n === 1 ? "file" : "files"}`,
167
+ inspect: (n) => `Inspected ${n} ${n === 1 ? "item" : "items"}`,
168
+ audit: (n) => `Audited ${n} ${n === 1 ? "item" : "items"}`,
169
+ unknown: (n) => `Used ${n} ${n === 1 ? "tool" : "tools"}`
170
+ };
171
+ /** The tool calls inside a fold, in order (a tool group's calls included). */
172
+ function foldCalls(items) {
173
+ return items.flatMap((item) => item.kind === "tool" ? [{
174
+ call: item.call,
175
+ part: item.part
176
+ }] : item.kind === "tool_group" ? item.calls.map((call, i) => ({
177
+ call,
178
+ part: item.parts?.[i]
179
+ })) : item.kind === "fold" ? foldCalls(item.items) : []);
180
+ }
181
+ /**
182
+ * What a fold's tool calls did, by verb in the order they first ran:
183
+ * "Read 6 files · Ran 2 commands". A fold without tool calls reads by its
184
+ * row count.
185
+ */
186
+ function foldSummary(items) {
187
+ const counts = /* @__PURE__ */ new Map();
188
+ for (const { call } of foldCalls(items)) counts.set(call.type, (counts.get(call.type) ?? 0) + 1);
189
+ if (!counts.size) return `${items.length} ${items.length === 1 ? "step" : "steps"}`;
190
+ const phrases = [...counts].map(([type, n]) => FOLD_VERBS[type](n));
191
+ return phrases.length > 3 ? `${phrases.slice(0, 3).join(" · ")} · ${phrases.length - 3} more` : phrases.join(" · ");
192
+ }
193
+ function FoldStep({ item, renderToolActions, viewerId }) {
194
+ const calls = foldCalls(item.items);
195
+ const failed = calls.filter(({ call }) => call.status === "error");
196
+ const running = calls.some(({ call }) => call.status === "running");
197
+ const firstError = failed.map(({ part }) => part?.state.status === "error" ? part.state.error : void 0).find(Boolean);
198
+ const total = calls.reduce((sum, { call }) => sum + (call.duration ?? 0), 0);
199
+ const title = item.summary ?? foldSummary(item.items);
200
+ const targets = [...new Set(calls.map(({ call }) => call.detail).filter((detail) => Boolean(detail)))];
201
+ return /* @__PURE__ */ jsx(RunRowShell, {
202
+ icon: /* @__PURE__ */ jsx(Layers, { className: "h-3.5 w-3.5" }),
203
+ title: running ? /* @__PURE__ */ jsx(TextShimmer, { children: title }) : title,
204
+ description: targets.join(", ") || void 0,
205
+ descriptionMono: true,
206
+ status: failed.length ? "error" : running ? "running" : "success",
207
+ durationMs: total > 0 && !running ? total : void 0,
208
+ collapsedError: failed.length ? `${failed.length} failed${firstError ? `: ${firstError}` : ""}` : void 0,
209
+ defaultOpen: item.defaultOpen,
210
+ contentClassName: "px-3 py-1.5",
211
+ children: /* @__PURE__ */ jsx("div", {
212
+ "data-timeline-fold": "",
213
+ className: "flex flex-col",
214
+ children: item.items.map((inner, index) => /* @__PURE__ */ jsx(AgentTimelineStep, {
215
+ item: inner,
216
+ previous: index > 0 ? agentTimelineStepKind(item.items[index - 1]) : void 0,
217
+ renderToolActions,
218
+ viewerId
219
+ }, inner.id))
220
+ })
221
+ });
222
+ }
156
223
  /** The transcript column every timeline sits in: centred, at the reading width. */
157
224
  function AgentTimelineColumn({ className, children }) {
158
225
  return /* @__PURE__ */ jsx("div", {
@@ -183,7 +250,8 @@ function TimelineItem({ item, headed, renderToolActions, viewerId }) {
183
250
  content: item.content,
184
251
  timestamp: item.timestamp,
185
252
  author: item.author,
186
- viewerId
253
+ viewerId,
254
+ collapsedLines: item.collapsedLines
187
255
  }) : /* @__PURE__ */ jsx(AssistantMessage, {
188
256
  item,
189
257
  author: headed ? item.author : void 0
@@ -214,6 +282,11 @@ function TimelineItem({ item, headed, renderToolActions, viewerId }) {
214
282
  }, call.id);
215
283
  })
216
284
  });
285
+ if (item.kind === "fold") return /* @__PURE__ */ jsx(FoldStep, {
286
+ item,
287
+ renderToolActions,
288
+ viewerId
289
+ });
217
290
  if (item.kind === "status") return /* @__PURE__ */ jsx(StatusCard, { item });
218
291
  if (item.kind === "artifact") return /* @__PURE__ */ jsx(ArtifactCard, { item });
219
292
  return item.content;
@@ -280,4 +353,4 @@ function AgentTimeline({ items, isThinking, emptyState, className, renderToolAct
280
353
  }) : null]
281
354
  });
282
355
  }
283
- export { AgentTimeline, AgentTimelineColumn, AgentTimelineStep, agentTimelineStepKind };
356
+ export { AgentTimeline, AgentTimelineColumn, AgentTimelineStep, agentTimelineStepKind, foldSummary };
@@ -19,9 +19,14 @@ export interface UserMessageProps {
19
19
  * someone else's: it sits at the start, on a card, under its author.
20
20
  */
21
21
  viewerId?: string;
22
+ /**
23
+ * Show a long message's first lines with a fade and a "Show more" inside the
24
+ * bubble, which opens it in place. Omit to show the whole message.
25
+ */
26
+ collapsedLines?: number;
22
27
  }
23
28
  /**
24
29
  * User speech retains the inverse-theme bubble and direct-content contract.
25
30
  * Application notes interrupt the bubble in source order, outside its attribution.
26
31
  */
27
- export declare const UserMessage: import("react").MemoExoticComponent<({ message: _message, parts, content, timestamp, actions, author, viewerId }: UserMessageProps) => import("react").JSX.Element | null>;
32
+ export declare const UserMessage: import("react").MemoExoticComponent<({ message: _message, parts, content, timestamp, actions, author, viewerId, collapsedLines }: UserMessageProps) => import("react").JSX.Element | null>;
@@ -1,8 +1,8 @@
1
1
  import { SyntheticText, userTextSegments } from "./synthetic-text.js";
2
2
  import { TRANSCRIPT_TEXT } from "./transcript-text.js";
3
3
  import { MessageAuthor, isViewerMessage } from "./message-author.js";
4
- import { memo } from "react";
5
- import { jsx, jsxs } from "react/jsx-runtime";
4
+ import { memo, useLayoutEffect, useRef, useState } from "react";
5
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
6
6
  // src/chat/user-message.tsx
7
7
  function formatTime(date) {
8
8
  return date.toLocaleTimeString(void 0, {
@@ -16,7 +16,51 @@ function MessageActions({ children, align = "end" }) {
16
16
  children
17
17
  });
18
18
  }
19
- function UserBubble({ text, timestamp, actions }) {
19
+ /**
20
+ * A message body that can fold to its first `lines` lines: the last shown line
21
+ * fades out and a "Show more" inside the bubble opens the rest in place. The
22
+ * button appears only when the text overflows.
23
+ */
24
+ function BubbleText({ text, lines }) {
25
+ const body = useRef(null);
26
+ const [open, setOpen] = useState(false);
27
+ const [over, setOver] = useState(false);
28
+ useLayoutEffect(() => {
29
+ const element = body.current;
30
+ if (!element || !lines || open) return;
31
+ const measure = () => setOver(element.scrollHeight > element.clientHeight + 1);
32
+ measure();
33
+ if (typeof ResizeObserver === "undefined") return;
34
+ const observer = new ResizeObserver(measure);
35
+ observer.observe(element);
36
+ return () => observer.disconnect();
37
+ }, [
38
+ lines,
39
+ open,
40
+ text
41
+ ]);
42
+ const folded = Boolean(lines) && !open;
43
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("div", {
44
+ ref: body,
45
+ className: `break-words whitespace-pre-wrap ${TRANSCRIPT_TEXT}`,
46
+ style: folded ? {
47
+ maxHeight: `${lines}lh`,
48
+ overflow: "hidden",
49
+ ...over ? {
50
+ maskImage: "linear-gradient(to bottom, #000 calc(100% - 1.5lh), transparent)",
51
+ WebkitMaskImage: "linear-gradient(to bottom, #000 calc(100% - 1.5lh), transparent)"
52
+ } : {}
53
+ } : void 0,
54
+ children: text
55
+ }), lines && (over || open) ? /* @__PURE__ */ jsx("button", {
56
+ type: "button",
57
+ "aria-expanded": open,
58
+ onClick: () => setOpen(!open),
59
+ className: "mt-1.5 text-sm font-medium underline-offset-2 opacity-75 transition-opacity hover:underline hover:opacity-100",
60
+ children: open ? "Show less" : "Show more"
61
+ }) : null] });
62
+ }
63
+ function UserBubble({ text, timestamp, actions, lines }) {
20
64
  return /* @__PURE__ */ jsx("div", {
21
65
  className: "flex justify-end",
22
66
  children: /* @__PURE__ */ jsxs("div", {
@@ -25,11 +69,11 @@ function UserBubble({ text, timestamp, actions }) {
25
69
  className: "relative w-full min-w-0 rounded-2xl bg-foreground px-4 py-3 text-background",
26
70
  children: [timestamp ? /* @__PURE__ */ jsx("span", {
27
71
  "data-user-message-time": "",
28
- className: "pointer-events-none absolute right-full top-1/2 mr-3 -translate-y-1/2 whitespace-nowrap text-[length:var(--font-size-xs)] text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100",
72
+ className: "pointer-events-none absolute right-full top-1/2 mr-3 -translate-y-1/2 whitespace-nowrap text-sm text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100",
29
73
  children: formatTime(timestamp)
30
- }) : null, /* @__PURE__ */ jsx("div", {
31
- className: `break-words whitespace-pre-wrap ${TRANSCRIPT_TEXT}`,
32
- children: text
74
+ }) : null, /* @__PURE__ */ jsx(BubbleText, {
75
+ text,
76
+ lines
33
77
  })]
34
78
  }), actions ? /* @__PURE__ */ jsx(MessageActions, { children: actions }) : null]
35
79
  })
@@ -40,7 +84,7 @@ function UserBubble({ text, timestamp, actions }) {
40
84
  * than the reader's inverse fill, so "mine" and "theirs" differ before the
41
85
  * names are read. The timestamp is revealed on the bubble's far side.
42
86
  */
43
- function OtherPersonBubble({ text, timestamp, actions, author }) {
87
+ function OtherPersonBubble({ text, timestamp, actions, author, lines }) {
44
88
  return /* @__PURE__ */ jsx("div", {
45
89
  className: "flex justify-start",
46
90
  children: /* @__PURE__ */ jsxs("div", {
@@ -51,11 +95,11 @@ function OtherPersonBubble({ text, timestamp, actions, author }) {
51
95
  className: "relative min-w-0 max-w-full rounded-2xl border border-border bg-card px-4 py-3 text-foreground",
52
96
  children: [timestamp ? /* @__PURE__ */ jsx("span", {
53
97
  "data-user-message-time": "",
54
- className: "pointer-events-none absolute left-full top-1/2 ml-3 -translate-y-1/2 whitespace-nowrap text-[length:var(--font-size-xs)] text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100",
98
+ className: "pointer-events-none absolute left-full top-1/2 ml-3 -translate-y-1/2 whitespace-nowrap text-sm text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100",
55
99
  children: formatTime(timestamp)
56
- }) : null, /* @__PURE__ */ jsx("div", {
57
- className: `break-words whitespace-pre-wrap ${TRANSCRIPT_TEXT}`,
58
- children: text
100
+ }) : null, /* @__PURE__ */ jsx(BubbleText, {
101
+ text,
102
+ lines
59
103
  })]
60
104
  }),
61
105
  actions ? /* @__PURE__ */ jsx(MessageActions, {
@@ -70,17 +114,19 @@ function OtherPersonBubble({ text, timestamp, actions, author }) {
70
114
  * User speech retains the inverse-theme bubble and direct-content contract.
71
115
  * Application notes interrupt the bubble in source order, outside its attribution.
72
116
  */
73
- const UserMessage = memo(({ message: _message, parts, content, timestamp, actions, author, viewerId }) => {
117
+ const UserMessage = memo(({ message: _message, parts, content, timestamp, actions, author, viewerId, collapsedLines }) => {
74
118
  const other = isViewerMessage(author, viewerId) ? void 0 : author;
75
119
  const bubble = (text, bubbleActions, headed = true, key) => other ? /* @__PURE__ */ jsx(OtherPersonBubble, {
76
120
  text,
77
121
  timestamp,
78
122
  actions: bubbleActions,
79
- author: headed ? other : void 0
123
+ author: headed ? other : void 0,
124
+ lines: collapsedLines
80
125
  }, key) : /* @__PURE__ */ jsx(UserBubble, {
81
126
  text,
82
127
  timestamp,
83
- actions: bubbleActions
128
+ actions: bubbleActions,
129
+ lines: collapsedLines
84
130
  }, key);
85
131
  if (content != null) return content.trim() ? bubble(content, actions) : null;
86
132
  const segments = userTextSegments(parts ?? []);
package/dist/chat.d.ts CHANGED
@@ -6,6 +6,6 @@ import { ChatMessage, ChatMessageProps, MessageRole } from "./chat/chat-message.
6
6
  import { MessageAuthor, MessageAuthorProps, isViewerMessage } from "./chat/message-author.js";
7
7
  import { Speaker, SpeakerHeader, SpeakerHeaderProps, SpeakerPart, SpeakerSection, SpeakerState, partitionBySpeaker } from "./chat/speaker.js";
8
8
  import { ThinkingIndicator, ThinkingIndicatorProps } from "./chat/thinking-indicator.js";
9
- import { AgentTimeline, AgentTimelineArtifactItem, AgentTimelineColumn, AgentTimelineCustomItem, AgentTimelineItem, AgentTimelineMessageItem, AgentTimelineProps, AgentTimelineStatusItem, AgentTimelineStep, AgentTimelineStepKind, AgentTimelineStepProps, AgentTimelineTone, AgentTimelineToolGroupItem, AgentTimelineToolItem, agentTimelineStepKind } from "./chat/agent-timeline.js";
9
+ import { AgentTimeline, AgentTimelineArtifactItem, AgentTimelineColumn, AgentTimelineCustomItem, AgentTimelineFoldItem, AgentTimelineItem, AgentTimelineMessageItem, AgentTimelineProps, AgentTimelineStatusItem, AgentTimelineStep, AgentTimelineStepKind, AgentTimelineStepProps, AgentTimelineTone, AgentTimelineToolGroupItem, AgentTimelineToolItem, agentTimelineStepKind, foldSummary } from "./chat/agent-timeline.js";
10
10
  import { TRANSCRIPT_TEXT } from "./chat/transcript-text.js";
11
- export { AgentTimeline, type AgentTimelineArtifactItem, AgentTimelineColumn, type AgentTimelineCustomItem, type AgentTimelineItem, type AgentTimelineMessageItem, type AgentTimelineProps, type AgentTimelineStatusItem, AgentTimelineStep, type AgentTimelineStepKind, type AgentTimelineStepProps, type AgentTimelineTone, type AgentTimelineToolGroupItem, type AgentTimelineToolItem, type ChatAuthor, ChatContainer, type ChatContainerProps, ChatMessage, type ChatMessageProps, MessageAuthor, type MessageAuthorProps, MessageList, type MessageListProps, type MessageRole, type Speaker, SpeakerHeader, type SpeakerHeaderProps, type SpeakerPart, type SpeakerSection, type SpeakerState, TRANSCRIPT_TEXT, ThinkingIndicator, type ThinkingIndicatorProps, UserMessage, type UserMessageProps, agentTimelineStepKind, isViewerMessage, partitionBySpeaker };
11
+ export { AgentTimeline, type AgentTimelineArtifactItem, AgentTimelineColumn, type AgentTimelineCustomItem, type AgentTimelineFoldItem, type AgentTimelineItem, type AgentTimelineMessageItem, type AgentTimelineProps, type AgentTimelineStatusItem, AgentTimelineStep, type AgentTimelineStepKind, type AgentTimelineStepProps, type AgentTimelineTone, type AgentTimelineToolGroupItem, type AgentTimelineToolItem, type ChatAuthor, ChatContainer, type ChatContainerProps, ChatMessage, type ChatMessageProps, MessageAuthor, type MessageAuthorProps, MessageList, type MessageListProps, type MessageRole, type Speaker, SpeakerHeader, type SpeakerHeaderProps, type SpeakerPart, type SpeakerSection, type SpeakerState, TRANSCRIPT_TEXT, ThinkingIndicator, type ThinkingIndicatorProps, UserMessage, type UserMessageProps, agentTimelineStepKind, foldSummary, isViewerMessage, partitionBySpeaker };
package/dist/chat.js CHANGED
@@ -3,8 +3,8 @@ import { MessageAuthor, isViewerMessage } from "./chat/message-author.js";
3
3
  import { UserMessage } from "./chat/user-message.js";
4
4
  import { MessageList } from "./chat/message-list.js";
5
5
  import { ThinkingIndicator } from "./chat/thinking-indicator.js";
6
- import { AgentTimeline, AgentTimelineColumn, AgentTimelineStep, agentTimelineStepKind } from "./chat/agent-timeline.js";
6
+ import { AgentTimeline, AgentTimelineColumn, AgentTimelineStep, agentTimelineStepKind, foldSummary } from "./chat/agent-timeline.js";
7
7
  import { ChatContainer } from "./chat/chat-container.js";
8
8
  import { ChatMessage } from "./chat/chat-message.js";
9
9
  import { SpeakerHeader, partitionBySpeaker } from "./chat/speaker.js";
10
- export { AgentTimeline, AgentTimelineColumn, AgentTimelineStep, ChatContainer, ChatMessage, MessageAuthor, MessageList, SpeakerHeader, TRANSCRIPT_TEXT, ThinkingIndicator, UserMessage, agentTimelineStepKind, isViewerMessage, partitionBySpeaker };
10
+ export { AgentTimeline, AgentTimelineColumn, AgentTimelineStep, ChatContainer, ChatMessage, MessageAuthor, MessageList, SpeakerHeader, TRANSCRIPT_TEXT, ThinkingIndicator, UserMessage, agentTimelineStepKind, foldSummary, isViewerMessage, partitionBySpeaker };
@@ -18,6 +18,26 @@ function getTabIcon(name) {
18
18
  if (["csv", "xlsx"].includes(ext)) return FileSpreadsheet;
19
19
  return FileCode;
20
20
  }
21
+ /**
22
+ * Each tab's folder hint: two open files with one name ("CHECK.md" twice) each show the nearest parent folder that
23
+ * tells them apart, as an editor does ("CHECK.md · route-a-lead-2"). A name no other tab shares shows none.
24
+ */
25
+ function folderHints(tabs) {
26
+ const hints = /* @__PURE__ */ new Map();
27
+ const byName = /* @__PURE__ */ new Map();
28
+ for (const tab of tabs) byName.set(tab.name, [...byName.get(tab.name) ?? [], tab]);
29
+ for (const same of byName.values()) {
30
+ if (same.length < 2) continue;
31
+ const parents = same.map((tab) => tab.path.replace(/\/+$/, "").split("/").slice(0, -1).reverse());
32
+ same.forEach((tab, i) => {
33
+ const own = parents[i];
34
+ const depth = own.findIndex((folder, d) => parents.every((other, j) => j === i || other[d] !== folder));
35
+ const hint = own[depth >= 0 ? depth : 0];
36
+ if (hint) hints.set(tab.id, hint);
37
+ });
38
+ }
39
+ return hints;
40
+ }
21
41
  /** A long name keeps its extension and the characters before it after the ellipsis. */
22
42
  function splitName(name) {
23
43
  if (name.length <= 18) return [name, ""];
@@ -33,6 +53,7 @@ function FileTabs({ tabs, activeId, onSelect, onClose, label = "Open files", cla
33
53
  if (strip && tab) strip.scrollLeft = Math.min(tab.offsetLeft, Math.max(strip.scrollLeft, tab.offsetLeft + tab.offsetWidth - strip.clientWidth));
34
54
  }, [activeId, tabs.length]);
35
55
  if (tabs.length === 0) return null;
56
+ const hints = folderHints(tabs);
36
57
  const tabButtons = () => [...stripRef.current?.querySelectorAll("[role=\"tab\"]") ?? []];
37
58
  const onKeyDown = (event) => {
38
59
  const at = tabButtons().indexOf(event.target);
@@ -59,18 +80,20 @@ function FileTabs({ tabs, activeId, onSelect, onClose, label = "Open files", cla
59
80
  "aria-label": label,
60
81
  onKeyDown,
61
82
  className: cn("relative flex h-10 min-w-0 items-end gap-1 overflow-x-auto px-2 shadow-[inset_0_-1px_0_hsl(var(--border))] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden", className),
62
- children: tabs.map((tab) => {
83
+ children: tabs.map((tab, index) => {
63
84
  const isActive = tab.id === activeId;
85
+ const divided = !isActive && tabs[index + 1] !== void 0 && tabs[index + 1].id !== activeId;
86
+ const hint = hints.get(tab.id);
64
87
  const Icon = getTabIcon(tab.name);
65
88
  const [head, tail] = splitName(tab.name);
66
89
  return /* @__PURE__ */ jsxs("div", {
67
90
  title: tab.path,
68
- className: cn("group relative flex h-[calc(100%-0.375rem)] max-w-72 shrink-0 items-center rounded-t-lg border border-b-0 text-sm", controlMotion, isActive ? "border-border bg-card text-foreground" : "border-transparent text-muted-foreground hover:bg-[var(--ix-hover-bg)] hover:text-foreground"),
91
+ className: cn("group relative flex h-[calc(100%-0.375rem)] max-w-72 shrink-0 items-center rounded-t-lg border border-b-0 text-sm", controlMotion, isActive ? "border-border bg-card text-foreground" : "border-transparent text-muted-foreground hover:bg-[var(--ix-hover-bg)] hover:text-foreground", divided && "after:absolute after:-right-[3px] after:top-1/2 after:h-4 after:w-px after:-translate-y-1/2 after:bg-border"),
69
92
  children: [/* @__PURE__ */ jsxs("button", {
70
93
  type: "button",
71
94
  role: "tab",
72
95
  "aria-selected": isActive,
73
- "aria-label": tab.dirty ? `${tab.name}, unsaved changes` : tab.name,
96
+ "aria-label": `${tab.name}${hint ? ` in ${hint}` : ""}${tab.dirty ? ", unsaved changes" : ""}`,
74
97
  tabIndex: isActive || !activeId && tab === tabs[0] ? 0 : -1,
75
98
  onClick: () => onSelect(tab.id),
76
99
  className: cn("flex h-full min-w-0 items-center gap-1.5 rounded-t-lg pl-3 pr-1", focusRingInset),
@@ -89,6 +112,10 @@ function FileTabs({ tabs, activeId, onSelect, onClose, label = "Open files", cla
89
112
  children: tail
90
113
  })]
91
114
  }),
115
+ hint && /* @__PURE__ */ jsxs("span", {
116
+ className: "min-w-0 truncate text-muted-foreground",
117
+ children: ["· ", hint]
118
+ }),
92
119
  tab.dirty && /* @__PURE__ */ jsx("span", {
93
120
  "aria-hidden": true,
94
121
  className: "size-1.5 shrink-0 rounded-full bg-[var(--accent-text)]"
package/dist/index.d.ts CHANGED
@@ -20,7 +20,7 @@ import { Speaker, SpeakerHeader, SpeakerHeaderProps, SpeakerPart, SpeakerSection
20
20
  import { ThinkingIndicator, ThinkingIndicatorProps } from "./chat/thinking-indicator.js";
21
21
  import { ToolCallStatus, ToolCallType } from "./run/tool-call-step.js";
22
22
  import { FeedSegment, ToolCallData, ToolCallFeed, ToolCallFeedProps, parseToolEvent } from "./run/tool-call-feed.js";
23
- import { AgentTimeline, AgentTimelineArtifactItem, AgentTimelineColumn, AgentTimelineCustomItem, AgentTimelineItem, AgentTimelineMessageItem, AgentTimelineProps, AgentTimelineStatusItem, AgentTimelineStep, AgentTimelineStepKind, AgentTimelineStepProps, AgentTimelineTone, AgentTimelineToolGroupItem, AgentTimelineToolItem, agentTimelineStepKind } from "./chat/agent-timeline.js";
23
+ import { AgentTimeline, AgentTimelineArtifactItem, AgentTimelineColumn, AgentTimelineCustomItem, AgentTimelineFoldItem, AgentTimelineItem, AgentTimelineMessageItem, AgentTimelineProps, AgentTimelineStatusItem, AgentTimelineStep, AgentTimelineStepKind, AgentTimelineStepProps, AgentTimelineTone, AgentTimelineToolGroupItem, AgentTimelineToolItem, agentTimelineStepKind, foldSummary } from "./chat/agent-timeline.js";
24
24
  import { TRANSCRIPT_TEXT } from "./chat/transcript-text.js";
25
25
  import "./chat.js";
26
26
  import { codeBlockGrammar } from "./lib/code-presentation.js";
@@ -145,4 +145,4 @@ import { QuestionPreview, QuestionPreviewProps } from "./tool-previews/question-
145
145
  import { DiffPreview, DiffPreviewProps } from "./tool-previews/diff-preview.js";
146
146
  import { RedactedDocSegment, RedactedDocument, RedactedDocumentData, RedactedDocumentProps, RevealResult } from "./redaction/redacted-document.js";
147
147
  import "./redaction.js";
148
- export { ActiveProjectActivity, ActiveSessionActivityOptions, ActiveSessionConnectionOptions, ActiveSessionConnectionState, ActiveSessionReconnectState, ActiveSessionRecord, ActiveSessionStatus, ActiveSessionTransportMode, ActiveSessionsState, AgentBranding, AgentStreamEvent, AgentTimeline, type AgentTimelineArtifactItem, AgentTimelineColumn, type AgentTimelineCustomItem, type AgentTimelineItem, type AgentTimelineMessageItem, type AgentTimelineProps, type AgentTimelineStatusItem, AgentTimelineStep, type AgentTimelineStepKind, type AgentTimelineStepProps, type AgentTimelineTone, type AgentTimelineToolGroupItem, type AgentTimelineToolItem, Alert, AlertDescription, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger, type AlertProps, AlertTitle, type AlertTone, ApiSchema, type ApiSchemaProps, AppendUserMessageOptions, ApplySdkEventOptions, ApprovalCard, type ApprovalCardProps, type ApprovalDiffFile, ApprovalDiffSummary, type ApprovalPlanStep, ArtifactPane, type ArtifactPaneProps, AuthHeader, type AuthHeaderProps, AuthUser, AutomationStreamEvent, Avatar, AvatarFallback, AvatarImage, type AvatarProps, Badge, type BadgeProps, BeginAssistantMessageOptions, BotStreamEvent, Breadcrumb, BreadcrumbEllipsis, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, Button, type ButtonProps, CATEGORY_TONES, CSV_PREVIEW_ROW_LIMIT, Calendar, type CalendarDate, type CalendarProps, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, type CardTitleProps, type CategoryTone, type ChartReferenceLine, ChatAuthor, ChatContainer, type ChatContainerProps, ChatMessage, type ChatMessageProps, Checkbox, Chip, type ChipProps, CodeBlock, type CodeBlockProps, Collapsible, CollapsibleContent, CollapsibleTrigger, CommandPreview, type CommandPreviewProps, CompleteAssistantMessageOptions, ComplianceBadge, type ComplianceBadgeProps, type ComplianceProduct, type ComplianceStatus, ConnectionState, ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, ContextMenuGroup, ContextMenuItem, ContextMenuLabel, ContextMenuPortal, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuShortcut, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger, CopyButton, type CopyButtonProps, CustomToolRenderer, DatePicker, type DatePickerProps, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, DiffPreview, type DiffPreviewProps, DisplayVariant, DropZone, type DropZoneProps, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, EmptyState, type EmptyStateProps, ExpandedToolDetail, type ExpandedToolDetailProps, type FeedSegment, Field, type FieldProps, FileArtifactPane, type FileArtifactPaneProps, FileCard, type FileCardProps, type FileDetails, type FileFormat, FileList, type FileListItem, type FileListProps, type FileNode, FilePreview, type FilePreviewKind, type FilePreviewProps, type FileTabData, FileTabs, type FileTabsProps, FileTree, type FileTreeProps, type FileTreeVisibilityOptions, type FileUploader, FilterField, type FilterFieldProps, FinalTextPart, GitHubLoginButton, type GitHubLoginButtonProps, GlobResultsPreview, type GlobResultsPreviewProps, GrepResultsPreview, type GrepResultsPreviewProps, GroupedMessage, Heading, type HeadingProps, type HeadingVariant, HelpText, type HelpTextProps, HoverCard, HoverCardContent, HoverCardDescription, HoverCardHeader, HoverCardTitle, HoverCardTrigger, IconTile, type IconTileProps, InlineCode, type InlineCodeProps, InlineThinkingItem, type InlineThinkingItemProps, InlineToolItem, type InlineToolItemProps, Input, type InputProps, Label, LiveDuration, LoginLayout, type LoginLayoutProps, Logo, type LogoProps, Markdown, type MarkdownLinkChip, type MarkdownLinkChipResolver, type MarkdownProps, MessageAuthor, type MessageAuthorProps, MessageList, type MessageListProps, type MessageRole, MessageRun, MessageUser, Metric, MetricChart, type MetricChartProps, type MetricProps, MetricStrip, type MetricStripProps, OPENUI_NODE_JSON_SCHEMA, OPENUI_NODE_TYPES, type OpenUIAction, type OpenUIActionsNode, OpenUIArtifactRenderer, type OpenUIArtifactRendererProps, type OpenUIBadgeNode, type OpenUICardNode, type OpenUICodeNode, type OpenUIComponentNode, type OpenUIGridNode, type OpenUIHeadingNode, type OpenUIKeyValueNode, type OpenUIMarkdownNode, type OpenUIPrimitive, type OpenUISchemaIssue, type OpenUISchemaValidation, type OpenUISeparatorNode, type OpenUIStackNode, type OpenUIStatNode, type OpenUITableNode, type OpenUITextNode, PageHeader, type PageHeaderProps, PageShell, type PageShellProps, Pagination, type PaginationProps, PdfViewer, type PdfViewerConfig, type PdfViewerProps, type PeriodDelta, Popover, PopoverAnchor, PopoverClose, PopoverContent, PopoverTrigger, Progress, QuestionPreview, type QuestionPreviewProps, RadioGroup, RadioGroupItem, RealtimeSessionOptions, RealtimeSessionRegistry, RealtimeSessionRegistryProps, RealtimeSessionState, RealtimeSessionTarget, ReasoningPart, type RedactedDocSegment, RedactedDocument, type RedactedDocumentData, type RedactedDocumentProps, RegisterActiveSessionOptions, type RevealResult, RichFileTree, type RichFileTreeGitEntry, type RichFileTreeGitStatus, type RichFileTreeProps, type RichFileTreeThemeVars, Run, RunGroup, type RunGroupProps, type RunPhase, RunPhaseList, RunRowShell, type RunRowShellProps, type RunRowStatus, RunRowStatusDot, RunStats, type RunStatus, RunStatusIcon, type RunStatusIconProps, SSEEvent, SdkSessionAttachment, SdkSessionEvent, SdkSessionSeed, SegmentedControl, type SegmentedControlOption, type SegmentedControlProps, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, Separator, type SeriesExtent, type SeriesPoint, type SeriesPointInput, type SeriesScale, SeriesSparkline, type SeriesSparklineProps, SessionMessage, SessionPart, SessionProjectKey, type SessionUser, SettingsList, type SettingsListAction, type SettingsListColumns, SettingsListItem, type SettingsListItemProps, type SettingsListMenuItem, type SettingsListProps, SettingsNote, type SettingsNoteProps, SettingsRow, type SettingsRowLayout, type SettingsRowProps, SettingsSection, type SettingsSectionProps, Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetOverlay, SheetPortal, SheetTitle, SheetTrigger, SidebarDropZone, type SidebarDropZoneProps, Skeleton, SkeletonCard, SkeletonTable, Slider, type Speaker, SpeakerHeader, type SpeakerHeaderProps, type SpeakerPart, type SpeakerSection, type SpeakerState, StatCard, type StatCardProps, type StatusMeta, StatusPill, type StatusPillProps, type StatusTone, Switch, TONE_CLASSES, TOOL_CATEGORY_ICONS, TRANSCRIPT_TEXT, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, type TableProps, TableRow, Tabs, TabsContent, TabsList, TabsTrigger, Tag, type TagProps, TangleKnot, TaskList, type TaskListItem, type TaskListProps, type TaskStatus, TaskStreamEvent, TerminalCursor, TerminalDisplay, TerminalInput, TerminalLine, TerminalStreamEvent, TextPart, TextShimmer, type TextShimmerProps, Textarea, type TextareaProps, ThemeToggle, ThinkingIndicator, type ThinkingIndicatorProps, TimeSelect, type TimeSelectProps, type Toast, ToastContainer, ToastProvider, type Tone, type ToneClasses, type ToolCallData, ToolCallFeed, type ToolCallFeedProps, type ToolCallStatus, type ToolCallType, ToolCategory, ToolDisplayMetadata, ToolPart, ToolState, ToolStatus, ToolTime, Toolbar, type ToolbarProps, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, type UploadFile, UploadProgress, type UploadProgressProps, UseAuthOptions, UseAuthResult, UseRunGroupsOptions, UseSSEStreamOptions, UseSSEStreamResult, UseSdkSessionOptions, UseSdkSessionReturn, UseToolCallStreamReturn, UserMenu, type UserMenuProps, UserMessage, type UserMessageProps, WebSearchPreview, type WebSearchPreviewProps, Wizard, type WizardProps, WizardStep, type WizardStepProps, WriteFilePreview, type WriteFilePreviewProps, activeSessionsAtom, addMessage, addParts, agentTimelineStepKind, badgeVariants, bumpActiveSessionActivity, buttonVariants, clearChat, cn, codeBlockGrammar, configurePdfViewer, copyText, createAuthFetcher, detectFileFormat, fenceInfo, fieldMessageId, fileExtension, filterFileTree, findUnsupportedOpenUINode, focusField, focusFieldInvalid, focusFieldWithin, focusRing, focusRingInset, formatBytes, formatCalendarDate, formatDuration, formatUptime, getActiveSession, getAllActiveSessions, getAllProjectActivity, getCodeLanguage, getFormatLabel, getSanitizedMarkdownHeadingIdFromRawFragment, getSessionsByActivity, getSessionsForNavbar, getSessionsForProject, getSyntaxLanguage, getToolCategory, getToolDisplayMetadata, getToolErrorText, getTotalRunningSessionCount, hasBackgroundRunningSessions, initialsOf, isStreamingAtom, isViewerMessage, ixControl, ixControlAfter, ixControlSoft, ixField, ixRow, ixSelected, ixTabUnderline, localToday, messagesAtom, normalizeSeries, numericColumns, parseCalendarDate, parseCsv, parseToolEvent, partMapAtom, partitionBySpeaker, periodDelta, plainTextFromMarkdown, registerActiveSession, resetActiveSessions, resolveFilePreviewKind, runStatus, seriesAreaPath, seriesExtent, seriesLinePath, seriesMean, seriesScale, setActiveSessionAttention, setActiveSessionConnection, setActiveSessionError, setActiveSessionRunning, setForegroundActiveSession, showsTaskPriority, statusMeta, tableCsv, taskProgress, taskStatus, timeAgo, toneFor, truncateText, unregisterActiveSession, updateActiveSessionMeta, updatePart, useActiveSession, useActiveSessions, useActiveSessionsState, useApiKey, useAuth, useAutoScroll, useDropdownMenu, useHasBackgroundRunningSessions, useLiveTime, useNavbarSessions, useProjectActivity, useProjectSessions, useRealtimeSession, useRunCollapseState, useRunGroups, useSSEStream, useSdkSession, useSessionsByActivity, useTheme, useToast, useToolCallStream, useTotalRunningSessions, validateOpenUIJsonArtifact, validateOpenUIJsonNode };
148
+ export { ActiveProjectActivity, ActiveSessionActivityOptions, ActiveSessionConnectionOptions, ActiveSessionConnectionState, ActiveSessionReconnectState, ActiveSessionRecord, ActiveSessionStatus, ActiveSessionTransportMode, ActiveSessionsState, AgentBranding, AgentStreamEvent, AgentTimeline, type AgentTimelineArtifactItem, AgentTimelineColumn, type AgentTimelineCustomItem, type AgentTimelineFoldItem, type AgentTimelineItem, type AgentTimelineMessageItem, type AgentTimelineProps, type AgentTimelineStatusItem, AgentTimelineStep, type AgentTimelineStepKind, type AgentTimelineStepProps, type AgentTimelineTone, type AgentTimelineToolGroupItem, type AgentTimelineToolItem, Alert, AlertDescription, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger, type AlertProps, AlertTitle, type AlertTone, ApiSchema, type ApiSchemaProps, AppendUserMessageOptions, ApplySdkEventOptions, ApprovalCard, type ApprovalCardProps, type ApprovalDiffFile, ApprovalDiffSummary, type ApprovalPlanStep, ArtifactPane, type ArtifactPaneProps, AuthHeader, type AuthHeaderProps, AuthUser, AutomationStreamEvent, Avatar, AvatarFallback, AvatarImage, type AvatarProps, Badge, type BadgeProps, BeginAssistantMessageOptions, BotStreamEvent, Breadcrumb, BreadcrumbEllipsis, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, Button, type ButtonProps, CATEGORY_TONES, CSV_PREVIEW_ROW_LIMIT, Calendar, type CalendarDate, type CalendarProps, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, type CardTitleProps, type CategoryTone, type ChartReferenceLine, ChatAuthor, ChatContainer, type ChatContainerProps, ChatMessage, type ChatMessageProps, Checkbox, Chip, type ChipProps, CodeBlock, type CodeBlockProps, Collapsible, CollapsibleContent, CollapsibleTrigger, CommandPreview, type CommandPreviewProps, CompleteAssistantMessageOptions, ComplianceBadge, type ComplianceBadgeProps, type ComplianceProduct, type ComplianceStatus, ConnectionState, ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, ContextMenuGroup, ContextMenuItem, ContextMenuLabel, ContextMenuPortal, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuShortcut, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger, CopyButton, type CopyButtonProps, CustomToolRenderer, DatePicker, type DatePickerProps, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, DiffPreview, type DiffPreviewProps, DisplayVariant, DropZone, type DropZoneProps, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, EmptyState, type EmptyStateProps, ExpandedToolDetail, type ExpandedToolDetailProps, type FeedSegment, Field, type FieldProps, FileArtifactPane, type FileArtifactPaneProps, FileCard, type FileCardProps, type FileDetails, type FileFormat, FileList, type FileListItem, type FileListProps, type FileNode, FilePreview, type FilePreviewKind, type FilePreviewProps, type FileTabData, FileTabs, type FileTabsProps, FileTree, type FileTreeProps, type FileTreeVisibilityOptions, type FileUploader, FilterField, type FilterFieldProps, FinalTextPart, GitHubLoginButton, type GitHubLoginButtonProps, GlobResultsPreview, type GlobResultsPreviewProps, GrepResultsPreview, type GrepResultsPreviewProps, GroupedMessage, Heading, type HeadingProps, type HeadingVariant, HelpText, type HelpTextProps, HoverCard, HoverCardContent, HoverCardDescription, HoverCardHeader, HoverCardTitle, HoverCardTrigger, IconTile, type IconTileProps, InlineCode, type InlineCodeProps, InlineThinkingItem, type InlineThinkingItemProps, InlineToolItem, type InlineToolItemProps, Input, type InputProps, Label, LiveDuration, LoginLayout, type LoginLayoutProps, Logo, type LogoProps, Markdown, type MarkdownLinkChip, type MarkdownLinkChipResolver, type MarkdownProps, MessageAuthor, type MessageAuthorProps, MessageList, type MessageListProps, type MessageRole, MessageRun, MessageUser, Metric, MetricChart, type MetricChartProps, type MetricProps, MetricStrip, type MetricStripProps, OPENUI_NODE_JSON_SCHEMA, OPENUI_NODE_TYPES, type OpenUIAction, type OpenUIActionsNode, OpenUIArtifactRenderer, type OpenUIArtifactRendererProps, type OpenUIBadgeNode, type OpenUICardNode, type OpenUICodeNode, type OpenUIComponentNode, type OpenUIGridNode, type OpenUIHeadingNode, type OpenUIKeyValueNode, type OpenUIMarkdownNode, type OpenUIPrimitive, type OpenUISchemaIssue, type OpenUISchemaValidation, type OpenUISeparatorNode, type OpenUIStackNode, type OpenUIStatNode, type OpenUITableNode, type OpenUITextNode, PageHeader, type PageHeaderProps, PageShell, type PageShellProps, Pagination, type PaginationProps, PdfViewer, type PdfViewerConfig, type PdfViewerProps, type PeriodDelta, Popover, PopoverAnchor, PopoverClose, PopoverContent, PopoverTrigger, Progress, QuestionPreview, type QuestionPreviewProps, RadioGroup, RadioGroupItem, RealtimeSessionOptions, RealtimeSessionRegistry, RealtimeSessionRegistryProps, RealtimeSessionState, RealtimeSessionTarget, ReasoningPart, type RedactedDocSegment, RedactedDocument, type RedactedDocumentData, type RedactedDocumentProps, RegisterActiveSessionOptions, type RevealResult, RichFileTree, type RichFileTreeGitEntry, type RichFileTreeGitStatus, type RichFileTreeProps, type RichFileTreeThemeVars, Run, RunGroup, type RunGroupProps, type RunPhase, RunPhaseList, RunRowShell, type RunRowShellProps, type RunRowStatus, RunRowStatusDot, RunStats, type RunStatus, RunStatusIcon, type RunStatusIconProps, SSEEvent, SdkSessionAttachment, SdkSessionEvent, SdkSessionSeed, SegmentedControl, type SegmentedControlOption, type SegmentedControlProps, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, Separator, type SeriesExtent, type SeriesPoint, type SeriesPointInput, type SeriesScale, SeriesSparkline, type SeriesSparklineProps, SessionMessage, SessionPart, SessionProjectKey, type SessionUser, SettingsList, type SettingsListAction, type SettingsListColumns, SettingsListItem, type SettingsListItemProps, type SettingsListMenuItem, type SettingsListProps, SettingsNote, type SettingsNoteProps, SettingsRow, type SettingsRowLayout, type SettingsRowProps, SettingsSection, type SettingsSectionProps, Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetOverlay, SheetPortal, SheetTitle, SheetTrigger, SidebarDropZone, type SidebarDropZoneProps, Skeleton, SkeletonCard, SkeletonTable, Slider, type Speaker, SpeakerHeader, type SpeakerHeaderProps, type SpeakerPart, type SpeakerSection, type SpeakerState, StatCard, type StatCardProps, type StatusMeta, StatusPill, type StatusPillProps, type StatusTone, Switch, TONE_CLASSES, TOOL_CATEGORY_ICONS, TRANSCRIPT_TEXT, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, type TableProps, TableRow, Tabs, TabsContent, TabsList, TabsTrigger, Tag, type TagProps, TangleKnot, TaskList, type TaskListItem, type TaskListProps, type TaskStatus, TaskStreamEvent, TerminalCursor, TerminalDisplay, TerminalInput, TerminalLine, TerminalStreamEvent, TextPart, TextShimmer, type TextShimmerProps, Textarea, type TextareaProps, ThemeToggle, ThinkingIndicator, type ThinkingIndicatorProps, TimeSelect, type TimeSelectProps, type Toast, ToastContainer, ToastProvider, type Tone, type ToneClasses, type ToolCallData, ToolCallFeed, type ToolCallFeedProps, type ToolCallStatus, type ToolCallType, ToolCategory, ToolDisplayMetadata, ToolPart, ToolState, ToolStatus, ToolTime, Toolbar, type ToolbarProps, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, type UploadFile, UploadProgress, type UploadProgressProps, UseAuthOptions, UseAuthResult, UseRunGroupsOptions, UseSSEStreamOptions, UseSSEStreamResult, UseSdkSessionOptions, UseSdkSessionReturn, UseToolCallStreamReturn, UserMenu, type UserMenuProps, UserMessage, type UserMessageProps, WebSearchPreview, type WebSearchPreviewProps, Wizard, type WizardProps, WizardStep, type WizardStepProps, WriteFilePreview, type WriteFilePreviewProps, activeSessionsAtom, addMessage, addParts, agentTimelineStepKind, badgeVariants, bumpActiveSessionActivity, buttonVariants, clearChat, cn, codeBlockGrammar, configurePdfViewer, copyText, createAuthFetcher, detectFileFormat, fenceInfo, fieldMessageId, fileExtension, filterFileTree, findUnsupportedOpenUINode, focusField, focusFieldInvalid, focusFieldWithin, focusRing, focusRingInset, foldSummary, formatBytes, formatCalendarDate, formatDuration, formatUptime, getActiveSession, getAllActiveSessions, getAllProjectActivity, getCodeLanguage, getFormatLabel, getSanitizedMarkdownHeadingIdFromRawFragment, getSessionsByActivity, getSessionsForNavbar, getSessionsForProject, getSyntaxLanguage, getToolCategory, getToolDisplayMetadata, getToolErrorText, getTotalRunningSessionCount, hasBackgroundRunningSessions, initialsOf, isStreamingAtom, isViewerMessage, ixControl, ixControlAfter, ixControlSoft, ixField, ixRow, ixSelected, ixTabUnderline, localToday, messagesAtom, normalizeSeries, numericColumns, parseCalendarDate, parseCsv, parseToolEvent, partMapAtom, partitionBySpeaker, periodDelta, plainTextFromMarkdown, registerActiveSession, resetActiveSessions, resolveFilePreviewKind, runStatus, seriesAreaPath, seriesExtent, seriesLinePath, seriesMean, seriesScale, setActiveSessionAttention, setActiveSessionConnection, setActiveSessionError, setActiveSessionRunning, setForegroundActiveSession, showsTaskPriority, statusMeta, tableCsv, taskProgress, taskStatus, timeAgo, toneFor, truncateText, unregisterActiveSession, updateActiveSessionMeta, updatePart, useActiveSession, useActiveSessions, useActiveSessionsState, useApiKey, useAuth, useAutoScroll, useDropdownMenu, useHasBackgroundRunningSessions, useLiveTime, useNavbarSessions, useProjectActivity, useProjectSessions, useRealtimeSession, useRunCollapseState, useRunGroups, useSSEStream, useSdkSession, useSessionsByActivity, useTheme, useToast, useToolCallStream, useTotalRunningSessions, validateOpenUIJsonArtifact, validateOpenUIJsonNode };
package/dist/index.js CHANGED
@@ -96,7 +96,7 @@ import { MessageAuthor, isViewerMessage } from "./chat/message-author.js";
96
96
  import { UserMessage } from "./chat/user-message.js";
97
97
  import { MessageList } from "./chat/message-list.js";
98
98
  import { ThinkingIndicator } from "./chat/thinking-indicator.js";
99
- import { AgentTimeline, AgentTimelineColumn, AgentTimelineStep, agentTimelineStepKind } from "./chat/agent-timeline.js";
99
+ import { AgentTimeline, AgentTimelineColumn, AgentTimelineStep, agentTimelineStepKind, foldSummary } from "./chat/agent-timeline.js";
100
100
  import { ChatContainer } from "./chat/chat-container.js";
101
101
  import { ChatMessage } from "./chat/chat-message.js";
102
102
  import { SpeakerHeader, partitionBySpeaker } from "./chat/speaker.js";
@@ -129,4 +129,4 @@ import { addMessage, addParts, clearChat, isStreamingAtom, messagesAtom, partMap
129
129
  import { copyText } from "./utils/copy-text.js";
130
130
  import { timeAgo } from "./utils/time-ago.js";
131
131
  import { RedactedDocument } from "./redaction/redacted-document.js";
132
- export { AgentTimeline, AgentTimelineColumn, AgentTimelineStep, Alert, AlertDescription, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger, AlertTitle, ApiSchema, ApprovalCard, ApprovalDiffSummary, ArtifactPane, AuthHeader, Avatar, AvatarFallback, AvatarImage, Badge, Breadcrumb, BreadcrumbEllipsis, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, Button, CATEGORY_TONES, CSV_PREVIEW_ROW_LIMIT, Calendar, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, ChatContainer, ChatMessage, Checkbox, Chip, CodeBlock, Collapsible, CollapsibleContent, CollapsibleTrigger, CommandPreview, ComplianceBadge, ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, ContextMenuGroup, ContextMenuItem, ContextMenuLabel, ContextMenuPortal, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuShortcut, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger, CopyButton, DatePicker, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, DiffPreview, DropZone, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, EmptyState, ExpandedToolDetail, Field, FileArtifactPane, FileCard, FileList, FilePreview, FileTabs, FileTree, FilterField, GitHubLoginButton, GlobResultsPreview, GrepResultsPreview, Heading, HelpText, HoverCard, HoverCardContent, HoverCardDescription, HoverCardHeader, HoverCardTitle, HoverCardTrigger, IconTile, InlineCode, InlineThinkingItem, InlineToolItem, Input, Label, LiveDuration, LoginLayout, Logo, Markdown, MessageAuthor, MessageList, Metric, MetricChart, MetricStrip, OPENUI_NODE_JSON_SCHEMA, OPENUI_NODE_TYPES, OpenUIArtifactRenderer, PageHeader, PageShell, Pagination, PdfViewer, Popover, PopoverAnchor, PopoverClose, PopoverContent, PopoverTrigger, Progress, QuestionPreview, RadioGroup, RadioGroupItem, RealtimeSessionRegistry, RedactedDocument, RichFileTree, RunGroup, RunPhaseList, RunRowShell, RunRowStatusDot, RunStatusIcon, SegmentedControl, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, Separator, SeriesSparkline, SettingsList, SettingsListItem, SettingsNote, SettingsRow, SettingsSection, Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetOverlay, SheetPortal, SheetTitle, SheetTrigger, SidebarDropZone, Skeleton, SkeletonCard, SkeletonTable, Slider, SpeakerHeader, StatCard, StatusPill, Switch, TONE_CLASSES, TOOL_CATEGORY_ICONS, TRANSCRIPT_TEXT, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow, Tabs, TabsContent, TabsList, TabsTrigger, Tag, TangleKnot, TaskList, TerminalCursor, TerminalDisplay, TerminalInput, TerminalLine, TextShimmer, Textarea, ThemeToggle, ThinkingIndicator, TimeSelect, ToastContainer, ToastProvider, ToolCallFeed, Toolbar, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, UploadProgress, UserMenu, UserMessage, WebSearchPreview, Wizard, WizardStep, WriteFilePreview, activeSessionsAtom, addMessage, addParts, agentTimelineStepKind, badgeVariants, bumpActiveSessionActivity, buttonVariants, clearChat, cn, codeBlockGrammar, configurePdfViewer, copyText, createAuthFetcher, detectFileFormat, fenceInfo, fieldMessageId, fileExtension, filterFileTree, findUnsupportedOpenUINode, focusField, focusFieldInvalid, focusFieldWithin, focusRing, focusRingInset, formatBytes, formatCalendarDate, formatDuration, formatUptime, getActiveSession, getAllActiveSessions, getAllProjectActivity, getCodeLanguage, getFormatLabel, getSanitizedMarkdownHeadingIdFromRawFragment, getSessionsByActivity, getSessionsForNavbar, getSessionsForProject, getSyntaxLanguage, getToolCategory, getToolDisplayMetadata, getToolErrorText, getTotalRunningSessionCount, hasBackgroundRunningSessions, initialsOf, isStreamingAtom, isViewerMessage, ixControl, ixControlAfter, ixControlSoft, ixField, ixRow, ixSelected, ixTabUnderline, localToday, messagesAtom, normalizeSeries, numericColumns, parseCalendarDate, parseCsv, parseToolEvent, partMapAtom, partitionBySpeaker, periodDelta, plainTextFromMarkdown, registerActiveSession, resetActiveSessions, resolveFilePreviewKind, runStatus, seriesAreaPath, seriesExtent, seriesLinePath, seriesMean, seriesScale, setActiveSessionAttention, setActiveSessionConnection, setActiveSessionError, setActiveSessionRunning, setForegroundActiveSession, showsTaskPriority, statusMeta, tableCsv, taskProgress, taskStatus, timeAgo, toneFor, truncateText, unregisterActiveSession, updateActiveSessionMeta, updatePart, useActiveSession, useActiveSessions, useActiveSessionsState, useApiKey, useAuth, useAutoScroll, useDropdownMenu, useHasBackgroundRunningSessions, useLiveTime, useNavbarSessions, useProjectActivity, useProjectSessions, useRealtimeSession, useRunCollapseState, useRunGroups, useSSEStream, useSdkSession, useSessionsByActivity, useTheme, useToast, useToolCallStream, useTotalRunningSessions, validateOpenUIJsonArtifact, validateOpenUIJsonNode };
132
+ export { AgentTimeline, AgentTimelineColumn, AgentTimelineStep, Alert, AlertDescription, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger, AlertTitle, ApiSchema, ApprovalCard, ApprovalDiffSummary, ArtifactPane, AuthHeader, Avatar, AvatarFallback, AvatarImage, Badge, Breadcrumb, BreadcrumbEllipsis, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, Button, CATEGORY_TONES, CSV_PREVIEW_ROW_LIMIT, Calendar, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, ChatContainer, ChatMessage, Checkbox, Chip, CodeBlock, Collapsible, CollapsibleContent, CollapsibleTrigger, CommandPreview, ComplianceBadge, ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, ContextMenuGroup, ContextMenuItem, ContextMenuLabel, ContextMenuPortal, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuShortcut, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger, CopyButton, DatePicker, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, DiffPreview, DropZone, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, EmptyState, ExpandedToolDetail, Field, FileArtifactPane, FileCard, FileList, FilePreview, FileTabs, FileTree, FilterField, GitHubLoginButton, GlobResultsPreview, GrepResultsPreview, Heading, HelpText, HoverCard, HoverCardContent, HoverCardDescription, HoverCardHeader, HoverCardTitle, HoverCardTrigger, IconTile, InlineCode, InlineThinkingItem, InlineToolItem, Input, Label, LiveDuration, LoginLayout, Logo, Markdown, MessageAuthor, MessageList, Metric, MetricChart, MetricStrip, OPENUI_NODE_JSON_SCHEMA, OPENUI_NODE_TYPES, OpenUIArtifactRenderer, PageHeader, PageShell, Pagination, PdfViewer, Popover, PopoverAnchor, PopoverClose, PopoverContent, PopoverTrigger, Progress, QuestionPreview, RadioGroup, RadioGroupItem, RealtimeSessionRegistry, RedactedDocument, RichFileTree, RunGroup, RunPhaseList, RunRowShell, RunRowStatusDot, RunStatusIcon, SegmentedControl, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, Separator, SeriesSparkline, SettingsList, SettingsListItem, SettingsNote, SettingsRow, SettingsSection, Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetOverlay, SheetPortal, SheetTitle, SheetTrigger, SidebarDropZone, Skeleton, SkeletonCard, SkeletonTable, Slider, SpeakerHeader, StatCard, StatusPill, Switch, TONE_CLASSES, TOOL_CATEGORY_ICONS, TRANSCRIPT_TEXT, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow, Tabs, TabsContent, TabsList, TabsTrigger, Tag, TangleKnot, TaskList, TerminalCursor, TerminalDisplay, TerminalInput, TerminalLine, TextShimmer, Textarea, ThemeToggle, ThinkingIndicator, TimeSelect, ToastContainer, ToastProvider, ToolCallFeed, Toolbar, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, UploadProgress, UserMenu, UserMessage, WebSearchPreview, Wizard, WizardStep, WriteFilePreview, activeSessionsAtom, addMessage, addParts, agentTimelineStepKind, badgeVariants, bumpActiveSessionActivity, buttonVariants, clearChat, cn, codeBlockGrammar, configurePdfViewer, copyText, createAuthFetcher, detectFileFormat, fenceInfo, fieldMessageId, fileExtension, filterFileTree, findUnsupportedOpenUINode, focusField, focusFieldInvalid, focusFieldWithin, focusRing, focusRingInset, foldSummary, formatBytes, formatCalendarDate, formatDuration, formatUptime, getActiveSession, getAllActiveSessions, getAllProjectActivity, getCodeLanguage, getFormatLabel, getSanitizedMarkdownHeadingIdFromRawFragment, getSessionsByActivity, getSessionsForNavbar, getSessionsForProject, getSyntaxLanguage, getToolCategory, getToolDisplayMetadata, getToolErrorText, getTotalRunningSessionCount, hasBackgroundRunningSessions, initialsOf, isStreamingAtom, isViewerMessage, ixControl, ixControlAfter, ixControlSoft, ixField, ixRow, ixSelected, ixTabUnderline, localToday, messagesAtom, normalizeSeries, numericColumns, parseCalendarDate, parseCsv, parseToolEvent, partMapAtom, partitionBySpeaker, periodDelta, plainTextFromMarkdown, registerActiveSession, resetActiveSessions, resolveFilePreviewKind, runStatus, seriesAreaPath, seriesExtent, seriesLinePath, seriesMean, seriesScale, setActiveSessionAttention, setActiveSessionConnection, setActiveSessionError, setActiveSessionRunning, setForegroundActiveSession, showsTaskPriority, statusMeta, tableCsv, taskProgress, taskStatus, timeAgo, toneFor, truncateText, unregisterActiveSession, updateActiveSessionMeta, updatePart, useActiveSession, useActiveSessions, useActiveSessionsState, useApiKey, useAuth, useAutoScroll, useDropdownMenu, useHasBackgroundRunningSessions, useLiveTime, useNavbarSessions, useProjectActivity, useProjectSessions, useRealtimeSession, useRunCollapseState, useRunGroups, useSSEStream, useSdkSession, useSessionsByActivity, useTheme, useToast, useToolCallStream, useTotalRunningSessions, validateOpenUIJsonArtifact, validateOpenUIJsonNode };
@@ -159,7 +159,7 @@ function SegmentedControl({ value, onValueChange, options, variant = "row", clas
159
159
  "hover:shadow-[inset_0_-2px_0_hsl(var(--hsl-ring)/0.4)] data-[state=active]:bg-[hsl(var(--hsl-ring)/0.07)] data-[state=active]:text-[var(--accent-text)] data-[state=active]:shadow-[inset_0_-2px_0_var(--ix-sel-ring)] aria-checked:bg-[hsl(var(--hsl-ring)/0.07)] aria-checked:text-[var(--accent-text)] aria-checked:shadow-[inset_0_-2px_0_var(--ix-sel-ring)]"
160
160
  ], active ? "text-[var(--accent-text)]" : "text-muted-foreground hover:text-foreground"),
161
161
  children: [/* @__PURE__ */ jsx("span", { children: option.label }), option.adornment && /* @__PURE__ */ jsx("span", {
162
- className: cn("text-xs", active ? "text-[var(--accent-text)]/80" : "text-muted-foreground"),
162
+ className: cn("text-sm tabular-nums", active ? "text-[var(--accent-text)]/80" : "text-muted-foreground"),
163
163
  children: option.adornment
164
164
  })]
165
165
  }, option.value);
@@ -51,14 +51,14 @@ function RunRowShell({ icon, title, description, descriptionMono = false, status
51
51
  children: [/* @__PURE__ */ jsxs("div", {
52
52
  "data-run-row": "",
53
53
  "data-state": open ? "open" : "closed",
54
- className: cn("group min-w-0 flex-1 overflow-hidden border transition-colors", open ? "border-border bg-[var(--md3-surface-container)]" : "border-transparent bg-transparent", expandable && !open && "hover:bg-[var(--md3-surface-container)]", SHAPE_CLASS[groupPosition], className),
54
+ className: cn("min-w-0 flex-1 overflow-hidden border transition-colors", open ? "border-border bg-[var(--md3-surface-container)]" : "border-transparent bg-transparent", expandable && !open && "hover:bg-[var(--md3-surface-container)]", SHAPE_CLASS[groupPosition], className),
55
55
  children: [
56
56
  /* @__PURE__ */ jsx(Collapsible.Trigger, {
57
57
  asChild: true,
58
58
  disabled: !expandable,
59
59
  children: /* @__PURE__ */ jsxs("button", {
60
60
  type: "button",
61
- className: cn("flex w-full items-center gap-2 text-left", HEADER_INSET, expandable ? "cursor-pointer" : "cursor-default"),
61
+ className: cn("group flex w-full items-center gap-2 text-left", HEADER_INSET, expandable ? "cursor-pointer" : "cursor-default"),
62
62
  children: [
63
63
  /* @__PURE__ */ jsxs("span", {
64
64
  "aria-hidden": true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/ui",
3
- "version": "11.50.3",
3
+ "version": "11.51.0",
4
4
  "description": "Generic React UI components for Tangle products — primitives, chat, run, files, editor, markdown.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -295,6 +295,45 @@ export const WithToolGroup: Story = {
295
295
  },
296
296
  }
297
297
 
298
+ // ---------------------------------------------------------------------------
299
+ // A settled multi-step turn folded into one row, under a long prompt that folds too
300
+ // ---------------------------------------------------------------------------
301
+
302
+ const read = (id: string, detail: string, status: 'success' | 'error' = 'success'): AgentTimelineItem => ({
303
+ id,
304
+ kind: 'tool',
305
+ call: { id, type: 'read', label: 'Read', status, detail, duration: 40 },
306
+ part: { type: 'tool', id, tool: 'read', state: status === 'error' ? { status: 'error', input: { path: detail }, error: `File not found: ${detail}` } : { status: 'completed', input: { path: detail }, output: '…' } },
307
+ })
308
+
309
+ export const FoldedSteps: Story = {
310
+ args: {
311
+ items: [
312
+ {
313
+ id: 'user-1',
314
+ kind: 'message',
315
+ role: 'user',
316
+ collapsedLines: 4,
317
+ content:
318
+ 'Build the most correct, most complete zkVM in Rust: every component of the paper, sound end to end, at 120 bits, measured against the paper\'s own tables, and then better. Start from the commission and the previous handoff; read the open blockers first. Keep one owner per path, and re-measure every gain on the same benchmark before you integrate it. Report the proving rate, proof size, verify time and peak memory at every release.',
319
+ timestamp: t(300),
320
+ },
321
+ { id: 'think-1', kind: 'custom', content: <p className="text-sm text-muted-foreground">Reasoning rows sit inside the fold with the calls they led to.</p> },
322
+ {
323
+ id: 'fold-1',
324
+ kind: 'fold',
325
+ items: [read('r1', 'inputs/commission.md'), read('r2', 'inputs/previous.md'), read('r3', 'inputs/claims.json'), read('r4', 'inputs/sources/handoff.md'), { id: 'b1', kind: 'tool', call: { id: 'b1', type: 'bash', label: 'Shell', status: 'success', detail: 'git -C repo log --oneline -5', duration: 120 } }],
326
+ },
327
+ { id: 'asst-1', kind: 'message', role: 'assistant', content: 'The handoff names four open blockers. I will take the proof-size one first, since the other three depend on it.' },
328
+ {
329
+ id: 'fold-2',
330
+ kind: 'fold',
331
+ items: [read('r5', 'inputs/sources/open-blockers.md'), read('r6', 'inputs/profile-examples/README.md', 'error'), read('r7', 'repo/README.md')],
332
+ },
333
+ ],
334
+ },
335
+ }
336
+
298
337
  // ---------------------------------------------------------------------------
299
338
  // Error state
300
339
  // ---------------------------------------------------------------------------
@@ -316,3 +316,27 @@ describe("AgentTimeline status cards", () => {
316
316
  expect(glyph("warning")).toContain("lucide-triangle-alert")
317
317
  })
318
318
  })
319
+
320
+ describe("AgentTimeline folds", () => {
321
+ const call = (id: string, type: "read" | "bash", detail: string, status: "success" | "error" = "success"): AgentTimelineItem => ({
322
+ id,
323
+ kind: "tool",
324
+ call: { id, type, label: type === "read" ? "Read" : "Shell", status, detail },
325
+ part: { type: "tool", id, tool: type, state: status === "error" ? { status: "error", input: { path: detail }, error: `File not found: ${detail}` } : { status: "completed", input: { path: detail }, output: "ok" } },
326
+ })
327
+
328
+ it("folds a run of calls into one row that names what they did and opens to the rows", async () => {
329
+ const user = userEvent.setup()
330
+ render(<AgentTimeline items={[{ id: "f", kind: "fold", items: [call("a", "read", "inputs/commission.md"), call("b", "read", "inputs/claims.json"), call("c", "bash", "ls repo")] }]} />)
331
+ const row = screen.getByRole("button", { name: /Read 2 files · Ran 1 command/ })
332
+ expect(screen.queryByText("Shell")).not.toBeInTheDocument()
333
+ await user.click(row)
334
+ expect(screen.getByText("Shell")).toBeInTheDocument()
335
+ expect(screen.getAllByText("Read")).toHaveLength(2)
336
+ })
337
+
338
+ it("keeps a failure visible under a closed fold", () => {
339
+ render(<AgentTimeline items={[{ id: "f", kind: "fold", items: [call("a", "read", "a.md"), call("b", "read", "b.md", "error")] }]} />)
340
+ expect(screen.getByText("1 failed: File not found: b.md")).toBeInTheDocument()
341
+ })
342
+ })
@@ -5,6 +5,7 @@ import {
5
5
  CircleDot,
6
6
  FileText,
7
7
  Info,
8
+ Layers,
8
9
  } from "lucide-react";
9
10
  import { focusRing } from "../lib/focus";
10
11
  import { cn } from "../lib/utils";
@@ -16,6 +17,8 @@ import { ThinkingIndicator } from "./thinking-indicator";
16
17
  import { type ToolCallData } from "../run/tool-call-feed";
17
18
  import { ToolCallGroup, ToolCallStep } from "../run/tool-call-step";
18
19
  import { statusMeta } from "../run/status-meta";
20
+ import { RunRowShell } from "../run/run-row-shell";
21
+ import { TextShimmer } from "../primitives/text-shimmer";
19
22
  import type { ChatAuthor } from "../types/message";
20
23
  import type { ToolPart } from "../types/parts";
21
24
 
@@ -28,6 +31,9 @@ export interface AgentTimelineMessageItem {
28
31
  content: string;
29
32
  toolCalls?: ReactNode;
30
33
  isStreaming?: boolean;
34
+ /** A user bubble longer than this many lines shows them with a fade and a
35
+ * "Show more" inside the bubble; omit to show the whole message. */
36
+ collapsedLines?: number;
31
37
  /** Shown beside a user bubble on hover. Assistant prose carries no
32
38
  * timestamp of its own — the turn's user message dates the exchange. */
33
39
  timestamp?: Date;
@@ -59,6 +65,23 @@ export interface AgentTimelineToolGroupItem {
59
65
  parts?: ToolPart[];
60
66
  }
61
67
 
68
+ /**
69
+ * A run of steps folded into one quiet row that names what they did ("Read 6
70
+ * files · Ran 2 commands") and opens to the rows themselves, as an agent app
71
+ * folds a settled multi-step turn. A failed call keeps the fold in the failure
72
+ * ink with its error under the row, so a failure never hides behind it.
73
+ */
74
+ export interface AgentTimelineFoldItem {
75
+ id: string;
76
+ kind: "fold";
77
+ /** The folded rows in order: tool calls, reasoning and other run rows. */
78
+ items: AgentTimelineItem[];
79
+ /** The row's words; by default what its tool calls did (`foldSummary`). */
80
+ summary?: string;
81
+ /** Start open, e.g. when a link points at one of its steps. */
82
+ defaultOpen?: boolean;
83
+ }
84
+
62
85
  export interface AgentTimelineStatusItem {
63
86
  id: string;
64
87
  kind: "status";
@@ -92,6 +115,7 @@ export type AgentTimelineItem =
92
115
  | AgentTimelineMessageItem
93
116
  | AgentTimelineToolItem
94
117
  | AgentTimelineToolGroupItem
118
+ | AgentTimelineFoldItem
95
119
  | AgentTimelineStatusItem
96
120
  | AgentTimelineArtifactItem
97
121
  | AgentTimelineCustomItem;
@@ -285,6 +309,82 @@ function ArtifactCard({ item }: { item: AgentTimelineArtifactItem }) {
285
309
  );
286
310
  }
287
311
 
312
+ const FOLD_VERBS: Record<ToolCallData["type"], (n: number) => string> = {
313
+ read: (n) => `Read ${n} ${n === 1 ? "file" : "files"}`,
314
+ write: (n) => `Wrote ${n} ${n === 1 ? "file" : "files"}`,
315
+ edit: (n) => `Edited ${n} ${n === 1 ? "file" : "files"}`,
316
+ bash: (n) => `Ran ${n} ${n === 1 ? "command" : "commands"}`,
317
+ grep: (n) => `Searched ${n} ${n === 1 ? "time" : "times"}`,
318
+ glob: (n) => `Matched ${n} ${n === 1 ? "pattern" : "patterns"}`,
319
+ list: (n) => `Listed ${n} ${n === 1 ? "folder" : "folders"}`,
320
+ download: (n) => `Downloaded ${n} ${n === 1 ? "file" : "files"}`,
321
+ inspect: (n) => `Inspected ${n} ${n === 1 ? "item" : "items"}`,
322
+ audit: (n) => `Audited ${n} ${n === 1 ? "item" : "items"}`,
323
+ unknown: (n) => `Used ${n} ${n === 1 ? "tool" : "tools"}`,
324
+ };
325
+
326
+ /** The tool calls inside a fold, in order (a tool group's calls included). */
327
+ function foldCalls(items: readonly AgentTimelineItem[]): { call: ToolCallData; part?: ToolPart }[] {
328
+ return items.flatMap((item) =>
329
+ item.kind === "tool"
330
+ ? [{ call: item.call, part: item.part }]
331
+ : item.kind === "tool_group"
332
+ ? item.calls.map((call, i) => ({ call, part: item.parts?.[i] }))
333
+ : item.kind === "fold"
334
+ ? foldCalls(item.items)
335
+ : [],
336
+ );
337
+ }
338
+
339
+ /**
340
+ * What a fold's tool calls did, by verb in the order they first ran:
341
+ * "Read 6 files · Ran 2 commands". A fold without tool calls reads by its
342
+ * row count.
343
+ */
344
+ export function foldSummary(items: readonly AgentTimelineItem[]): string {
345
+ const counts = new Map<ToolCallData["type"], number>();
346
+ for (const { call } of foldCalls(items)) counts.set(call.type, (counts.get(call.type) ?? 0) + 1);
347
+ if (!counts.size) return `${items.length} ${items.length === 1 ? "step" : "steps"}`;
348
+ const phrases = [...counts].map(([type, n]) => FOLD_VERBS[type](n));
349
+ return phrases.length > 3 ? `${phrases.slice(0, 3).join(" · ")} · ${phrases.length - 3} more` : phrases.join(" · ");
350
+ }
351
+
352
+ function FoldStep({ item, renderToolActions, viewerId }: { item: AgentTimelineFoldItem; renderToolActions?: (part: ToolPart) => ReactNode; viewerId?: string }) {
353
+ const calls = foldCalls(item.items);
354
+ const failed = calls.filter(({ call }) => call.status === "error");
355
+ const running = calls.some(({ call }) => call.status === "running");
356
+ const firstError = failed.map(({ part }) => (part?.state.status === "error" ? part.state.error : undefined)).find(Boolean);
357
+ const total = calls.reduce((sum, { call }) => sum + (call.duration ?? 0), 0);
358
+ const title = item.summary ?? foldSummary(item.items);
359
+ // What the calls acted on, in order and once each: the row's quiet second half.
360
+ const targets = [...new Set(calls.map(({ call }) => call.detail).filter((detail): detail is string => Boolean(detail)))];
361
+ return (
362
+ <RunRowShell
363
+ icon={<Layers className="h-3.5 w-3.5" />}
364
+ title={running ? <TextShimmer>{title}</TextShimmer> : title}
365
+ description={targets.join(", ") || undefined}
366
+ descriptionMono
367
+ status={failed.length ? "error" : running ? "running" : "success"}
368
+ durationMs={total > 0 && !running ? total : undefined}
369
+ collapsedError={failed.length ? `${failed.length} failed${firstError ? `: ${firstError}` : ""}` : undefined}
370
+ defaultOpen={item.defaultOpen}
371
+ contentClassName="px-3 py-1.5"
372
+ >
373
+ <div data-timeline-fold="" className="flex flex-col">
374
+ {item.items.map((inner, index) => (
375
+ <AgentTimelineStep
376
+ key={inner.id}
377
+ item={inner}
378
+ previous={index > 0 ? agentTimelineStepKind(item.items[index - 1]) : undefined}
379
+ renderToolActions={renderToolActions}
380
+ viewerId={viewerId}
381
+ />
382
+ ))}
383
+ </div>
384
+ </RunRowShell>
385
+ );
386
+ }
387
+
288
388
  /** The transcript column every timeline sits in: centred, at the reading width. */
289
389
  export function AgentTimelineColumn({ className, children }: { className?: string; children?: ReactNode }) {
290
390
  return <div className={cn("mx-auto flex w-full max-w-5xl flex-col px-4 py-4", className)}>{children}</div>;
@@ -317,7 +417,7 @@ export function AgentTimelineStep({ item, previous, headed, renderToolActions, v
317
417
  function TimelineItem({ item, headed, renderToolActions, viewerId }: Omit<AgentTimelineStepProps, "previous">): ReactNode {
318
418
  if (item.kind === "message") {
319
419
  return item.role === "user" ? (
320
- <UserMessage content={item.content} timestamp={item.timestamp} author={item.author} viewerId={viewerId} />
420
+ <UserMessage content={item.content} timestamp={item.timestamp} author={item.author} viewerId={viewerId} collapsedLines={item.collapsedLines} />
321
421
  ) : (
322
422
  <AssistantMessage item={item} author={headed ? item.author : undefined} />
323
423
  );
@@ -361,6 +461,8 @@ function TimelineItem({ item, headed, renderToolActions, viewerId }: Omit<AgentT
361
461
  );
362
462
  }
363
463
 
464
+ if (item.kind === "fold") return <FoldStep item={item} renderToolActions={renderToolActions} viewerId={viewerId} />;
465
+
364
466
  if (item.kind === "status") return <StatusCard item={item} />;
365
467
  if (item.kind === "artifact") return <ArtifactCard item={item} />;
366
468
  return item.content;
package/src/chat/index.ts CHANGED
@@ -19,6 +19,7 @@ export {
19
19
  AgentTimelineColumn,
20
20
  AgentTimelineStep,
21
21
  agentTimelineStepKind,
22
+ foldSummary,
22
23
  type AgentTimelineProps,
23
24
  type AgentTimelineStepProps,
24
25
  type AgentTimelineStepKind,
@@ -26,6 +27,7 @@ export {
26
27
  type AgentTimelineMessageItem,
27
28
  type AgentTimelineToolItem,
28
29
  type AgentTimelineToolGroupItem,
30
+ type AgentTimelineFoldItem,
29
31
  type AgentTimelineStatusItem,
30
32
  type AgentTimelineArtifactItem,
31
33
  type AgentTimelineCustomItem,
@@ -1,5 +1,5 @@
1
1
  import { TRANSCRIPT_TEXT } from "./transcript-text";
2
- import { memo, type ReactNode } from "react";
2
+ import { memo, useLayoutEffect, useRef, useState, type ReactNode } from "react";
3
3
  import type { ChatAuthor, SessionMessage } from "../types/message";
4
4
  import type { SessionPart } from "../types/parts";
5
5
  import { isViewerMessage, MessageAuthor } from "./message-author";
@@ -23,6 +23,11 @@ export interface UserMessageProps {
23
23
  * someone else's: it sits at the start, on a card, under its author.
24
24
  */
25
25
  viewerId?: string;
26
+ /**
27
+ * Show a long message's first lines with a fade and a "Show more" inside the
28
+ * bubble, which opens it in place. Omit to show the whole message.
29
+ */
30
+ collapsedLines?: number;
26
31
  }
27
32
 
28
33
  function formatTime(date: Date): string {
@@ -37,10 +42,58 @@ function MessageActions({ children, align = "end" }: { children: ReactNode; alig
37
42
  );
38
43
  }
39
44
 
40
- function UserBubble({ text, timestamp, actions }: {
45
+ /**
46
+ * A message body that can fold to its first `lines` lines: the last shown line
47
+ * fades out and a "Show more" inside the bubble opens the rest in place. The
48
+ * button appears only when the text overflows.
49
+ */
50
+ function BubbleText({ text, lines }: { text: string; lines?: number }) {
51
+ const body = useRef<HTMLDivElement>(null);
52
+ const [open, setOpen] = useState(false);
53
+ const [over, setOver] = useState(false);
54
+ useLayoutEffect(() => {
55
+ const element = body.current;
56
+ if (!element || !lines || open) return;
57
+ const measure = () => setOver(element.scrollHeight > element.clientHeight + 1);
58
+ measure();
59
+ if (typeof ResizeObserver === "undefined") return;
60
+ const observer = new ResizeObserver(measure);
61
+ observer.observe(element);
62
+ return () => observer.disconnect();
63
+ }, [lines, open, text]);
64
+ const folded = Boolean(lines) && !open;
65
+ return (
66
+ <>
67
+ <div
68
+ ref={body}
69
+ className={`break-words whitespace-pre-wrap ${TRANSCRIPT_TEXT}`}
70
+ style={folded ? {
71
+ maxHeight: `${lines}lh`,
72
+ overflow: "hidden",
73
+ ...(over ? { maskImage: "linear-gradient(to bottom, #000 calc(100% - 1.5lh), transparent)", WebkitMaskImage: "linear-gradient(to bottom, #000 calc(100% - 1.5lh), transparent)" } : {}),
74
+ } : undefined}
75
+ >
76
+ {text}
77
+ </div>
78
+ {lines && (over || open) ? (
79
+ <button
80
+ type="button"
81
+ aria-expanded={open}
82
+ onClick={() => setOpen(!open)}
83
+ className="mt-1.5 text-sm font-medium underline-offset-2 opacity-75 transition-opacity hover:underline hover:opacity-100"
84
+ >
85
+ {open ? "Show less" : "Show more"}
86
+ </button>
87
+ ) : null}
88
+ </>
89
+ );
90
+ }
91
+
92
+ function UserBubble({ text, timestamp, actions, lines }: {
41
93
  text: string;
42
94
  timestamp?: Date;
43
95
  actions?: ReactNode;
96
+ lines?: number;
44
97
  }) {
45
98
  return (
46
99
  <div className="flex justify-end">
@@ -49,14 +102,12 @@ function UserBubble({ text, timestamp, actions }: {
49
102
  {timestamp ? (
50
103
  <span
51
104
  data-user-message-time=""
52
- className="pointer-events-none absolute right-full top-1/2 mr-3 -translate-y-1/2 whitespace-nowrap text-[length:var(--font-size-xs)] text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100"
105
+ className="pointer-events-none absolute right-full top-1/2 mr-3 -translate-y-1/2 whitespace-nowrap text-sm text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100"
53
106
  >
54
107
  {formatTime(timestamp)}
55
108
  </span>
56
109
  ) : null}
57
- <div className={`break-words whitespace-pre-wrap ${TRANSCRIPT_TEXT}`}>
58
- {text}
59
- </div>
110
+ <BubbleText text={text} lines={lines} />
60
111
  </div>
61
112
  {actions ? <MessageActions>{actions}</MessageActions> : null}
62
113
  </div>
@@ -69,10 +120,11 @@ function UserBubble({ text, timestamp, actions }: {
69
120
  * than the reader's inverse fill, so "mine" and "theirs" differ before the
70
121
  * names are read. The timestamp is revealed on the bubble's far side.
71
122
  */
72
- function OtherPersonBubble({ text, timestamp, actions, author }: {
123
+ function OtherPersonBubble({ text, timestamp, actions, author, lines }: {
73
124
  text: string;
74
125
  timestamp?: Date;
75
126
  actions?: ReactNode;
127
+ lines?: number;
76
128
  /** The author line; absent on a later segment of the same message. */
77
129
  author?: ChatAuthor;
78
130
  }) {
@@ -84,14 +136,12 @@ function OtherPersonBubble({ text, timestamp, actions, author }: {
84
136
  {timestamp ? (
85
137
  <span
86
138
  data-user-message-time=""
87
- className="pointer-events-none absolute left-full top-1/2 ml-3 -translate-y-1/2 whitespace-nowrap text-[length:var(--font-size-xs)] text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100"
139
+ className="pointer-events-none absolute left-full top-1/2 ml-3 -translate-y-1/2 whitespace-nowrap text-sm text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100"
88
140
  >
89
141
  {formatTime(timestamp)}
90
142
  </span>
91
143
  ) : null}
92
- <div className={`break-words whitespace-pre-wrap ${TRANSCRIPT_TEXT}`}>
93
- {text}
94
- </div>
144
+ <BubbleText text={text} lines={lines} />
95
145
  </div>
96
146
  {actions ? <MessageActions align="start">{actions}</MessageActions> : null}
97
147
  </div>
@@ -104,14 +154,14 @@ function OtherPersonBubble({ text, timestamp, actions, author }: {
104
154
  * Application notes interrupt the bubble in source order, outside its attribution.
105
155
  */
106
156
  export const UserMessage = memo(
107
- ({ message: _message, parts, content, timestamp, actions, author, viewerId }: UserMessageProps) => {
157
+ ({ message: _message, parts, content, timestamp, actions, author, viewerId, collapsedLines }: UserMessageProps) => {
108
158
  const other = isViewerMessage(author, viewerId) ? undefined : author;
109
159
  // `headed` puts the author line on the first spoken segment only.
110
160
  const bubble = (text: string, bubbleActions?: ReactNode, headed = true, key?: number) =>
111
161
  other ? (
112
- <OtherPersonBubble key={key} text={text} timestamp={timestamp} actions={bubbleActions} author={headed ? other : undefined} />
162
+ <OtherPersonBubble key={key} text={text} timestamp={timestamp} actions={bubbleActions} author={headed ? other : undefined} lines={collapsedLines} />
113
163
  ) : (
114
- <UserBubble key={key} text={text} timestamp={timestamp} actions={bubbleActions} />
164
+ <UserBubble key={key} text={text} timestamp={timestamp} actions={bubbleActions} lines={collapsedLines} />
115
165
  );
116
166
  // Explicit direct content (including empty content) still overrides parts.
117
167
  if (content != null) {
@@ -0,0 +1,28 @@
1
+ import type { Meta, StoryObj } from '@storybook/react'
2
+ import { useState } from 'react'
3
+ import { FileTabs, type FileTabData } from './file-tabs'
4
+
5
+ const meta: Meta<typeof FileTabs> = { title: 'Files/FileTabs', component: FileTabs, parameters: { layout: 'fullscreen' } }
6
+ export default meta
7
+
8
+ const TABS: FileTabData[] = [
9
+ { id: 'a', name: 'CHECK.md', path: 'code/route-a-lead-2/CHECK.md' },
10
+ { id: 'b', name: 'CHECK.md', path: 'code/rs3-merge-lead/CHECK.md' },
11
+ { id: 'c', name: 'approaches.md', path: 'pages/magnetar-rust/approaches.md' },
12
+ { id: 'd', name: 'gaps.json', path: 'code/route-a-lead-2/gaps.json', dirty: true },
13
+ ]
14
+
15
+ /** Two files of one name tell themselves apart by folder; inactive tabs keep their slot with a divider. */
16
+ export const SameNames: StoryObj<typeof FileTabs> = {
17
+ render: () => {
18
+ const [active, setActive] = useState('c')
19
+ return (
20
+ <div className="bg-[var(--bg-root)] p-6">
21
+ <div className="rounded-[var(--radius-xl)] border border-border bg-background">
22
+ <FileTabs tabs={TABS} activeId={active} onSelect={setActive} onClose={() => {}} />
23
+ <div className="h-40 bg-card" />
24
+ </div>
25
+ </div>
26
+ )
27
+ },
28
+ }
@@ -63,4 +63,21 @@ describe("FileTabs", () => {
63
63
  fireEvent.click(screen.getByRole("button", { name: "Close README.md" }));
64
64
  expect(screen.getAllByRole("tab")).toHaveLength(2);
65
65
  });
66
+
67
+ it("tells two open files of one name apart by their nearest differing folder", () => {
68
+ render(
69
+ <FileTabs
70
+ tabs={[
71
+ { id: "x", name: "CHECK.md", path: "code/route-a-lead-2/CHECK.md" },
72
+ { id: "y", name: "CHECK.md", path: "code/rs3-merge-lead/CHECK.md" },
73
+ { id: "z", name: "plan.md", path: "pages/plan.md" },
74
+ ]}
75
+ activeId="x"
76
+ onSelect={() => {}}
77
+ onClose={() => {}}
78
+ />,
79
+ );
80
+ expect(screen.getAllByRole("tab").map((tab) => tab.getAttribute("aria-label"))).toEqual(["CHECK.md in route-a-lead-2", "CHECK.md in rs3-merge-lead", "plan.md"]);
81
+ expect(screen.getByText("· route-a-lead-2")).toBeInTheDocument();
82
+ });
66
83
  });
@@ -36,6 +36,28 @@ function getTabIcon(name: string) {
36
36
  return FileCode;
37
37
  }
38
38
 
39
+ /**
40
+ * Each tab's folder hint: two open files with one name ("CHECK.md" twice) each show the nearest parent folder that
41
+ * tells them apart, as an editor does ("CHECK.md · route-a-lead-2"). A name no other tab shares shows none.
42
+ */
43
+ function folderHints(tabs: readonly FileTabData[]): Map<string, string> {
44
+ const hints = new Map<string, string>();
45
+ const byName = new Map<string, FileTabData[]>();
46
+ for (const tab of tabs) byName.set(tab.name, [...(byName.get(tab.name) ?? []), tab]);
47
+ for (const same of byName.values()) {
48
+ if (same.length < 2) continue;
49
+ const parents = same.map((tab) => tab.path.replace(/\/+$/, "").split("/").slice(0, -1).reverse());
50
+ same.forEach((tab, i) => {
51
+ const own = parents[i]!;
52
+ // The first folder, walking up, that no other same-named tab has at that depth.
53
+ const depth = own.findIndex((folder, d) => parents.every((other, j) => j === i || other[d] !== folder));
54
+ const hint = own[depth >= 0 ? depth : 0];
55
+ if (hint) hints.set(tab.id, hint);
56
+ });
57
+ }
58
+ return hints;
59
+ }
60
+
39
61
  /** A long name keeps its extension and the characters before it after the ellipsis. */
40
62
  function splitName(name: string): [string, string] {
41
63
  if (name.length <= 18) return [name, ""];
@@ -55,6 +77,7 @@ export function FileTabs({ tabs, activeId, onSelect, onClose, label = "Open file
55
77
  }, [activeId, tabs.length]);
56
78
 
57
79
  if (tabs.length === 0) return null;
80
+ const hints = folderHints(tabs);
58
81
 
59
82
  const tabButtons = () => [...(stripRef.current?.querySelectorAll<HTMLButtonElement>('[role="tab"]') ?? [])];
60
83
  const onKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
@@ -84,8 +107,12 @@ export function FileTabs({ tabs, activeId, onSelect, onClose, label = "Open file
84
107
  className,
85
108
  )}
86
109
  >
87
- {tabs.map((tab) => {
110
+ {tabs.map((tab, index) => {
88
111
  const isActive = tab.id === activeId;
112
+ // An inactive tab keeps a tab's slot: a 1px divider on its right, except beside the active tab, so the strip
113
+ // reads as tabs rather than loose words.
114
+ const divided = !isActive && tabs[index + 1] !== undefined && tabs[index + 1]!.id !== activeId;
115
+ const hint = hints.get(tab.id);
89
116
  const Icon = getTabIcon(tab.name);
90
117
  const [head, tail] = splitName(tab.name);
91
118
 
@@ -99,13 +126,14 @@ export function FileTabs({ tabs, activeId, onSelect, onClose, label = "Open file
99
126
  isActive
100
127
  ? "border-border bg-card text-foreground"
101
128
  : "border-transparent text-muted-foreground hover:bg-[var(--ix-hover-bg)] hover:text-foreground",
129
+ divided && "after:absolute after:-right-[3px] after:top-1/2 after:h-4 after:w-px after:-translate-y-1/2 after:bg-border",
102
130
  )}
103
131
  >
104
132
  <button
105
133
  type="button"
106
134
  role="tab"
107
135
  aria-selected={isActive}
108
- aria-label={tab.dirty ? `${tab.name}, unsaved changes` : tab.name}
136
+ aria-label={`${tab.name}${hint ? ` in ${hint}` : ""}${tab.dirty ? ", unsaved changes" : ""}`}
109
137
  tabIndex={isActive || (!activeId && tab === tabs[0]) ? 0 : -1}
110
138
  onClick={() => onSelect(tab.id)}
111
139
  className={cn("flex h-full min-w-0 items-center gap-1.5 rounded-t-lg pl-3 pr-1", focusRingInset)}
@@ -115,6 +143,7 @@ export function FileTabs({ tabs, activeId, onSelect, onClose, label = "Open file
115
143
  <span className="overflow-hidden text-ellipsis whitespace-pre">{head}</span>
116
144
  {tail && <span className="shrink-0 whitespace-pre">{tail}</span>}
117
145
  </span>
146
+ {hint && <span className="min-w-0 truncate text-muted-foreground">· {hint}</span>}
118
147
  {tab.dirty && <span aria-hidden className="size-1.5 shrink-0 rounded-full bg-[var(--accent-text)]" />}
119
148
  </button>
120
149
  <button
@@ -271,7 +271,7 @@ export function SegmentedControl<T extends string = string>({
271
271
  {option.adornment && (
272
272
  <span
273
273
  className={cn(
274
- "text-xs",
274
+ "text-sm tabular-nums",
275
275
  active ? "text-[var(--accent-text)]/80" : "text-muted-foreground",
276
276
  )}
277
277
  >
@@ -113,7 +113,7 @@ export function RunRowShell({
113
113
  data-run-row=""
114
114
  data-state={open ? "open" : "closed"}
115
115
  className={cn(
116
- "group min-w-0 flex-1 overflow-hidden border transition-colors",
116
+ "min-w-0 flex-1 overflow-hidden border transition-colors",
117
117
  open
118
118
  ? "border-border bg-[var(--md3-surface-container)]"
119
119
  : "border-transparent bg-transparent",
@@ -125,8 +125,11 @@ export function RunRowShell({
125
125
  <Collapsible.Trigger asChild disabled={!expandable}>
126
126
  <button
127
127
  type="button"
128
+ // The header is the hover group, not the whole card: rows nested in an
129
+ // open row (a fold's steps) keep their own glyphs while the
130
+ // pointer is over them.
128
131
  className={cn(
129
- "flex w-full items-center gap-2 text-left",
132
+ "group flex w-full items-center gap-2 text-left",
130
133
  HEADER_INSET,
131
134
  expandable ? "cursor-pointer" : "cursor-default",
132
135
  )}