@brainpilot/web 0.0.4 → 0.0.6

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 (114) hide show
  1. package/dist/assets/index-Br55rkHb.css +1 -0
  2. package/dist/assets/index-CeUzk-ej.js +445 -0
  3. package/dist/index.html +2 -2
  4. package/index.html +13 -0
  5. package/package.json +12 -3
  6. package/src/App.tsx +10 -0
  7. package/src/__tests__/agentsReducer.test.ts +67 -0
  8. package/src/__tests__/api.test.ts +221 -0
  9. package/src/__tests__/chatScrollMemory.test.ts +49 -0
  10. package/src/__tests__/demoConversation.test.ts +73 -0
  11. package/src/__tests__/demoReset.test.ts +24 -0
  12. package/src/__tests__/messageGroups.test.ts +80 -0
  13. package/src/__tests__/newUiComponents.test.tsx +101 -0
  14. package/src/__tests__/newUiEvents.test.ts +236 -0
  15. package/src/__tests__/runningToast.test.ts +29 -0
  16. package/src/__tests__/tokenUsage.test.ts +48 -0
  17. package/src/__tests__/toolDisplay.test.ts +55 -0
  18. package/src/__tests__/traceReducer.test.ts +62 -0
  19. package/src/components/chat/AskUserCard.tsx +123 -0
  20. package/src/components/chat/AutoRetryIndicator.tsx +71 -0
  21. package/src/components/chat/ComposerInput.tsx +73 -0
  22. package/src/components/chat/ComposerSendButton.tsx +26 -0
  23. package/src/components/chat/MarkdownMessage.tsx +24 -0
  24. package/src/components/chat/MessageStream.tsx +505 -0
  25. package/src/components/chat/PromptComposer.tsx +489 -0
  26. package/src/components/chat/SystemMessageBubble.tsx +46 -0
  27. package/src/components/chat/chatScrollMemory.ts +49 -0
  28. package/src/components/demo/DemoFileTree.tsx +146 -0
  29. package/src/components/demo/DemoView.tsx +730 -0
  30. package/src/components/demo/TraceNodeModal.tsx +80 -0
  31. package/src/components/demo/demoBundle.ts +223 -0
  32. package/src/components/demo/demoCache.ts +42 -0
  33. package/src/components/demo/demoReset.ts +16 -0
  34. package/src/components/files/FilePreviewView.tsx +153 -0
  35. package/src/components/files/FileSidebar.tsx +664 -0
  36. package/src/components/files/filePreview.ts +113 -0
  37. package/src/components/primitives/CustomSelect.tsx +200 -0
  38. package/src/components/primitives/IconButton.tsx +27 -0
  39. package/src/components/quota/DiskQuotaCriticalDialog.tsx +56 -0
  40. package/src/components/quota/DiskQuotaWarningDialog.tsx +65 -0
  41. package/src/components/quota/QuotaFileManager.tsx +197 -0
  42. package/src/components/search/SearchDialog.tsx +101 -0
  43. package/src/components/session/AgentNetwork.tsx +1233 -0
  44. package/src/components/session/AgentTraceViews.tsx +346 -0
  45. package/src/components/session/AnalyticsTab.tsx +220 -0
  46. package/src/components/session/GlobalOverview.tsx +108 -0
  47. package/src/components/session/NodeTooltip.tsx +127 -0
  48. package/src/components/session/TimelineTab.tsx +320 -0
  49. package/src/components/session/TraceGraphView.tsx +307 -0
  50. package/src/components/session/TraceNodeDetail.tsx +179 -0
  51. package/src/components/session/agentAnalytics.ts +397 -0
  52. package/src/components/session/agentNetworkShared.ts +339 -0
  53. package/src/components/session/traceLayout.ts +182 -0
  54. package/src/components/settings/SettingsDialog.tsx +737 -0
  55. package/src/components/shell/DesktopShell.tsx +261 -0
  56. package/src/components/shell/SandboxBuildingOverlay.tsx +73 -0
  57. package/src/components/shell/SandboxStatus.tsx +287 -0
  58. package/src/components/shell/TerminalDrawer.tsx +387 -0
  59. package/src/components/sidebar/Sidebar.tsx +191 -0
  60. package/src/config.ts +10 -0
  61. package/src/contexts/AppProviders.tsx +20 -0
  62. package/src/contexts/AuthContext.tsx +61 -0
  63. package/src/contexts/PreferencesContext.tsx +125 -0
  64. package/src/contexts/SSEContext.tsx +264 -0
  65. package/src/contexts/SandboxContext.tsx +310 -0
  66. package/src/contexts/SessionContext.tsx +919 -0
  67. package/src/contexts/agentsReducer.ts +49 -0
  68. package/src/contexts/draftStore.ts +103 -0
  69. package/src/contexts/messageFilters.ts +29 -0
  70. package/src/contexts/messageGroups.ts +77 -0
  71. package/src/contexts/messageReducer.ts +401 -0
  72. package/src/contexts/newUiEvents.ts +190 -0
  73. package/src/contexts/runningToast.ts +33 -0
  74. package/src/contexts/traceReducer.ts +62 -0
  75. package/src/contexts/turnTimer.test.ts +97 -0
  76. package/src/contexts/turnTimer.ts +108 -0
  77. package/src/contexts/useTurnTimer.ts +104 -0
  78. package/src/contracts/backend.ts +897 -0
  79. package/src/contracts/demoBundle.ts +83 -0
  80. package/src/i18n/messages/analytics.ts +106 -0
  81. package/src/i18n/messages/chat.ts +130 -0
  82. package/src/i18n/messages/contexts.ts +42 -0
  83. package/src/i18n/messages/demo.ts +80 -0
  84. package/src/i18n/messages/files.ts +82 -0
  85. package/src/i18n/messages/network.ts +190 -0
  86. package/src/i18n/messages/profile.ts +44 -0
  87. package/src/i18n/messages/quota.ts +36 -0
  88. package/src/i18n/messages/sandbox.ts +116 -0
  89. package/src/i18n/messages/search.ts +16 -0
  90. package/src/i18n/messages/settings.ts +188 -0
  91. package/src/i18n/messages/shell.ts +38 -0
  92. package/src/i18n/messages/sidebar.ts +52 -0
  93. package/src/i18n/messages/terminal.ts +22 -0
  94. package/src/i18n/messages/trace.ts +136 -0
  95. package/src/i18n/messages.ts +32 -0
  96. package/src/i18n/translate.ts +46 -0
  97. package/src/i18n/types.ts +15 -0
  98. package/src/i18n/useT.ts +15 -0
  99. package/src/main.tsx +13 -0
  100. package/src/mocks/backend.ts +729 -0
  101. package/src/styles/global.css +7578 -0
  102. package/src/styles/tokens.css +161 -0
  103. package/src/utils/api.ts +724 -0
  104. package/src/utils/download.ts +18 -0
  105. package/src/utils/format.ts +7 -0
  106. package/src/utils/toolDisplay.ts +74 -0
  107. package/src/utils/zip.ts +119 -0
  108. package/src/vite-env.d.ts +1 -0
  109. package/tsconfig.app.json +22 -0
  110. package/tsconfig.json +7 -0
  111. package/tsconfig.node.json +13 -0
  112. package/vite.config.ts +13 -0
  113. package/dist/assets/index-Cd0Mi_WU.css +0 -1
  114. package/dist/assets/index-FGg-DeYR.js +0 -448
@@ -0,0 +1,724 @@
1
+ import {
2
+ AgentStatus,
3
+ FileContent,
4
+ FileEntry,
5
+ McpServerEntry,
6
+ ProviderCreate,
7
+ ProviderProfile,
8
+ ProviderUpdate,
9
+ Sandbox,
10
+ SandboxStats,
11
+ Session,
12
+ SessionMessageEntry,
13
+ SessionStateSnapshot,
14
+ SettingsData,
15
+ TraceGraph,
16
+ normalizeFileContent,
17
+ normalizeFileEntry,
18
+ normalizeMcpServer,
19
+ normalizeProviderProfile,
20
+ normalizeSandbox,
21
+ normalizeSandboxStats,
22
+ normalizeSession,
23
+ normalizeSessionState,
24
+ normalizeSettings,
25
+ normalizeTraceGraph,
26
+ normalizeUser,
27
+ serializeMcpConfig,
28
+ serializeProviderCreate,
29
+ serializeProviderUpdate,
30
+ serializeSettings,
31
+ User,
32
+ } from "../contracts/backend";
33
+ import { runtimeConfig } from "../config";
34
+ import { mockBackend } from "../mocks/backend";
35
+ import { RawAgUiEvent } from "../contracts/demoBundle";
36
+
37
+ const API_BASE = "/api";
38
+
39
+ // Trust-front: the hosted gateway authenticates via an httpOnly cookie that the
40
+ // browser carries automatically. The frontend never reads, stores, or attaches a
41
+ // token — it just makes credentialed requests.
42
+ //
43
+ // #106: callers that drive composer state (postMessage / create) pass a
44
+ // `timeoutMs`. A hung request used to leave `isSending` true forever (the
45
+ // `finally` that resets it never ran), permanently disabling the composer and
46
+ // silently dropping the user's input. With a timeout the request rejects, the
47
+ // caller's catch surfaces a recoverable error, and `isSending` is released.
48
+ function apiFetch(
49
+ input: RequestInfo | URL,
50
+ init: RequestInit & { timeoutMs?: number } = {},
51
+ ): Promise<Response> {
52
+ const { timeoutMs, signal, ...rest } = init;
53
+ if (timeoutMs == null) {
54
+ return fetch(input, { credentials: "include", signal, ...rest });
55
+ }
56
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
57
+ // Honour an upstream signal too, if one was supplied.
58
+ const merged = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
59
+ return fetch(input, { credentials: "include", signal: merged, ...rest });
60
+ }
61
+
62
+ /** #106: default ceiling for composer-driving requests (create / postMessage). */
63
+ const SEND_TIMEOUT_MS = 30_000;
64
+
65
+ function authHeaders(json = true): Record<string, string> {
66
+ return json ? { "Content-Type": "application/json" } : {};
67
+ }
68
+
69
+ async function parseError(res: Response): Promise<string> {
70
+ const contentType = res.headers.get("content-type") || "";
71
+ if (contentType.includes("application/json")) {
72
+ const body = (await res.json().catch(() => null)) as { detail?: unknown } | null;
73
+ if (typeof body?.detail === "string") {
74
+ return body.detail;
75
+ }
76
+ }
77
+ const text = await res.text().catch(() => "");
78
+ return text || `Request failed (${res.status})`;
79
+ }
80
+
81
+ async function handleJson<T>(res: Response): Promise<T> {
82
+ if (!res.ok) {
83
+ throw new Error(await parseError(res));
84
+ }
85
+ if (res.status === 204) {
86
+ return undefined as T;
87
+ }
88
+ return (await res.json()) as T;
89
+ }
90
+
91
+ /** #47: encode a Blob/File as base64 (without the data: prefix) for upload. */
92
+ function blobToBase64(blob: Blob): Promise<string> {
93
+ return new Promise((resolve, reject) => {
94
+ const reader = new FileReader();
95
+ reader.onerror = () => reject(reader.error ?? new Error("file read failed"));
96
+ reader.onload = () => {
97
+ const result = reader.result as string;
98
+ // strip the "data:<mime>;base64," prefix
99
+ const comma = result.indexOf(",");
100
+ resolve(comma >= 0 ? result.slice(comma + 1) : result);
101
+ };
102
+ reader.readAsDataURL(blob);
103
+ });
104
+ }
105
+
106
+ export function getSSEUrl(sessionId: string): string {
107
+ // Same origin; relative path lets EventSource follow the current host/port and
108
+ // carry the auth cookie automatically — no token in the query string.
109
+ return `${API_BASE}/sessions/${encodeURIComponent(sessionId)}/sse`;
110
+ }
111
+
112
+ export function getTerminalWsUrl(sandboxId: string, cols = 80, rows = 24): string {
113
+ const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
114
+ const params = new URLSearchParams({ cols: String(cols), rows: String(rows) });
115
+ return `${protocol}//${window.location.host}${API_BASE}/sandbox/${sandboxId}/terminal?${params}`;
116
+ }
117
+
118
+ export const api = {
119
+ async getVersion(): Promise<{ version: string }> {
120
+ if (runtimeConfig.useMockBackend) {
121
+ return mockBackend.version();
122
+ }
123
+ return handleJson(await apiFetch(`${API_BASE}/version`));
124
+ },
125
+
126
+ auth: {
127
+ async me(): Promise<User> {
128
+ if (runtimeConfig.useMockBackend) {
129
+ return mockBackend.me();
130
+ }
131
+ const raw = await handleJson<unknown>(await apiFetch(`${API_BASE}/auth/me`, { headers: authHeaders() }));
132
+ return normalizeUser(raw as Parameters<typeof normalizeUser>[0]);
133
+ },
134
+ },
135
+
136
+ sandbox: {
137
+ async list(): Promise<Sandbox[]> {
138
+ if (runtimeConfig.useMockBackend) {
139
+ return mockBackend.listSandboxes();
140
+ }
141
+ const raw = await handleJson<unknown[]>(await apiFetch(`${API_BASE}/sandbox/list`, { headers: authHeaders() }));
142
+ return raw.map((item) => normalizeSandbox(item as Parameters<typeof normalizeSandbox>[0]));
143
+ },
144
+
145
+ async create(sandboxName = "default"): Promise<Sandbox> {
146
+ if (runtimeConfig.useMockBackend) {
147
+ return mockBackend.createSandbox(sandboxName);
148
+ }
149
+ const raw = await handleJson<unknown>(
150
+ await apiFetch(`${API_BASE}/sandbox/create`, {
151
+ method: "POST",
152
+ headers: authHeaders(),
153
+ body: JSON.stringify({ sandbox_name: sandboxName }),
154
+ }),
155
+ );
156
+ return normalizeSandbox(raw as Parameters<typeof normalizeSandbox>[0]);
157
+ },
158
+
159
+ async rebuild(sandboxId: string): Promise<Sandbox> {
160
+ if (runtimeConfig.useMockBackend) {
161
+ return mockBackend.rebuildSandbox();
162
+ }
163
+ const params = new URLSearchParams({ sandbox_id: sandboxId });
164
+ const raw = await handleJson<unknown>(
165
+ await apiFetch(`${API_BASE}/sandbox/rebuild?${params}`, {
166
+ method: "POST",
167
+ headers: authHeaders(),
168
+ }),
169
+ );
170
+ return normalizeSandbox(raw as Parameters<typeof normalizeSandbox>[0]);
171
+ },
172
+
173
+ async destroy(sandboxId: string): Promise<void> {
174
+ if (runtimeConfig.useMockBackend) {
175
+ return mockBackend.destroySandbox();
176
+ }
177
+ await handleJson<void>(
178
+ await apiFetch(`${API_BASE}/sandbox/${sandboxId}`, {
179
+ method: "DELETE",
180
+ headers: authHeaders(),
181
+ }),
182
+ );
183
+ },
184
+
185
+ async stats(sandboxId: string): Promise<SandboxStats> {
186
+ if (runtimeConfig.useMockBackend) {
187
+ return mockBackend.sandboxStats();
188
+ }
189
+ const raw = await handleJson<unknown>(
190
+ await apiFetch(`${API_BASE}/sandbox/${sandboxId}/stats`, { headers: authHeaders() }),
191
+ );
192
+ return normalizeSandboxStats(raw);
193
+ },
194
+
195
+ async logs(sandboxId: string, tail = 200): Promise<string> {
196
+ if (runtimeConfig.useMockBackend) {
197
+ return mockBackend.sandboxLogs();
198
+ }
199
+ const params = new URLSearchParams({ tail: String(tail) });
200
+ const raw = await handleJson<{ logs?: string }>(
201
+ await apiFetch(`${API_BASE}/sandbox/${sandboxId}/logs?${params}`, { headers: authHeaders() }),
202
+ );
203
+ return raw.logs || "";
204
+ },
205
+
206
+ async reloadConfig(sandboxId: string): Promise<{ status: string }> {
207
+ if (runtimeConfig.useMockBackend) {
208
+ return { status: 'ok' }
209
+ }
210
+ const res = await apiFetch(`${API_BASE}/sandbox/reload-config?sandbox_id=${sandboxId}`, {
211
+ method: 'POST',
212
+ headers: authHeaders(),
213
+ })
214
+ if (!res.ok) {
215
+ const body = await res.json().catch(() => ({}))
216
+ throw new Error(body.detail || `Reload failed (${res.status})`)
217
+ }
218
+ return handleJson(res)
219
+ },
220
+
221
+ async health(sandboxId: string): Promise<Record<string, unknown>> {
222
+ if (runtimeConfig.useMockBackend) {
223
+ return mockBackend.sandboxHealth();
224
+ }
225
+ return handleJson<Record<string, unknown>>(
226
+ await apiFetch(`${API_BASE}/sandbox/${sandboxId}/health`, { headers: authHeaders() }),
227
+ );
228
+ },
229
+
230
+ async listFiles(sandboxId: string, path = "/workspace"): Promise<FileEntry[]> {
231
+ if (runtimeConfig.useMockBackend) {
232
+ return mockBackend.listFiles(sandboxId, path);
233
+ }
234
+ const params = new URLSearchParams({ path });
235
+ const raw = await handleJson<unknown[]>(
236
+ await apiFetch(`${API_BASE}/sandbox/${sandboxId}/files?${params}`, { headers: authHeaders() }),
237
+ );
238
+ return raw.map((item) => normalizeFileEntry(item as Parameters<typeof normalizeFileEntry>[0]));
239
+ },
240
+
241
+ async readFile(sandboxId: string, path: string): Promise<FileContent> {
242
+ if (runtimeConfig.useMockBackend) {
243
+ return mockBackend.readFile(sandboxId, path);
244
+ }
245
+ const params = new URLSearchParams({ path });
246
+ const raw = await handleJson<unknown>(
247
+ await apiFetch(`${API_BASE}/sandbox/${sandboxId}/files/content?${params}`, { headers: authHeaders() }),
248
+ );
249
+ return normalizeFileContent(raw);
250
+ },
251
+
252
+ async readRawFile(sandboxId: string, path: string): Promise<Blob> {
253
+ if (runtimeConfig.useMockBackend) {
254
+ return mockBackend.readRawFile(sandboxId, path);
255
+ }
256
+ const params = new URLSearchParams({ path });
257
+ const res = await apiFetch(`${API_BASE}/sandbox/${sandboxId}/files/raw?${params}`, {
258
+ headers: authHeaders(false),
259
+ });
260
+ if (!res.ok) {
261
+ throw new Error(await parseError(res));
262
+ }
263
+ return res.blob();
264
+ },
265
+
266
+ async readFileBlob(sandboxId: string, path: string): Promise<Blob> {
267
+ return this.readRawFile(sandboxId, path);
268
+ },
269
+
270
+ async deleteFile(sandboxId: string, path: string): Promise<void> {
271
+ if (runtimeConfig.useMockBackend) {
272
+ return mockBackend.deleteFile(sandboxId, path);
273
+ }
274
+ const params = new URLSearchParams({ path });
275
+ const res = await apiFetch(`${API_BASE}/sandbox/${sandboxId}/files?${params}`, {
276
+ method: "DELETE",
277
+ headers: authHeaders(),
278
+ });
279
+ if (!res.ok) {
280
+ throw new Error(await parseError(res));
281
+ }
282
+ },
283
+
284
+ // #47: upload a file into the workspace (base64 over the JSON byte chain).
285
+ async uploadFile(sandboxId: string, path: string, file: Blob): Promise<{ path: string; size: number }> {
286
+ const contentBase64 = await blobToBase64(file);
287
+ const res = await apiFetch(`${API_BASE}/sandbox/${sandboxId}/files`, {
288
+ method: "POST",
289
+ headers: { ...authHeaders(), "content-type": "application/json" },
290
+ body: JSON.stringify({ path, contentBase64 }),
291
+ });
292
+ if (!res.ok) {
293
+ throw new Error(await parseError(res));
294
+ }
295
+ return handleJson(res);
296
+ },
297
+ },
298
+
299
+ sessions: {
300
+ async list(): Promise<Session[]> {
301
+ if (runtimeConfig.useMockBackend) {
302
+ return mockBackend.listSessions();
303
+ }
304
+ // Runtime returns the protocol envelope `{ sessions: [...] }` (see
305
+ // ListSessionsResponseSchema). Unwrap it; tolerate a bare array (legacy /
306
+ // mock) and fall back to [] so an unexpected shape never throws
307
+ // `.map is not a function` into SessionContext's error banner.
308
+ const raw = await handleJson<{ sessions?: unknown[] } | unknown[]>(
309
+ await apiFetch(`${API_BASE}/sessions`, { headers: authHeaders() }),
310
+ );
311
+ const list = Array.isArray(raw)
312
+ ? raw
313
+ : Array.isArray((raw as { sessions?: unknown[] })?.sessions)
314
+ ? (raw as { sessions: unknown[] }).sessions
315
+ : [];
316
+ return list.map((item) => normalizeSession(item as Parameters<typeof normalizeSession>[0]));
317
+ },
318
+
319
+ async get(sessionId: string): Promise<Session> {
320
+ if (runtimeConfig.useMockBackend) {
321
+ return mockBackend.getSession(sessionId);
322
+ }
323
+ const raw = await handleJson<unknown>(await apiFetch(`${API_BASE}/sessions/${sessionId}`, { headers: authHeaders() }));
324
+ return normalizeSession(raw as Parameters<typeof normalizeSession>[0]);
325
+ },
326
+
327
+ async create(
328
+ title = "New research session",
329
+ opts: { providerId?: string; modelId?: string } = {},
330
+ ): Promise<Session> {
331
+ if (runtimeConfig.useMockBackend) {
332
+ return mockBackend.createSession(title);
333
+ }
334
+ const raw = await handleJson<unknown>(
335
+ await apiFetch(`${API_BASE}/sessions`, {
336
+ method: "POST",
337
+ headers: authHeaders(),
338
+ timeoutMs: SEND_TIMEOUT_MS,
339
+ body: JSON.stringify({
340
+ title,
341
+ ...(opts.providerId ? { providerId: opts.providerId } : {}),
342
+ ...(opts.modelId ? { modelId: opts.modelId } : {}),
343
+ }),
344
+ }),
345
+ );
346
+ // The runtime's POST /sessions returns the envelope `{ id, session }`
347
+ // (server.ts), unlike GET /sessions[/:id] which return the bare session.
348
+ // Unwrap `session` if present so normalizeSession reads the real `title`
349
+ // instead of falling back to `Session <id8>` (#96). Tolerate a bare
350
+ // object too (mock / future shape change).
351
+ const envelope = raw as { session?: unknown } | null;
352
+ const sessionRaw = envelope && typeof envelope === "object" && "session" in envelope
353
+ ? envelope.session
354
+ : raw;
355
+ return normalizeSession(sessionRaw as Parameters<typeof normalizeSession>[0]);
356
+ },
357
+
358
+ async update(sessionId: string, title: string): Promise<Session> {
359
+ if (runtimeConfig.useMockBackend) {
360
+ return mockBackend.updateSession(sessionId, title);
361
+ }
362
+ const raw = await handleJson<unknown>(
363
+ await apiFetch(`${API_BASE}/sessions/${sessionId}`, {
364
+ method: "PUT",
365
+ headers: authHeaders(),
366
+ body: JSON.stringify({ title }),
367
+ }),
368
+ );
369
+ return normalizeSession(raw as Parameters<typeof normalizeSession>[0]);
370
+ },
371
+
372
+ async remove(sessionId: string): Promise<void> {
373
+ if (runtimeConfig.useMockBackend) {
374
+ return mockBackend.removeSession(sessionId);
375
+ }
376
+ await handleJson<void>(
377
+ await apiFetch(`${API_BASE}/sessions/${sessionId}`, {
378
+ method: "DELETE",
379
+ headers: authHeaders(),
380
+ }),
381
+ );
382
+ },
383
+
384
+ async interrupt(sessionId: string): Promise<{ interrupted: boolean }> {
385
+ if (runtimeConfig.useMockBackend) {
386
+ return { interrupted: true };
387
+ }
388
+ // #90: Stop = whole-session interrupt. Hit the dedicated interrupt route
389
+ // (RUNTIME_ROUTES.interrupt), NOT /messages — the messages endpoint's body
390
+ // schema rejects {type:"interrupt"} so the agent was never actually
391
+ // stopped. Empty body = interrupt every agent in the session.
392
+ return handleJson<{ interrupted: boolean }>(
393
+ await apiFetch(`${API_BASE}/sessions/${sessionId}/interrupt`, {
394
+ method: "POST",
395
+ headers: { ...authHeaders(), "Content-Type": "application/json" },
396
+ body: JSON.stringify({}),
397
+ }),
398
+ );
399
+ },
400
+
401
+ async postMessage(
402
+ sessionId: string,
403
+ payload: { content: string; uuid: string; timestamp: string; type?: string },
404
+ ): Promise<{ status: string }> {
405
+ if (runtimeConfig.useMockBackend) {
406
+ // Mock path: route through the existing mock helper if needed by the UI.
407
+ return { status: "ok" };
408
+ }
409
+ return handleJson<{ status: string }>(
410
+ await apiFetch(`${API_BASE}/sessions/${sessionId}/messages`, {
411
+ method: "POST",
412
+ headers: { ...authHeaders(), "Content-Type": "application/json" },
413
+ timeoutMs: SEND_TIMEOUT_MS,
414
+ body: JSON.stringify({
415
+ type: payload.type ?? "user_message",
416
+ content: payload.content,
417
+ session_id: sessionId,
418
+ data: { uuid: payload.uuid, timestamp: payload.timestamp },
419
+ }),
420
+ }),
421
+ );
422
+ },
423
+
424
+ async commands(sessionId: string): Promise<{ commands: string[] }> {
425
+ if (runtimeConfig.useMockBackend) {
426
+ // ✅ 已通过真实 API 测试:/compact /context /cost 有效
427
+ // ❌ 已移除:/usage(返回空) /clear /init(Unknown skill)
428
+ return { commands: ["/compact", "/context", "/cost"] };
429
+ }
430
+ return handleJson<{ commands: string[] }>(
431
+ await apiFetch(`${API_BASE}/sessions/${sessionId}/commands`, { headers: authHeaders() }),
432
+ );
433
+ },
434
+
435
+ // 修正6 — answer an ask_user (user_input_request) prompt. Posts a
436
+ // user_input_response back through the same /messages endpoint the
437
+ // composer uses, carrying the request_id so the runtime can match it.
438
+ async respondToInput(
439
+ sessionId: string,
440
+ payload: { requestId: string; answer: string },
441
+ ): Promise<{ status: string }> {
442
+ if (runtimeConfig.useMockBackend) {
443
+ return { status: "ok" };
444
+ }
445
+ return handleJson<{ status: string }>(
446
+ await apiFetch(`${API_BASE}/sessions/${sessionId}/messages`, {
447
+ method: "POST",
448
+ headers: { ...authHeaders(), "Content-Type": "application/json" },
449
+ body: JSON.stringify({
450
+ type: "user_input_response",
451
+ session_id: sessionId,
452
+ request_id: payload.requestId,
453
+ answer: payload.answer,
454
+ }),
455
+ }),
456
+ );
457
+ },
458
+
459
+ async getTrace(sessionId: string): Promise<TraceGraph> {
460
+ if (runtimeConfig.useMockBackend) {
461
+ return mockBackend.getTrace(sessionId);
462
+ }
463
+ const raw = await handleJson<unknown>(
464
+ await apiFetch(`${API_BASE}/sessions/${sessionId}/trace`, { headers: authHeaders() }),
465
+ );
466
+ return normalizeTraceGraph(raw);
467
+ },
468
+
469
+ async getEvents(sessionId: string): Promise<RawAgUiEvent[]> {
470
+ if (runtimeConfig.useMockBackend) {
471
+ return mockBackend.getSessionEvents(sessionId);
472
+ }
473
+ // `/sessions/:id/events` is an SSE alias in backend-core (sseHandler), not
474
+ // a JSON route — so res.json() would reject. Treat any non-ok / non-JSON
475
+ // response as "no events" and let callers (demoBundle) use their ordered
476
+ // fallback instead of crashing on a stream body.
477
+ const res = await apiFetch(`${API_BASE}/sessions/${sessionId}/events`, { headers: authHeaders() });
478
+ if (!res.ok) return [];
479
+ const contentType = res.headers.get("content-type") || "";
480
+ if (!contentType.includes("application/json")) return [];
481
+ const raw = (await res.json().catch(() => ({}))) as { events?: unknown[] };
482
+ return Array.isArray(raw.events) ? (raw.events as RawAgUiEvent[]) : [];
483
+ },
484
+
485
+ /**
486
+ * Persisted AG-UI event history from `events.jsonl` — used to rehydrate
487
+ * the chat list (and trace/agents seed) when a session is activated after
488
+ * a runtime restart. SSE only replays the in-memory ring buffer; this
489
+ * endpoint walks the on-disk log and returns the tail when long. Pass
490
+ * `limit: 0` to request the full log for lossless rehydrate.
491
+ *
492
+ * Tolerates any non-200 / non-JSON response by returning an empty
493
+ * envelope, so callers can fall through to whatever live data the SSE
494
+ * stream eventually delivers.
495
+ */
496
+ async getHistory(
497
+ sessionId: string,
498
+ opts: { limit?: number } = {},
499
+ ): Promise<{ events: RawAgUiEvent[]; total: number; truncated: boolean }> {
500
+ if (runtimeConfig.useMockBackend) {
501
+ return { events: [], total: 0, truncated: false };
502
+ }
503
+ const qs = opts.limit !== undefined ? `?limit=${encodeURIComponent(opts.limit)}` : "";
504
+ const res = await apiFetch(
505
+ `${API_BASE}/sessions/${sessionId}/history${qs}`,
506
+ { headers: authHeaders() },
507
+ );
508
+ if (!res.ok) return { events: [], total: 0, truncated: false };
509
+ const raw = (await res.json().catch(() => null)) as
510
+ | { events?: unknown[]; total?: number; truncated?: boolean }
511
+ | null;
512
+ return {
513
+ events: Array.isArray(raw?.events) ? (raw!.events as RawAgUiEvent[]) : [],
514
+ total: typeof raw?.total === "number" ? raw!.total : 0,
515
+ truncated: Boolean(raw?.truncated),
516
+ };
517
+ },
518
+
519
+ async state(sessionId: string): Promise<SessionStateSnapshot> {
520
+ if (runtimeConfig.useMockBackend) {
521
+ return mockBackend.state();
522
+ }
523
+ const raw = await handleJson<unknown>(
524
+ await apiFetch(`${API_BASE}/sessions/${sessionId}/state`, { headers: authHeaders() }),
525
+ );
526
+ return normalizeSessionState(raw);
527
+ },
528
+ },
529
+
530
+ ui: {
531
+ async promptSuggestions(): Promise<string[]> {
532
+ if (runtimeConfig.useMockBackend) {
533
+ return mockBackend.promptSuggestions();
534
+ }
535
+ return [];
536
+ },
537
+ },
538
+
539
+ settings: {
540
+ async get(): Promise<SettingsData> {
541
+ if (runtimeConfig.useMockBackend) {
542
+ return mockBackend.getSettings();
543
+ }
544
+ const raw = await handleJson<unknown>(await apiFetch(`${API_BASE}/settings`, { headers: authHeaders() }));
545
+ return normalizeSettings(raw as Parameters<typeof normalizeSettings>[0]);
546
+ },
547
+
548
+ async update(data: Partial<SettingsData>): Promise<SettingsData> {
549
+ if (runtimeConfig.useMockBackend) {
550
+ return mockBackend.updateSettings(data);
551
+ }
552
+ const raw = await handleJson<unknown>(
553
+ await apiFetch(`${API_BASE}/settings`, {
554
+ method: "PUT",
555
+ headers: authHeaders(),
556
+ body: JSON.stringify(serializeSettings(data)),
557
+ }),
558
+ );
559
+ return normalizeSettings(raw as Parameters<typeof normalizeSettings>[0]);
560
+ },
561
+
562
+ async resetConfig(): Promise<void> {
563
+ if (runtimeConfig.useMockBackend) {
564
+ return mockBackend.resetConfig();
565
+ }
566
+ await handleJson<void>(
567
+ await apiFetch(`${API_BASE}/settings/reset-config`, {
568
+ method: "POST",
569
+ headers: authHeaders(),
570
+ }),
571
+ );
572
+ },
573
+ },
574
+
575
+ mcpServers: {
576
+ async list(): Promise<McpServerEntry[]> {
577
+ if (runtimeConfig.useMockBackend) {
578
+ return mockBackend.listMcpServers();
579
+ }
580
+ const raw = await handleJson<unknown[]>(await apiFetch(`${API_BASE}/mcp-servers`, { headers: authHeaders() }));
581
+ return raw.map(normalizeMcpServer);
582
+ },
583
+
584
+ async add(name: string, config: Omit<McpServerEntry, "name">): Promise<McpServerEntry> {
585
+ if (runtimeConfig.useMockBackend) {
586
+ return mockBackend.addMcpServer(name, config);
587
+ }
588
+ const raw = await handleJson<unknown>(
589
+ await apiFetch(`${API_BASE}/mcp-servers`, {
590
+ method: "POST",
591
+ headers: authHeaders(),
592
+ body: JSON.stringify({ name, config: serializeMcpConfig(config) }),
593
+ }),
594
+ );
595
+ return normalizeMcpServer(raw);
596
+ },
597
+
598
+ async update(name: string, config: Omit<McpServerEntry, "name">): Promise<McpServerEntry> {
599
+ if (runtimeConfig.useMockBackend) {
600
+ return mockBackend.updateMcpServer(name, config);
601
+ }
602
+ const raw = await handleJson<unknown>(
603
+ await apiFetch(`${API_BASE}/mcp-servers/${name}`, {
604
+ method: "PUT",
605
+ headers: authHeaders(),
606
+ body: JSON.stringify(serializeMcpConfig(config)),
607
+ }),
608
+ );
609
+ return normalizeMcpServer(raw);
610
+ },
611
+
612
+ async remove(name: string): Promise<void> {
613
+ if (runtimeConfig.useMockBackend) {
614
+ return mockBackend.removeMcpServer(name);
615
+ }
616
+ await handleJson<void>(
617
+ await apiFetch(`${API_BASE}/mcp-servers/${name}`, {
618
+ method: "DELETE",
619
+ headers: authHeaders(),
620
+ }),
621
+ );
622
+ },
623
+ },
624
+
625
+ providers: {
626
+ async list(): Promise<ProviderProfile[]> {
627
+ if (runtimeConfig.useMockBackend) {
628
+ return mockBackend.listProviders();
629
+ }
630
+ const raw = await handleJson<unknown[]>(
631
+ await apiFetch(`${API_BASE}/provider/profiles`, { headers: authHeaders() }),
632
+ );
633
+ return raw.map((item) => normalizeProviderProfile(item as Parameters<typeof normalizeProviderProfile>[0]));
634
+ },
635
+
636
+ async create(data: ProviderCreate): Promise<ProviderProfile> {
637
+ if (runtimeConfig.useMockBackend) {
638
+ return mockBackend.createProvider(data);
639
+ }
640
+ const raw = await handleJson<unknown>(
641
+ await apiFetch(`${API_BASE}/provider/profiles`, {
642
+ method: "POST",
643
+ headers: authHeaders(),
644
+ body: JSON.stringify(serializeProviderCreate(data)),
645
+ }),
646
+ );
647
+ return normalizeProviderProfile(raw as Parameters<typeof normalizeProviderProfile>[0]);
648
+ },
649
+
650
+ async update(id: string, data: ProviderUpdate): Promise<ProviderProfile> {
651
+ if (runtimeConfig.useMockBackend) {
652
+ return mockBackend.updateProvider(id, data);
653
+ }
654
+ const raw = await handleJson<unknown>(
655
+ await apiFetch(`${API_BASE}/provider/profiles/${id}`, {
656
+ method: "PUT",
657
+ headers: authHeaders(),
658
+ body: JSON.stringify(serializeProviderUpdate(data)),
659
+ }),
660
+ );
661
+ return normalizeProviderProfile(raw as Parameters<typeof normalizeProviderProfile>[0]);
662
+ },
663
+
664
+ async remove(id: string): Promise<void> {
665
+ if (runtimeConfig.useMockBackend) {
666
+ return mockBackend.removeProvider(id);
667
+ }
668
+ await handleJson<void>(
669
+ await apiFetch(`${API_BASE}/provider/profiles/${id}`, {
670
+ method: "DELETE",
671
+ headers: authHeaders(),
672
+ }),
673
+ );
674
+ },
675
+
676
+ async getActive(): Promise<ProviderProfile | null> {
677
+ if (runtimeConfig.useMockBackend) {
678
+ return mockBackend.getActiveProvider();
679
+ }
680
+ const res = await apiFetch(`${API_BASE}/provider/profiles/active`, { headers: authHeaders() });
681
+ if (res.status === 204) {
682
+ return null;
683
+ }
684
+ return normalizeProviderProfile((await handleJson<unknown>(res)) as Parameters<typeof normalizeProviderProfile>[0]);
685
+ },
686
+
687
+ async setActive(id: string): Promise<ProviderProfile> {
688
+ if (runtimeConfig.useMockBackend) {
689
+ return mockBackend.setActiveProvider(id);
690
+ }
691
+ const raw = await handleJson<unknown>(
692
+ await apiFetch(`${API_BASE}/provider/profiles/active`, {
693
+ method: "POST",
694
+ headers: authHeaders(),
695
+ body: JSON.stringify({ id }),
696
+ }),
697
+ );
698
+ return normalizeProviderProfile(raw as Parameters<typeof normalizeProviderProfile>[0]);
699
+ },
700
+
701
+ async health(): Promise<ProviderProfile[]> {
702
+ if (runtimeConfig.useMockBackend) {
703
+ return mockBackend.listProvidersHealth();
704
+ }
705
+ const raw = await handleJson<unknown[]>(
706
+ await apiFetch(`${API_BASE}/provider/profiles/health`, { headers: authHeaders() }),
707
+ );
708
+ return raw.map((item) => normalizeProviderProfile(item as Parameters<typeof normalizeProviderProfile>[0]));
709
+ },
710
+
711
+ async test(id: string): Promise<ProviderProfile> {
712
+ if (runtimeConfig.useMockBackend) {
713
+ return mockBackend.testProvider(id);
714
+ }
715
+ const raw = await handleJson<unknown>(
716
+ await apiFetch(`${API_BASE}/provider/profiles/${id}/test`, {
717
+ method: "POST",
718
+ headers: authHeaders(),
719
+ }),
720
+ );
721
+ return normalizeProviderProfile(raw as Parameters<typeof normalizeProviderProfile>[0]);
722
+ },
723
+ },
724
+ };