@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,455 @@
1
+ import { useStore } from "@nanostores/react";
2
+ import { atom } from "nanostores";
3
+ import { useMemo } from "react";
4
+
5
+ export type SessionProjectKey = string | number;
6
+ export type ActiveSessionStatus = "idle" | "running" | "attention-needed" | "error";
7
+ export type ActiveSessionReconnectState = "idle" | "reconnecting" | "failed";
8
+ export type ActiveSessionConnectionState =
9
+ | "disconnected"
10
+ | "connecting"
11
+ | "connected"
12
+ | "reconnecting"
13
+ | "error";
14
+ export type ActiveSessionTransportMode = "websocket" | "sse" | "polling" | "custom";
15
+
16
+ export interface ActiveSessionRecord {
17
+ sessionId: string;
18
+ projectId: SessionProjectKey | null;
19
+ projectLabel?: string;
20
+ title?: string;
21
+ href?: string;
22
+ registeredAt: number;
23
+ lastActivityAt: number;
24
+ lastEventAt: number | null;
25
+ status: ActiveSessionStatus;
26
+ isRunning: boolean;
27
+ isForeground: boolean;
28
+ needsAttention: boolean;
29
+ connectionState: ActiveSessionConnectionState;
30
+ reconnectState: ActiveSessionReconnectState;
31
+ transportMode: ActiveSessionTransportMode | null;
32
+ lastError: string | null;
33
+ metadata?: Record<string, unknown>;
34
+ }
35
+
36
+ export interface ActiveSessionsState {
37
+ sessions: Record<string, ActiveSessionRecord>;
38
+ lastUpdatedAt: number;
39
+ }
40
+
41
+ export interface RegisterActiveSessionOptions {
42
+ sessionId: string;
43
+ projectId?: SessionProjectKey | null;
44
+ projectLabel?: string;
45
+ title?: string;
46
+ href?: string;
47
+ metadata?: Record<string, unknown>;
48
+ }
49
+
50
+ export interface ActiveSessionConnectionOptions {
51
+ connectionState: ActiveSessionConnectionState;
52
+ reconnectState?: ActiveSessionReconnectState;
53
+ transportMode?: ActiveSessionTransportMode | null;
54
+ lastError?: string | null;
55
+ lastEventAt?: number | null;
56
+ }
57
+
58
+ export interface ActiveSessionActivityOptions {
59
+ lastEventAt?: number | null;
60
+ }
61
+
62
+ export interface ActiveProjectActivity {
63
+ projectId: SessionProjectKey;
64
+ projectLabel?: string;
65
+ activeSessionCount: number;
66
+ runningSessionIds: string[];
67
+ lastActivityAt: number;
68
+ }
69
+
70
+ const INITIAL_STATE: ActiveSessionsState = {
71
+ sessions: {},
72
+ lastUpdatedAt: 0,
73
+ };
74
+
75
+ const STATUS_PRIORITY: Record<ActiveSessionStatus, number> = {
76
+ running: 0,
77
+ error: 1,
78
+ "attention-needed": 2,
79
+ idle: 3,
80
+ };
81
+
82
+ const DEFAULT_RECORD = {
83
+ status: "idle",
84
+ isRunning: false,
85
+ isForeground: false,
86
+ needsAttention: false,
87
+ connectionState: "disconnected",
88
+ reconnectState: "idle",
89
+ transportMode: null,
90
+ lastError: null,
91
+ lastEventAt: null,
92
+ } satisfies Pick<
93
+ ActiveSessionRecord,
94
+ | "status"
95
+ | "isRunning"
96
+ | "isForeground"
97
+ | "needsAttention"
98
+ | "connectionState"
99
+ | "reconnectState"
100
+ | "transportMode"
101
+ | "lastError"
102
+ | "lastEventAt"
103
+ >;
104
+
105
+ export const activeSessionsAtom = atom<ActiveSessionsState>(INITIAL_STATE);
106
+
107
+ let foregroundSessionId: string | null = null;
108
+
109
+ function createRecord(
110
+ options: RegisterActiveSessionOptions,
111
+ now: number,
112
+ ): ActiveSessionRecord {
113
+ return {
114
+ sessionId: options.sessionId,
115
+ projectId: options.projectId ?? null,
116
+ projectLabel: options.projectLabel,
117
+ title: options.title,
118
+ href: options.href,
119
+ registeredAt: now,
120
+ lastActivityAt: now,
121
+ metadata: options.metadata,
122
+ ...DEFAULT_RECORD,
123
+ isForeground: options.sessionId === foregroundSessionId,
124
+ };
125
+ }
126
+
127
+ function resolveStatus(session: ActiveSessionRecord): ActiveSessionStatus {
128
+ if (session.isRunning) return "running";
129
+ if (session.lastError || session.reconnectState === "failed") return "error";
130
+ if (session.needsAttention) return "attention-needed";
131
+ return "idle";
132
+ }
133
+
134
+ function normalizeSession(session: ActiveSessionRecord): ActiveSessionRecord {
135
+ return {
136
+ ...session,
137
+ status: resolveStatus(session),
138
+ };
139
+ }
140
+
141
+ function updateSession(
142
+ sessionId: string,
143
+ updater: (record: ActiveSessionRecord, now: number) => ActiveSessionRecord,
144
+ ): void {
145
+ const current = activeSessionsAtom.get();
146
+ const existing = current.sessions[sessionId];
147
+ if (!existing) return;
148
+
149
+ const now = Date.now();
150
+ activeSessionsAtom.set({
151
+ sessions: {
152
+ ...current.sessions,
153
+ [sessionId]: normalizeSession(updater(existing, now)),
154
+ },
155
+ lastUpdatedAt: now,
156
+ });
157
+ }
158
+
159
+ export function registerActiveSession(options: RegisterActiveSessionOptions): void {
160
+ const now = Date.now();
161
+ const current = activeSessionsAtom.get();
162
+ const existing = current.sessions[options.sessionId];
163
+
164
+ activeSessionsAtom.set({
165
+ sessions: {
166
+ ...current.sessions,
167
+ [options.sessionId]: normalizeSession(
168
+ existing
169
+ ? {
170
+ ...existing,
171
+ projectId: existing.projectId ?? options.projectId ?? null,
172
+ projectLabel: options.projectLabel ?? existing.projectLabel,
173
+ title: options.title ?? existing.title,
174
+ href: options.href ?? existing.href,
175
+ metadata: options.metadata ?? existing.metadata,
176
+ lastActivityAt: now,
177
+ isForeground: options.sessionId === foregroundSessionId || existing.isForeground,
178
+ }
179
+ : createRecord(options, now),
180
+ ),
181
+ },
182
+ lastUpdatedAt: now,
183
+ });
184
+ }
185
+
186
+ export function unregisterActiveSession(sessionId: string): void {
187
+ const current = activeSessionsAtom.get();
188
+ if (!current.sessions[sessionId]) return;
189
+
190
+ const { [sessionId]: _removed, ...remaining } = current.sessions;
191
+ if (foregroundSessionId === sessionId) {
192
+ foregroundSessionId = null;
193
+ }
194
+
195
+ activeSessionsAtom.set({
196
+ sessions: remaining,
197
+ lastUpdatedAt: Date.now(),
198
+ });
199
+ }
200
+
201
+ export function setForegroundActiveSession(sessionId: string | null): void {
202
+ foregroundSessionId = sessionId;
203
+ const current = activeSessionsAtom.get();
204
+ const now = Date.now();
205
+ let changed = false;
206
+
207
+ const sessions = Object.fromEntries(
208
+ Object.entries(current.sessions).map(([id, session]) => {
209
+ const isForeground = id === sessionId;
210
+ if (session.isForeground !== isForeground) {
211
+ changed = true;
212
+ }
213
+
214
+ return [
215
+ id,
216
+ {
217
+ ...session,
218
+ isForeground,
219
+ lastActivityAt: isForeground ? now : session.lastActivityAt,
220
+ },
221
+ ];
222
+ }),
223
+ );
224
+
225
+ if (!changed) return;
226
+
227
+ activeSessionsAtom.set({
228
+ sessions,
229
+ lastUpdatedAt: now,
230
+ });
231
+ }
232
+
233
+ export function updateActiveSessionMeta(
234
+ sessionId: string,
235
+ meta: Partial<Pick<ActiveSessionRecord, "title" | "href" | "projectId" | "projectLabel" | "metadata">>,
236
+ ): void {
237
+ updateSession(sessionId, (session, now) => ({
238
+ ...session,
239
+ ...meta,
240
+ lastActivityAt: now,
241
+ }));
242
+ }
243
+
244
+ export function setActiveSessionConnection(
245
+ sessionId: string,
246
+ options: ActiveSessionConnectionOptions,
247
+ ): void {
248
+ updateSession(sessionId, (session, now) => ({
249
+ ...session,
250
+ connectionState: options.connectionState,
251
+ reconnectState: options.reconnectState ?? session.reconnectState,
252
+ transportMode: options.transportMode ?? session.transportMode,
253
+ lastError: options.lastError === undefined ? session.lastError : options.lastError,
254
+ lastEventAt: options.lastEventAt ?? session.lastEventAt,
255
+ lastActivityAt: now,
256
+ }));
257
+ }
258
+
259
+ export function setActiveSessionRunning(
260
+ sessionId: string,
261
+ isRunning: boolean,
262
+ options?: ActiveSessionActivityOptions,
263
+ ): void {
264
+ updateSession(sessionId, (session, now) => ({
265
+ ...session,
266
+ isRunning,
267
+ needsAttention: isRunning ? false : session.needsAttention,
268
+ lastError: isRunning ? null : session.lastError,
269
+ lastEventAt: options?.lastEventAt ?? session.lastEventAt,
270
+ lastActivityAt: now,
271
+ }));
272
+ }
273
+
274
+ export function setActiveSessionAttention(
275
+ sessionId: string,
276
+ needsAttention: boolean,
277
+ options?: ActiveSessionActivityOptions,
278
+ ): void {
279
+ updateSession(sessionId, (session, now) => ({
280
+ ...session,
281
+ needsAttention,
282
+ isRunning: needsAttention ? false : session.isRunning,
283
+ lastEventAt: options?.lastEventAt ?? session.lastEventAt,
284
+ lastActivityAt: now,
285
+ }));
286
+ }
287
+
288
+ export function setActiveSessionError(sessionId: string, error: string | null): void {
289
+ updateSession(sessionId, (session, now) => ({
290
+ ...session,
291
+ isRunning: false,
292
+ needsAttention: false,
293
+ lastError: error,
294
+ reconnectState: error ? "failed" : "idle",
295
+ connectionState: error ? "error" : session.connectionState,
296
+ lastEventAt: now,
297
+ lastActivityAt: now,
298
+ }));
299
+ }
300
+
301
+ export function bumpActiveSessionActivity(
302
+ sessionId: string,
303
+ options?: ActiveSessionActivityOptions,
304
+ ): void {
305
+ updateSession(sessionId, (session, now) => ({
306
+ ...session,
307
+ lastEventAt: options?.lastEventAt ?? session.lastEventAt ?? now,
308
+ lastActivityAt: now,
309
+ }));
310
+ }
311
+
312
+ export function resetActiveSessions(): void {
313
+ foregroundSessionId = null;
314
+ activeSessionsAtom.set(INITIAL_STATE);
315
+ }
316
+
317
+ export function getAllActiveSessions(state: ActiveSessionsState): ActiveSessionRecord[] {
318
+ return Object.values(state.sessions);
319
+ }
320
+
321
+ export function getActiveSession(
322
+ state: ActiveSessionsState,
323
+ sessionId: string,
324
+ ): ActiveSessionRecord | null {
325
+ return state.sessions[sessionId] ?? null;
326
+ }
327
+
328
+ export function getSessionsForProject(
329
+ state: ActiveSessionsState,
330
+ projectId: SessionProjectKey,
331
+ ): ActiveSessionRecord[] {
332
+ return Object.values(state.sessions).filter((session) => session.projectId === projectId);
333
+ }
334
+
335
+ export function getSessionsForNavbar(
336
+ state: ActiveSessionsState,
337
+ projectId?: SessionProjectKey | null,
338
+ ): ActiveSessionRecord[] {
339
+ const sessions =
340
+ projectId == null ? Object.values(state.sessions) : getSessionsForProject(state, projectId);
341
+
342
+ return [...sessions].sort((left, right) => {
343
+ if (left.isForeground !== right.isForeground) {
344
+ return Number(right.isForeground) - Number(left.isForeground);
345
+ }
346
+
347
+ const statusDiff = STATUS_PRIORITY[left.status] - STATUS_PRIORITY[right.status];
348
+ if (statusDiff !== 0) {
349
+ return statusDiff;
350
+ }
351
+
352
+ return right.lastActivityAt - left.lastActivityAt;
353
+ });
354
+ }
355
+
356
+ export function getSessionsByActivity(state: ActiveSessionsState): ActiveSessionRecord[] {
357
+ return [...Object.values(state.sessions)].sort(
358
+ (left, right) => right.lastActivityAt - left.lastActivityAt,
359
+ );
360
+ }
361
+
362
+ export function getTotalRunningSessionCount(state: ActiveSessionsState): number {
363
+ return Object.values(state.sessions).filter((session) => session.isRunning).length;
364
+ }
365
+
366
+ export function hasBackgroundRunningSessions(state: ActiveSessionsState): boolean {
367
+ return Object.values(state.sessions).some(
368
+ (session) => session.isRunning && !session.isForeground,
369
+ );
370
+ }
371
+
372
+ export function getAllProjectActivity(state: ActiveSessionsState): ActiveProjectActivity[] {
373
+ const grouped = new Map<SessionProjectKey, ActiveProjectActivity>();
374
+
375
+ for (const session of Object.values(state.sessions)) {
376
+ if (session.projectId == null) continue;
377
+
378
+ const existing = grouped.get(session.projectId);
379
+ if (existing) {
380
+ existing.activeSessionCount += 1;
381
+ existing.lastActivityAt = Math.max(existing.lastActivityAt, session.lastActivityAt);
382
+ if (session.isRunning) {
383
+ existing.runningSessionIds.push(session.sessionId);
384
+ }
385
+ if (!existing.projectLabel && session.projectLabel) {
386
+ existing.projectLabel = session.projectLabel;
387
+ }
388
+ continue;
389
+ }
390
+
391
+ grouped.set(session.projectId, {
392
+ projectId: session.projectId,
393
+ projectLabel: session.projectLabel,
394
+ activeSessionCount: 1,
395
+ runningSessionIds: session.isRunning ? [session.sessionId] : [],
396
+ lastActivityAt: session.lastActivityAt,
397
+ });
398
+ }
399
+
400
+ return [...grouped.values()].sort((left, right) => right.lastActivityAt - left.lastActivityAt);
401
+ }
402
+
403
+ export function useActiveSessionsState(): ActiveSessionsState {
404
+ return useStore(activeSessionsAtom);
405
+ }
406
+
407
+ export function useActiveSessions(): ActiveSessionRecord[] {
408
+ const state = useStore(activeSessionsAtom);
409
+ return useMemo(() => getAllActiveSessions(state), [state]);
410
+ }
411
+
412
+ export function useActiveSession(sessionId: string | null): ActiveSessionRecord | null {
413
+ const state = useStore(activeSessionsAtom);
414
+ return useMemo(
415
+ () => (sessionId ? getActiveSession(state, sessionId) : null),
416
+ [sessionId, state],
417
+ );
418
+ }
419
+
420
+ export function useProjectSessions(
421
+ projectId: SessionProjectKey | null,
422
+ ): ActiveSessionRecord[] {
423
+ const state = useStore(activeSessionsAtom);
424
+ return useMemo(
425
+ () => (projectId == null ? [] : getSessionsForProject(state, projectId)),
426
+ [projectId, state],
427
+ );
428
+ }
429
+
430
+ export function useNavbarSessions(
431
+ projectId?: SessionProjectKey | null,
432
+ ): ActiveSessionRecord[] {
433
+ const state = useStore(activeSessionsAtom);
434
+ return useMemo(() => getSessionsForNavbar(state, projectId), [projectId, state]);
435
+ }
436
+
437
+ export function useSessionsByActivity(): ActiveSessionRecord[] {
438
+ const state = useStore(activeSessionsAtom);
439
+ return useMemo(() => getSessionsByActivity(state), [state]);
440
+ }
441
+
442
+ export function useProjectActivity(): ActiveProjectActivity[] {
443
+ const state = useStore(activeSessionsAtom);
444
+ return useMemo(() => getAllProjectActivity(state), [state]);
445
+ }
446
+
447
+ export function useTotalRunningSessions(): number {
448
+ const state = useStore(activeSessionsAtom);
449
+ return useMemo(() => getTotalRunningSessionCount(state), [state]);
450
+ }
451
+
452
+ export function useHasBackgroundRunningSessions(): boolean {
453
+ const state = useStore(activeSessionsAtom);
454
+ return useMemo(() => hasBackgroundRunningSessions(state), [state]);
455
+ }
@@ -0,0 +1,43 @@
1
+ import { atom, map } from 'nanostores';
2
+ import type { SessionMessage } from '../types/message';
3
+ import type { SessionPart } from '../types/parts';
4
+
5
+ /** Ordered list of messages in the current chat session. */
6
+ export const messagesAtom = atom<SessionMessage[]>([]);
7
+
8
+ /** Map of message ID → parts for that message. */
9
+ export const partMapAtom = map<Record<string, SessionPart[]>>({});
10
+
11
+ /** Whether the assistant is currently streaming a response. */
12
+ export const isStreamingAtom = atom(false);
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Helpers
16
+ // ---------------------------------------------------------------------------
17
+
18
+ let insertionCounter = 0;
19
+
20
+ export function addMessage(msg: SessionMessage) {
21
+ const withIndex = { ...msg, _insertionIndex: insertionCounter++ };
22
+ messagesAtom.set([...messagesAtom.get(), withIndex]);
23
+ }
24
+
25
+ export function addParts(messageId: string, parts: SessionPart[]) {
26
+ const current = partMapAtom.get();
27
+ const existing = current[messageId] ?? [];
28
+ partMapAtom.setKey(messageId, [...existing, ...parts]);
29
+ }
30
+
31
+ export function updatePart(messageId: string, partIndex: number, part: SessionPart) {
32
+ const current = partMapAtom.get();
33
+ const existing = [...(current[messageId] ?? [])];
34
+ existing[partIndex] = part;
35
+ partMapAtom.setKey(messageId, existing);
36
+ }
37
+
38
+ export function clearChat() {
39
+ messagesAtom.set([]);
40
+ partMapAtom.set({});
41
+ isStreamingAtom.set(false);
42
+ insertionCounter = 0;
43
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./active-sessions-store";
2
+ export * from "./chat-store";
@@ -0,0 +1,116 @@
1
+ import { memo, useState } from "react";
2
+ import {
3
+ Terminal,
4
+ ChevronDown,
5
+ ChevronRight,
6
+ Loader2,
7
+ } from "lucide-react";
8
+ import { cn } from "../lib/utils";
9
+ import type { ToolPart } from "../types/parts";
10
+ import { PreviewCard, PreviewError, PreviewLoading } from "./preview-primitives";
11
+
12
+ export interface CommandPreviewProps {
13
+ part: ToolPart;
14
+ }
15
+
16
+ /** Extract stdout/stderr/exitCode from tool output. */
17
+ function extractCommandOutput(output: unknown): {
18
+ stdout: string;
19
+ stderr: string;
20
+ exitCode: number;
21
+ } {
22
+ if (!output) return { stdout: "", stderr: "", exitCode: 0 };
23
+
24
+ if (typeof output === "string") return { stdout: output, stderr: "", exitCode: 0 };
25
+
26
+ if (typeof output === "object" && output !== null) {
27
+ const obj = output as Record<string, unknown>;
28
+ return {
29
+ stdout: String(obj.stdout ?? obj.output ?? ""),
30
+ stderr: String(obj.stderr ?? ""),
31
+ exitCode: Number(obj.exitCode ?? obj.exit_code ?? obj.code ?? 0),
32
+ };
33
+ }
34
+
35
+ return { stdout: String(output), stderr: "", exitCode: 0 };
36
+ }
37
+
38
+ /**
39
+ * Terminal-style command output preview.
40
+ * Shows the command, exit code, and stdout/stderr with expand/collapse.
41
+ */
42
+ export const CommandPreview = memo(({ part }: CommandPreviewProps) => {
43
+ const [expanded, setExpanded] = useState(true);
44
+ const input = part.state.input as Record<string, unknown> | undefined;
45
+ const command =
46
+ typeof input?.command === "string" ? input.command : String(input ?? "");
47
+
48
+ const output =
49
+ part.state.status === "completed"
50
+ ? extractCommandOutput(part.state.output)
51
+ : null;
52
+
53
+ const isError = output ? output.exitCode !== 0 : part.state.status === "error";
54
+ const errorText = part.state.error;
55
+ const lineCount = output?.stdout ? output.stdout.split("\n").length : 0;
56
+
57
+ return (
58
+ <PreviewCard
59
+ icon={<Terminal className="h-4 w-4" />}
60
+ title="Command"
61
+ description={command}
62
+ meta={
63
+ output ? (
64
+ <span
65
+ className={cn(
66
+ "inline-flex items-center rounded-full border px-2 py-0.5 font-mono",
67
+ isError
68
+ ? "border-[var(--surface-danger-border)] bg-[var(--surface-danger-bg)] text-[var(--surface-danger-text)]"
69
+ : "border-[var(--surface-success-border)] bg-[var(--surface-success-bg)] text-[var(--surface-success-text)]",
70
+ )}
71
+ >
72
+ exit {output.exitCode}
73
+ </span>
74
+ ) : null
75
+ }
76
+ >
77
+ <button
78
+ onClick={() => setExpanded((value) => !value)}
79
+ className="flex w-full items-center gap-2 rounded-[var(--radius-md)] border border-border bg-muted px-3 py-2 text-left transition-colors hover:border-[var(--border-accent-hover)] hover:bg-accent/45"
80
+ >
81
+ <code className="min-w-0 flex-1 truncate text-xs font-mono text-foreground">
82
+ {command}
83
+ </code>
84
+ {lineCount > 0 ? (
85
+ <span className="shrink-0 text-xs text-muted-foreground">
86
+ {lineCount} line{lineCount === 1 ? "" : "s"}
87
+ </span>
88
+ ) : null}
89
+ {expanded ? (
90
+ <ChevronDown className="h-3.5 w-3.5 text-muted-foreground" />
91
+ ) : (
92
+ <ChevronRight className="h-3.5 w-3.5 text-muted-foreground" />
93
+ )}
94
+ </button>
95
+
96
+ {part.state.status === "running" ? <PreviewLoading /> : null}
97
+ {errorText ? <PreviewError error={errorText} /> : null}
98
+
99
+ {expanded && output ? (
100
+ <div className="overflow-hidden rounded-[var(--radius-md)] border border-border bg-muted">
101
+ {output.stdout ? (
102
+ <pre className="max-h-80 overflow-auto px-3 py-3 text-xs font-mono whitespace-pre-wrap break-all text-foreground">
103
+ {output.stdout}
104
+ </pre>
105
+ ) : null}
106
+ {output.stderr ? (
107
+ <pre className="max-h-80 overflow-auto border-t border-border px-3 py-3 text-xs font-mono whitespace-pre-wrap break-all text-red-200">
108
+ {output.stderr}
109
+ </pre>
110
+ ) : null}
111
+ </div>
112
+ ) : null}
113
+ </PreviewCard>
114
+ );
115
+ });
116
+ CommandPreview.displayName = "CommandPreview";
@@ -0,0 +1,85 @@
1
+ import { memo } from "react";
2
+ import { GitCompareArrows } from "lucide-react";
3
+ import type { ToolPart } from "../types/parts";
4
+ import { CodeBlock } from "../markdown/code-block";
5
+ import { PreviewCard, PreviewEmpty, PreviewError, PreviewLoading } from "./preview-primitives";
6
+
7
+ function extractString(record: Record<string, unknown>, ...keys: string[]) {
8
+ for (const key of keys) {
9
+ const value = record[key];
10
+ if (typeof value === "string" && value.length > 0) {
11
+ return value;
12
+ }
13
+ }
14
+
15
+ return undefined;
16
+ }
17
+
18
+ function extractDiffPayload(part: ToolPart): {
19
+ path?: string;
20
+ diff?: string;
21
+ before?: string;
22
+ after?: string;
23
+ } {
24
+ const sources = [part.state.input, part.state.output, part.state.metadata];
25
+
26
+ for (const source of sources) {
27
+ if (!source || typeof source !== "object") {
28
+ continue;
29
+ }
30
+
31
+ const record = source as Record<string, unknown>;
32
+ const diff = extractString(record, "diff", "patch", "unifiedDiff", "unified_diff");
33
+ const before = extractString(record, "oldString", "old_string", "before", "previous");
34
+ const after = extractString(record, "newString", "new_string", "after", "updated");
35
+ const path = extractString(record, "file_path", "filePath", "path");
36
+
37
+ if (diff || before || after) {
38
+ return { path, diff, before, after };
39
+ }
40
+ }
41
+
42
+ return {};
43
+ }
44
+
45
+ export interface DiffPreviewProps {
46
+ part: ToolPart;
47
+ }
48
+
49
+ export const DiffPreview = memo(({ part }: DiffPreviewProps) => {
50
+ const payload = extractDiffPayload(part);
51
+
52
+ return (
53
+ <PreviewCard
54
+ icon={<GitCompareArrows className="h-4 w-4" />}
55
+ title={payload.path ? `Changes for ${payload.path}` : "File changes"}
56
+ >
57
+ {part.state.status === "running" ? <PreviewLoading label="Computing changes…" /> : null}
58
+ {part.state.error ? <PreviewError error={part.state.error} /> : null}
59
+ {!payload.diff && !payload.before && !payload.after ? (
60
+ <PreviewEmpty label="No structured diff payload was provided." />
61
+ ) : null}
62
+ {payload.diff ? (
63
+ <CodeBlock code={payload.diff} language="diff" className="rounded-[var(--radius-md)]" />
64
+ ) : null}
65
+ {!payload.diff && payload.before ? (
66
+ <div className="space-y-2">
67
+ <div className="text-xs font-semibold uppercase tracking-[0.08em] text-muted-foreground">
68
+ Before
69
+ </div>
70
+ <CodeBlock code={payload.before} language="text" className="rounded-[var(--radius-md)]" />
71
+ </div>
72
+ ) : null}
73
+ {!payload.diff && payload.after ? (
74
+ <div className="space-y-2">
75
+ <div className="text-xs font-semibold uppercase tracking-[0.08em] text-muted-foreground">
76
+ After
77
+ </div>
78
+ <CodeBlock code={payload.after} language="text" className="rounded-[var(--radius-md)]" />
79
+ </div>
80
+ ) : null}
81
+ </PreviewCard>
82
+ );
83
+ });
84
+
85
+ DiffPreview.displayName = "DiffPreview";