@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,111 @@
1
+ import { useMemo } from "react";
2
+ import type { SessionMessage } from "../types/message";
3
+ import type { SessionPart } from "../types/parts";
4
+ import type { Run, RunStats, GroupedMessage } from "../types/run";
5
+ import { getToolCategory } from "../utils/tool-display";
6
+
7
+ function computeRunStats(
8
+ messages: SessionMessage[],
9
+ partMap: Record<string, SessionPart[]>,
10
+ ): RunStats {
11
+ const stats: RunStats = {
12
+ toolCount: 0,
13
+ messageCount: messages.length,
14
+ thinkingDurationMs: 0,
15
+ textPartCount: 0,
16
+ toolCategories: new Set(),
17
+ };
18
+
19
+ for (const msg of messages) {
20
+ const parts = partMap[msg.id] ?? [];
21
+ for (const part of parts) {
22
+ switch (part.type) {
23
+ case "tool":
24
+ stats.toolCount++;
25
+ stats.toolCategories.add(getToolCategory(part.tool));
26
+ break;
27
+ case "text":
28
+ if (!part.synthetic) stats.textPartCount++;
29
+ break;
30
+ case "reasoning": {
31
+ const start = part.time?.start;
32
+ const end = part.time?.end;
33
+ if (start && end) stats.thinkingDurationMs += end - start;
34
+ break;
35
+ }
36
+ }
37
+ }
38
+ }
39
+ return stats;
40
+ }
41
+
42
+ function getLastTextContent(
43
+ messages: SessionMessage[],
44
+ partMap: Record<string, SessionPart[]>,
45
+ ): string | null {
46
+ for (let i = messages.length - 1; i >= 0; i--) {
47
+ const parts = partMap[messages[i].id] ?? [];
48
+ for (let j = parts.length - 1; j >= 0; j--) {
49
+ const part = parts[j];
50
+ if (part.type === "text" && !part.synthetic && part.text.trim()) {
51
+ return part.text.trim();
52
+ }
53
+ }
54
+ }
55
+ return null;
56
+ }
57
+
58
+ export interface UseRunGroupsOptions {
59
+ messages: SessionMessage[];
60
+ partMap: Record<string, SessionPart[]>;
61
+ isStreaming: boolean;
62
+ }
63
+
64
+ export function useRunGroups({
65
+ messages,
66
+ partMap,
67
+ isStreaming,
68
+ }: UseRunGroupsOptions): GroupedMessage[] {
69
+ return useMemo(() => {
70
+ const groups: GroupedMessage[] = [];
71
+ let currentRunMessages: SessionMessage[] = [];
72
+
73
+ function flushRun(streaming: boolean) {
74
+ if (currentRunMessages.length === 0) return;
75
+ const msgs = [...currentRunMessages];
76
+ const stats = computeRunStats(msgs, partMap);
77
+ const summaryText = getLastTextContent(msgs, partMap);
78
+ const isComplete = !streaming;
79
+
80
+ groups.push({
81
+ type: "run",
82
+ run: {
83
+ id: msgs[0].id,
84
+ messages: msgs,
85
+ isComplete,
86
+ isStreaming: streaming,
87
+ stats,
88
+ summaryText,
89
+ finalTextPart: null,
90
+ },
91
+ });
92
+ currentRunMessages = [];
93
+ }
94
+
95
+ for (let i = 0; i < messages.length; i++) {
96
+ const msg = messages[i];
97
+ if (msg.role === "user") {
98
+ flushRun(false);
99
+ groups.push({ type: "user", message: msg });
100
+ } else {
101
+ currentRunMessages.push(msg);
102
+ }
103
+ }
104
+
105
+ if (currentRunMessages.length > 0) {
106
+ flushRun(isStreaming);
107
+ }
108
+
109
+ return groups;
110
+ }, [messages, partMap, isStreaming]);
111
+ }
@@ -0,0 +1,575 @@
1
+ import { useCallback, useMemo, useRef, useState } from "react";
2
+ import type { SessionMessage } from "../types/message";
3
+ import type { ReasoningPart, SessionPart, TextPart, ToolPart, ToolStatus } from "../types/parts";
4
+
5
+ export interface SdkSessionAttachment {
6
+ name: string;
7
+ size?: number;
8
+ }
9
+
10
+ export interface SdkSessionSeed {
11
+ id: string;
12
+ role: SessionMessage["role"];
13
+ createdAt?: number | string | Date;
14
+ content?: string;
15
+ attachments?: SdkSessionAttachment[];
16
+ parts?: SessionPart[];
17
+ }
18
+
19
+ export interface SdkSessionEvent {
20
+ type: string;
21
+ data?: Record<string, unknown>;
22
+ }
23
+
24
+ export interface BeginAssistantMessageOptions {
25
+ id?: string;
26
+ role?: Extract<SessionMessage["role"], "assistant" | "system">;
27
+ createdAt?: number | string | Date;
28
+ }
29
+
30
+ export interface AppendUserMessageOptions {
31
+ id?: string;
32
+ role?: Extract<SessionMessage["role"], "user" | "system">;
33
+ content: string;
34
+ createdAt?: number | string | Date;
35
+ attachments?: SdkSessionAttachment[];
36
+ }
37
+
38
+ export interface CompleteAssistantMessageOptions {
39
+ messageId?: string;
40
+ finalText?: string;
41
+ }
42
+
43
+ export interface ApplySdkEventOptions {
44
+ messageId?: string;
45
+ }
46
+
47
+ export interface UseSdkSessionOptions {
48
+ initialMessages?: SdkSessionSeed[];
49
+ }
50
+
51
+ export interface UseSdkSessionReturn {
52
+ messages: SessionMessage[];
53
+ partMap: Record<string, SessionPart[]>;
54
+ isStreaming: boolean;
55
+ activeAssistantMessageId: string | null;
56
+ replaceHistory: (messages: SdkSessionSeed[]) => void;
57
+ appendUserMessage: (message: AppendUserMessageOptions) => string;
58
+ beginAssistantMessage: (options?: BeginAssistantMessageOptions) => string;
59
+ applySdkEvent: (event: SdkSessionEvent, options?: ApplySdkEventOptions) => void;
60
+ completeAssistantMessage: (options?: CompleteAssistantMessageOptions) => void;
61
+ failAssistantMessage: (error: string, options?: { messageId?: string }) => void;
62
+ setStreaming: (value: boolean) => void;
63
+ reset: () => void;
64
+ }
65
+
66
+ interface ConversationState {
67
+ messages: SessionMessage[];
68
+ partMap: Record<string, SessionPart[]>;
69
+ }
70
+
71
+ type GatewayPart = Record<string, unknown>;
72
+
73
+ function uid(): string {
74
+ if (typeof globalThis.crypto !== "undefined" && typeof globalThis.crypto.randomUUID === "function") {
75
+ try {
76
+ return globalThis.crypto.randomUUID();
77
+ } catch {
78
+ // Fall through to the local fallback below.
79
+ }
80
+ }
81
+
82
+ return `sdk-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
83
+ }
84
+
85
+ function toMillis(value: number | string | Date | undefined): number | undefined {
86
+ if (value == null) return undefined;
87
+ if (typeof value === "number") return value;
88
+ if (value instanceof Date) return value.getTime();
89
+
90
+ const millis = new Date(value).getTime();
91
+ return Number.isFinite(millis) ? millis : undefined;
92
+ }
93
+
94
+ function asRecord(value: unknown): Record<string, unknown> | undefined {
95
+ return value && typeof value === "object" && !Array.isArray(value)
96
+ ? (value as Record<string, unknown>)
97
+ : undefined;
98
+ }
99
+
100
+ function asString(value: unknown): string | undefined {
101
+ return typeof value === "string" && value.length > 0 ? value : undefined;
102
+ }
103
+
104
+ function textPartsFromContent(
105
+ content: string,
106
+ attachments?: SdkSessionAttachment[],
107
+ ): SessionPart[] {
108
+ const attachmentText = attachments?.length
109
+ ? `\n\nAttachments:\n${attachments
110
+ .map((attachment) => `- ${attachment.name}`)
111
+ .join("\n")}`
112
+ : "";
113
+ const text = `${content}${attachmentText}`.trim();
114
+
115
+ return text ? [{ type: "text", text } satisfies TextPart] : [];
116
+ }
117
+
118
+ function normalizeTime(value: unknown): ToolPart["state"]["time"] | undefined {
119
+ const record = asRecord(value);
120
+ if (!record) return undefined;
121
+
122
+ const start = Number(record.start ?? record.startedAt ?? record.started_at);
123
+ const end = Number(record.end ?? record.completedAt ?? record.completed_at);
124
+
125
+ if (!Number.isFinite(start) && !Number.isFinite(end)) {
126
+ return undefined;
127
+ }
128
+
129
+ return {
130
+ start: Number.isFinite(start) ? start : undefined,
131
+ end: Number.isFinite(end) ? end : undefined,
132
+ };
133
+ }
134
+
135
+ function normalizeStatus(value: unknown, output: unknown, error: unknown): ToolStatus {
136
+ if (value === "pending" || value === "running" || value === "completed" || value === "error") {
137
+ return value;
138
+ }
139
+
140
+ if (typeof error === "string" && error.length > 0) {
141
+ return "error";
142
+ }
143
+
144
+ if (output !== undefined) {
145
+ return "completed";
146
+ }
147
+
148
+ return "running";
149
+ }
150
+
151
+ function resolveToolIdentity(rawPart: GatewayPart): string {
152
+ return String(
153
+ rawPart.id ??
154
+ rawPart.callID ??
155
+ rawPart.callId ??
156
+ rawPart.toolUseId ??
157
+ rawPart.toolCallId ??
158
+ rawPart.tool ??
159
+ rawPart.name ??
160
+ "tool",
161
+ );
162
+ }
163
+
164
+ function normalizePart(rawPart: GatewayPart): SessionPart | null {
165
+ const type = String(rawPart.type ?? "");
166
+
167
+ if (type === "text") {
168
+ return {
169
+ type: "text",
170
+ text: asString(rawPart.text) ?? asString(rawPart.content) ?? "",
171
+ };
172
+ }
173
+
174
+ if (type === "reasoning") {
175
+ return {
176
+ type: "reasoning",
177
+ text: asString(rawPart.text) ?? asString(rawPart.content) ?? "",
178
+ time: normalizeTime(rawPart.time),
179
+ } satisfies ReasoningPart;
180
+ }
181
+
182
+ if (type === "tool") {
183
+ const stateRecord = asRecord(rawPart.state);
184
+ const input = stateRecord?.input ?? rawPart.input;
185
+ const output = stateRecord?.output ?? rawPart.output;
186
+ const error = stateRecord?.error ?? rawPart.error;
187
+
188
+ return {
189
+ type: "tool",
190
+ id: resolveToolIdentity(rawPart),
191
+ tool: String(rawPart.tool ?? rawPart.name ?? "tool"),
192
+ callID:
193
+ rawPart.callID != null || rawPart.callId != null
194
+ ? String(rawPart.callID ?? rawPart.callId)
195
+ : undefined,
196
+ state: {
197
+ status: normalizeStatus(stateRecord?.status ?? rawPart.status, output, error),
198
+ input,
199
+ output,
200
+ error: typeof error === "string" ? error : undefined,
201
+ metadata: asRecord(stateRecord?.metadata) ?? asRecord(rawPart.metadata),
202
+ time: normalizeTime(stateRecord?.time ?? rawPart.time),
203
+ },
204
+ } satisfies ToolPart;
205
+ }
206
+
207
+ return null;
208
+ }
209
+
210
+ function getPartKey(rawPart: GatewayPart): string {
211
+ const type = String(rawPart.type ?? "unknown");
212
+
213
+ if (type === "tool") {
214
+ return `tool:${resolveToolIdentity(rawPart)}`;
215
+ }
216
+
217
+ if (type === "reasoning") {
218
+ return `reasoning:${String(rawPart.id ?? rawPart.partId ?? rawPart.index ?? "current")}`;
219
+ }
220
+
221
+ return `text:${String(rawPart.id ?? rawPart.partId ?? rawPart.index ?? "current")}`;
222
+ }
223
+
224
+ function mergePart(existing: SessionPart | undefined, incoming: SessionPart, delta?: string): SessionPart {
225
+ if (!existing) {
226
+ if (incoming.type === "text" && delta) {
227
+ return { type: "text", text: delta };
228
+ }
229
+
230
+ return incoming;
231
+ }
232
+
233
+ if (existing.type === "text" && incoming.type === "text") {
234
+ return {
235
+ type: "text",
236
+ text: delta ? `${existing.text}${delta}` : incoming.text,
237
+ synthetic: incoming.synthetic ?? existing.synthetic,
238
+ };
239
+ }
240
+
241
+ if (existing.type === "reasoning" && incoming.type === "reasoning") {
242
+ return {
243
+ ...existing,
244
+ ...incoming,
245
+ text:
246
+ delta && incoming.text === existing.text
247
+ ? `${existing.text}${delta}`
248
+ : incoming.text || existing.text,
249
+ time: incoming.time ?? existing.time,
250
+ };
251
+ }
252
+
253
+ if (existing.type === "tool" && incoming.type === "tool") {
254
+ return {
255
+ ...existing,
256
+ ...incoming,
257
+ state: {
258
+ ...existing.state,
259
+ ...incoming.state,
260
+ time: incoming.state.time ?? existing.state.time,
261
+ },
262
+ };
263
+ }
264
+
265
+ return incoming;
266
+ }
267
+
268
+ function mapSeeds(messages: SdkSessionSeed[]): ConversationState {
269
+ return {
270
+ messages: messages.map((message, index) => ({
271
+ id: message.id,
272
+ role: message.role,
273
+ _insertionIndex: index,
274
+ time: {
275
+ created: toMillis(message.createdAt) ?? Date.now(),
276
+ },
277
+ })),
278
+ partMap: Object.fromEntries(
279
+ messages.map((message) => [
280
+ message.id,
281
+ message.parts ?? textPartsFromContent(message.content ?? "", message.attachments),
282
+ ]),
283
+ ),
284
+ };
285
+ }
286
+
287
+ export function useSdkSession({
288
+ initialMessages = [],
289
+ }: UseSdkSessionOptions = {}): UseSdkSessionReturn {
290
+ const initialConversation = useMemo(
291
+ () => mapSeeds(initialMessages),
292
+ [initialMessages],
293
+ );
294
+ const [conversation, setConversation] = useState<ConversationState>(initialConversation);
295
+ const [isStreaming, setIsStreaming] = useState(false);
296
+ const activeAssistantIdRef = useRef<string | null>(null);
297
+ const insertionIndexRef = useRef(initialConversation.messages.length);
298
+ const partIndexRef = useRef<Record<string, Record<string, number>>>({});
299
+
300
+ const replaceHistory = useCallback((messages: SdkSessionSeed[]) => {
301
+ const next = mapSeeds(messages);
302
+ setConversation(next);
303
+ setIsStreaming(false);
304
+ activeAssistantIdRef.current = null;
305
+ insertionIndexRef.current = next.messages.length;
306
+ partIndexRef.current = {};
307
+ }, []);
308
+
309
+ const appendUserMessage = useCallback(
310
+ ({
311
+ id = uid(),
312
+ role = "user",
313
+ content,
314
+ createdAt,
315
+ attachments,
316
+ }: AppendUserMessageOptions) => {
317
+ setConversation((prev) => ({
318
+ messages: [
319
+ ...prev.messages,
320
+ {
321
+ id,
322
+ role,
323
+ _insertionIndex: insertionIndexRef.current++,
324
+ time: {
325
+ created: toMillis(createdAt) ?? Date.now(),
326
+ },
327
+ },
328
+ ],
329
+ partMap: {
330
+ ...prev.partMap,
331
+ [id]: textPartsFromContent(content, attachments),
332
+ },
333
+ }));
334
+
335
+ return id;
336
+ },
337
+ [],
338
+ );
339
+
340
+ const beginAssistantMessage = useCallback(
341
+ ({
342
+ id = uid(),
343
+ role = "assistant",
344
+ createdAt,
345
+ }: BeginAssistantMessageOptions = {}) => {
346
+ setConversation((prev) => ({
347
+ messages: [
348
+ ...prev.messages,
349
+ {
350
+ id,
351
+ role,
352
+ _insertionIndex: insertionIndexRef.current++,
353
+ time: {
354
+ created: toMillis(createdAt) ?? Date.now(),
355
+ },
356
+ },
357
+ ],
358
+ partMap: {
359
+ ...prev.partMap,
360
+ [id]: prev.partMap[id] ?? [],
361
+ },
362
+ }));
363
+
364
+ activeAssistantIdRef.current = id;
365
+ partIndexRef.current[id] = partIndexRef.current[id] ?? {};
366
+ setIsStreaming(true);
367
+ return id;
368
+ },
369
+ [],
370
+ );
371
+
372
+ const completeAssistantMessage = useCallback(
373
+ ({ messageId, finalText }: CompleteAssistantMessageOptions = {}) => {
374
+ const targetId = messageId ?? activeAssistantIdRef.current;
375
+ if (!targetId) {
376
+ setIsStreaming(false);
377
+ return;
378
+ }
379
+
380
+ if (finalText) {
381
+ setConversation((prev) => {
382
+ const existingParts = prev.partMap[targetId] ?? [];
383
+ const nextParts = [...existingParts];
384
+ const textIndex = nextParts.findIndex((part) => part.type === "text");
385
+
386
+ if (textIndex === -1) {
387
+ nextParts.push({ type: "text", text: finalText });
388
+ } else {
389
+ nextParts[textIndex] = { type: "text", text: finalText };
390
+ }
391
+
392
+ return {
393
+ ...prev,
394
+ partMap: {
395
+ ...prev.partMap,
396
+ [targetId]: nextParts,
397
+ },
398
+ };
399
+ });
400
+ }
401
+
402
+ delete partIndexRef.current[targetId];
403
+ if (activeAssistantIdRef.current === targetId) {
404
+ activeAssistantIdRef.current = null;
405
+ }
406
+ setIsStreaming(false);
407
+ },
408
+ [],
409
+ );
410
+
411
+ const failAssistantMessage = useCallback(
412
+ (error: string, options?: { messageId?: string }) => {
413
+ const targetId = options?.messageId ?? activeAssistantIdRef.current;
414
+ if (!targetId) {
415
+ setIsStreaming(false);
416
+ return;
417
+ }
418
+
419
+ setConversation((prev) => ({
420
+ ...prev,
421
+ partMap: {
422
+ ...prev.partMap,
423
+ [targetId]: [{ type: "text", text: `Error: ${error}` }],
424
+ },
425
+ }));
426
+
427
+ delete partIndexRef.current[targetId];
428
+ if (activeAssistantIdRef.current === targetId) {
429
+ activeAssistantIdRef.current = null;
430
+ }
431
+ setIsStreaming(false);
432
+ },
433
+ [],
434
+ );
435
+
436
+ const applySdkEvent = useCallback(
437
+ (event: SdkSessionEvent, options?: ApplySdkEventOptions) => {
438
+ const eventData = asRecord(event.data) ?? {};
439
+
440
+ if (event.type === "message.updated") {
441
+ const id =
442
+ asString(eventData.id) ??
443
+ asString(eventData.messageId) ??
444
+ options?.messageId;
445
+ const role = (asString(eventData.role) ?? "assistant") as SessionMessage["role"];
446
+
447
+ if (!id) {
448
+ return;
449
+ }
450
+
451
+ setConversation((prev) => {
452
+ if (prev.messages.some((message) => message.id === id)) {
453
+ return prev;
454
+ }
455
+
456
+ return {
457
+ ...prev,
458
+ messages: [
459
+ ...prev.messages,
460
+ {
461
+ id,
462
+ role,
463
+ _insertionIndex: insertionIndexRef.current++,
464
+ time: { created: Date.now() },
465
+ },
466
+ ],
467
+ partMap: {
468
+ ...prev.partMap,
469
+ [id]: prev.partMap[id] ?? [],
470
+ },
471
+ };
472
+ });
473
+
474
+ if (role === "assistant" || role === "system") {
475
+ activeAssistantIdRef.current = id;
476
+ partIndexRef.current[id] = partIndexRef.current[id] ?? {};
477
+ setIsStreaming(true);
478
+ }
479
+ return;
480
+ }
481
+
482
+ if (event.type === "message.part.updated") {
483
+ const rawPart = asRecord(eventData.part) ?? eventData;
484
+ const targetId = options?.messageId ?? activeAssistantIdRef.current;
485
+ const delta = asString(eventData.delta);
486
+
487
+ if (!targetId || !rawPart) {
488
+ return;
489
+ }
490
+
491
+ const normalizedPart = normalizePart(rawPart);
492
+ if (!normalizedPart) {
493
+ return;
494
+ }
495
+
496
+ const key = getPartKey(rawPart);
497
+ setConversation((prev) => {
498
+ const existingParts = prev.partMap[targetId] ?? [];
499
+ const nextParts = [...existingParts];
500
+ const indexMap =
501
+ partIndexRef.current[targetId] ??
502
+ (partIndexRef.current[targetId] = {});
503
+ const existingIndex = indexMap[key];
504
+
505
+ if (existingIndex == null) {
506
+ indexMap[key] = nextParts.length;
507
+ nextParts.push(mergePart(undefined, normalizedPart, delta));
508
+ } else {
509
+ nextParts[existingIndex] = mergePart(
510
+ nextParts[existingIndex],
511
+ normalizedPart,
512
+ delta,
513
+ );
514
+ }
515
+
516
+ return {
517
+ ...prev,
518
+ partMap: {
519
+ ...prev.partMap,
520
+ [targetId]: nextParts,
521
+ },
522
+ };
523
+ });
524
+
525
+ activeAssistantIdRef.current = targetId;
526
+ setIsStreaming(true);
527
+ return;
528
+ }
529
+
530
+ if (event.type === "result") {
531
+ completeAssistantMessage({
532
+ messageId: options?.messageId,
533
+ finalText: asString(eventData.finalText),
534
+ });
535
+ return;
536
+ }
537
+
538
+ if (event.type === "done") {
539
+ completeAssistantMessage({ messageId: options?.messageId });
540
+ return;
541
+ }
542
+
543
+ if (event.type === "error") {
544
+ failAssistantMessage(
545
+ asString(eventData.message) ?? "Agent error",
546
+ { messageId: options?.messageId },
547
+ );
548
+ }
549
+ },
550
+ [completeAssistantMessage, failAssistantMessage],
551
+ );
552
+
553
+ const reset = useCallback(() => {
554
+ setConversation({ messages: [], partMap: {} });
555
+ setIsStreaming(false);
556
+ activeAssistantIdRef.current = null;
557
+ insertionIndexRef.current = 0;
558
+ partIndexRef.current = {};
559
+ }, []);
560
+
561
+ return {
562
+ messages: conversation.messages,
563
+ partMap: conversation.partMap,
564
+ isStreaming,
565
+ activeAssistantMessageId: activeAssistantIdRef.current,
566
+ replaceHistory,
567
+ appendUserMessage,
568
+ beginAssistantMessage,
569
+ applySdkEvent,
570
+ completeAssistantMessage,
571
+ failAssistantMessage,
572
+ setStreaming: setIsStreaming,
573
+ reset,
574
+ };
575
+ }