@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,98 @@
1
+ import { memo, useMemo } from "react";
2
+ import { FileText, FolderOpen } from "lucide-react";
3
+ import type { ToolPart } from "../types/parts";
4
+ import { PreviewCard, PreviewEmpty, PreviewError, PreviewLoading } from "./preview-primitives";
5
+
6
+ function coerceString(value: unknown): string | undefined {
7
+ return typeof value === "string" && value.trim() ? value : undefined;
8
+ }
9
+
10
+ function extractPattern(input: unknown): string | undefined {
11
+ if (!input || typeof input !== "object") {
12
+ return undefined;
13
+ }
14
+
15
+ const record = input as Record<string, unknown>;
16
+ return coerceString(record.pattern) ?? coerceString(record.path);
17
+ }
18
+
19
+ function extractFiles(output: unknown): string[] {
20
+ if (Array.isArray(output)) {
21
+ return output.map((item) => String(item)).filter(Boolean);
22
+ }
23
+
24
+ if (output && typeof output === "object") {
25
+ const record = output as Record<string, unknown>;
26
+ const candidate = record.files ?? record.matches ?? record.paths ?? record.results;
27
+
28
+ if (Array.isArray(candidate)) {
29
+ return candidate
30
+ .map((item) =>
31
+ typeof item === "string"
32
+ ? item
33
+ : item && typeof item === "object"
34
+ ? String(
35
+ (item as Record<string, unknown>).path ??
36
+ (item as Record<string, unknown>).file ??
37
+ "",
38
+ )
39
+ : "",
40
+ )
41
+ .filter(Boolean);
42
+ }
43
+ }
44
+
45
+ if (typeof output === "string") {
46
+ return output
47
+ .split("\n")
48
+ .map((line) => line.trim())
49
+ .filter(Boolean);
50
+ }
51
+
52
+ return [];
53
+ }
54
+
55
+ export interface GlobResultsPreviewProps {
56
+ part: ToolPart;
57
+ }
58
+
59
+ export const GlobResultsPreview = memo(({ part }: GlobResultsPreviewProps) => {
60
+ const pattern = extractPattern(part.state.input);
61
+ const files = useMemo(() => extractFiles(part.state.output), [part.state.output]);
62
+
63
+ return (
64
+ <PreviewCard
65
+ icon={<FolderOpen className="h-4 w-4" />}
66
+ title={pattern ? `Files for ${pattern}` : "File results"}
67
+ description={files.length > 0 ? `${files.length} path${files.length === 1 ? "" : "s"}` : undefined}
68
+ >
69
+ {part.state.status === "running" ? <PreviewLoading label="Collecting files…" /> : null}
70
+ {part.state.error ? <PreviewError error={part.state.error} /> : null}
71
+ {part.state.status === "completed" && files.length === 0 ? (
72
+ <PreviewEmpty label="No matching files were returned." />
73
+ ) : null}
74
+ {files.length > 0 ? (
75
+ <div className="grid gap-2 sm:grid-cols-2">
76
+ {files.map((file) => {
77
+ const isDirectory = file.endsWith("/");
78
+ const Icon = isDirectory ? FolderOpen : FileText;
79
+
80
+ return (
81
+ <div
82
+ key={file}
83
+ className="flex items-center gap-2 rounded-[var(--radius-md)] border border-border bg-muted/55 px-3 py-2"
84
+ >
85
+ <Icon className="h-4 w-4 shrink-0 text-primary" />
86
+ <span className="min-w-0 truncate text-xs font-mono text-foreground">
87
+ {file}
88
+ </span>
89
+ </div>
90
+ );
91
+ })}
92
+ </div>
93
+ ) : null}
94
+ </PreviewCard>
95
+ );
96
+ });
97
+
98
+ GlobResultsPreview.displayName = "GlobResultsPreview";
@@ -0,0 +1,157 @@
1
+ import { memo, useMemo } from "react";
2
+ import { Search } from "lucide-react";
3
+ import type { ToolPart } from "../types/parts";
4
+ import { PreviewCard, PreviewEmpty, PreviewError, PreviewLoading } from "./preview-primitives";
5
+
6
+ interface GrepMatch {
7
+ path: string;
8
+ line?: number;
9
+ text: string;
10
+ }
11
+
12
+ function coerceString(value: unknown): string | undefined {
13
+ return typeof value === "string" && value.trim() ? value : undefined;
14
+ }
15
+
16
+ function parseMatch(value: unknown): GrepMatch | null {
17
+ if (typeof value === "string") {
18
+ const [path, maybeLine, ...rest] = value.split(":");
19
+ const line = Number(maybeLine);
20
+
21
+ if (rest.length > 0 && Number.isFinite(line)) {
22
+ return {
23
+ path,
24
+ line,
25
+ text: rest.join(":").trim(),
26
+ };
27
+ }
28
+
29
+ return {
30
+ path: "match",
31
+ text: value,
32
+ };
33
+ }
34
+
35
+ if (!value || typeof value !== "object") {
36
+ return null;
37
+ }
38
+
39
+ const record = value as Record<string, unknown>;
40
+ const path =
41
+ coerceString(record.path) ??
42
+ coerceString(record.file) ??
43
+ coerceString(record.filePath) ??
44
+ "match";
45
+ const lineValue = record.line ?? record.lineNumber ?? record.line_number;
46
+ const line = typeof lineValue === "number" ? lineValue : Number(lineValue);
47
+ const text =
48
+ coerceString(record.text) ??
49
+ coerceString(record.content) ??
50
+ coerceString(record.lineText) ??
51
+ coerceString(record.preview);
52
+
53
+ if (!text) {
54
+ return null;
55
+ }
56
+
57
+ return {
58
+ path,
59
+ line: Number.isFinite(line) ? line : undefined,
60
+ text,
61
+ };
62
+ }
63
+
64
+ function extractPattern(input: unknown): string | undefined {
65
+ if (!input || typeof input !== "object") {
66
+ return undefined;
67
+ }
68
+
69
+ const record = input as Record<string, unknown>;
70
+ return coerceString(record.pattern) ?? coerceString(record.query);
71
+ }
72
+
73
+ function extractMatches(output: unknown): GrepMatch[] {
74
+ if (Array.isArray(output)) {
75
+ return output.map(parseMatch).filter((value): value is GrepMatch => value !== null);
76
+ }
77
+
78
+ if (output && typeof output === "object") {
79
+ const record = output as Record<string, unknown>;
80
+ const candidates = [record.matches, record.results, record.output];
81
+
82
+ for (const candidate of candidates) {
83
+ if (Array.isArray(candidate)) {
84
+ return candidate.map(parseMatch).filter((value): value is GrepMatch => value !== null);
85
+ }
86
+ }
87
+ }
88
+
89
+ if (typeof output === "string") {
90
+ return output
91
+ .split("\n")
92
+ .map((line) => line.trim())
93
+ .filter(Boolean)
94
+ .map(parseMatch)
95
+ .filter((value): value is GrepMatch => value !== null);
96
+ }
97
+
98
+ return [];
99
+ }
100
+
101
+ export interface GrepResultsPreviewProps {
102
+ part: ToolPart;
103
+ }
104
+
105
+ export const GrepResultsPreview = memo(({ part }: GrepResultsPreviewProps) => {
106
+ const pattern = extractPattern(part.state.input);
107
+ const matches = useMemo(() => extractMatches(part.state.output), [part.state.output]);
108
+ const groupedMatches = useMemo(() => {
109
+ const groups = new Map<string, GrepMatch[]>();
110
+
111
+ for (const match of matches) {
112
+ const list = groups.get(match.path) ?? [];
113
+ list.push(match);
114
+ groups.set(match.path, list);
115
+ }
116
+
117
+ return [...groups.entries()];
118
+ }, [matches]);
119
+
120
+ return (
121
+ <PreviewCard
122
+ icon={<Search className="h-4 w-4" />}
123
+ title={pattern ? `Search results for "${pattern}"` : "Search results"}
124
+ description={matches.length > 0 ? `${matches.length} match${matches.length === 1 ? "" : "es"}` : undefined}
125
+ >
126
+ {part.state.status === "running" ? <PreviewLoading label="Searching files…" /> : null}
127
+ {part.state.error ? <PreviewError error={part.state.error} /> : null}
128
+ {part.state.status === "completed" && matches.length === 0 ? (
129
+ <PreviewEmpty label="No matching files or lines were returned." />
130
+ ) : null}
131
+ {groupedMatches.map(([path, pathMatches]) => (
132
+ <div
133
+ key={path}
134
+ className="rounded-[var(--radius-md)] border border-border bg-muted/55"
135
+ >
136
+ <div className="border-b border-border px-3 py-2 text-xs font-medium text-foreground">
137
+ {path}
138
+ </div>
139
+ <div className="divide-y divide-border">
140
+ {pathMatches.map((match, index) => (
141
+ <div key={`${path}-${match.line ?? index}-${index}`} className="grid grid-cols-[auto_minmax(0,1fr)] gap-3 px-3 py-2">
142
+ <div className="pt-0.5 text-xs font-mono text-muted-foreground">
143
+ {match.line ?? "·"}
144
+ </div>
145
+ <pre className="overflow-x-auto whitespace-pre-wrap break-words text-xs font-mono text-foreground">
146
+ {match.text}
147
+ </pre>
148
+ </div>
149
+ ))}
150
+ </div>
151
+ </div>
152
+ ))}
153
+ </PreviewCard>
154
+ );
155
+ });
156
+
157
+ GrepResultsPreview.displayName = "GrepResultsPreview";
@@ -0,0 +1,22 @@
1
+ export { CommandPreview, type CommandPreviewProps } from "./command-preview";
2
+ export {
3
+ WriteFilePreview,
4
+ type WriteFilePreviewProps,
5
+ } from "./write-file-preview";
6
+ export {
7
+ GrepResultsPreview,
8
+ type GrepResultsPreviewProps,
9
+ } from "./grep-results-preview";
10
+ export {
11
+ GlobResultsPreview,
12
+ type GlobResultsPreviewProps,
13
+ } from "./glob-results-preview";
14
+ export {
15
+ WebSearchPreview,
16
+ type WebSearchPreviewProps,
17
+ } from "./web-search-preview";
18
+ export {
19
+ QuestionPreview,
20
+ type QuestionPreviewProps,
21
+ } from "./question-preview";
22
+ export { DiffPreview, type DiffPreviewProps } from "./diff-preview";
@@ -0,0 +1,84 @@
1
+ import type { ReactNode } from "react";
2
+ import { Loader2 } from "lucide-react";
3
+ import { cn } from "../lib/utils";
4
+
5
+ export interface PreviewCardProps {
6
+ icon: ReactNode;
7
+ title: string;
8
+ description?: string;
9
+ meta?: ReactNode;
10
+ children?: ReactNode;
11
+ className?: string;
12
+ }
13
+
14
+ export function PreviewCard({
15
+ icon,
16
+ title,
17
+ description,
18
+ meta,
19
+ children,
20
+ className,
21
+ }: PreviewCardProps) {
22
+ return (
23
+ <div
24
+ className={cn(
25
+ "overflow-hidden rounded-[var(--radius-lg)] border border-border bg-card shadow-[var(--shadow-card)]",
26
+ className,
27
+ )}
28
+ >
29
+ <div className="flex items-center gap-2.5 border-b border-border bg-card px-[var(--tool-card-px)] py-[var(--tool-card-py)]">
30
+ <div className="flex h-[var(--tool-icon-size)] w-[var(--tool-icon-size)] shrink-0 items-center justify-center rounded-[var(--radius-sm)] border border-[var(--border-accent)] bg-muted text-primary">
31
+ {icon}
32
+ </div>
33
+ <div className="min-w-0 flex-1">
34
+ <div className="flex items-center gap-2">
35
+ <span className="text-xs font-semibold text-foreground">{title}</span>
36
+ {description ? (
37
+ <span className="truncate text-xs font-mono text-muted-foreground">{description}</span>
38
+ ) : null}
39
+ </div>
40
+ </div>
41
+ {meta ? (
42
+ <div className="shrink-0 text-xs text-muted-foreground">{meta}</div>
43
+ ) : null}
44
+ </div>
45
+ {children ? <div className="space-y-2 px-[var(--tool-card-px)] py-[var(--tool-card-py)]">{children}</div> : null}
46
+ </div>
47
+ );
48
+ }
49
+
50
+ export function PreviewEmpty({
51
+ label,
52
+ className,
53
+ }: {
54
+ label: string;
55
+ className?: string;
56
+ }) {
57
+ return (
58
+ <div
59
+ className={cn(
60
+ "rounded-[var(--radius-md)] border border-dashed border-border bg-muted px-3 py-2.5 text-xs text-muted-foreground",
61
+ className,
62
+ )}
63
+ >
64
+ {label}
65
+ </div>
66
+ );
67
+ }
68
+
69
+ export function PreviewError({ error }: { error: string }) {
70
+ return (
71
+ <div className="rounded-[var(--radius-md)] border border-[var(--surface-danger-border)] bg-[var(--surface-danger-bg)] px-3 py-3 text-sm text-[var(--surface-danger-text)]">
72
+ {error}
73
+ </div>
74
+ );
75
+ }
76
+
77
+ export function PreviewLoading({ label = "Running…" }: { label?: string }) {
78
+ return (
79
+ <div className="flex items-center gap-2 rounded-[var(--radius-md)] border border-border bg-muted px-3 py-2 text-xs text-muted-foreground">
80
+ <Loader2 className="h-4 w-4 animate-spin text-primary" />
81
+ <span>{label}</span>
82
+ </div>
83
+ );
84
+ }
@@ -0,0 +1,101 @@
1
+ import { memo, useMemo } from "react";
2
+ import { MessageSquareQuote } from "lucide-react";
3
+ import type { ToolPart } from "../types/parts";
4
+ import { PreviewCard, PreviewEmpty, PreviewError, PreviewLoading } from "./preview-primitives";
5
+
6
+ function toQuestionList(input: unknown): string[] {
7
+ if (!input) {
8
+ return [];
9
+ }
10
+
11
+ if (typeof input === "string") {
12
+ return [input];
13
+ }
14
+
15
+ if (Array.isArray(input)) {
16
+ return input.map((item) => String(item)).filter(Boolean);
17
+ }
18
+
19
+ if (typeof input === "object") {
20
+ const record = input as Record<string, unknown>;
21
+ const value = record.questions ?? record.question ?? record.prompt;
22
+
23
+ if (Array.isArray(value)) {
24
+ return value.map((item) => String(item)).filter(Boolean);
25
+ }
26
+
27
+ if (typeof value === "string") {
28
+ return [value];
29
+ }
30
+ }
31
+
32
+ return [];
33
+ }
34
+
35
+ function toAnswerList(output: unknown): string[] {
36
+ if (!output) {
37
+ return [];
38
+ }
39
+
40
+ if (typeof output === "string") {
41
+ return [output];
42
+ }
43
+
44
+ if (Array.isArray(output)) {
45
+ return output.map((item) => String(item)).filter(Boolean);
46
+ }
47
+
48
+ if (typeof output === "object") {
49
+ const record = output as Record<string, unknown>;
50
+ const value = record.answers ?? record.answer ?? record.response;
51
+
52
+ if (Array.isArray(value)) {
53
+ return value.map((item) => String(item)).filter(Boolean);
54
+ }
55
+
56
+ if (typeof value === "string") {
57
+ return [value];
58
+ }
59
+ }
60
+
61
+ return [];
62
+ }
63
+
64
+ export interface QuestionPreviewProps {
65
+ part: ToolPart;
66
+ }
67
+
68
+ export const QuestionPreview = memo(({ part }: QuestionPreviewProps) => {
69
+ const questions = useMemo(() => toQuestionList(part.state.input), [part.state.input]);
70
+ const answers = useMemo(() => toAnswerList(part.state.output), [part.state.output]);
71
+
72
+ return (
73
+ <PreviewCard
74
+ icon={<MessageSquareQuote className="h-4 w-4" />}
75
+ title="Agent question"
76
+ description={questions.length > 1 ? `${questions.length} questions require attention` : undefined}
77
+ >
78
+ {part.state.status === "running" ? <PreviewLoading label="Waiting for an answer…" /> : null}
79
+ {part.state.error ? <PreviewError error={part.state.error} /> : null}
80
+ {questions.length === 0 ? <PreviewEmpty label="No question text was provided." /> : null}
81
+ {questions.map((question, index) => (
82
+ <div
83
+ key={`${question}-${index}`}
84
+ className="rounded-[var(--radius-md)] border border-border bg-muted px-3 py-3"
85
+ >
86
+ <div className="text-xs font-semibold uppercase tracking-[0.08em] text-muted-foreground">
87
+ Question {index + 1}
88
+ </div>
89
+ <div className="mt-2 text-sm leading-relaxed text-foreground">
90
+ {question}
91
+ </div>
92
+ <div className="mt-3 rounded-[var(--radius-sm)] border border-border bg-card px-3 py-2 text-sm text-foreground">
93
+ {answers[index] ?? (part.state.status === "completed" ? "No answer recorded." : "Awaiting answer")}
94
+ </div>
95
+ </div>
96
+ ))}
97
+ </PreviewCard>
98
+ );
99
+ });
100
+
101
+ QuestionPreview.displayName = "QuestionPreview";
@@ -0,0 +1,117 @@
1
+ import { memo, useMemo } from "react";
2
+ import { Globe } from "lucide-react";
3
+ import type { ToolPart } from "../types/parts";
4
+ import { PreviewCard, PreviewEmpty, PreviewError, PreviewLoading } from "./preview-primitives";
5
+
6
+ interface SearchResult {
7
+ title: string;
8
+ url: string;
9
+ snippet?: string;
10
+ }
11
+
12
+ function coerceString(value: unknown): string | undefined {
13
+ return typeof value === "string" && value.trim() ? value : undefined;
14
+ }
15
+
16
+ function extractQuery(input: unknown, output: unknown): string | undefined {
17
+ if (input && typeof input === "object") {
18
+ const record = input as Record<string, unknown>;
19
+ const query = coerceString(record.query) ?? coerceString(record.url);
20
+ if (query) return query;
21
+ }
22
+
23
+ if (output && typeof output === "object") {
24
+ const record = output as Record<string, unknown>;
25
+ return coerceString(record.query);
26
+ }
27
+
28
+ return undefined;
29
+ }
30
+
31
+ function parseResult(value: unknown): SearchResult | null {
32
+ if (!value || typeof value !== "object") {
33
+ return null;
34
+ }
35
+
36
+ const record = value as Record<string, unknown>;
37
+ const title = coerceString(record.title) ?? coerceString(record.name) ?? "Untitled result";
38
+ const url = coerceString(record.url) ?? coerceString(record.link);
39
+
40
+ if (!url) {
41
+ return null;
42
+ }
43
+
44
+ return {
45
+ title,
46
+ url,
47
+ snippet:
48
+ coerceString(record.snippet) ??
49
+ coerceString(record.description) ??
50
+ coerceString(record.text),
51
+ };
52
+ }
53
+
54
+ function extractResults(output: unknown): SearchResult[] {
55
+ if (output && typeof output === "object") {
56
+ const record = output as Record<string, unknown>;
57
+ const candidate = record.results ?? record.items ?? record.output;
58
+
59
+ if (Array.isArray(candidate)) {
60
+ return candidate.map(parseResult).filter((value): value is SearchResult => value !== null);
61
+ }
62
+ }
63
+
64
+ if (Array.isArray(output)) {
65
+ return output.map(parseResult).filter((value): value is SearchResult => value !== null);
66
+ }
67
+
68
+ return [];
69
+ }
70
+
71
+ export interface WebSearchPreviewProps {
72
+ part: ToolPart;
73
+ }
74
+
75
+ export const WebSearchPreview = memo(({ part }: WebSearchPreviewProps) => {
76
+ const query = extractQuery(part.state.input, part.state.output);
77
+ const results = useMemo(() => extractResults(part.state.output), [part.state.output]);
78
+
79
+ return (
80
+ <PreviewCard
81
+ icon={<Globe className="h-4 w-4" />}
82
+ title={query ? `Web results for ${query}` : "Web results"}
83
+ description={results.length > 0 ? `${results.length} result${results.length === 1 ? "" : "s"}` : undefined}
84
+ >
85
+ {part.state.status === "running" ? <PreviewLoading label="Searching the web…" /> : null}
86
+ {part.state.error ? <PreviewError error={part.state.error} /> : null}
87
+ {part.state.status === "completed" && results.length === 0 ? (
88
+ <PreviewEmpty label="No web results were returned." />
89
+ ) : null}
90
+ <div className="space-y-2">
91
+ {results.map((result) => (
92
+ <a
93
+ key={result.url}
94
+ href={result.url}
95
+ target="_blank"
96
+ rel="noreferrer"
97
+ className="block rounded-[var(--radius-md)] border border-border bg-muted/55 px-3 py-3 transition-colors hover:border-[var(--border-accent-hover)] hover:bg-accent/50"
98
+ >
99
+ <div className="text-sm font-medium text-foreground">
100
+ {result.title}
101
+ </div>
102
+ <div className="mt-1 truncate text-xs text-primary">
103
+ {result.url}
104
+ </div>
105
+ {result.snippet ? (
106
+ <p className="mt-2 text-sm leading-relaxed text-muted-foreground">
107
+ {result.snippet}
108
+ </p>
109
+ ) : null}
110
+ </a>
111
+ ))}
112
+ </div>
113
+ </PreviewCard>
114
+ );
115
+ });
116
+
117
+ WebSearchPreview.displayName = "WebSearchPreview";
@@ -0,0 +1,80 @@
1
+ import { memo } from "react";
2
+ import { FileEdit } from "lucide-react";
3
+ import type { ToolPart } from "../types/parts";
4
+ import { CodeBlock, CopyButton } from "../markdown/code-block";
5
+ import { PreviewCard, PreviewError, PreviewLoading } from "./preview-primitives";
6
+
7
+ export interface WriteFilePreviewProps {
8
+ part: ToolPart;
9
+ }
10
+
11
+ function extractWriteContent(
12
+ input: unknown,
13
+ ): { path: string; content: string } | null {
14
+ if (!input || typeof input !== "object") return null;
15
+ const obj = input as Record<string, unknown>;
16
+ const path = String(obj.file_path ?? obj.path ?? obj.filePath ?? "unknown");
17
+ const content = String(obj.content ?? obj.contents ?? obj.data ?? "");
18
+ return { path, content };
19
+ }
20
+
21
+ function getLanguageFromPath(path: string): string | undefined {
22
+ const ext = path.split(".").pop()?.toLowerCase();
23
+ const map: Record<string, string> = {
24
+ ts: "typescript",
25
+ tsx: "tsx",
26
+ js: "javascript",
27
+ jsx: "jsx",
28
+ rs: "rust",
29
+ py: "python",
30
+ go: "go",
31
+ rb: "ruby",
32
+ json: "json",
33
+ yaml: "yaml",
34
+ yml: "yaml",
35
+ toml: "toml",
36
+ md: "markdown",
37
+ css: "css",
38
+ scss: "scss",
39
+ html: "html",
40
+ sh: "bash",
41
+ bash: "bash",
42
+ zsh: "bash",
43
+ sql: "sql",
44
+ sol: "solidity",
45
+ proto: "protobuf",
46
+ };
47
+ return ext ? map[ext] : undefined;
48
+ }
49
+
50
+ /**
51
+ * Preview for file write/create operations.
52
+ * Shows file path, line count, and the written content.
53
+ */
54
+ export const WriteFilePreview = memo(({ part }: WriteFilePreviewProps) => {
55
+ const write = extractWriteContent(part.state.input);
56
+ if (!write) return null;
57
+
58
+ const lineCount = write.content.split("\n").length;
59
+ const language = getLanguageFromPath(write.path);
60
+
61
+ return (
62
+ <PreviewCard
63
+ icon={<FileEdit className="h-4 w-4" />}
64
+ title="Write file"
65
+ description={write.path}
66
+ meta={
67
+ <span className="text-xs text-muted-foreground">
68
+ +{lineCount} line{lineCount !== 1 ? "s" : ""}
69
+ </span>
70
+ }
71
+ >
72
+ {part.state.status === "running" ? <PreviewLoading label="Writing file…" /> : null}
73
+ {part.state.error ? <PreviewError error={part.state.error} /> : null}
74
+ <CodeBlock code={write.content} language={language} className="rounded-[var(--radius-md)]">
75
+ <CopyButton text={write.content} />
76
+ </CodeBlock>
77
+ </PreviewCard>
78
+ );
79
+ });
80
+ WriteFilePreview.displayName = "WriteFilePreview";
@@ -0,0 +1,11 @@
1
+ /** Visual branding for a run group header — injected by consuming app. */
2
+ export interface AgentBranding {
3
+ label: string;
4
+ accentClass: string;
5
+ bgClass: string;
6
+ containerBgClass: string;
7
+ borderClass: string;
8
+ /** CSS class for the agent icon (legacy). Ignored when using lucide-react icons. */
9
+ iconClass: string;
10
+ textClass: string;
11
+ }
@@ -0,0 +1,5 @@
1
+ export * from "./branding";
2
+ export * from "./message";
3
+ export * from "./parts";
4
+ export * from "./run";
5
+ export * from "./tool-display";
@@ -0,0 +1,13 @@
1
+ /** A single message in a chat session. */
2
+ export interface SessionMessage {
3
+ id: string;
4
+ role: 'user' | 'assistant' | 'system';
5
+ sessionID?: string;
6
+ time?: {
7
+ created?: number;
8
+ updated?: number;
9
+ completed?: number;
10
+ };
11
+ /** Monotonically increasing insertion index for stable ordering. */
12
+ _insertionIndex?: number;
13
+ }