@tangle-network/ui 1.0.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 (220) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/LICENSE +21 -0
  3. package/README.md +33 -0
  4. package/dist/active-sessions-store-CeOmXgv5.d.ts +85 -0
  5. package/dist/artifact-pane-DvJyPWV4.d.ts +24 -0
  6. package/dist/auth.d.ts +74 -0
  7. package/dist/auth.js +15 -0
  8. package/dist/button-CMQuQEW_.d.ts +17 -0
  9. package/dist/chat.d.ts +232 -0
  10. package/dist/chat.js +30 -0
  11. package/dist/chunk-2NFQRQOD.js +1009 -0
  12. package/dist/chunk-2VH6PUXD.js +186 -0
  13. package/dist/chunk-34A66VBG.js +214 -0
  14. package/dist/chunk-3OI2QKFD.js +0 -0
  15. package/dist/chunk-4CLN43XT.js +45 -0
  16. package/dist/chunk-54SQQMMM.js +156 -0
  17. package/dist/chunk-5Z5ZYMOJ.js +0 -0
  18. package/dist/chunk-66BNMOVT.js +167 -0
  19. package/dist/chunk-6BGQA4BQ.js +0 -0
  20. package/dist/chunk-7UO2ZMRQ.js +133 -0
  21. package/dist/chunk-BX6AQMUS.js +183 -0
  22. package/dist/chunk-CD53GZOM.js +59 -0
  23. package/dist/chunk-CSAIKY36.js +54 -0
  24. package/dist/chunk-EEE55AVS.js +1201 -0
  25. package/dist/chunk-GYPQXTJU.js +230 -0
  26. package/dist/chunk-HFL6R6IF.js +37 -0
  27. package/dist/chunk-HJKCSXCH.js +737 -0
  28. package/dist/chunk-LISXUB4D.js +1222 -0
  29. package/dist/chunk-LQS34IGP.js +0 -0
  30. package/dist/chunk-MKTSMWVD.js +109 -0
  31. package/dist/chunk-NKDZ7GZE.js +192 -0
  32. package/dist/chunk-OEX7NZE3.js +321 -0
  33. package/dist/chunk-Q56BYXQF.js +61 -0
  34. package/dist/chunk-Q7EIIWTC.js +0 -0
  35. package/dist/chunk-REJESC5U.js +117 -0
  36. package/dist/chunk-RQGKSCEZ.js +0 -0
  37. package/dist/chunk-RQHJBTEU.js +10 -0
  38. package/dist/chunk-TMFOPHHN.js +299 -0
  39. package/dist/chunk-XGKULLYE.js +40 -0
  40. package/dist/chunk-XIHMJ7ZQ.js +614 -0
  41. package/dist/chunk-YJ2G3XO5.js +1048 -0
  42. package/dist/chunk-YNN4O57I.js +754 -0
  43. package/dist/code-block-DjXf8eOG.d.ts +19 -0
  44. package/dist/document-editor-pane-A5LT5H4N.js +12 -0
  45. package/dist/document-editor-pane-DyDEX_Zm.d.ts +124 -0
  46. package/dist/editor.d.ts +120 -0
  47. package/dist/editor.js +34 -0
  48. package/dist/files.d.ts +175 -0
  49. package/dist/files.js +20 -0
  50. package/dist/hooks.d.ts +56 -0
  51. package/dist/hooks.js +41 -0
  52. package/dist/index.d.ts +43 -0
  53. package/dist/index.js +446 -0
  54. package/dist/markdown.d.ts +15 -0
  55. package/dist/markdown.js +14 -0
  56. package/dist/message-BHWbxBtT.d.ts +15 -0
  57. package/dist/openui.d.ts +115 -0
  58. package/dist/openui.js +12 -0
  59. package/dist/parts-dj7AcUg0.d.ts +36 -0
  60. package/dist/primitives.d.ts +332 -0
  61. package/dist/primitives.js +191 -0
  62. package/dist/run-PfLmDAox.d.ts +41 -0
  63. package/dist/run.d.ts +69 -0
  64. package/dist/run.js +36 -0
  65. package/dist/sdk-hooks.d.ts +285 -0
  66. package/dist/sdk-hooks.js +31 -0
  67. package/dist/stores.d.ts +17 -0
  68. package/dist/stores.js +76 -0
  69. package/dist/tool-call-feed-Bs3MyQMT.d.ts +68 -0
  70. package/dist/tool-display-z4JcDmMQ.d.ts +32 -0
  71. package/dist/tool-previews.d.ts +48 -0
  72. package/dist/tool-previews.js +21 -0
  73. package/dist/types.d.ts +19 -0
  74. package/dist/types.js +1 -0
  75. package/dist/utils.d.ts +45 -0
  76. package/dist/utils.js +32 -0
  77. package/package.json +193 -0
  78. package/src/auth/auth.tsx +228 -0
  79. package/src/auth/index.ts +13 -0
  80. package/src/auth/login-layout.tsx +46 -0
  81. package/src/chat/agent-timeline.stories.tsx +429 -0
  82. package/src/chat/agent-timeline.tsx +360 -0
  83. package/src/chat/chat-container.tsx +486 -0
  84. package/src/chat/chat-input.stories.tsx +142 -0
  85. package/src/chat/chat-input.tsx +389 -0
  86. package/src/chat/chat-message.stories.tsx +237 -0
  87. package/src/chat/chat-message.tsx +129 -0
  88. package/src/chat/index.ts +18 -0
  89. package/src/chat/message-list.stories.tsx +336 -0
  90. package/src/chat/message-list.tsx +79 -0
  91. package/src/chat/thinking-indicator.stories.tsx +56 -0
  92. package/src/chat/thinking-indicator.tsx +30 -0
  93. package/src/chat/user-message.stories.tsx +92 -0
  94. package/src/chat/user-message.tsx +43 -0
  95. package/src/editor/document-editor-pane.tsx +351 -0
  96. package/src/editor/editor-provider.tsx +428 -0
  97. package/src/editor/editor-toolbar.tsx +130 -0
  98. package/src/editor/index.ts +31 -0
  99. package/src/editor/markdown-conversion.ts +21 -0
  100. package/src/editor/markdown-document-editor.tsx +137 -0
  101. package/src/editor/tiptap-editor.tsx +331 -0
  102. package/src/editor/use-editor.ts +221 -0
  103. package/src/files/file-artifact-pane.tsx +183 -0
  104. package/src/files/file-preview.tsx +342 -0
  105. package/src/files/file-tabs.tsx +71 -0
  106. package/src/files/file-tree.tsx +258 -0
  107. package/src/files/index.ts +17 -0
  108. package/src/files/rich-file-tree.stories.tsx +104 -0
  109. package/src/files/rich-file-tree.test.tsx +42 -0
  110. package/src/files/rich-file-tree.tsx +232 -0
  111. package/src/hooks/index.ts +10 -0
  112. package/src/hooks/use-auth.ts +153 -0
  113. package/src/hooks/use-auto-scroll.ts +59 -0
  114. package/src/hooks/use-dropdown-menu.ts +40 -0
  115. package/src/hooks/use-live-time.test.tsx +40 -0
  116. package/src/hooks/use-live-time.ts +27 -0
  117. package/src/hooks/use-realtime-session.ts +319 -0
  118. package/src/hooks/use-run-collapse-state.ts +25 -0
  119. package/src/hooks/use-run-groups.ts +111 -0
  120. package/src/hooks/use-sdk-session.ts +575 -0
  121. package/src/hooks/use-sse-stream.ts +475 -0
  122. package/src/hooks/use-tool-call-stream.ts +96 -0
  123. package/src/index.ts +14 -0
  124. package/src/lib/utils.ts +6 -0
  125. package/src/markdown/code-block.tsx +198 -0
  126. package/src/markdown/index.ts +2 -0
  127. package/src/markdown/markdown.stories.tsx +190 -0
  128. package/src/markdown/markdown.tsx +62 -0
  129. package/src/openui/index.ts +20 -0
  130. package/src/openui/openui-artifact-renderer.tsx +542 -0
  131. package/src/primitives/artifact-pane.tsx +91 -0
  132. package/src/primitives/avatar.stories.tsx +95 -0
  133. package/src/primitives/avatar.tsx +47 -0
  134. package/src/primitives/badge.stories.tsx +57 -0
  135. package/src/primitives/badge.tsx +97 -0
  136. package/src/primitives/button.stories.tsx +48 -0
  137. package/src/primitives/button.tsx +115 -0
  138. package/src/primitives/card.stories.tsx +53 -0
  139. package/src/primitives/card.tsx +98 -0
  140. package/src/primitives/code-block.stories.tsx +115 -0
  141. package/src/primitives/code-block.tsx +22 -0
  142. package/src/primitives/design-tokens.stories.tsx +162 -0
  143. package/src/primitives/dialog.stories.tsx +176 -0
  144. package/src/primitives/dialog.tsx +137 -0
  145. package/src/primitives/drop-zone.stories.tsx +123 -0
  146. package/src/primitives/drop-zone.tsx +131 -0
  147. package/src/primitives/dropdown-menu.stories.tsx +122 -0
  148. package/src/primitives/dropdown-menu.tsx +214 -0
  149. package/src/primitives/empty-state.stories.tsx +81 -0
  150. package/src/primitives/empty-state.tsx +40 -0
  151. package/src/primitives/index.ts +118 -0
  152. package/src/primitives/input.stories.tsx +113 -0
  153. package/src/primitives/input.tsx +136 -0
  154. package/src/primitives/label.stories.tsx +84 -0
  155. package/src/primitives/label.tsx +24 -0
  156. package/src/primitives/progress.stories.tsx +93 -0
  157. package/src/primitives/progress.tsx +50 -0
  158. package/src/primitives/segmented-control.test.tsx +328 -0
  159. package/src/primitives/segmented-control.tsx +154 -0
  160. package/src/primitives/select.stories.tsx +164 -0
  161. package/src/primitives/select.tsx +158 -0
  162. package/src/primitives/sidebar-drop-zone.stories.tsx +100 -0
  163. package/src/primitives/sidebar-drop-zone.tsx +149 -0
  164. package/src/primitives/skeleton.stories.tsx +79 -0
  165. package/src/primitives/skeleton.tsx +55 -0
  166. package/src/primitives/stat-card.stories.tsx +137 -0
  167. package/src/primitives/stat-card.tsx +97 -0
  168. package/src/primitives/switch.stories.tsx +85 -0
  169. package/src/primitives/switch.tsx +28 -0
  170. package/src/primitives/table.stories.tsx +170 -0
  171. package/src/primitives/table.tsx +116 -0
  172. package/src/primitives/tabs.stories.tsx +180 -0
  173. package/src/primitives/tabs.tsx +71 -0
  174. package/src/primitives/terminal-display.stories.tsx +191 -0
  175. package/src/primitives/terminal-display.tsx +189 -0
  176. package/src/primitives/theme-toggle.stories.tsx +32 -0
  177. package/src/primitives/theme-toggle.tsx +96 -0
  178. package/src/primitives/toast.stories.tsx +155 -0
  179. package/src/primitives/toast.tsx +190 -0
  180. package/src/primitives/upload-progress.stories.tsx +120 -0
  181. package/src/primitives/upload-progress.tsx +110 -0
  182. package/src/run/expanded-tool-detail.stories.tsx +182 -0
  183. package/src/run/expanded-tool-detail.tsx +186 -0
  184. package/src/run/index.ts +13 -0
  185. package/src/run/inline-thinking-item.stories.tsx +136 -0
  186. package/src/run/inline-thinking-item.tsx +120 -0
  187. package/src/run/inline-tool-item.stories.tsx +222 -0
  188. package/src/run/inline-tool-item.tsx +190 -0
  189. package/src/run/run-group.stories.tsx +322 -0
  190. package/src/run/run-group.tsx +569 -0
  191. package/src/run/run-item-primitives.tsx +17 -0
  192. package/src/run/tool-call-feed.stories.tsx +294 -0
  193. package/src/run/tool-call-feed.tsx +192 -0
  194. package/src/run/tool-call-step.stories.tsx +198 -0
  195. package/src/run/tool-call-step.tsx +240 -0
  196. package/src/sdk-hooks.ts +38 -0
  197. package/src/stores/active-sessions-store.ts +455 -0
  198. package/src/stores/chat-store.ts +43 -0
  199. package/src/stores/index.ts +2 -0
  200. package/src/tool-previews/command-preview.tsx +116 -0
  201. package/src/tool-previews/diff-preview.tsx +85 -0
  202. package/src/tool-previews/glob-results-preview.tsx +98 -0
  203. package/src/tool-previews/grep-results-preview.tsx +157 -0
  204. package/src/tool-previews/index.ts +22 -0
  205. package/src/tool-previews/preview-primitives.tsx +84 -0
  206. package/src/tool-previews/question-preview.tsx +101 -0
  207. package/src/tool-previews/web-search-preview.tsx +117 -0
  208. package/src/tool-previews/write-file-preview.tsx +80 -0
  209. package/src/types/branding.ts +11 -0
  210. package/src/types/index.ts +5 -0
  211. package/src/types/message.ts +13 -0
  212. package/src/types/parts.ts +51 -0
  213. package/src/types/run.ts +56 -0
  214. package/src/types/tool-display.ts +41 -0
  215. package/src/utils/copy-text.ts +30 -0
  216. package/src/utils/format.test.ts +43 -0
  217. package/src/utils/format.ts +56 -0
  218. package/src/utils/index.ts +10 -0
  219. package/src/utils/time-ago.ts +9 -0
  220. package/src/utils/tool-display.ts +238 -0
@@ -0,0 +1,51 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Part primitives — generic equivalents of blueprint-agent's
3
+ // DevContainerSessionPart, decoupled from @opencode-ai/sdk.
4
+ // ---------------------------------------------------------------------------
5
+
6
+ export interface TextPart {
7
+ type: 'text';
8
+ text: string;
9
+ /** If true this text was synthesised client-side (e.g. echo of user input). */
10
+ synthetic?: boolean;
11
+ }
12
+
13
+ // -- Tool parts -------------------------------------------------------------
14
+
15
+ export type ToolStatus = 'pending' | 'running' | 'completed' | 'error';
16
+
17
+ export interface ToolTime {
18
+ start?: number;
19
+ end?: number;
20
+ }
21
+
22
+ export interface ToolState {
23
+ status: ToolStatus;
24
+ input?: unknown;
25
+ output?: unknown;
26
+ error?: string;
27
+ metadata?: Record<string, unknown>;
28
+ time?: ToolTime;
29
+ }
30
+
31
+ export interface ToolPart {
32
+ type: 'tool';
33
+ /** Unique ID for this tool invocation. */
34
+ id: string;
35
+ /** Tool name (e.g. "bash", "read", "write", "grep", "glob"). */
36
+ tool: string;
37
+ state: ToolState;
38
+ callID?: string;
39
+ }
40
+
41
+ // -- Reasoning parts --------------------------------------------------------
42
+
43
+ export interface ReasoningPart {
44
+ type: 'reasoning';
45
+ text: string;
46
+ time?: ToolTime;
47
+ }
48
+
49
+ // -- Union ------------------------------------------------------------------
50
+
51
+ export type SessionPart = TextPart | ToolPart | ReasoningPart;
@@ -0,0 +1,56 @@
1
+ import type { SessionMessage } from "./message";
2
+
3
+ /** Broad category of a tool invocation, used for display grouping. */
4
+ export type ToolCategory =
5
+ | "command"
6
+ | "write"
7
+ | "read"
8
+ | "search"
9
+ | "edit"
10
+ | "task"
11
+ | "web"
12
+ | "todo"
13
+ | "other";
14
+
15
+ export interface RunStats {
16
+ toolCount: number;
17
+ messageCount: number;
18
+ thinkingDurationMs: number;
19
+ textPartCount: number;
20
+ toolCategories: Set<ToolCategory>;
21
+ }
22
+
23
+ export interface FinalTextPart {
24
+ messageId: string;
25
+ partIndex: number;
26
+ text: string;
27
+ }
28
+
29
+ /**
30
+ * A Run is a consecutive group of assistant messages that form one
31
+ * logical "turn" of the agent. Runs are collapsible in the UI and
32
+ * show a summary header when collapsed.
33
+ */
34
+ export interface Run {
35
+ id: string;
36
+ messages: SessionMessage[];
37
+ isComplete: boolean;
38
+ isStreaming: boolean;
39
+ stats: RunStats;
40
+ summaryText: string | null;
41
+ finalTextPart: FinalTextPart | null;
42
+ }
43
+
44
+ // -- Grouped messages for rendering -----------------------------------------
45
+
46
+ export interface MessageRun {
47
+ type: "run";
48
+ run: Run;
49
+ }
50
+
51
+ export interface MessageUser {
52
+ type: "user";
53
+ message: SessionMessage;
54
+ }
55
+
56
+ export type GroupedMessage = MessageRun | MessageUser;
@@ -0,0 +1,41 @@
1
+ import type { ToolPart } from "./parts";
2
+ import type { ReactNode } from "react";
3
+
4
+ /**
5
+ * Variant-specific rendering instructions for tool output.
6
+ * Maps directly to specialised preview components.
7
+ */
8
+ export type DisplayVariant =
9
+ | "command"
10
+ | "write-file"
11
+ | "read-file"
12
+ | "diff"
13
+ | "question"
14
+ | "web-search"
15
+ | "grep"
16
+ | "glob"
17
+ | "default";
18
+
19
+ /**
20
+ * Custom renderer for tool details. Return a ReactNode to override the
21
+ * default ExpandedToolDetail, or null to fall back to the built-in renderer.
22
+ */
23
+ export type CustomToolRenderer = (part: ToolPart) => ReactNode | null;
24
+
25
+ /**
26
+ * Visual metadata for a tool invocation — computed from the tool name,
27
+ * input, and output by `getToolDisplayMetadata()`.
28
+ */
29
+ export interface ToolDisplayMetadata {
30
+ title: string;
31
+ description?: string;
32
+ inputTitle?: string;
33
+ outputTitle?: string;
34
+ inputLanguage?: string;
35
+ outputLanguage?: string;
36
+ hasDiffOutput?: boolean;
37
+ diffFilePath?: string;
38
+ displayVariant?: DisplayVariant;
39
+ commandSnippet?: string;
40
+ targetPath?: string;
41
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Copy text to clipboard with a non-secure-context fallback.
3
+ * Returns true if a copy strategy likely succeeded.
4
+ */
5
+ export async function copyText(text: string): Promise<boolean> {
6
+ if (typeof navigator !== 'undefined' && navigator.clipboard?.writeText) {
7
+ try {
8
+ await navigator.clipboard.writeText(text);
9
+ return true;
10
+ } catch {
11
+ // Fall through to legacy fallback.
12
+ }
13
+ }
14
+
15
+ if (typeof document === 'undefined') return false;
16
+
17
+ try {
18
+ const textarea = document.createElement('textarea');
19
+ textarea.value = text;
20
+ textarea.style.position = 'fixed';
21
+ textarea.style.opacity = '0';
22
+ document.body.appendChild(textarea);
23
+ textarea.select();
24
+ const copied = document.execCommand('copy');
25
+ document.body.removeChild(textarea);
26
+ return copied;
27
+ } catch {
28
+ return false;
29
+ }
30
+ }
@@ -0,0 +1,43 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { formatBytes, formatUptime } from "./format";
3
+
4
+ describe("formatUptime", () => {
5
+ it("renders seconds for short durations", () => {
6
+ expect(formatUptime(0)).toBe("0s");
7
+ expect(formatUptime(7_500)).toBe("7s");
8
+ });
9
+
10
+ it("renders minutes and seconds under an hour", () => {
11
+ expect(formatUptime(90_000)).toBe("1m 30s");
12
+ expect(formatUptime(59 * 60_000 + 12_000)).toBe("59m 12s");
13
+ });
14
+
15
+ it("renders hours and minutes under a day", () => {
16
+ expect(formatUptime(3_600_000)).toBe("1h 0m");
17
+ expect(formatUptime(5 * 3_600_000 + 30 * 60_000)).toBe("5h 30m");
18
+ });
19
+
20
+ it("renders days and hours for multi-day durations", () => {
21
+ expect(formatUptime(86_400_000)).toBe("1d 0h");
22
+ expect(formatUptime(86_400_000 * 3 + 3_600_000 * 4)).toBe("3d 4h");
23
+ });
24
+
25
+ it("handles invalid input", () => {
26
+ expect(formatUptime(Number.NaN)).toBe("—");
27
+ expect(formatUptime(-1)).toBe("—");
28
+ });
29
+ });
30
+
31
+ describe("formatBytes", () => {
32
+ it("renders bytes, KB, MB, GB progressively", () => {
33
+ expect(formatBytes(512)).toBe("512 B");
34
+ expect(formatBytes(2_048)).toBe("2.0 KB");
35
+ expect(formatBytes(5 * 1024 * 1024)).toBe("5.0 MB");
36
+ expect(formatBytes(2.5 * 1024 * 1024 * 1024)).toBe("2.50 GB");
37
+ });
38
+
39
+ it("handles invalid input", () => {
40
+ expect(formatBytes(Number.NaN)).toBe("—");
41
+ expect(formatBytes(-1)).toBe("—");
42
+ });
43
+ });
@@ -0,0 +1,56 @@
1
+ /** Format a duration in milliseconds to a human-readable string. */
2
+ export function formatDuration(ms: number): string {
3
+ if (ms < 1000) return "<1s";
4
+ const seconds = Math.floor(ms / 1000);
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
+
11
+ /** Truncate text to `max` characters, appending "..." if truncated. */
12
+ export function truncateText(text: string, max: number): string {
13
+ const cleaned = text.replace(/\s+/g, " ").trim();
14
+ if (cleaned.length <= max) return cleaned;
15
+ return cleaned.slice(0, max).trim() + "...";
16
+ }
17
+
18
+ /**
19
+ * Format an uptime duration in milliseconds with progressive
20
+ * granularity, so short-lived sandboxes don't render as "0d 0h".
21
+ * - < 60s → "Ns"
22
+ * - < 60m → "Nm Ss"
23
+ * - < 24h → "Nh Mm"
24
+ * - otherwise → "Nd Hh"
25
+ */
26
+ export function formatUptime(ms: number): string {
27
+ if (!Number.isFinite(ms) || ms < 0) return "—";
28
+ const totalSeconds = Math.floor(ms / 1000);
29
+ if (totalSeconds < 60) return `${totalSeconds}s`;
30
+ const minutes = Math.floor(totalSeconds / 60);
31
+ const seconds = totalSeconds % 60;
32
+ if (minutes < 60) return `${minutes}m ${seconds}s`;
33
+ const hours = Math.floor(minutes / 60);
34
+ const remMinutes = minutes % 60;
35
+ if (hours < 24) return `${hours}h ${remMinutes}m`;
36
+ const days = Math.floor(hours / 24);
37
+ const remHours = hours % 24;
38
+ return `${days}d ${remHours}h`;
39
+ }
40
+
41
+ /**
42
+ * Format a byte count using binary units (KiB/MiB/GiB, surfaced as
43
+ * "KB/MB/GB" for readability). KB and MB use one decimal below 10 and
44
+ * round above; GB keeps two decimals below 10 so half-GB changes stay
45
+ * visible on memory dashboards, and drops to one decimal above.
46
+ */
47
+ export function formatBytes(bytes: number): string {
48
+ if (!Number.isFinite(bytes) || bytes < 0) return "—";
49
+ if (bytes < 1024) return `${Math.round(bytes)} B`;
50
+ const kb = bytes / 1024;
51
+ if (kb < 1024) return `${kb < 10 ? kb.toFixed(1) : Math.round(kb)} KB`;
52
+ const mb = kb / 1024;
53
+ if (mb < 1024) return `${mb < 10 ? mb.toFixed(1) : Math.round(mb)} MB`;
54
+ const gb = mb / 1024;
55
+ return `${gb < 10 ? gb.toFixed(2) : gb.toFixed(1)} GB`;
56
+ }
@@ -0,0 +1,10 @@
1
+ export { copyText } from './copy-text';
2
+ export { formatBytes, formatDuration, formatUptime, truncateText } from './format';
3
+ export { timeAgo } from './time-ago';
4
+ export {
5
+ getToolCategory,
6
+ getToolDisplayMetadata,
7
+ getToolErrorText,
8
+ TOOL_CATEGORY_ICONS,
9
+ } from './tool-display';
10
+ export { cn } from '../lib/utils';
@@ -0,0 +1,9 @@
1
+ export function timeAgo(ts: number): string {
2
+ const secs = Math.floor((Date.now() - ts) / 1000);
3
+ if (secs < 5) return 'just now';
4
+ if (secs < 60) return `${secs}s ago`;
5
+ const mins = Math.floor(secs / 60);
6
+ if (mins < 60) return `${mins}m ago`;
7
+ const hrs = Math.floor(mins / 60);
8
+ return `${hrs}h ago`;
9
+ }
@@ -0,0 +1,238 @@
1
+ import type { ToolPart } from "../types/parts";
2
+ import type { ToolDisplayMetadata } from "../types/tool-display";
3
+ import type { ToolCategory } from "../types/run";
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // Tool name normalisation
7
+ // ---------------------------------------------------------------------------
8
+
9
+ const TOOL_NAME_PREFIX = "tool:";
10
+
11
+ function normalizeToolName(tool: string | undefined): string {
12
+ const n = tool?.toLowerCase() ?? "";
13
+ return n.startsWith(TOOL_NAME_PREFIX) ? n.slice(TOOL_NAME_PREFIX.length) : n;
14
+ }
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // Input helpers
18
+ // ---------------------------------------------------------------------------
19
+
20
+ function extractString(obj: unknown, key: string): string | undefined {
21
+ if (
22
+ typeof obj === "object" &&
23
+ obj !== null &&
24
+ key in (obj as Record<string, unknown>)
25
+ ) {
26
+ const val = (obj as Record<string, unknown>)[key];
27
+ if (typeof val === "string") return val;
28
+ }
29
+ return undefined;
30
+ }
31
+
32
+ function extractCommand(input: unknown): string | undefined {
33
+ if (typeof input === "string") return input;
34
+ return extractString(input, "command") ?? extractString(input, "cmd");
35
+ }
36
+
37
+ function extractFilePath(input: unknown): string | undefined {
38
+ return (
39
+ extractString(input, "file_path") ??
40
+ extractString(input, "path") ??
41
+ extractString(input, "filePath") ??
42
+ extractString(input, "file")
43
+ );
44
+ }
45
+
46
+ function cleanPath(path: string): string {
47
+ const parts = path.split("/");
48
+ return parts.length > 3 ? ".../" + parts.slice(-2).join("/") : path;
49
+ }
50
+
51
+ // ---------------------------------------------------------------------------
52
+ // Category icons (emoji shorthand used by compact views)
53
+ // ---------------------------------------------------------------------------
54
+
55
+ export const TOOL_CATEGORY_ICONS: Record<ToolCategory, string> = {
56
+ command: "terminal",
57
+ write: "file-plus",
58
+ read: "file-text",
59
+ search: "search",
60
+ edit: "file-edit",
61
+ task: "cpu",
62
+ web: "globe",
63
+ todo: "check-square",
64
+ other: "box",
65
+ };
66
+
67
+ // ---------------------------------------------------------------------------
68
+ // Category classification
69
+ // ---------------------------------------------------------------------------
70
+
71
+ export function getToolCategory(toolName: string): ToolCategory {
72
+ const name = normalizeToolName(toolName);
73
+ switch (name) {
74
+ case "bash":
75
+ case "shell":
76
+ case "command":
77
+ case "execute":
78
+ return "command";
79
+ case "write":
80
+ case "write_file":
81
+ case "create_file":
82
+ return "write";
83
+ case "read":
84
+ case "read_file":
85
+ case "cat":
86
+ return "read";
87
+ case "grep":
88
+ case "search":
89
+ case "rg":
90
+ return "search";
91
+ case "edit":
92
+ case "patch":
93
+ case "sed":
94
+ return "edit";
95
+ case "glob":
96
+ case "find":
97
+ case "ls":
98
+ return "search";
99
+ case "web_search":
100
+ case "web_fetch":
101
+ case "fetch":
102
+ return "web";
103
+ case "task":
104
+ case "agent":
105
+ case "spawn":
106
+ return "task";
107
+ case "todo":
108
+ case "todo_write":
109
+ return "todo";
110
+ default:
111
+ return "other";
112
+ }
113
+ }
114
+
115
+ // ---------------------------------------------------------------------------
116
+ // Main metadata resolver
117
+ // ---------------------------------------------------------------------------
118
+
119
+ export function getToolDisplayMetadata(part: ToolPart): ToolDisplayMetadata {
120
+ const name = normalizeToolName(part.tool);
121
+ const input = part.state.status !== "pending" ? part.state.input : undefined;
122
+ const filePath = extractFilePath(input);
123
+ const command = extractCommand(input);
124
+
125
+ switch (name) {
126
+ case "bash":
127
+ case "shell":
128
+ case "command":
129
+ case "execute":
130
+ return {
131
+ title: "Run command",
132
+ description: command ? truncateCommand(command) : undefined,
133
+ displayVariant: "command",
134
+ commandSnippet: command,
135
+ };
136
+
137
+ case "write":
138
+ case "write_file":
139
+ case "create_file":
140
+ return {
141
+ title: filePath ? `Write ${cleanPath(filePath)}` : "Write file",
142
+ description: filePath,
143
+ displayVariant: "write-file",
144
+ targetPath: filePath,
145
+ };
146
+
147
+ case "edit":
148
+ case "patch":
149
+ return {
150
+ title: filePath ? `Edit ${cleanPath(filePath)}` : "Edit file",
151
+ description: filePath,
152
+ hasDiffOutput: true,
153
+ diffFilePath: filePath,
154
+ displayVariant: "diff",
155
+ targetPath: filePath,
156
+ };
157
+
158
+ case "read":
159
+ case "read_file":
160
+ case "cat":
161
+ return {
162
+ title: filePath ? `Read ${cleanPath(filePath)}` : "Read file",
163
+ description: filePath,
164
+ displayVariant: "read-file",
165
+ targetPath: filePath,
166
+ };
167
+
168
+ case "grep":
169
+ case "search":
170
+ case "rg": {
171
+ const pattern = extractString(input, "pattern");
172
+ return {
173
+ title: pattern ? `Search: ${pattern}` : "Search",
174
+ description: pattern,
175
+ displayVariant: "grep",
176
+ };
177
+ }
178
+
179
+ case "glob":
180
+ case "find":
181
+ case "ls": {
182
+ const pattern = extractString(input, "pattern");
183
+ return {
184
+ title: pattern ? `Find: ${pattern}` : "Find files",
185
+ description: pattern,
186
+ displayVariant: "glob",
187
+ };
188
+ }
189
+
190
+ case "web_search":
191
+ case "web_fetch":
192
+ case "fetch": {
193
+ const query =
194
+ extractString(input, "query") ?? extractString(input, "url");
195
+ return {
196
+ title: query ? `Web: ${truncateCommand(query)}` : "Web search",
197
+ description: query,
198
+ displayVariant: "web-search",
199
+ };
200
+ }
201
+
202
+ case "task":
203
+ case "agent":
204
+ case "spawn": {
205
+ const desc =
206
+ extractString(input, "description") ??
207
+ extractString(input, "prompt");
208
+ return {
209
+ title: desc ? `Task: ${truncateCommand(desc)}` : "Agent task",
210
+ description: desc,
211
+ };
212
+ }
213
+
214
+ default:
215
+ return {
216
+ title: part.tool || "Tool",
217
+ description:
218
+ command ??
219
+ filePath ??
220
+ extractString(input, "pattern") ??
221
+ extractString(input, "query"),
222
+ };
223
+ }
224
+ }
225
+
226
+ function truncateCommand(cmd: string): string {
227
+ const first = cmd.split("\n")[0];
228
+ return first.length > 60 ? first.slice(0, 57) + "..." : first;
229
+ }
230
+
231
+ /** Extract error text from a tool part, if any. */
232
+ export function getToolErrorText(
233
+ part: ToolPart,
234
+ fallback?: string,
235
+ ): string | undefined {
236
+ if (part.state.status !== "error") return undefined;
237
+ return part.state.error || fallback;
238
+ }