@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,627 @@
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
+ function apiFetch(input: RequestInfo | URL, init: RequestInit = {}): Promise<Response> {
43
+ return fetch(input, { credentials: "include", ...init });
44
+ }
45
+
46
+ function authHeaders(json = true): Record<string, string> {
47
+ return json ? { "Content-Type": "application/json" } : {};
48
+ }
49
+
50
+ async function parseError(res: Response): Promise<string> {
51
+ const contentType = res.headers.get("content-type") || "";
52
+ if (contentType.includes("application/json")) {
53
+ const body = (await res.json().catch(() => null)) as { detail?: unknown } | null;
54
+ if (typeof body?.detail === "string") {
55
+ return body.detail;
56
+ }
57
+ }
58
+ const text = await res.text().catch(() => "");
59
+ return text || `Request failed (${res.status})`;
60
+ }
61
+
62
+ async function handleJson<T>(res: Response): Promise<T> {
63
+ if (!res.ok) {
64
+ throw new Error(await parseError(res));
65
+ }
66
+ if (res.status === 204) {
67
+ return undefined as T;
68
+ }
69
+ return (await res.json()) as T;
70
+ }
71
+
72
+ export function getSSEUrl(sessionId: string): string {
73
+ // Same origin; relative path lets EventSource follow the current host/port and
74
+ // carry the auth cookie automatically — no token in the query string.
75
+ return `${API_BASE}/sessions/${encodeURIComponent(sessionId)}/sse`;
76
+ }
77
+
78
+ export function getTerminalWsUrl(sandboxId: string, cols = 80, rows = 24): string {
79
+ const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
80
+ const params = new URLSearchParams({ cols: String(cols), rows: String(rows) });
81
+ return `${protocol}//${window.location.host}${API_BASE}/sandbox/${sandboxId}/terminal?${params}`;
82
+ }
83
+
84
+ export const api = {
85
+ async getVersion(): Promise<{ version: string }> {
86
+ if (runtimeConfig.useMockBackend) {
87
+ return mockBackend.version();
88
+ }
89
+ return handleJson(await apiFetch(`${API_BASE}/version`));
90
+ },
91
+
92
+ auth: {
93
+ async me(): Promise<User> {
94
+ if (runtimeConfig.useMockBackend) {
95
+ return mockBackend.me();
96
+ }
97
+ const raw = await handleJson<unknown>(await apiFetch(`${API_BASE}/auth/me`, { headers: authHeaders() }));
98
+ return normalizeUser(raw as Parameters<typeof normalizeUser>[0]);
99
+ },
100
+ },
101
+
102
+ sandbox: {
103
+ async list(): Promise<Sandbox[]> {
104
+ if (runtimeConfig.useMockBackend) {
105
+ return mockBackend.listSandboxes();
106
+ }
107
+ const raw = await handleJson<unknown[]>(await apiFetch(`${API_BASE}/sandbox/list`, { headers: authHeaders() }));
108
+ return raw.map((item) => normalizeSandbox(item as Parameters<typeof normalizeSandbox>[0]));
109
+ },
110
+
111
+ async create(sandboxName = "default"): Promise<Sandbox> {
112
+ if (runtimeConfig.useMockBackend) {
113
+ return mockBackend.createSandbox(sandboxName);
114
+ }
115
+ const raw = await handleJson<unknown>(
116
+ await apiFetch(`${API_BASE}/sandbox/create`, {
117
+ method: "POST",
118
+ headers: authHeaders(),
119
+ body: JSON.stringify({ sandbox_name: sandboxName }),
120
+ }),
121
+ );
122
+ return normalizeSandbox(raw as Parameters<typeof normalizeSandbox>[0]);
123
+ },
124
+
125
+ async rebuild(sandboxId: string): Promise<Sandbox> {
126
+ if (runtimeConfig.useMockBackend) {
127
+ return mockBackend.rebuildSandbox();
128
+ }
129
+ const params = new URLSearchParams({ sandbox_id: sandboxId });
130
+ const raw = await handleJson<unknown>(
131
+ await apiFetch(`${API_BASE}/sandbox/rebuild?${params}`, {
132
+ method: "POST",
133
+ headers: authHeaders(),
134
+ }),
135
+ );
136
+ return normalizeSandbox(raw as Parameters<typeof normalizeSandbox>[0]);
137
+ },
138
+
139
+ async destroy(sandboxId: string): Promise<void> {
140
+ if (runtimeConfig.useMockBackend) {
141
+ return mockBackend.destroySandbox();
142
+ }
143
+ await handleJson<void>(
144
+ await apiFetch(`${API_BASE}/sandbox/${sandboxId}`, {
145
+ method: "DELETE",
146
+ headers: authHeaders(),
147
+ }),
148
+ );
149
+ },
150
+
151
+ async stats(sandboxId: string): Promise<SandboxStats> {
152
+ if (runtimeConfig.useMockBackend) {
153
+ return mockBackend.sandboxStats();
154
+ }
155
+ const raw = await handleJson<unknown>(
156
+ await apiFetch(`${API_BASE}/sandbox/${sandboxId}/stats`, { headers: authHeaders() }),
157
+ );
158
+ return normalizeSandboxStats(raw);
159
+ },
160
+
161
+ async logs(sandboxId: string, tail = 200): Promise<string> {
162
+ if (runtimeConfig.useMockBackend) {
163
+ return mockBackend.sandboxLogs();
164
+ }
165
+ const params = new URLSearchParams({ tail: String(tail) });
166
+ const raw = await handleJson<{ logs?: string }>(
167
+ await apiFetch(`${API_BASE}/sandbox/${sandboxId}/logs?${params}`, { headers: authHeaders() }),
168
+ );
169
+ return raw.logs || "";
170
+ },
171
+
172
+ async reloadConfig(sandboxId: string): Promise<{ status: string }> {
173
+ if (runtimeConfig.useMockBackend) {
174
+ return { status: 'ok' }
175
+ }
176
+ const res = await apiFetch(`${API_BASE}/sandbox/reload-config?sandbox_id=${sandboxId}`, {
177
+ method: 'POST',
178
+ headers: authHeaders(),
179
+ })
180
+ if (!res.ok) {
181
+ const body = await res.json().catch(() => ({}))
182
+ throw new Error(body.detail || `Reload failed (${res.status})`)
183
+ }
184
+ return handleJson(res)
185
+ },
186
+
187
+ async health(sandboxId: string): Promise<Record<string, unknown>> {
188
+ if (runtimeConfig.useMockBackend) {
189
+ return mockBackend.sandboxHealth();
190
+ }
191
+ return handleJson<Record<string, unknown>>(
192
+ await apiFetch(`${API_BASE}/sandbox/${sandboxId}/health`, { headers: authHeaders() }),
193
+ );
194
+ },
195
+
196
+ async listFiles(sandboxId: string, path = "/workspace"): Promise<FileEntry[]> {
197
+ if (runtimeConfig.useMockBackend) {
198
+ return mockBackend.listFiles(sandboxId, path);
199
+ }
200
+ const params = new URLSearchParams({ path });
201
+ const raw = await handleJson<unknown[]>(
202
+ await apiFetch(`${API_BASE}/sandbox/${sandboxId}/files?${params}`, { headers: authHeaders() }),
203
+ );
204
+ return raw.map((item) => normalizeFileEntry(item as Parameters<typeof normalizeFileEntry>[0]));
205
+ },
206
+
207
+ async readFile(sandboxId: string, path: string): Promise<FileContent> {
208
+ if (runtimeConfig.useMockBackend) {
209
+ return mockBackend.readFile(sandboxId, path);
210
+ }
211
+ const params = new URLSearchParams({ path });
212
+ const raw = await handleJson<unknown>(
213
+ await apiFetch(`${API_BASE}/sandbox/${sandboxId}/files/content?${params}`, { headers: authHeaders() }),
214
+ );
215
+ return normalizeFileContent(raw);
216
+ },
217
+
218
+ async readRawFile(sandboxId: string, path: string): Promise<Blob> {
219
+ if (runtimeConfig.useMockBackend) {
220
+ return mockBackend.readRawFile(sandboxId, path);
221
+ }
222
+ const params = new URLSearchParams({ path });
223
+ const res = await apiFetch(`${API_BASE}/sandbox/${sandboxId}/files/raw?${params}`, {
224
+ headers: authHeaders(false),
225
+ });
226
+ if (!res.ok) {
227
+ throw new Error(await parseError(res));
228
+ }
229
+ return res.blob();
230
+ },
231
+
232
+ async readFileBlob(sandboxId: string, path: string): Promise<Blob> {
233
+ return this.readRawFile(sandboxId, path);
234
+ },
235
+
236
+ async deleteFile(sandboxId: string, path: string): Promise<void> {
237
+ if (runtimeConfig.useMockBackend) {
238
+ return mockBackend.deleteFile(sandboxId, path);
239
+ }
240
+ const params = new URLSearchParams({ path });
241
+ const res = await apiFetch(`${API_BASE}/sandbox/${sandboxId}/files?${params}`, {
242
+ method: "DELETE",
243
+ headers: authHeaders(),
244
+ });
245
+ if (!res.ok) {
246
+ throw new Error(await parseError(res));
247
+ }
248
+ },
249
+ },
250
+
251
+ sessions: {
252
+ async list(): Promise<Session[]> {
253
+ if (runtimeConfig.useMockBackend) {
254
+ return mockBackend.listSessions();
255
+ }
256
+ // Runtime returns the protocol envelope `{ sessions: [...] }` (see
257
+ // ListSessionsResponseSchema). Unwrap it; tolerate a bare array (legacy /
258
+ // mock) and fall back to [] so an unexpected shape never throws
259
+ // `.map is not a function` into SessionContext's error banner.
260
+ const raw = await handleJson<{ sessions?: unknown[] } | unknown[]>(
261
+ await apiFetch(`${API_BASE}/sessions`, { headers: authHeaders() }),
262
+ );
263
+ const list = Array.isArray(raw)
264
+ ? raw
265
+ : Array.isArray((raw as { sessions?: unknown[] })?.sessions)
266
+ ? (raw as { sessions: unknown[] }).sessions
267
+ : [];
268
+ return list.map((item) => normalizeSession(item as Parameters<typeof normalizeSession>[0]));
269
+ },
270
+
271
+ async get(sessionId: string): Promise<Session> {
272
+ if (runtimeConfig.useMockBackend) {
273
+ return mockBackend.getSession(sessionId);
274
+ }
275
+ const raw = await handleJson<unknown>(await apiFetch(`${API_BASE}/sessions/${sessionId}`, { headers: authHeaders() }));
276
+ return normalizeSession(raw as Parameters<typeof normalizeSession>[0]);
277
+ },
278
+
279
+ async create(
280
+ title = "New research session",
281
+ opts: { providerId?: string; modelId?: string } = {},
282
+ ): Promise<Session> {
283
+ if (runtimeConfig.useMockBackend) {
284
+ return mockBackend.createSession(title);
285
+ }
286
+ const raw = await handleJson<unknown>(
287
+ await apiFetch(`${API_BASE}/sessions`, {
288
+ method: "POST",
289
+ headers: authHeaders(),
290
+ body: JSON.stringify({
291
+ title,
292
+ ...(opts.providerId ? { providerId: opts.providerId } : {}),
293
+ ...(opts.modelId ? { modelId: opts.modelId } : {}),
294
+ }),
295
+ }),
296
+ );
297
+ return normalizeSession(raw as Parameters<typeof normalizeSession>[0]);
298
+ },
299
+
300
+ async update(sessionId: string, title: string): Promise<Session> {
301
+ if (runtimeConfig.useMockBackend) {
302
+ return mockBackend.updateSession(sessionId, title);
303
+ }
304
+ const raw = await handleJson<unknown>(
305
+ await apiFetch(`${API_BASE}/sessions/${sessionId}`, {
306
+ method: "PUT",
307
+ headers: authHeaders(),
308
+ body: JSON.stringify({ title }),
309
+ }),
310
+ );
311
+ return normalizeSession(raw as Parameters<typeof normalizeSession>[0]);
312
+ },
313
+
314
+ async remove(sessionId: string): Promise<void> {
315
+ if (runtimeConfig.useMockBackend) {
316
+ return mockBackend.removeSession(sessionId);
317
+ }
318
+ await handleJson<void>(
319
+ await apiFetch(`${API_BASE}/sessions/${sessionId}`, {
320
+ method: "DELETE",
321
+ headers: authHeaders(),
322
+ }),
323
+ );
324
+ },
325
+
326
+ async interrupt(sessionId: string): Promise<{ status: string }> {
327
+ if (runtimeConfig.useMockBackend) {
328
+ return { status: "ok" };
329
+ }
330
+ return handleJson<{ status: string }>(
331
+ await apiFetch(`${API_BASE}/sessions/${sessionId}/messages`, {
332
+ method: "POST",
333
+ headers: { ...authHeaders(), "Content-Type": "application/json" },
334
+ body: JSON.stringify({ type: "interrupt", session_id: sessionId }),
335
+ }),
336
+ );
337
+ },
338
+
339
+ async postMessage(
340
+ sessionId: string,
341
+ payload: { content: string; uuid: string; timestamp: string; type?: string },
342
+ ): Promise<{ status: string }> {
343
+ if (runtimeConfig.useMockBackend) {
344
+ // Mock path: route through the existing mock helper if needed by the UI.
345
+ return { status: "ok" };
346
+ }
347
+ return handleJson<{ status: string }>(
348
+ await apiFetch(`${API_BASE}/sessions/${sessionId}/messages`, {
349
+ method: "POST",
350
+ headers: { ...authHeaders(), "Content-Type": "application/json" },
351
+ body: JSON.stringify({
352
+ type: payload.type ?? "user_message",
353
+ content: payload.content,
354
+ session_id: sessionId,
355
+ data: { uuid: payload.uuid, timestamp: payload.timestamp },
356
+ }),
357
+ }),
358
+ );
359
+ },
360
+
361
+ async commands(sessionId: string): Promise<{ commands: string[] }> {
362
+ if (runtimeConfig.useMockBackend) {
363
+ // ✅ 已通过真实 API 测试:/compact /context /cost 有效
364
+ // ❌ 已移除:/usage(返回空) /clear /init(Unknown skill)
365
+ return { commands: ["/compact", "/context", "/cost"] };
366
+ }
367
+ return handleJson<{ commands: string[] }>(
368
+ await apiFetch(`${API_BASE}/sessions/${sessionId}/commands`, { headers: authHeaders() }),
369
+ );
370
+ },
371
+
372
+ // 修正6 — answer an ask_user (user_input_request) prompt. Posts a
373
+ // user_input_response back through the same /messages endpoint the
374
+ // composer uses, carrying the request_id so the runtime can match it.
375
+ async respondToInput(
376
+ sessionId: string,
377
+ payload: { requestId: string; answer: string },
378
+ ): Promise<{ status: string }> {
379
+ if (runtimeConfig.useMockBackend) {
380
+ return { status: "ok" };
381
+ }
382
+ return handleJson<{ status: string }>(
383
+ await apiFetch(`${API_BASE}/sessions/${sessionId}/messages`, {
384
+ method: "POST",
385
+ headers: { ...authHeaders(), "Content-Type": "application/json" },
386
+ body: JSON.stringify({
387
+ type: "user_input_response",
388
+ session_id: sessionId,
389
+ request_id: payload.requestId,
390
+ answer: payload.answer,
391
+ }),
392
+ }),
393
+ );
394
+ },
395
+
396
+ async getTrace(sessionId: string): Promise<TraceGraph> {
397
+ if (runtimeConfig.useMockBackend) {
398
+ return mockBackend.getTrace(sessionId);
399
+ }
400
+ const raw = await handleJson<unknown>(
401
+ await apiFetch(`${API_BASE}/sessions/${sessionId}/trace`, { headers: authHeaders() }),
402
+ );
403
+ return normalizeTraceGraph(raw);
404
+ },
405
+
406
+ async getEvents(sessionId: string): Promise<RawAgUiEvent[]> {
407
+ if (runtimeConfig.useMockBackend) {
408
+ return mockBackend.getSessionEvents(sessionId);
409
+ }
410
+ // `/sessions/:id/events` is an SSE alias in backend-core (sseHandler), not
411
+ // a JSON route — so res.json() would reject. Treat any non-ok / non-JSON
412
+ // response as "no events" and let callers (demoBundle) use their ordered
413
+ // fallback instead of crashing on a stream body.
414
+ const res = await apiFetch(`${API_BASE}/sessions/${sessionId}/events`, { headers: authHeaders() });
415
+ if (!res.ok) return [];
416
+ const contentType = res.headers.get("content-type") || "";
417
+ if (!contentType.includes("application/json")) return [];
418
+ const raw = (await res.json().catch(() => ({}))) as { events?: unknown[] };
419
+ return Array.isArray(raw.events) ? (raw.events as RawAgUiEvent[]) : [];
420
+ },
421
+
422
+ async state(sessionId: string): Promise<SessionStateSnapshot> {
423
+ if (runtimeConfig.useMockBackend) {
424
+ return mockBackend.state();
425
+ }
426
+ const raw = await handleJson<unknown>(
427
+ await apiFetch(`${API_BASE}/sessions/${sessionId}/state`, { headers: authHeaders() }),
428
+ );
429
+ return normalizeSessionState(raw);
430
+ },
431
+ },
432
+
433
+ ui: {
434
+ async promptSuggestions(): Promise<string[]> {
435
+ if (runtimeConfig.useMockBackend) {
436
+ return mockBackend.promptSuggestions();
437
+ }
438
+ return [];
439
+ },
440
+ },
441
+
442
+ settings: {
443
+ async get(): Promise<SettingsData> {
444
+ if (runtimeConfig.useMockBackend) {
445
+ return mockBackend.getSettings();
446
+ }
447
+ const raw = await handleJson<unknown>(await apiFetch(`${API_BASE}/settings`, { headers: authHeaders() }));
448
+ return normalizeSettings(raw as Parameters<typeof normalizeSettings>[0]);
449
+ },
450
+
451
+ async update(data: Partial<SettingsData>): Promise<SettingsData> {
452
+ if (runtimeConfig.useMockBackend) {
453
+ return mockBackend.updateSettings(data);
454
+ }
455
+ const raw = await handleJson<unknown>(
456
+ await apiFetch(`${API_BASE}/settings`, {
457
+ method: "PUT",
458
+ headers: authHeaders(),
459
+ body: JSON.stringify(serializeSettings(data)),
460
+ }),
461
+ );
462
+ return normalizeSettings(raw as Parameters<typeof normalizeSettings>[0]);
463
+ },
464
+
465
+ async resetConfig(): Promise<void> {
466
+ if (runtimeConfig.useMockBackend) {
467
+ return mockBackend.resetConfig();
468
+ }
469
+ await handleJson<void>(
470
+ await apiFetch(`${API_BASE}/settings/reset-config`, {
471
+ method: "POST",
472
+ headers: authHeaders(),
473
+ }),
474
+ );
475
+ },
476
+ },
477
+
478
+ mcpServers: {
479
+ async list(): Promise<McpServerEntry[]> {
480
+ if (runtimeConfig.useMockBackend) {
481
+ return mockBackend.listMcpServers();
482
+ }
483
+ const raw = await handleJson<unknown[]>(await apiFetch(`${API_BASE}/mcp-servers`, { headers: authHeaders() }));
484
+ return raw.map(normalizeMcpServer);
485
+ },
486
+
487
+ async add(name: string, config: Omit<McpServerEntry, "name">): Promise<McpServerEntry> {
488
+ if (runtimeConfig.useMockBackend) {
489
+ return mockBackend.addMcpServer(name, config);
490
+ }
491
+ const raw = await handleJson<unknown>(
492
+ await apiFetch(`${API_BASE}/mcp-servers`, {
493
+ method: "POST",
494
+ headers: authHeaders(),
495
+ body: JSON.stringify({ name, config: serializeMcpConfig(config) }),
496
+ }),
497
+ );
498
+ return normalizeMcpServer(raw);
499
+ },
500
+
501
+ async update(name: string, config: Omit<McpServerEntry, "name">): Promise<McpServerEntry> {
502
+ if (runtimeConfig.useMockBackend) {
503
+ return mockBackend.updateMcpServer(name, config);
504
+ }
505
+ const raw = await handleJson<unknown>(
506
+ await apiFetch(`${API_BASE}/mcp-servers/${name}`, {
507
+ method: "PUT",
508
+ headers: authHeaders(),
509
+ body: JSON.stringify(serializeMcpConfig(config)),
510
+ }),
511
+ );
512
+ return normalizeMcpServer(raw);
513
+ },
514
+
515
+ async remove(name: string): Promise<void> {
516
+ if (runtimeConfig.useMockBackend) {
517
+ return mockBackend.removeMcpServer(name);
518
+ }
519
+ await handleJson<void>(
520
+ await apiFetch(`${API_BASE}/mcp-servers/${name}`, {
521
+ method: "DELETE",
522
+ headers: authHeaders(),
523
+ }),
524
+ );
525
+ },
526
+ },
527
+
528
+ providers: {
529
+ async list(): Promise<ProviderProfile[]> {
530
+ if (runtimeConfig.useMockBackend) {
531
+ return mockBackend.listProviders();
532
+ }
533
+ const raw = await handleJson<unknown[]>(
534
+ await apiFetch(`${API_BASE}/provider/profiles`, { headers: authHeaders() }),
535
+ );
536
+ return raw.map((item) => normalizeProviderProfile(item as Parameters<typeof normalizeProviderProfile>[0]));
537
+ },
538
+
539
+ async create(data: ProviderCreate): Promise<ProviderProfile> {
540
+ if (runtimeConfig.useMockBackend) {
541
+ return mockBackend.createProvider(data);
542
+ }
543
+ const raw = await handleJson<unknown>(
544
+ await apiFetch(`${API_BASE}/provider/profiles`, {
545
+ method: "POST",
546
+ headers: authHeaders(),
547
+ body: JSON.stringify(serializeProviderCreate(data)),
548
+ }),
549
+ );
550
+ return normalizeProviderProfile(raw as Parameters<typeof normalizeProviderProfile>[0]);
551
+ },
552
+
553
+ async update(id: string, data: ProviderUpdate): Promise<ProviderProfile> {
554
+ if (runtimeConfig.useMockBackend) {
555
+ return mockBackend.updateProvider(id, data);
556
+ }
557
+ const raw = await handleJson<unknown>(
558
+ await apiFetch(`${API_BASE}/provider/profiles/${id}`, {
559
+ method: "PUT",
560
+ headers: authHeaders(),
561
+ body: JSON.stringify(serializeProviderUpdate(data)),
562
+ }),
563
+ );
564
+ return normalizeProviderProfile(raw as Parameters<typeof normalizeProviderProfile>[0]);
565
+ },
566
+
567
+ async remove(id: string): Promise<void> {
568
+ if (runtimeConfig.useMockBackend) {
569
+ return mockBackend.removeProvider(id);
570
+ }
571
+ await handleJson<void>(
572
+ await apiFetch(`${API_BASE}/provider/profiles/${id}`, {
573
+ method: "DELETE",
574
+ headers: authHeaders(),
575
+ }),
576
+ );
577
+ },
578
+
579
+ async getActive(): Promise<ProviderProfile | null> {
580
+ if (runtimeConfig.useMockBackend) {
581
+ return mockBackend.getActiveProvider();
582
+ }
583
+ const res = await apiFetch(`${API_BASE}/provider/profiles/active`, { headers: authHeaders() });
584
+ if (res.status === 204) {
585
+ return null;
586
+ }
587
+ return normalizeProviderProfile((await handleJson<unknown>(res)) as Parameters<typeof normalizeProviderProfile>[0]);
588
+ },
589
+
590
+ async setActive(id: string): Promise<ProviderProfile> {
591
+ if (runtimeConfig.useMockBackend) {
592
+ return mockBackend.setActiveProvider(id);
593
+ }
594
+ const raw = await handleJson<unknown>(
595
+ await apiFetch(`${API_BASE}/provider/profiles/active`, {
596
+ method: "POST",
597
+ headers: authHeaders(),
598
+ body: JSON.stringify({ id }),
599
+ }),
600
+ );
601
+ return normalizeProviderProfile(raw as Parameters<typeof normalizeProviderProfile>[0]);
602
+ },
603
+
604
+ async health(): Promise<ProviderProfile[]> {
605
+ if (runtimeConfig.useMockBackend) {
606
+ return mockBackend.listProvidersHealth();
607
+ }
608
+ const raw = await handleJson<unknown[]>(
609
+ await apiFetch(`${API_BASE}/provider/profiles/health`, { headers: authHeaders() }),
610
+ );
611
+ return raw.map((item) => normalizeProviderProfile(item as Parameters<typeof normalizeProviderProfile>[0]));
612
+ },
613
+
614
+ async test(id: string): Promise<ProviderProfile> {
615
+ if (runtimeConfig.useMockBackend) {
616
+ return mockBackend.testProvider(id);
617
+ }
618
+ const raw = await handleJson<unknown>(
619
+ await apiFetch(`${API_BASE}/provider/profiles/${id}/test`, {
620
+ method: "POST",
621
+ headers: authHeaders(),
622
+ }),
623
+ );
624
+ return normalizeProviderProfile(raw as Parameters<typeof normalizeProviderProfile>[0]);
625
+ },
626
+ },
627
+ };
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Browser download helpers shared across the file sidebar and the demo export.
3
+ */
4
+
5
+ export function sanitizeDownloadName(name: string): string {
6
+ return name.replace(/[\\/:*?"<>|]+/g, "-");
7
+ }
8
+
9
+ export function downloadBlob(blob: Blob, filename: string): void {
10
+ const href = URL.createObjectURL(blob);
11
+ const link = document.createElement("a");
12
+ link.href = href;
13
+ link.download = sanitizeDownloadName(filename);
14
+ document.body.appendChild(link);
15
+ link.click();
16
+ link.remove();
17
+ window.setTimeout(() => URL.revokeObjectURL(href), 1000);
18
+ }
@@ -0,0 +1,7 @@
1
+ export function formatBytes(bytes: number) {
2
+ if (bytes === 0) return "0 B";
3
+ const units = ["B", "KB", "MB", "GB"];
4
+ const index = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1);
5
+ const value = bytes / 1024 ** index;
6
+ return `${value >= 10 ? value.toFixed(0) : value.toFixed(1)} ${units[index]}`;
7
+ }