@brainpilot/web 0.0.4 → 0.0.5

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 (97) hide show
  1. package/dist/assets/index-C-8G4D4j.js +448 -0
  2. package/dist/assets/index-C501m5OS.css +1 -0
  3. package/dist/index.html +2 -2
  4. package/index.html +13 -0
  5. package/package.json +9 -3
  6. package/src/App.tsx +10 -0
  7. package/src/__tests__/api.test.ts +103 -0
  8. package/src/__tests__/messageGroups.test.ts +80 -0
  9. package/src/__tests__/newUiComponents.test.tsx +101 -0
  10. package/src/__tests__/newUiEvents.test.ts +236 -0
  11. package/src/components/chat/AskUserCard.tsx +123 -0
  12. package/src/components/chat/AutoRetryIndicator.tsx +71 -0
  13. package/src/components/chat/ComposerInput.tsx +73 -0
  14. package/src/components/chat/ComposerSendButton.tsx +26 -0
  15. package/src/components/chat/MarkdownMessage.tsx +24 -0
  16. package/src/components/chat/MessageStream.tsx +464 -0
  17. package/src/components/chat/PromptComposer.tsx +398 -0
  18. package/src/components/chat/SystemMessageBubble.tsx +46 -0
  19. package/src/components/demo/DemoFileTree.tsx +146 -0
  20. package/src/components/demo/DemoView.tsx +668 -0
  21. package/src/components/demo/TraceNodeModal.tsx +76 -0
  22. package/src/components/demo/demoBundle.ts +218 -0
  23. package/src/components/demo/demoCache.ts +42 -0
  24. package/src/components/files/FilePreviewView.tsx +153 -0
  25. package/src/components/files/FileSidebar.tsx +664 -0
  26. package/src/components/files/filePreview.ts +113 -0
  27. package/src/components/primitives/CustomSelect.tsx +200 -0
  28. package/src/components/primitives/IconButton.tsx +27 -0
  29. package/src/components/quota/DiskQuotaCriticalDialog.tsx +56 -0
  30. package/src/components/quota/DiskQuotaWarningDialog.tsx +65 -0
  31. package/src/components/quota/QuotaFileManager.tsx +197 -0
  32. package/src/components/search/SearchDialog.tsx +101 -0
  33. package/src/components/session/AgentNetwork.tsx +1240 -0
  34. package/src/components/session/AgentTraceViews.tsx +381 -0
  35. package/src/components/session/AnalyticsTab.tsx +386 -0
  36. package/src/components/session/GlobalOverview.tsx +108 -0
  37. package/src/components/session/NodeTooltip.tsx +127 -0
  38. package/src/components/session/TimelineTab.tsx +320 -0
  39. package/src/components/session/TraceGraphView.tsx +301 -0
  40. package/src/components/session/TraceNodeDetail.tsx +142 -0
  41. package/src/components/session/agentAnalytics.ts +397 -0
  42. package/src/components/session/agentNetworkShared.ts +329 -0
  43. package/src/components/session/traceLayout.ts +150 -0
  44. package/src/components/settings/SettingsDialog.tsx +719 -0
  45. package/src/components/shell/DesktopShell.tsx +236 -0
  46. package/src/components/shell/SandboxBuildingOverlay.tsx +73 -0
  47. package/src/components/shell/SandboxStatus.tsx +287 -0
  48. package/src/components/shell/TerminalDrawer.tsx +387 -0
  49. package/src/components/sidebar/Sidebar.tsx +187 -0
  50. package/src/config.ts +10 -0
  51. package/src/contexts/AppProviders.tsx +20 -0
  52. package/src/contexts/AuthContext.tsx +61 -0
  53. package/src/contexts/PreferencesContext.tsx +125 -0
  54. package/src/contexts/SSEContext.tsx +175 -0
  55. package/src/contexts/SandboxContext.tsx +310 -0
  56. package/src/contexts/SessionContext.tsx +608 -0
  57. package/src/contexts/draftStore.ts +103 -0
  58. package/src/contexts/messageFilters.ts +29 -0
  59. package/src/contexts/messageGroups.ts +77 -0
  60. package/src/contexts/messageReducer.ts +401 -0
  61. package/src/contexts/newUiEvents.ts +190 -0
  62. package/src/contracts/backend.ts +846 -0
  63. package/src/contracts/demoBundle.ts +83 -0
  64. package/src/i18n/messages/analytics.ts +96 -0
  65. package/src/i18n/messages/chat.ts +108 -0
  66. package/src/i18n/messages/contexts.ts +40 -0
  67. package/src/i18n/messages/demo.ts +80 -0
  68. package/src/i18n/messages/files.ts +82 -0
  69. package/src/i18n/messages/network.ts +186 -0
  70. package/src/i18n/messages/profile.ts +40 -0
  71. package/src/i18n/messages/quota.ts +36 -0
  72. package/src/i18n/messages/sandbox.ts +116 -0
  73. package/src/i18n/messages/search.ts +16 -0
  74. package/src/i18n/messages/settings.ts +184 -0
  75. package/src/i18n/messages/shell.ts +38 -0
  76. package/src/i18n/messages/sidebar.ts +52 -0
  77. package/src/i18n/messages/terminal.ts +22 -0
  78. package/src/i18n/messages/trace.ts +84 -0
  79. package/src/i18n/messages.ts +32 -0
  80. package/src/i18n/translate.ts +46 -0
  81. package/src/i18n/types.ts +15 -0
  82. package/src/i18n/useT.ts +15 -0
  83. package/src/main.tsx +13 -0
  84. package/src/mocks/backend.ts +722 -0
  85. package/src/styles/global.css +7429 -0
  86. package/src/styles/tokens.css +161 -0
  87. package/src/utils/api.ts +627 -0
  88. package/src/utils/download.ts +18 -0
  89. package/src/utils/format.ts +7 -0
  90. package/src/utils/zip.ts +119 -0
  91. package/src/vite-env.d.ts +1 -0
  92. package/tsconfig.app.json +22 -0
  93. package/tsconfig.json +7 -0
  94. package/tsconfig.node.json +13 -0
  95. package/vite.config.ts +13 -0
  96. package/dist/assets/index-Cd0Mi_WU.css +0 -1
  97. package/dist/assets/index-FGg-DeYR.js +0 -448
@@ -0,0 +1,846 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Protocol is the single source of truth for wire/domain types.
3
+ //
4
+ // `@brainpilot/protocol` owns the canonical domain models (camelCase,
5
+ // post-normalize shape) and the AG-UI wire event union (snake_case). We import
6
+ // the domain types here and re-export them so the rest of `src/` keeps importing
7
+ // from "../contracts/backend" unchanged. This file is now a thin adapter: it
8
+ // keeps the `normalize*`/`serialize*` functions, the UI-only types the protocol
9
+ // does not model (Sandbox*, ChatMessage, raw API shapes, message filters), and
10
+ // the *post-normalize camelCase* AG-UI event/message shapes the components rely
11
+ // on (protocol models the snake_case wire shape, which differs — see below).
12
+ // ---------------------------------------------------------------------------
13
+ import type {
14
+ Session,
15
+ AgentStatus,
16
+ SessionStateSnapshot,
17
+ SettingsData,
18
+ McpServerEntry,
19
+ ModelHealth,
20
+ ProviderProfile,
21
+ FileEntry,
22
+ FileContent,
23
+ TraceNode,
24
+ TraceNodeStatus,
25
+ TraceParent,
26
+ TraceArtifact,
27
+ TraceTimestamp,
28
+ TraceGraph,
29
+ } from "@brainpilot/protocol";
30
+
31
+ // Re-export the canonical protocol domain types under their existing names so
32
+ // all `import { … } from "../contracts/backend"` sites continue to resolve.
33
+ export type {
34
+ Session,
35
+ AgentStatus,
36
+ SessionStateSnapshot,
37
+ SettingsData,
38
+ McpServerEntry,
39
+ ModelHealth,
40
+ ProviderProfile,
41
+ FileEntry,
42
+ FileContent,
43
+ TraceNode,
44
+ TraceNodeStatus,
45
+ TraceParent,
46
+ TraceArtifact,
47
+ TraceTimestamp,
48
+ TraceGraph,
49
+ };
50
+
51
+ export type SandboxStatus =
52
+ | "creating"
53
+ | "running"
54
+ | "stopped"
55
+ | "error"
56
+ | "quota_exceeded"
57
+ | string;
58
+
59
+ export interface User {
60
+ id: string;
61
+ username: string;
62
+ createdAt: string;
63
+ }
64
+
65
+ export interface AuthToken {
66
+ accessToken: string;
67
+ tokenType: string;
68
+ user: User;
69
+ }
70
+
71
+ export interface Sandbox {
72
+ id: string;
73
+ name: string;
74
+ status: SandboxStatus;
75
+ port: number | null;
76
+ userId: string;
77
+ createdAt: string;
78
+ containerName?: string;
79
+ hostApiUrl?: string;
80
+ }
81
+
82
+ export interface SandboxStats {
83
+ sandboxId: string;
84
+ sandboxName: string;
85
+ status: SandboxStatus;
86
+ memory: {
87
+ usedBytes: number;
88
+ limitBytes: number;
89
+ percent: number;
90
+ };
91
+ cpu: {
92
+ usedPercent: number;
93
+ quotaPercent: number;
94
+ onlineCpus: number;
95
+ };
96
+ pids: {
97
+ current: number;
98
+ limit: number | null;
99
+ };
100
+ disk: {
101
+ workspaceUsedBytes: number;
102
+ quotaBytes: number;
103
+ percentOfQuota: number;
104
+ };
105
+ gpu?: {
106
+ name?: string;
107
+ memoryUsedBytes?: number;
108
+ memoryTotalBytes?: number;
109
+ utilizationPercent?: number;
110
+ } | null;
111
+ }
112
+
113
+ // FileEntry, FileContent, Session — now imported from @brainpilot/protocol (see top).
114
+
115
+ export interface ChatMessage {
116
+ id: string;
117
+ role: "user" | "assistant" | "system";
118
+ content: string;
119
+ createdAt: string;
120
+ agent?: string;
121
+ streaming?: boolean;
122
+ kind?: "text" | "thinking" | "tool" | "error" | "status" | "hook" | "system_message" | "ask_user" | "auto_retry";
123
+ toolName?: string;
124
+ toolInput?: unknown;
125
+ toolResult?: unknown;
126
+ reasoning?: string;
127
+ toolCallId?: string;
128
+ // Hook diagnostic event metadata — set when kind === "hook"
129
+ hookFamily?: string; // "expert_reply" | "principal_trace"
130
+ hookPhase?: string; // "reset_clean" | "reset_dirty" | "flag_set" | "reminder" | "fallback"
131
+ hookLevel?: string; // "debug" | "info" | "warning" | "error"
132
+ hookData?: Record<string, unknown>;
133
+ // ── 修正6 new-UI payloads (post-normalize, camelCase view shape) ──
134
+ // kind === "system_message": 4-level styled bubble (doc §6)
135
+ systemMessage?: SystemMessageView;
136
+ // kind === "ask_user": interactive user_input_request card (doc §6)
137
+ askUser?: AskUserView;
138
+ // kind === "auto_retry": auto-retry countdown + cancel indicator (doc §6)
139
+ autoRetry?: AutoRetryView;
140
+ }
141
+
142
+ /** View-model for a `system_message` AG-UI event (post-normalize). */
143
+ export interface SystemMessageView {
144
+ level: "info" | "warning" | "error" | "fatal";
145
+ message: string;
146
+ details?: string;
147
+ agent?: string;
148
+ /** fatal events are non-recoverable; drives the emphasized red styling. */
149
+ recoverable: boolean;
150
+ timestamp?: string;
151
+ }
152
+
153
+ /** View-model for a `user_input_request` AG-UI event (ask_user, post-normalize). */
154
+ export interface AskUserView {
155
+ requestId: string;
156
+ agent: string;
157
+ question: string;
158
+ options?: string[];
159
+ allowFreeText?: boolean;
160
+ timeoutSec?: number;
161
+ /** Set once the user has answered, so the card renders as resolved. */
162
+ answer?: string;
163
+ }
164
+
165
+ /** View-model for an auto-retry indicator, surfaced from Pi `auto_retry_start`. */
166
+ export interface AutoRetryView {
167
+ attempt: number;
168
+ maxAttempts: number;
169
+ delayMs: number;
170
+ reason?: string;
171
+ /** Set once cancelled / superseded, so the countdown stops. */
172
+ cancelled?: boolean;
173
+ }
174
+
175
+ /** A predicate that decides whether a message should be hidden from display. */
176
+ export interface MessageFilterRule {
177
+ id: string;
178
+ name: string;
179
+ description: string;
180
+ enabled: boolean;
181
+ /** Returns true if the message should be HIDDEN. */
182
+ test: (msg: ChatMessage, allMessages: ChatMessage[]) => boolean;
183
+ }
184
+
185
+ /** Serializable filter configuration (for persistence / settings). */
186
+ export interface MessageFilterConfig {
187
+ id: string;
188
+ enabled: boolean;
189
+ }
190
+
191
+ // TraceParent, TraceArtifact, TraceTimestamp, AgentStatus, SessionStateSnapshot,
192
+ // SettingsData, McpServerEntry, ModelHealth, ProviderProfile — now imported from
193
+ // @brainpilot/protocol (see top).
194
+
195
+ export interface ProviderCreate {
196
+ name: string;
197
+ baseUrl: string;
198
+ apiKey: string;
199
+ models?: string[];
200
+ icon?: string;
201
+ iconColor?: string;
202
+ notes?: string;
203
+ }
204
+
205
+ export interface ProviderUpdate {
206
+ name?: string;
207
+ baseUrl?: string;
208
+ apiKey?: string;
209
+ models?: string[];
210
+ icon?: string;
211
+ iconColor?: string;
212
+ notes?: string;
213
+ }
214
+
215
+ // TraceNodeStatus, TraceNode, TraceGraph — now imported from @brainpilot/protocol (see top).
216
+
217
+ export type SessionMessagePart =
218
+ | string
219
+ | { type?: "text"; text?: string }
220
+ | { type?: "thinking"; thinking?: string }
221
+ | { type?: "tool_use"; id?: string; name?: string; input?: unknown }
222
+ | { type?: "tool_result"; tool_use_id?: string; content?: unknown; is_error?: boolean };
223
+
224
+ export interface SessionMessageEntry {
225
+ type: string;
226
+ timestamp?: string;
227
+ uuid?: string;
228
+ isMeta?: boolean;
229
+ sourceToolUseID?: string;
230
+ message?: {
231
+ role?: string;
232
+ content?: string | SessionMessagePart[];
233
+ agent?: string;
234
+ };
235
+ }
236
+
237
+ // TODO(dead-code): SessionEventsResponse / SessionEventEntry / normalizeSessionEvent
238
+ // are leftovers from the pre-AG-UI polling protocol (removed in 2026-05-18).
239
+ // Kept commented out for now; will be fully deleted once issue-4 fallback removal lands.
240
+ // export interface SessionEventsResponse {
241
+ // events: SessionEventEntry[];
242
+ // nextOffset: number;
243
+ // hasMore: boolean;
244
+ // }
245
+ //
246
+ // export interface SessionEventEntry {
247
+ // seq: number;
248
+ // timestamp: string;
249
+ // type: string;
250
+ // sessionId?: string;
251
+ // data: Record<string, unknown>;
252
+ // }
253
+
254
+ /**
255
+ * AG-UI message shape (as it appears in MESSAGES_SNAPSHOT.messages).
256
+ * Mirrors `Message` in /root/ag-ui/sdks/typescript/packages/core/src/types.ts.
257
+ */
258
+ export interface AgUiMessage {
259
+ id: string;
260
+ role: string;
261
+ content?: string;
262
+ toolCalls?: Array<{ id: string; name: string; arguments: string }>;
263
+ toolCallId?: string;
264
+ error?: boolean;
265
+ // MAS extension: source agent for this message. Populated by `fold.py` for
266
+ // MESSAGES_SNAPSHOT so refreshes preserve Expert attribution. Camelized
267
+ // automatically from the wire's `agent_name` by `normalizeAgUiEvent`.
268
+ agentName?: string;
269
+ // MAS extension: true iff fold.py saw `*_START` but no matching `*_END`
270
+ // (and no `RUN_FINISHED/RUN_ERROR` terminator). Used by the snapshot path to
271
+ // resume the streaming indicator on the in-progress message after refresh.
272
+ unfinished?: boolean;
273
+ // MAS extension: message kind for snapshot path parity with live path.
274
+ kind?: string;
275
+ hookFamily?: string;
276
+ hookPhase?: string;
277
+ hookLevel?: string;
278
+ hookData?: Record<string, unknown>;
279
+ }
280
+
281
+ /**
282
+ * AG-UI canonical event. Flat shape — no `data` wrapper. Field set varies by
283
+ * `type`; consumers should narrow before accessing event-specific fields.
284
+ *
285
+ * The full AG-UI EventType enum is defined upstream
286
+ * (`/root/ag-ui/sdks/typescript/packages/core/src/events.ts`); we only model
287
+ * the subset we emit.
288
+ */
289
+ export interface AgUiEvent {
290
+ type: string;
291
+ runId?: string;
292
+ threadId?: string;
293
+ sessionId?: string;
294
+ agentName?: string;
295
+
296
+ // Text/reasoning messages
297
+ messageId?: string;
298
+ role?: string;
299
+ delta?: string;
300
+
301
+ // Tool calls
302
+ toolCallId?: string;
303
+ toolCallName?: string;
304
+ parentMessageId?: string;
305
+ content?: string; // TOOL_CALL_RESULT
306
+ isError?: boolean;
307
+
308
+ // RUN_*
309
+ message?: string; // RUN_ERROR
310
+ code?: string; // RUN_ERROR
311
+ result?: unknown; // RUN_FINISHED
312
+ parentRunId?: string;
313
+
314
+ // MESSAGES_SNAPSHOT
315
+ messages?: AgUiMessage[];
316
+
317
+ // CUSTOM
318
+ name?: string;
319
+ value?: unknown;
320
+
321
+ // Forward-compatible extras
322
+ [extra: string]: unknown;
323
+ }
324
+
325
+ /**
326
+ * Back-compat alias — components historically import `WebSocketEvent`. We keep
327
+ * the name as an alias of `AgUiEvent` so the rest of the codebase compiles
328
+ * without a sweeping rename.
329
+ */
330
+ export type WebSocketEvent = AgUiEvent;
331
+
332
+ interface RawUser {
333
+ id?: string;
334
+ username?: string;
335
+ created_at?: string;
336
+ createdAt?: string;
337
+ }
338
+
339
+ interface RawToken {
340
+ access_token?: string;
341
+ accessToken?: string;
342
+ token_type?: string;
343
+ tokenType?: string;
344
+ user?: RawUser;
345
+ }
346
+
347
+ interface RawSandbox {
348
+ id?: string;
349
+ name?: string;
350
+ sandbox_name?: string;
351
+ status?: string;
352
+ port?: number | null;
353
+ user_id?: string;
354
+ userId?: string;
355
+ created_at?: string;
356
+ createdAt?: string;
357
+ container_name?: string;
358
+ containerName?: string;
359
+ host_api_url?: string;
360
+ hostApiUrl?: string;
361
+ }
362
+
363
+ interface RawSession {
364
+ id?: string;
365
+ title?: string;
366
+ created_at?: string;
367
+ createdAt?: string;
368
+ updated_at?: string;
369
+ updatedAt?: string;
370
+ }
371
+
372
+ interface RawFileEntry {
373
+ name?: string;
374
+ type?: string;
375
+ size?: number;
376
+ modified?: number | string;
377
+ permissions?: string;
378
+ }
379
+
380
+ interface RawSettingsData {
381
+ model?: string;
382
+ api_key?: string;
383
+ apiKey?: string;
384
+ base_url?: string;
385
+ baseUrl?: string;
386
+ }
387
+
388
+ interface RawModelHealth {
389
+ model?: string;
390
+ status?: string;
391
+ latency_ms?: number;
392
+ latencyMs?: number;
393
+ checked_at?: number;
394
+ checkedAt?: number;
395
+ error?: string;
396
+ }
397
+
398
+ interface RawProviderProfile {
399
+ id?: string;
400
+ name?: string;
401
+ base_url?: string;
402
+ baseUrl?: string;
403
+ models?: string[];
404
+ icon?: string;
405
+ icon_color?: string;
406
+ iconColor?: string;
407
+ notes?: string;
408
+ is_active?: boolean;
409
+ isActive?: boolean;
410
+ api_key_masked?: string;
411
+ apiKeyMasked?: string;
412
+ created_at?: number;
413
+ createdAt?: number;
414
+ updated_at?: number;
415
+ updatedAt?: number;
416
+ health_status?: string;
417
+ healthStatus?: string;
418
+ health_checked_at?: number;
419
+ healthCheckedAt?: number;
420
+ model_health?: RawModelHealth[];
421
+ modelHealth?: RawModelHealth[];
422
+ }
423
+
424
+ type Dict = Record<string, unknown>;
425
+
426
+ function stringValue(value: unknown, fallback = ""): string {
427
+ return typeof value === "string" && value.length > 0 ? value : fallback;
428
+ }
429
+
430
+ function optionalString(value: unknown): string | undefined {
431
+ return typeof value === "string" && value.length > 0 ? value : undefined;
432
+ }
433
+
434
+ function numberValue(value: unknown, fallback = 0): number {
435
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
436
+ }
437
+
438
+ function optionalNumber(value: unknown): number | undefined {
439
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
440
+ }
441
+
442
+ function isoValue(value: unknown): string {
443
+ return typeof value === "string" && value.length > 0 ? value : new Date().toISOString();
444
+ }
445
+
446
+ function asDict(value: unknown): Dict {
447
+ return value && typeof value === "object" && !Array.isArray(value) ? (value as Dict) : {};
448
+ }
449
+
450
+ function asOptionalUnknownRecord(value: unknown): Record<string, unknown> | undefined {
451
+ return value && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>) : undefined;
452
+ }
453
+
454
+ function asOptionalRecord(value: unknown): Record<string, string> | undefined {
455
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
456
+ return undefined;
457
+ }
458
+ const entries = Object.entries(value as Record<string, unknown>)
459
+ .filter((entry): entry is [string, string] => typeof entry[1] === "string");
460
+ return entries.length > 0 ? Object.fromEntries(entries) : undefined;
461
+ }
462
+
463
+ function normalizeStringArray(value: unknown): string[] {
464
+ return Array.isArray(value) ? value.filter((item): item is string => typeof item === "string") : [];
465
+ }
466
+
467
+ function camelizeKey(key: string): string {
468
+ return key.replace(/_([a-z])/g, (_, char: string) => char.toUpperCase());
469
+ }
470
+
471
+ function camelizeObject(value: unknown): unknown {
472
+ if (Array.isArray(value)) {
473
+ return value.map(camelizeObject);
474
+ }
475
+ if (!value || typeof value !== "object") {
476
+ return value;
477
+ }
478
+ return Object.fromEntries(
479
+ Object.entries(value as Dict).map(([key, nested]) => [camelizeKey(key), camelizeObject(nested)]),
480
+ );
481
+ }
482
+
483
+ export function normalizeUser(raw: RawUser): User {
484
+ const username = stringValue(raw.username, "user");
485
+ return {
486
+ id: stringValue(raw.id, username),
487
+ username,
488
+ createdAt: isoValue(raw.createdAt ?? raw.created_at),
489
+ };
490
+ }
491
+
492
+ export function normalizeToken(raw: RawToken): AuthToken {
493
+ return {
494
+ accessToken: stringValue(raw.accessToken ?? raw.access_token),
495
+ tokenType: stringValue(raw.tokenType ?? raw.token_type, "bearer"),
496
+ user: normalizeUser(raw.user ?? {}),
497
+ };
498
+ }
499
+
500
+ export function normalizeSandbox(raw: RawSandbox): Sandbox {
501
+ return {
502
+ id: stringValue(raw.id),
503
+ name: stringValue(raw.name ?? raw.sandbox_name, "default"),
504
+ status: stringValue(raw.status, "stopped"),
505
+ port: typeof raw.port === "number" ? raw.port : null,
506
+ userId: stringValue(raw.userId ?? raw.user_id),
507
+ createdAt: isoValue(raw.createdAt ?? raw.created_at),
508
+ containerName: optionalString(raw.containerName ?? raw.container_name),
509
+ hostApiUrl: optionalString(raw.hostApiUrl ?? raw.host_api_url),
510
+ };
511
+ }
512
+
513
+ export function normalizeSandboxStats(rawValue: unknown): SandboxStats {
514
+ const raw = asDict(rawValue);
515
+ const memory = asDict(raw.memory);
516
+ const cpu = asDict(raw.cpu);
517
+ const pids = asDict(raw.pids);
518
+ const disk = asDict(raw.disk);
519
+ const gpu = raw.gpu === null || raw.gpu === undefined ? null : asDict(raw.gpu);
520
+
521
+ return {
522
+ sandboxId: stringValue(raw.sandboxId ?? raw.sandbox_id),
523
+ sandboxName: stringValue(raw.sandboxName ?? raw.sandbox_name, "default"),
524
+ status: stringValue(raw.status, "stopped"),
525
+ memory: {
526
+ usedBytes: numberValue(memory.usedBytes ?? memory.used_bytes),
527
+ limitBytes: numberValue(memory.limitBytes ?? memory.limit_bytes),
528
+ percent: numberValue(memory.percent),
529
+ },
530
+ cpu: {
531
+ usedPercent: numberValue(cpu.usedPercent ?? cpu.used_percent),
532
+ quotaPercent: numberValue(cpu.quotaPercent ?? cpu.quota_percent),
533
+ onlineCpus: numberValue(cpu.onlineCpus ?? cpu.online_cpus),
534
+ },
535
+ pids: {
536
+ current: numberValue(pids.current),
537
+ limit: typeof pids.limit === "number" ? pids.limit : null,
538
+ },
539
+ disk: {
540
+ workspaceUsedBytes: numberValue(disk.workspaceUsedBytes ?? disk.workspace_used_bytes),
541
+ quotaBytes: numberValue(disk.quotaBytes ?? disk.quota_bytes),
542
+ percentOfQuota: numberValue(disk.percentOfQuota ?? disk.percent_of_quota),
543
+ },
544
+ gpu: gpu
545
+ ? {
546
+ name: optionalString(gpu.name),
547
+ memoryUsedBytes: numberValue(gpu.memoryUsedBytes ?? gpu.memory_used_bytes),
548
+ memoryTotalBytes: numberValue(gpu.memoryTotalBytes ?? gpu.memory_total_bytes),
549
+ utilizationPercent: numberValue(gpu.utilizationPercent ?? gpu.utilization_percent),
550
+ }
551
+ : null,
552
+ };
553
+ }
554
+
555
+ export function normalizeFileEntry(raw: RawFileEntry): FileEntry {
556
+ const modified = typeof raw.modified === "string" ? Number(raw.modified) : raw.modified;
557
+ return {
558
+ name: stringValue(raw.name),
559
+ type: raw.type === "folder" || raw.type === "symlink" ? raw.type : "file",
560
+ size: numberValue(raw.size),
561
+ modified: numberValue(modified),
562
+ permissions: stringValue(raw.permissions, ""),
563
+ };
564
+ }
565
+
566
+ export function normalizeFileContent(rawValue: unknown): FileContent {
567
+ const raw = asDict(rawValue);
568
+ return {
569
+ path: stringValue(raw.path),
570
+ content: stringValue(raw.content),
571
+ size: numberValue(raw.size),
572
+ };
573
+ }
574
+
575
+ export function normalizeSession(raw: RawSession): Session {
576
+ const id = stringValue(raw.id);
577
+ return {
578
+ id,
579
+ title: stringValue(raw.title, id ? `Session ${id.slice(0, 8)}` : "Untitled session"),
580
+ createdAt: isoValue(raw.createdAt ?? raw.created_at),
581
+ updatedAt: isoValue(raw.updatedAt ?? raw.updated_at ?? raw.createdAt ?? raw.created_at),
582
+ };
583
+ }
584
+
585
+ export function normalizeSettings(raw: RawSettingsData): SettingsData {
586
+ return {
587
+ model: stringValue(raw.model),
588
+ apiKey: stringValue(raw.apiKey ?? raw.api_key),
589
+ baseUrl: stringValue(raw.baseUrl ?? raw.base_url),
590
+ };
591
+ }
592
+ export function serializeSettings(data: Partial<SettingsData>): Record<string, string> {
593
+ return {
594
+ ...(data.model !== undefined ? { model: data.model } : {}),
595
+ ...(data.apiKey !== undefined ? { api_key: data.apiKey } : {}),
596
+ ...(data.baseUrl !== undefined ? { base_url: data.baseUrl } : {}),
597
+ };
598
+ }
599
+
600
+
601
+ export function normalizeMcpServer(rawValue: unknown): McpServerEntry {
602
+ const raw = asDict(rawValue);
603
+ return {
604
+ name: stringValue(raw.name),
605
+ type: raw.type === "stdio" || raw.type === "sse" ? raw.type : "http",
606
+ command: optionalString(raw.command),
607
+ args: Array.isArray(raw.args) ? raw.args.filter((arg): arg is string => typeof arg === "string") : undefined,
608
+ env: asOptionalRecord(raw.env),
609
+ url: optionalString(raw.url),
610
+ headers: asOptionalRecord(raw.headers),
611
+ timeout: optionalNumber(raw.timeout),
612
+ };
613
+ }
614
+
615
+ export function serializeMcpConfig(config: Omit<McpServerEntry, "name">): Record<string, unknown> {
616
+ return Object.fromEntries(Object.entries(config).filter(([, value]) => value !== undefined && value !== ""));
617
+ }
618
+
619
+ function normalizeModelHealth(raw: RawModelHealth | undefined): ModelHealth {
620
+ if (!raw || typeof raw !== "object") {
621
+ return { model: "", status: "unknown" };
622
+ }
623
+ return {
624
+ model: stringValue(raw.model),
625
+ status: (raw.status as ModelHealth["status"]) || "unknown",
626
+ latencyMs: optionalNumber(raw.latencyMs ?? raw.latency_ms),
627
+ checkedAt: optionalNumber(raw.checkedAt ?? raw.checked_at),
628
+ error: optionalString(raw.error),
629
+ };
630
+ }
631
+
632
+ export function normalizeProviderProfile(raw: RawProviderProfile): ProviderProfile {
633
+ const modelHealthRaw = Array.isArray(raw.modelHealth)
634
+ ? raw.modelHealth
635
+ : Array.isArray(raw.model_health)
636
+ ? raw.model_health
637
+ : [];
638
+ return {
639
+ id: stringValue(raw.id),
640
+ name: stringValue(raw.name),
641
+ baseUrl: stringValue(raw.baseUrl ?? raw.base_url),
642
+ models: Array.isArray(raw.models) ? raw.models : [],
643
+ icon: stringValue(raw.icon, "circle"),
644
+ iconColor: stringValue(raw.iconColor ?? raw.icon_color, "#111111"),
645
+ notes: stringValue(raw.notes),
646
+ isActive: Boolean(raw.isActive ?? raw.is_active),
647
+ apiKeyMasked: stringValue(raw.apiKeyMasked ?? raw.api_key_masked),
648
+ createdAt: numberValue(raw.createdAt ?? raw.created_at),
649
+ updatedAt: numberValue(raw.updatedAt ?? raw.updated_at),
650
+ healthStatus: (raw.healthStatus ?? raw.health_status ?? "unknown") as ProviderProfile["healthStatus"],
651
+ healthCheckedAt: optionalNumber(raw.healthCheckedAt ?? raw.health_checked_at),
652
+ modelHealth: modelHealthRaw.map(normalizeModelHealth),
653
+ };
654
+ }
655
+
656
+ export function serializeProviderCreate(data: ProviderCreate): Record<string, unknown> {
657
+ return {
658
+ name: data.name,
659
+ base_url: data.baseUrl,
660
+ api_key: data.apiKey,
661
+ models: data.models,
662
+ icon: data.icon,
663
+ icon_color: data.iconColor,
664
+ notes: data.notes,
665
+ };
666
+ }
667
+
668
+ export function serializeProviderUpdate(data: ProviderUpdate): Record<string, unknown> {
669
+ return {
670
+ ...(data.name !== undefined ? { name: data.name } : {}),
671
+ ...(data.baseUrl !== undefined ? { base_url: data.baseUrl } : {}),
672
+ ...(data.apiKey !== undefined ? { api_key: data.apiKey } : {}),
673
+ ...(data.models !== undefined ? { models: data.models } : {}),
674
+ ...(data.icon !== undefined ? { icon: data.icon } : {}),
675
+ ...(data.iconColor !== undefined ? { icon_color: data.iconColor } : {}),
676
+ ...(data.notes !== undefined ? { notes: data.notes } : {}),
677
+ };
678
+ }
679
+
680
+ function normalizeTraceNode(rawValue: unknown): TraceNode {
681
+ const raw = asDict(rawValue);
682
+ const parents = Array.isArray(raw.parents)
683
+ ? raw.parents.map((parent) => {
684
+ const item = asDict(parent);
685
+ return {
686
+ id: stringValue(item.id),
687
+ relation: optionalString(item.relation),
688
+ explanation: optionalString(item.explanation),
689
+ edgeType: optionalString(item.edgeType ?? item.edge_type),
690
+ };
691
+ }).filter((parent) => parent.id)
692
+ : [];
693
+ const artifacts = Array.isArray(raw.artifacts)
694
+ ? raw.artifacts.map((artifact) => {
695
+ const item = asDict(artifact);
696
+ return {
697
+ path: stringValue(item.path),
698
+ type: optionalString(item.type),
699
+ };
700
+ }).filter((artifact) => artifact.path)
701
+ : [];
702
+ const description = optionalString(raw.description);
703
+ const timestamp = asDict(raw.timestamp);
704
+ const parentIds = normalizeStringArray(raw.parentIds ?? raw.parent_ids);
705
+ return {
706
+ id: stringValue(raw.id ?? raw.node_id),
707
+ title: stringValue(raw.title, stringValue(raw.id ?? raw.node_id, "Trace node")),
708
+ type: stringValue(raw.type ?? raw.node_type, "step"),
709
+ nodeType: optionalString(raw.nodeType ?? raw.node_type),
710
+ status: stringValue(raw.status, "done"),
711
+ agent: optionalString(raw.agent ?? raw.agent_name),
712
+ description,
713
+ summary: optionalString(raw.summary) ?? description,
714
+ content: optionalString(raw.content) ?? description,
715
+ reason: optionalString(raw.reason),
716
+ context: optionalString(raw.context),
717
+ parents,
718
+ artifacts,
719
+ parentIds: parentIds.length ? parentIds : parents.map((parent) => parent.id),
720
+ childIds: normalizeStringArray(raw.childIds ?? raw.child_ids),
721
+ createdAt: optionalString(raw.createdAt ?? raw.created_at),
722
+ updatedAt: optionalString(raw.updatedAt ?? raw.updated_at),
723
+ timestamp: {
724
+ createdAt: optionalString(timestamp.createdAt ?? timestamp.created_at),
725
+ startedAt: optionalString(timestamp.startedAt ?? timestamp.started_at),
726
+ completedAt: optionalString(timestamp.completedAt ?? timestamp.completed_at),
727
+ },
728
+ durationMs: optionalNumber(raw.durationMs ?? raw.duration_ms),
729
+ errorMessage: optionalString(raw.errorMessage ?? raw.error_message),
730
+ toolCalls: normalizeStringArray(raw.toolCalls ?? raw.tool_calls),
731
+ metadata: asOptionalUnknownRecord(raw.metadata),
732
+ };
733
+ }
734
+
735
+ /**
736
+ * Normalize a wire SessionState (snake_case) into the camelCase frontend
737
+ * shape. Used by `api.sessions.state()` and by the `CUSTOM:session_state`
738
+ * SSE event consumer.
739
+ */
740
+ export function normalizeSessionState(rawValue: unknown): SessionStateSnapshot {
741
+ const raw = asDict(rawValue);
742
+ const camelized = camelizeObject(raw) as Record<string, unknown>;
743
+ const rs = (camelized.runState ?? {}) as Record<string, unknown>;
744
+ const agentsRaw = Array.isArray(camelized.agents) ? camelized.agents : [];
745
+ const agents: AgentStatus[] = agentsRaw.map((entry) => {
746
+ const a = asDict(entry);
747
+ return {
748
+ name: stringValue(a.name),
749
+ status: stringValue(a.status, "idle"),
750
+ task: stringValue(a.task, ""),
751
+ updatedAt: optionalString(a.updatedAt),
752
+ alive: typeof a.alive === "boolean" ? a.alive : undefined,
753
+ };
754
+ });
755
+ return {
756
+ runState: {
757
+ active: rs.active === true,
758
+ runId: optionalString(rs.runId) ?? null,
759
+ },
760
+ agents,
761
+ lastActivityTs: stringValue(camelized.lastActivityTs, ""),
762
+ };
763
+ }
764
+
765
+
766
+ export function normalizeTraceGraph(rawValue: unknown): TraceGraph {
767
+ const raw = asDict(rawValue);
768
+ const meta = asDict(raw.meta);
769
+ const nodes = Array.isArray(raw.nodes) ? raw.nodes.map(normalizeTraceNode) : [];
770
+ const childIdsByParent = new Map<string, Set<string>>();
771
+ for (const node of nodes) {
772
+ for (const parentId of node.parentIds) {
773
+ const children = childIdsByParent.get(parentId) ?? new Set<string>();
774
+ children.add(node.id);
775
+ childIdsByParent.set(parentId, children);
776
+ }
777
+ }
778
+ return {
779
+ meta: {
780
+ ...(camelizeObject(meta) as Record<string, unknown>),
781
+ sessionId: stringValue(meta.sessionId ?? meta.session_id),
782
+ userId: optionalString(meta.userId ?? meta.user_id),
783
+ projectName: optionalString(meta.projectName ?? meta.project_name),
784
+ currentFocus: optionalString(meta.currentFocus ?? meta.current_focus),
785
+ createdAt: optionalString(meta.createdAt ?? meta.created_at),
786
+ },
787
+ nodes: nodes.map((node) => ({
788
+ ...node,
789
+ childIds: node.childIds.length ? node.childIds : Array.from(childIdsByParent.get(node.id) ?? []),
790
+ })),
791
+ };
792
+ }
793
+
794
+ // TODO(dead-code): normalizeSessionEvent is a leftover from the pre-AG-UI polling protocol.
795
+ // Kept commented out for now; will be fully deleted once issue-4 fallback removal lands.
796
+ // export function normalizeSessionEvent(rawValue: unknown): SessionEventEntry {
797
+ // const raw = asDict(rawValue);
798
+ // const seq = numberValue(raw._seq ?? raw.seq, -1);
799
+ // const timestamp = stringValue(raw._ts ?? raw.timestamp, new Date().toISOString());
800
+ // const type = stringValue(raw.type, "unknown");
801
+ // const sessionId = optionalString(raw.session_id ?? raw.sessionId);
802
+ // const data = { ...raw };
803
+ // delete data._seq;
804
+ // delete data.seq;
805
+ // delete data._ts;
806
+ // delete data.timestamp;
807
+ // delete data.type;
808
+ // delete data.session_id;
809
+ // delete data.sessionId;
810
+ //
811
+ // return {
812
+ // seq,
813
+ // timestamp,
814
+ // type,
815
+ // sessionId,
816
+ // data: camelizeObject(data) as Dict,
817
+ // };
818
+ // }
819
+
820
+ /**
821
+ * Normalize a raw AG-UI event JSON from the wire (snake_case) to the
822
+ * camelCase TypeScript shape. Fields are passed through 1:1; `_seq` is
823
+ * intentionally dropped — the new contract relies on AG-UI's
824
+ * messageId/toolCallId for state aggregation rather than transport-level
825
+ * sequence numbers.
826
+ */
827
+ export function normalizeAgUiEvent(rawValue: unknown): AgUiEvent {
828
+ const raw = asDict(rawValue);
829
+ // The whole payload is flat — camelize every key, including nested messages.
830
+ const camelized = camelizeObject(raw) as Record<string, unknown>;
831
+ return camelized as AgUiEvent;
832
+ }
833
+
834
+ /** Back-compat alias for callers still using the old name. */
835
+ export const normalizeWebSocketEvent = normalizeAgUiEvent;
836
+
837
+ // TODO(dead-code): makeUserMessage is unused; remove once confirmed safe.
838
+ // export function makeUserMessage(content: string, sessionId: string): ChatMessage {
839
+ // return {
840
+ // id: crypto.randomUUID(),
841
+ // role: "user",
842
+ // content,
843
+ // createdAt: new Date().toISOString(),
844
+ // agent: "user",
845
+ // };
846
+ // }