@schlessera/brain-ui-react 0.34.0 → 0.35.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 (223) hide show
  1. package/README.md +12 -0
  2. package/dist/components/activity/activity-page.d.ts.map +1 -1
  3. package/dist/components/activity/activity-page.js +8 -149
  4. package/dist/components/activity/activity-page.js.map +1 -1
  5. package/dist/components/activity/activity-rollups.d.ts +5 -0
  6. package/dist/components/activity/activity-rollups.d.ts.map +1 -0
  7. package/dist/components/activity/activity-rollups.js +39 -0
  8. package/dist/components/activity/activity-rollups.js.map +1 -0
  9. package/dist/components/activity/activity-run-detail.d.ts +11 -0
  10. package/dist/components/activity/activity-run-detail.d.ts.map +1 -0
  11. package/dist/components/activity/activity-run-detail.js +111 -0
  12. package/dist/components/activity/activity-run-detail.js.map +1 -0
  13. package/dist/components/activity/activity-run-list.d.ts +15 -0
  14. package/dist/components/activity/activity-run-list.d.ts.map +1 -0
  15. package/dist/components/activity/activity-run-list.js +25 -0
  16. package/dist/components/activity/activity-run-list.js.map +1 -0
  17. package/dist/components/chat/brain-markdown-code.d.ts +4 -0
  18. package/dist/components/chat/brain-markdown-code.d.ts.map +1 -0
  19. package/dist/components/chat/brain-markdown-code.js +47 -0
  20. package/dist/components/chat/brain-markdown-code.js.map +1 -0
  21. package/dist/components/chat/brain-markdown-entities.d.ts +14 -0
  22. package/dist/components/chat/brain-markdown-entities.d.ts.map +1 -0
  23. package/dist/components/chat/brain-markdown-entities.js +93 -0
  24. package/dist/components/chat/brain-markdown-entities.js.map +1 -0
  25. package/dist/components/chat/brain-markdown-links.d.ts +36 -0
  26. package/dist/components/chat/brain-markdown-links.d.ts.map +1 -0
  27. package/dist/components/chat/brain-markdown-links.js +176 -0
  28. package/dist/components/chat/brain-markdown-links.js.map +1 -0
  29. package/dist/components/chat/brain-markdown-share.d.ts +15 -0
  30. package/dist/components/chat/brain-markdown-share.d.ts.map +1 -0
  31. package/dist/components/chat/brain-markdown-share.js +40 -0
  32. package/dist/components/chat/brain-markdown-share.js.map +1 -0
  33. package/dist/components/chat/brain-markdown.d.ts +1 -34
  34. package/dist/components/chat/brain-markdown.d.ts.map +1 -1
  35. package/dist/components/chat/brain-markdown.js +9 -402
  36. package/dist/components/chat/brain-markdown.js.map +1 -1
  37. package/dist/components/chat/composer.d.ts.map +1 -1
  38. package/dist/components/chat/composer.js +14 -1
  39. package/dist/components/chat/composer.js.map +1 -1
  40. package/dist/components/chat/session-drawer.d.ts.map +1 -1
  41. package/dist/components/chat/session-drawer.js +25 -10
  42. package/dist/components/chat/session-drawer.js.map +1 -1
  43. package/dist/components/chat/tool-views.d.ts +2 -2
  44. package/dist/components/chat/tool-views.d.ts.map +1 -1
  45. package/dist/components/chat/tool-views.js +2 -16
  46. package/dist/components/chat/tool-views.js.map +1 -1
  47. package/dist/components/chat/use-markdown-highlight.d.ts +12 -0
  48. package/dist/components/chat/use-markdown-highlight.d.ts.map +1 -0
  49. package/dist/components/chat/use-markdown-highlight.js +56 -0
  50. package/dist/components/chat/use-markdown-highlight.js.map +1 -0
  51. package/dist/components/connectivity/connection-gate.d.ts.map +1 -1
  52. package/dist/components/connectivity/connection-gate.js +94 -8
  53. package/dist/components/connectivity/connection-gate.js.map +1 -1
  54. package/dist/components/connectivity/connection-state.d.ts +12 -0
  55. package/dist/components/connectivity/connection-state.d.ts.map +1 -0
  56. package/dist/components/connectivity/connection-state.js +23 -0
  57. package/dist/components/connectivity/connection-state.js.map +1 -0
  58. package/dist/components/graph/graph-canvas-lazy.d.ts +2 -0
  59. package/dist/components/graph/graph-canvas-lazy.d.ts.map +1 -0
  60. package/dist/components/graph/graph-canvas-lazy.js +4 -0
  61. package/dist/components/graph/graph-canvas-lazy.js.map +1 -0
  62. package/dist/components/graph/graph-controls.js +4 -1
  63. package/dist/components/graph/graph-controls.js.map +1 -1
  64. package/dist/components/graph/graph-maintenance.d.ts +8 -0
  65. package/dist/components/graph/graph-maintenance.d.ts.map +1 -0
  66. package/dist/components/graph/graph-maintenance.js +115 -0
  67. package/dist/components/graph/graph-maintenance.js.map +1 -0
  68. package/dist/components/graph/graph-page.d.ts.map +1 -1
  69. package/dist/components/graph/graph-page.js +11 -306
  70. package/dist/components/graph/graph-page.js.map +1 -1
  71. package/dist/components/graph/graph-scene.d.ts +8 -0
  72. package/dist/components/graph/graph-scene.d.ts.map +1 -0
  73. package/dist/components/graph/graph-scene.js +208 -0
  74. package/dist/components/graph/graph-scene.js.map +1 -0
  75. package/dist/components/graph/graph-spinner.d.ts +2 -0
  76. package/dist/components/graph/graph-spinner.d.ts.map +1 -0
  77. package/dist/components/graph/graph-spinner.js +6 -0
  78. package/dist/components/graph/graph-spinner.js.map +1 -0
  79. package/dist/components/layout/app-shell.d.ts.map +1 -1
  80. package/dist/components/layout/app-shell.js +2 -1
  81. package/dist/components/layout/app-shell.js.map +1 -1
  82. package/dist/components/quick-actions/add-modal.d.ts.map +1 -1
  83. package/dist/components/quick-actions/add-modal.js +47 -4
  84. package/dist/components/quick-actions/add-modal.js.map +1 -1
  85. package/dist/components/quick-actions/search-modal.d.ts.map +1 -1
  86. package/dist/components/quick-actions/search-modal.js +46 -18
  87. package/dist/components/quick-actions/search-modal.js.map +1 -1
  88. package/dist/components/quick-actions/streaming-modal.d.ts.map +1 -1
  89. package/dist/components/quick-actions/streaming-modal.js +4 -1
  90. package/dist/components/quick-actions/streaming-modal.js.map +1 -1
  91. package/dist/components/settings/devices-agents-tab.d.ts +5 -0
  92. package/dist/components/settings/devices-agents-tab.d.ts.map +1 -0
  93. package/dist/components/settings/devices-agents-tab.js +96 -0
  94. package/dist/components/settings/devices-agents-tab.js.map +1 -0
  95. package/dist/components/settings/one-time-agent-credential.d.ts +3 -0
  96. package/dist/components/settings/one-time-agent-credential.d.ts.map +1 -0
  97. package/dist/components/settings/one-time-agent-credential.js +55 -0
  98. package/dist/components/settings/one-time-agent-credential.js.map +1 -0
  99. package/dist/components/settings/settings-panel.d.ts.map +1 -1
  100. package/dist/components/settings/settings-panel.js +16 -3
  101. package/dist/components/settings/settings-panel.js.map +1 -1
  102. package/dist/hooks/use-vpn-status.d.ts +6 -1
  103. package/dist/hooks/use-vpn-status.d.ts.map +1 -1
  104. package/dist/hooks/use-vpn-status.js +67 -30
  105. package/dist/hooks/use-vpn-status.js.map +1 -1
  106. package/dist/hooks/use-websocket.d.ts +3 -2
  107. package/dist/hooks/use-websocket.d.ts.map +1 -1
  108. package/dist/hooks/use-websocket.js +65 -254
  109. package/dist/hooks/use-websocket.js.map +1 -1
  110. package/dist/hooks/websocket-handlers/activity.d.ts +5 -0
  111. package/dist/hooks/websocket-handlers/activity.d.ts.map +1 -0
  112. package/dist/hooks/websocket-handlers/activity.js +10 -0
  113. package/dist/hooks/websocket-handlers/activity.js.map +1 -0
  114. package/dist/hooks/websocket-handlers/chat.d.ts +17 -0
  115. package/dist/hooks/websocket-handlers/chat.d.ts.map +1 -0
  116. package/dist/hooks/websocket-handlers/chat.js +175 -0
  117. package/dist/hooks/websocket-handlers/chat.js.map +1 -0
  118. package/dist/hooks/websocket-handlers/connection.d.ts +6 -0
  119. package/dist/hooks/websocket-handlers/connection.d.ts.map +1 -0
  120. package/dist/hooks/websocket-handlers/connection.js +27 -0
  121. package/dist/hooks/websocket-handlers/connection.js.map +1 -0
  122. package/dist/hooks/websocket-handlers/index.d.ts +4 -0
  123. package/dist/hooks/websocket-handlers/index.d.ts.map +1 -0
  124. package/dist/hooks/websocket-handlers/index.js +19 -0
  125. package/dist/hooks/websocket-handlers/index.js.map +1 -0
  126. package/dist/hooks/websocket-handlers/mask.d.ts +4 -0
  127. package/dist/hooks/websocket-handlers/mask.d.ts.map +1 -0
  128. package/dist/hooks/websocket-handlers/mask.js +14 -0
  129. package/dist/hooks/websocket-handlers/mask.js.map +1 -0
  130. package/dist/hooks/websocket-handlers/provider.d.ts +4 -0
  131. package/dist/hooks/websocket-handlers/provider.d.ts.map +1 -0
  132. package/dist/hooks/websocket-handlers/provider.js +31 -0
  133. package/dist/hooks/websocket-handlers/provider.js.map +1 -0
  134. package/dist/hooks/websocket-handlers/types.d.ts +24 -0
  135. package/dist/hooks/websocket-handlers/types.d.ts.map +1 -0
  136. package/dist/hooks/websocket-handlers/types.js +2 -0
  137. package/dist/hooks/websocket-handlers/types.js.map +1 -0
  138. package/dist/lib/api-client.d.ts +32 -0
  139. package/dist/lib/api-client.d.ts.map +1 -1
  140. package/dist/lib/api-client.js +11 -0
  141. package/dist/lib/api-client.js.map +1 -1
  142. package/dist/lib/format-time.d.ts +3 -0
  143. package/dist/lib/format-time.d.ts.map +1 -0
  144. package/dist/lib/format-time.js +25 -0
  145. package/dist/lib/format-time.js.map +1 -0
  146. package/dist/lib/push-registration.d.ts +7 -0
  147. package/dist/lib/push-registration.d.ts.map +1 -0
  148. package/dist/lib/push-registration.js +26 -0
  149. package/dist/lib/push-registration.js.map +1 -0
  150. package/dist/stores/activity-store-helpers.d.ts +50 -0
  151. package/dist/stores/activity-store-helpers.d.ts.map +1 -0
  152. package/dist/stores/activity-store-helpers.js +143 -0
  153. package/dist/stores/activity-store-helpers.js.map +1 -0
  154. package/dist/stores/activity-store-types.d.ts +48 -0
  155. package/dist/stores/activity-store-types.d.ts.map +1 -0
  156. package/dist/stores/activity-store-types.js +2 -0
  157. package/dist/stores/activity-store-types.js.map +1 -0
  158. package/dist/stores/activity-store.d.ts +2 -78
  159. package/dist/stores/activity-store.d.ts.map +1 -1
  160. package/dist/stores/activity-store.js +22 -142
  161. package/dist/stores/activity-store.js.map +1 -1
  162. package/dist/stores/connection-store.d.ts +14 -2
  163. package/dist/stores/connection-store.d.ts.map +1 -1
  164. package/dist/stores/connection-store.js +17 -0
  165. package/dist/stores/connection-store.js.map +1 -1
  166. package/dist/stores/file-store.d.ts.map +1 -1
  167. package/dist/stores/file-store.js +54 -18
  168. package/dist/stores/file-store.js.map +1 -1
  169. package/dist/stores/principal-store.d.ts +16 -0
  170. package/dist/stores/principal-store.d.ts.map +1 -0
  171. package/dist/stores/principal-store.js +29 -0
  172. package/dist/stores/principal-store.js.map +1 -0
  173. package/dist/stores/ui-store.d.ts +1 -1
  174. package/dist/stores/ui-store.d.ts.map +1 -1
  175. package/dist/styles.css +1 -1
  176. package/package.json +2 -2
  177. package/src/components/activity/activity-page.tsx +6 -477
  178. package/src/components/activity/activity-rollups.tsx +94 -0
  179. package/src/components/activity/activity-run-detail.tsx +301 -0
  180. package/src/components/activity/activity-run-list.tsx +97 -0
  181. package/src/components/chat/brain-markdown-code.tsx +60 -0
  182. package/src/components/chat/brain-markdown-entities.tsx +116 -0
  183. package/src/components/chat/brain-markdown-links.tsx +247 -0
  184. package/src/components/chat/brain-markdown-share.ts +47 -0
  185. package/src/components/chat/brain-markdown.tsx +27 -535
  186. package/src/components/chat/composer.tsx +14 -1
  187. package/src/components/chat/session-drawer.tsx +29 -10
  188. package/src/components/chat/tool-views.tsx +2 -13
  189. package/src/components/chat/use-markdown-highlight.ts +63 -0
  190. package/src/components/connectivity/connection-gate.tsx +128 -14
  191. package/src/components/connectivity/connection-state.ts +37 -0
  192. package/src/components/graph/graph-canvas-lazy.tsx +4 -0
  193. package/src/components/graph/graph-controls.tsx +4 -1
  194. package/src/components/graph/graph-maintenance.tsx +304 -0
  195. package/src/components/graph/graph-page.tsx +11 -840
  196. package/src/components/graph/graph-scene.tsx +548 -0
  197. package/src/components/graph/graph-spinner.tsx +9 -0
  198. package/src/components/layout/app-shell.tsx +2 -0
  199. package/src/components/quick-actions/add-modal.tsx +49 -4
  200. package/src/components/quick-actions/search-modal.tsx +46 -18
  201. package/src/components/quick-actions/streaming-modal.tsx +3 -1
  202. package/src/components/settings/devices-agents-tab.tsx +231 -0
  203. package/src/components/settings/one-time-agent-credential.tsx +131 -0
  204. package/src/components/settings/settings-panel.tsx +20 -5
  205. package/src/hooks/use-vpn-status.ts +71 -31
  206. package/src/hooks/use-websocket.ts +74 -275
  207. package/src/hooks/websocket-handlers/activity.ts +13 -0
  208. package/src/hooks/websocket-handlers/chat.ts +210 -0
  209. package/src/hooks/websocket-handlers/connection.ts +30 -0
  210. package/src/hooks/websocket-handlers/index.ts +59 -0
  211. package/src/hooks/websocket-handlers/mask.ts +17 -0
  212. package/src/hooks/websocket-handlers/provider.ts +35 -0
  213. package/src/hooks/websocket-handlers/types.ts +27 -0
  214. package/src/lib/api-client.ts +40 -1
  215. package/src/lib/format-time.ts +20 -0
  216. package/src/lib/push-registration.ts +29 -0
  217. package/src/stores/activity-store-helpers.ts +164 -0
  218. package/src/stores/activity-store-types.ts +55 -0
  219. package/src/stores/activity-store.ts +21 -217
  220. package/src/stores/connection-store.ts +33 -2
  221. package/src/stores/file-store.ts +42 -12
  222. package/src/stores/principal-store.ts +38 -0
  223. package/src/stores/ui-store.ts +1 -1
@@ -0,0 +1,55 @@
1
+ import type {
2
+ ActivitySpan,
3
+ ActivitySpanEvent,
4
+ ServerActivityDelta,
5
+ ServerActivitySnapshot,
6
+ } from "@schlessera/brain-ui-sdk/protocol";
7
+ import type { ActivityIntent } from "../lib/api-client.js";
8
+
9
+ export interface ActivityState {
10
+ /** server_hello said this host records activity. */
11
+ supported: boolean;
12
+ /** Session ids this connection has an activity subscription for. */
13
+ subscribed: Record<string, true>;
14
+ /** Bumped on every new connection so subscribe effects re-send. */
15
+ connectionEpoch: number;
16
+ /** Live span state per run, keyed runId -> spanId. */
17
+ spans: Record<string, Record<string, ActivitySpan>>;
18
+ /** Ordered events per span (already sorted by eventIndex). */
19
+ events: Record<string, ActivitySpanEvent[]>;
20
+ /** Per-run high-water seq from the snapshot; deltas at or below drop. */
21
+ highWater: Record<string, number>;
22
+ /**
23
+ * Newest APPLIED delta seq per run. Kept apart from `highWater` on
24
+ * purpose: the snapshot staleness guard compares against the max of both
25
+ * (a snapshot older than an applied delta must not roll it back), while
26
+ * the delta gate keeps comparing against the snapshot floor alone so
27
+ * out-of-order delta delivery stays tolerated (insert-if-absent absorbs it).
28
+ */
29
+ deltaSeq: Record<string, number>;
30
+ /** spanId -> runId reverse index, for timing lookups by toolUseId. */
31
+ spanRun: Record<string, string>;
32
+
33
+ /** Unacknowledged notification intents — the guaranteed-tier inbox. */
34
+ inbox: ActivityIntent[];
35
+ loadInbox: () => Promise<void>;
36
+ acknowledgeIntent: (id: number) => Promise<void>;
37
+ acknowledgeAllIntents: () => Promise<void>;
38
+ setSupported: (supported: boolean) => void;
39
+ /** New connection: server-side subscriptions are gone; re-subscribe lazily. */
40
+ resetSubscriptions: () => void;
41
+ bumpConnectionEpoch: () => void;
42
+ markSubscribed: (sessionId: string) => void;
43
+ applySnapshot: (msg: ServerActivitySnapshot) => void;
44
+ applyDelta: (msg: ServerActivityDelta) => void;
45
+ clear: () => void;
46
+ }
47
+
48
+ /** The five indexes that make up the mirror, mutated together during merges. */
49
+ export interface MirrorMaps {
50
+ spans: Record<string, Record<string, ActivitySpan>>;
51
+ events: Record<string, ActivitySpanEvent[]>;
52
+ highWater: Record<string, number>;
53
+ deltaSeq: Record<string, number>;
54
+ spanRun: Record<string, string>;
55
+ }
@@ -1,19 +1,30 @@
1
1
  import { create } from "zustand";
2
- import type {
3
- ActivitySpan,
4
- ActivitySpanEvent,
5
- ServerActivityDelta,
6
- ServerActivitySnapshot,
7
- } from "@schlessera/brain-ui-sdk/protocol";
8
- import { api, type ActivityIntent } from "../lib/api-client.js";
2
+ import { api } from "../lib/api-client.js";
9
3
  import { registerDevHandle } from "../config.js";
4
+ import type { ActivityState, MirrorMaps } from "./activity-store-types.js";
5
+ import {
6
+ evictRun,
7
+ insertEvent,
8
+ pruneTerminalRuns,
9
+ rootSpanOf,
10
+ } from "./activity-store-helpers.js";
11
+
12
+ export {
13
+ EMPTY_EVENTS,
14
+ childSpans,
15
+ eventsFor,
16
+ narrativeEventsFor,
17
+ payloadEventsFor,
18
+ runEvents,
19
+ runSpans,
20
+ spanForTool,
21
+ subagentSpans,
22
+ timingFor,
23
+ } from "./activity-store-helpers.js";
10
24
 
11
25
  /** How many finished runs' details are fetched when a session opens. */
12
26
  const HISTORY_RUN_LIMIT = 10;
13
27
 
14
- /** Bound on the mirror: how many TERMINAL runs stay held. Live runs never count. */
15
- const MAX_TERMINAL_RUNS = 50;
16
-
17
28
  /**
18
29
  * Client mirror of the server's activity record, fed by the snapshot-then-
19
30
  * delta stream. The server computes; this store only holds and indexes what
@@ -34,45 +45,6 @@ const MAX_TERMINAL_RUNS = 50;
34
45
  * evicted wholesale (spans, events, high-water, reverse index). Live runs
35
46
  * are never evicted; evicted history remains fetchable over REST.
36
47
  */
37
- interface ActivityState {
38
- /** server_hello said this host records activity. */
39
- supported: boolean;
40
- /** Session ids this connection has an activity subscription for. */
41
- subscribed: Record<string, true>;
42
- /** Bumped on every new connection so subscribe effects re-send. */
43
- connectionEpoch: number;
44
- /** Live span state per run, keyed runId -> spanId. */
45
- spans: Record<string, Record<string, ActivitySpan>>;
46
- /** Ordered events per span (already sorted by eventIndex). */
47
- events: Record<string, ActivitySpanEvent[]>;
48
- /** Per-run high-water seq from the snapshot; deltas at or below drop. */
49
- highWater: Record<string, number>;
50
- /**
51
- * Newest APPLIED delta seq per run. Kept apart from `highWater` on
52
- * purpose: the snapshot staleness guard compares against the max of both
53
- * (a snapshot older than an applied delta must not roll it back), while
54
- * the delta gate keeps comparing against the snapshot floor alone so
55
- * out-of-order delta delivery stays tolerated (insert-if-absent absorbs it).
56
- */
57
- deltaSeq: Record<string, number>;
58
- /** spanId -> runId reverse index, for timing lookups by toolUseId. */
59
- spanRun: Record<string, string>;
60
-
61
- /** Unacknowledged notification intents — the guaranteed-tier inbox. */
62
- inbox: ActivityIntent[];
63
- loadInbox: () => Promise<void>;
64
- acknowledgeIntent: (id: number) => Promise<void>;
65
- acknowledgeAllIntents: () => Promise<void>;
66
- setSupported: (supported: boolean) => void;
67
- /** New connection: server-side subscriptions are gone; re-subscribe lazily. */
68
- resetSubscriptions: () => void;
69
- bumpConnectionEpoch: () => void;
70
- markSubscribed: (sessionId: string) => void;
71
- applySnapshot: (msg: ServerActivitySnapshot) => void;
72
- applyDelta: (msg: ServerActivityDelta) => void;
73
- clear: () => void;
74
- }
75
-
76
48
  export const useActivityStore = create<ActivityState>((set, get) => ({
77
49
  supported: false,
78
50
  subscribed: {},
@@ -309,174 +281,6 @@ export async function loadSpanPayloads(spanId: string): Promise<void> {
309
281
  }
310
282
  }
311
283
 
312
- /** The four indexes that make up the mirror, mutated together during merges. */
313
- interface MirrorMaps {
314
- spans: Record<string, Record<string, ActivitySpan>>;
315
- events: Record<string, ActivitySpanEvent[]>;
316
- highWater: Record<string, number>;
317
- deltaSeq: Record<string, number>;
318
- spanRun: Record<string, string>;
319
- }
320
-
321
- function rootSpanOf(byId: Record<string, ActivitySpan>): ActivitySpan | undefined {
322
- return Object.values(byId).find((span) => span.parentSpanId === undefined);
323
- }
324
-
325
- /** Drop a whole run from the mirror — spans, events, high-water, reverse index. */
326
- function evictRun(maps: MirrorMaps, runId: string): void {
327
- for (const spanId of Object.keys(maps.spans[runId] ?? {})) {
328
- delete maps.events[spanId];
329
- delete maps.spanRun[spanId];
330
- }
331
- delete maps.spans[runId];
332
- delete maps.highWater[runId];
333
- delete maps.deltaSeq[runId];
334
- }
335
-
336
- /**
337
- * Enforce the terminal-run bound in place. Returns whether anything was
338
- * evicted. Runs whose root span is still open (or unknown) are never touched.
339
- */
340
- function pruneTerminalRuns(maps: MirrorMaps): boolean {
341
- const terminal: Array<{ runId: string; startedAt: number }> = [];
342
- for (const [runId, byId] of Object.entries(maps.spans)) {
343
- const root = rootSpanOf(byId);
344
- if (root && root.outcome !== undefined) {
345
- terminal.push({ runId, startedAt: root.startedAt });
346
- }
347
- }
348
- if (terminal.length <= MAX_TERMINAL_RUNS) return false;
349
- terminal.sort((a, b) => a.startedAt - b.startedAt);
350
- for (const { runId } of terminal.slice(0, terminal.length - MAX_TERMINAL_RUNS)) {
351
- evictRun(maps, runId);
352
- }
353
- return true;
354
- }
355
-
356
- function insertEvent(
357
- list: ActivitySpanEvent[] | undefined,
358
- event: ActivitySpanEvent
359
- ): ActivitySpanEvent[] {
360
- const existing = list ?? [];
361
- if (existing.some((e) => e.eventIndex === event.eventIndex)) return existing;
362
- const next = [...existing, event];
363
- next.sort((a, b) => a.eventIndex - b.eventIndex);
364
- return next;
365
- }
366
-
367
- // --- Selectors (plain functions over the state, like activeChat) ---
368
-
369
- /** All spans of a run, roots first, then by start time. */
370
- export function runSpans(state: ActivityState, runId: string): ActivitySpan[] {
371
- const byId = state.spans[runId];
372
- if (!byId) return [];
373
- return Object.values(byId).sort(
374
- (a, b) =>
375
- (a.parentSpanId ? 1 : 0) - (b.parentSpanId ? 1 : 0) || a.startedAt - b.startedAt
376
- );
377
- }
378
-
379
- /** Direct children of a span (e.g. the tool calls inside a subagent). */
380
- export function childSpans(state: ActivityState, parentSpanId: string): ActivitySpan[] {
381
- const runId = state.spanRun[parentSpanId];
382
- if (!runId) return [];
383
- return Object.values(state.spans[runId] ?? {})
384
- .filter((s) => s.parentSpanId === parentSpanId)
385
- .sort((a, b) => a.startedAt - b.startedAt);
386
- }
387
-
388
- /** The subagent spans spawned by a turn's Agent tool calls, live status included. */
389
- export function subagentSpans(state: ActivityState, sessionId: string): ActivitySpan[] {
390
- const out: ActivitySpan[] = [];
391
- for (const byId of Object.values(state.spans)) {
392
- for (const span of Object.values(byId)) {
393
- if (span.kind === "subagent" && span.sessionId === sessionId) out.push(span);
394
- }
395
- }
396
- return out.sort((a, b) => a.startedAt - b.startedAt);
397
- }
398
-
399
- /** Stable empty result for `eventsFor` — a fresh `[]` per call would defeat
400
- * reference-equality checks in zustand selectors. */
401
- export const EMPTY_EVENTS: ActivitySpanEvent[] = Object.freeze(
402
- [] as ActivitySpanEvent[]
403
- ) as ActivitySpanEvent[];
404
-
405
- /** Ordered events of a span; the shared frozen empty array when none. */
406
- export function eventsFor(state: ActivityState, spanId: string): ActivitySpanEvent[] {
407
- return state.events[spanId] ?? EMPTY_EVENTS;
408
- }
409
-
410
- /** Payload event types the tool expander owns — every OTHER type belongs to
411
- * the narrative stream (`narrativeEventsFor`) so nothing recorded is
412
- * rendered nowhere. */
413
- const TOOL_PAYLOAD_TYPES = new Set(["tool_input", "tool_output"]);
414
-
415
- /**
416
- * The recorded input/output payload events of a tool span (AE7). Returns a
417
- * fresh array per call — subscribe through `useShallow` (like `childSpans`).
418
- */
419
- export function payloadEventsFor(state: ActivityState, spanId: string): ActivitySpanEvent[] {
420
- const events = state.events[spanId];
421
- if (!events) return EMPTY_EVENTS;
422
- const payloads = events.filter((e) => TOOL_PAYLOAD_TYPES.has(e.eventType));
423
- return payloads.length > 0 ? payloads : EMPTY_EVENTS;
424
- }
425
-
426
- /**
427
- * Everything recorded against a span that is NOT a tool input/output payload:
428
- * transcript excerpts, job output, and any span-sink event type a producer
429
- * invents. Rendered as the span's narrative so an unknown type degrades to a
430
- * labelled block rather than to invisibility.
431
- */
432
- export function narrativeEventsFor(
433
- state: ActivityState,
434
- spanId: string
435
- ): ActivitySpanEvent[] {
436
- const events = state.events[spanId];
437
- if (!events) return EMPTY_EVENTS;
438
- const rest = events.filter((e) => !TOOL_PAYLOAD_TYPES.has(e.eventType));
439
- return rest.length > 0 ? rest : EMPTY_EVENTS;
440
- }
441
-
442
- /** Every event recorded under a run, ordered by time — the run detail's
443
- * narrative stream and the raw-trace dump both read through this. */
444
- export function runEvents(state: ActivityState, runId: string): ActivitySpanEvent[] {
445
- const byId = state.spans[runId];
446
- if (!byId) return EMPTY_EVENTS;
447
- const out: ActivitySpanEvent[] = [];
448
- for (const spanId of Object.keys(byId)) {
449
- const events = state.events[spanId];
450
- if (events) out.push(...events);
451
- }
452
- if (out.length === 0) return EMPTY_EVENTS;
453
- return out.sort((a, b) => a.ts - b.ts || a.eventIndex - b.eventIndex);
454
- }
455
-
456
- /** The span behind one tool call (span ids ARE toolUseIds), if streamed. */
457
- export function spanForTool(state: ActivityState, toolUseId: string): ActivitySpan | null {
458
- const runId = state.spanRun[toolUseId];
459
- return runId ? (state.spans[runId]?.[toolUseId] ?? null) : null;
460
- }
461
-
462
- /**
463
- * Server-stamped timing for a tool call. One clock for live and reloaded
464
- * views: when the activity stream carries the span, its timestamps win over
465
- * client stamps (AE2 — live and revisit must render identical timings).
466
- * `waitUntil` marks the approval-wait/execution boundary.
467
- */
468
- export function timingFor(
469
- state: ActivityState,
470
- toolUseId: string
471
- ): { startedAt: number; endedAt?: number } | null {
472
- const span = spanForTool(state, toolUseId);
473
- if (!span) return null;
474
- return {
475
- startedAt: span.waitUntil ?? span.startedAt,
476
- ...(span.endedAt !== undefined ? { endedAt: span.endedAt } : {}),
477
- };
478
- }
479
-
480
284
  registerDevHandle(() => {
481
285
  if (typeof window === "undefined") return;
482
286
  (window as unknown as { __activityStore?: typeof useActivityStore }).__activityStore =
@@ -1,7 +1,7 @@
1
1
  import { create } from "zustand";
2
2
 
3
- type WsStatus = "connecting" | "connected" | "disconnected";
4
- type VpnStatus =
3
+ export type WsStatus = "connecting" | "connected" | "disconnected";
4
+ export type VpnStatus =
5
5
  | "connected"
6
6
  | "forbidden"
7
7
  | "unauthorized"
@@ -27,9 +27,21 @@ export interface ConnectionError {
27
27
  interface ConnectionState {
28
28
  wsStatus: WsStatus;
29
29
  vpnStatus: VpnStatus;
30
+ handshakeFailures: number;
31
+ lastCloseCode: number | null;
32
+ /**
33
+ * Monotonic count of sockets that reached `open`. A probe started before a
34
+ * socket opened compares this to decide whether its answer is still the
35
+ * newest evidence — comparing wsStatus twice cannot see a drop and a
36
+ * replacement open that both land inside one probe.
37
+ */
38
+ socketOpens: number;
30
39
  lastError: ConnectionError | null;
31
40
  setWsStatus: (status: WsStatus) => void;
32
41
  setVpnStatus: (status: VpnStatus) => void;
42
+ recordWsClose: (opened: boolean, code: number) => void;
43
+ /** A socket reached `open`: the refusal evidence so far is spent. */
44
+ noteSocketOpen: () => void;
33
45
  reportError: (code: string, message: string) => void;
34
46
  clearError: () => void;
35
47
  }
@@ -37,9 +49,28 @@ interface ConnectionState {
37
49
  export const useConnectionStore = create<ConnectionState>((set) => ({
38
50
  wsStatus: "disconnected",
39
51
  vpnStatus: "checking",
52
+ handshakeFailures: 0,
53
+ lastCloseCode: null,
54
+ socketOpens: 0,
40
55
  lastError: null,
41
56
  setWsStatus: (wsStatus) => set({ wsStatus }),
42
57
  setVpnStatus: (vpnStatus) => set({ vpnStatus }),
58
+ recordWsClose: (opened, lastCloseCode) =>
59
+ set((state) => ({
60
+ lastCloseCode,
61
+ handshakeFailures: opened
62
+ ? state.handshakeFailures
63
+ : state.handshakeFailures + 1,
64
+ })),
65
+ noteSocketOpen: () =>
66
+ set((state) => ({
67
+ handshakeFailures: 0,
68
+ // Cleared here, not on the next close: the server accepts the upgrade and
69
+ // THEN closes with 4008 when it is at its connection cap, so the code
70
+ // must survive an open to be read, and must not outlive the next one.
71
+ lastCloseCode: null,
72
+ socketOpens: state.socketOpens + 1,
73
+ })),
43
74
  reportError: (code, message) => set({ lastError: { code, message, at: Date.now() } }),
44
75
  clearError: () => set({ lastError: null }),
45
76
  }));
@@ -86,8 +86,8 @@ async function fetchTree(path: string): Promise<FileEntry[]> {
86
86
  return data.entries;
87
87
  }
88
88
 
89
- async function fetchContent(path: string): Promise<FileContentResponse> {
90
- const res = await fetch(`${apiBase()}/files/content?path=${encodeURIComponent(path)}`);
89
+ async function fetchContent(path: string, signal: AbortSignal): Promise<FileContentResponse> {
90
+ const res = await fetch(`${apiBase()}/files/content?path=${encodeURIComponent(path)}`, { signal });
91
91
  if (!res.ok) {
92
92
  const body = await res.json().catch(() => ({ error: res.statusText }));
93
93
  const err = new Error(body.error || `HTTP ${res.status}`);
@@ -98,8 +98,8 @@ async function fetchContent(path: string): Promise<FileContentResponse> {
98
98
  return res.json();
99
99
  }
100
100
 
101
- async function fetchResolve(path: string): Promise<FileResolveResponse> {
102
- const res = await fetch(`${apiBase()}/files/resolve?path=${encodeURIComponent(path)}`);
101
+ async function fetchResolve(path: string, signal: AbortSignal): Promise<FileResolveResponse> {
102
+ const res = await fetch(`${apiBase()}/files/resolve?path=${encodeURIComponent(path)}`, { signal });
103
103
  if (!res.ok) {
104
104
  const body = await res.json().catch(() => ({ error: res.statusText }));
105
105
  throw new Error(body.error || `HTTP ${res.status}`);
@@ -116,6 +116,10 @@ async function fetchWikilinks(): Promise<WikilinkMapResponse> {
116
116
  return res.json();
117
117
  }
118
118
 
119
+ let fileRequest: AbortController | null = null;
120
+ let directoryRequest: AbortController | null = null;
121
+ let cacheEpoch = 0;
122
+
119
123
  export const useFileStore = create<FileState>((set, get) => ({
120
124
  dirCache: {},
121
125
  loadingDirs: new Set(),
@@ -135,6 +139,7 @@ export const useFileStore = create<FileState>((set, get) => ({
135
139
  wikilinkLoading: false,
136
140
 
137
141
  loadDir: async (path) => {
142
+ const epoch = cacheEpoch;
138
143
  const { dirCache, loadingDirs } = get();
139
144
  if (dirCache[path] || loadingDirs.has(path)) return;
140
145
  const next = new Set(loadingDirs);
@@ -142,6 +147,7 @@ export const useFileStore = create<FileState>((set, get) => ({
142
147
  set({ loadingDirs: next });
143
148
  try {
144
149
  const entries = await fetchTree(path);
150
+ if (epoch !== cacheEpoch) return;
145
151
  set((s) => {
146
152
  const loading = new Set(s.loadingDirs);
147
153
  loading.delete(path);
@@ -154,6 +160,7 @@ export const useFileStore = create<FileState>((set, get) => ({
154
160
  };
155
161
  });
156
162
  } catch (err) {
163
+ if (epoch !== cacheEpoch) return;
157
164
  set((s) => {
158
165
  const loading = new Set(s.loadingDirs);
159
166
  loading.delete(path);
@@ -189,13 +196,19 @@ export const useFileStore = create<FileState>((set, get) => ({
189
196
  },
190
197
 
191
198
  openFile: async (path) => {
199
+ fileRequest?.abort();
200
+ directoryRequest?.abort();
201
+ const request = new AbortController();
202
+ fileRequest = request;
203
+ const ownsRequest = () => fileRequest === request && !request.signal.aborted;
192
204
  const normalized = path.replace(/^\/+/, "").replace(/\\/g, "/");
193
205
  set({ currentPath: normalized, contentLoading: true, contentError: null, currentContent: null, treeExpanded: false });
194
206
 
195
207
  // Fire-and-await: load ancestor chain then content in parallel
196
208
  const ancestorsPromise = (async () => {
197
209
  try {
198
- const r = await fetchResolve(normalized);
210
+ const r = await fetchResolve(normalized, request.signal);
211
+ if (!ownsRequest()) return;
199
212
  // Expand each ancestor and root, load their contents
200
213
  const dirsToLoad = ["", ...r.ancestors];
201
214
  set((s) => {
@@ -210,12 +223,12 @@ export const useFileStore = create<FileState>((set, get) => ({
210
223
  })();
211
224
 
212
225
  try {
213
- const content = await fetchContent(normalized);
226
+ const content = await fetchContent(normalized, request.signal);
214
227
  // Drop a response the user has already navigated away from. Two rapid
215
228
  // clicks race, and without this the SLOWER fetch wins: the viewer showed
216
229
  // the newer file's path with the older file's content, which reads as
217
230
  // corruption rather than as a stale load.
218
- if (get().currentPath !== normalized) return;
231
+ if (!ownsRequest()) return;
219
232
  set({ currentContent: content, contentLoading: false });
220
233
  // Default mode: prefer preview when available; persist otherwise
221
234
  const { viewMode } = get();
@@ -233,7 +246,7 @@ export const useFileStore = create<FileState>((set, get) => ({
233
246
  else if (e.message === "invalid_path") msg = "Invalid path.";
234
247
  // Same guard: a failure for a file the user already left must not
235
248
  // replace the file they are now looking at with an error.
236
- if (get().currentPath !== normalized) return;
249
+ if (!ownsRequest()) return;
237
250
  set({ contentError: msg, contentLoading: false });
238
251
  } finally {
239
252
  await ancestorsPromise;
@@ -241,10 +254,15 @@ export const useFileStore = create<FileState>((set, get) => ({
241
254
  },
242
255
 
243
256
  closeFile: () => {
244
- set({ currentPath: null, currentContent: null, contentError: null, treeExpanded: true });
257
+ fileRequest?.abort();
258
+ fileRequest = null;
259
+ set({ currentPath: null, currentContent: null, contentError: null, contentLoading: false, treeExpanded: true });
245
260
  },
246
261
 
247
262
  openDir: async (path) => {
263
+ directoryRequest?.abort();
264
+ const request = new AbortController();
265
+ directoryRequest = request;
248
266
  const normalized = path
249
267
  .replace(/^\/+/, "")
250
268
  .replace(/\\/g, "/")
@@ -258,7 +276,8 @@ export const useFileStore = create<FileState>((set, get) => ({
258
276
  set({ treeExpanded: true, highlightedDir: normalized });
259
277
 
260
278
  try {
261
- const r = await fetchResolve(normalized);
279
+ const r = await fetchResolve(normalized, request.signal);
280
+ if (directoryRequest !== request || request.signal.aborted) return;
262
281
  const dirs = ["", ...r.ancestors, normalized];
263
282
  set((s) => {
264
283
  const next = new Set(s.expandedDirs);
@@ -278,11 +297,13 @@ export const useFileStore = create<FileState>((set, get) => ({
278
297
  setHighlightedDir: (path) => set({ highlightedDir: path }),
279
298
 
280
299
  ensureWikilinks: async () => {
300
+ const epoch = cacheEpoch;
281
301
  const { wikilinkLoaded, wikilinkLoading } = get();
282
302
  if (wikilinkLoaded || wikilinkLoading) return;
283
303
  set({ wikilinkLoading: true });
284
304
  try {
285
305
  const data = await fetchWikilinks();
306
+ if (epoch !== cacheEpoch) return;
286
307
  set({
287
308
  wikilinkMap: data.slugs,
288
309
  wikilinkLoaded: true,
@@ -290,6 +311,7 @@ export const useFileStore = create<FileState>((set, get) => ({
290
311
  });
291
312
  } catch {
292
313
  // Soft-fail — leave the map empty; wikilinks will render as plain text
314
+ if (epoch !== cacheEpoch) return;
293
315
  set({ wikilinkLoading: false });
294
316
  }
295
317
  },
@@ -304,15 +326,23 @@ export const useFileStore = create<FileState>((set, get) => ({
304
326
  return { frontmatterCollapsed: next };
305
327
  }),
306
328
 
307
- reset: () =>
329
+ reset: () => {
330
+ fileRequest?.abort();
331
+ directoryRequest?.abort();
332
+ fileRequest = null;
333
+ directoryRequest = null;
334
+ cacheEpoch++;
308
335
  set({
336
+ dirCache: {}, loadingDirs: new Set(), dirErrors: {}, expandedDirs: new Set([""]),
337
+ highlightedDir: null, wikilinkMap: {}, wikilinkLoaded: false, wikilinkLoading: false,
309
338
  currentPath: null,
310
339
  currentContent: null,
311
340
  contentLoading: false,
312
341
  contentError: null,
313
342
  viewMode: "preview",
314
343
  treeExpanded: true,
315
- }),
344
+ });
345
+ },
316
346
  }));
317
347
 
318
348
  /**
@@ -0,0 +1,38 @@
1
+ import { create } from "zustand";
2
+ import { api, type MintedAgent } from "../lib/api-client.js";
3
+
4
+ interface PrincipalState {
5
+ mintPending: boolean;
6
+ mintError: string | null;
7
+ oneTimeCredential: MintedAgent | null;
8
+ mintAgent: (label: string, ttlDays: number) => Promise<void>;
9
+ acknowledgeAgentCredential: () => void;
10
+ }
11
+
12
+ /**
13
+ * Mint state lives above Settings because the response contains an
14
+ * unrecoverable, one-time value. Closing a panel or changing views must not
15
+ * discard either the request in flight or the value it eventually returns.
16
+ */
17
+ export const usePrincipalStore = create<PrincipalState>((set, get) => ({
18
+ mintPending: false,
19
+ mintError: null,
20
+ oneTimeCredential: null,
21
+
22
+ mintAgent: async (label, ttlDays) => {
23
+ if (get().mintPending || get().oneTimeCredential) return;
24
+ set({ mintPending: true, mintError: null });
25
+ try {
26
+ const oneTimeCredential = await api.principalMint(label, ttlDays);
27
+ set({ mintPending: false, oneTimeCredential });
28
+ } catch (error) {
29
+ set({
30
+ mintPending: false,
31
+ mintError:
32
+ error instanceof Error ? error.message : "Could not create agent access",
33
+ });
34
+ }
35
+ },
36
+
37
+ acknowledgeAgentCredential: () => set({ oneTimeCredential: null }),
38
+ }));
@@ -1,7 +1,7 @@
1
1
  import { create } from "zustand";
2
2
 
3
3
  /** Which tab the settings panel opens on. */
4
- export type SettingsTab = "models" | "skills" | "security";
4
+ export type SettingsTab = "models" | "skills" | "security" | "devices";
5
5
 
6
6
  /** Full-screen surface currently shown inside the AppShell. */
7
7
  export type ActiveView = "chat" | "graph" | "activity";