@brainpilot/web 0.0.5 → 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.
- package/dist/assets/index-Br55rkHb.css +1 -0
- package/dist/assets/index-CeUzk-ej.js +445 -0
- package/dist/index.html +2 -2
- package/package.json +5 -2
- package/src/__tests__/agentsReducer.test.ts +67 -0
- package/src/__tests__/api.test.ts +118 -0
- package/src/__tests__/chatScrollMemory.test.ts +49 -0
- package/src/__tests__/demoConversation.test.ts +73 -0
- package/src/__tests__/demoReset.test.ts +24 -0
- package/src/__tests__/runningToast.test.ts +29 -0
- package/src/__tests__/tokenUsage.test.ts +48 -0
- package/src/__tests__/toolDisplay.test.ts +55 -0
- package/src/__tests__/traceReducer.test.ts +62 -0
- package/src/components/chat/MessageStream.tsx +97 -56
- package/src/components/chat/PromptComposer.tsx +120 -29
- package/src/components/chat/chatScrollMemory.ts +49 -0
- package/src/components/demo/DemoView.tsx +91 -29
- package/src/components/demo/TraceNodeModal.tsx +6 -2
- package/src/components/demo/demoBundle.ts +7 -2
- package/src/components/demo/demoReset.ts +16 -0
- package/src/components/session/AgentNetwork.tsx +68 -75
- package/src/components/session/AgentTraceViews.tsx +35 -70
- package/src/components/session/AnalyticsTab.tsx +58 -224
- package/src/components/session/TraceGraphView.tsx +36 -30
- package/src/components/session/TraceNodeDetail.tsx +61 -24
- package/src/components/session/agentNetworkShared.ts +10 -0
- package/src/components/session/traceLayout.ts +32 -0
- package/src/components/settings/SettingsDialog.tsx +19 -1
- package/src/components/shell/DesktopShell.tsx +39 -14
- package/src/components/sidebar/Sidebar.tsx +6 -2
- package/src/contexts/SSEContext.tsx +90 -1
- package/src/contexts/SessionContext.tsx +354 -43
- package/src/contexts/agentsReducer.ts +49 -0
- package/src/contexts/runningToast.ts +33 -0
- package/src/contexts/traceReducer.ts +62 -0
- package/src/contexts/turnTimer.test.ts +97 -0
- package/src/contexts/turnTimer.ts +108 -0
- package/src/contexts/useTurnTimer.ts +104 -0
- package/src/contracts/backend.ts +53 -2
- package/src/i18n/messages/analytics.ts +16 -6
- package/src/i18n/messages/chat.ts +26 -4
- package/src/i18n/messages/contexts.ts +2 -0
- package/src/i18n/messages/network.ts +13 -9
- package/src/i18n/messages/profile.ts +4 -0
- package/src/i18n/messages/settings.ts +4 -0
- package/src/i18n/messages/trace.ts +69 -17
- package/src/mocks/backend.ts +7 -0
- package/src/styles/global.css +204 -55
- package/src/utils/api.ts +105 -8
- package/src/utils/toolDisplay.ts +74 -0
- package/dist/assets/index-C-8G4D4j.js +0 -448
- package/dist/assets/index-C501m5OS.css +0 -1
package/src/utils/api.ts
CHANGED
|
@@ -39,10 +39,29 @@ const API_BASE = "/api";
|
|
|
39
39
|
// Trust-front: the hosted gateway authenticates via an httpOnly cookie that the
|
|
40
40
|
// browser carries automatically. The frontend never reads, stores, or attaches a
|
|
41
41
|
// token — it just makes credentialed requests.
|
|
42
|
-
|
|
43
|
-
|
|
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 });
|
|
44
60
|
}
|
|
45
61
|
|
|
62
|
+
/** #106: default ceiling for composer-driving requests (create / postMessage). */
|
|
63
|
+
const SEND_TIMEOUT_MS = 30_000;
|
|
64
|
+
|
|
46
65
|
function authHeaders(json = true): Record<string, string> {
|
|
47
66
|
return json ? { "Content-Type": "application/json" } : {};
|
|
48
67
|
}
|
|
@@ -69,6 +88,21 @@ async function handleJson<T>(res: Response): Promise<T> {
|
|
|
69
88
|
return (await res.json()) as T;
|
|
70
89
|
}
|
|
71
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
|
+
|
|
72
106
|
export function getSSEUrl(sessionId: string): string {
|
|
73
107
|
// Same origin; relative path lets EventSource follow the current host/port and
|
|
74
108
|
// carry the auth cookie automatically — no token in the query string.
|
|
@@ -246,6 +280,20 @@ export const api = {
|
|
|
246
280
|
throw new Error(await parseError(res));
|
|
247
281
|
}
|
|
248
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
|
+
},
|
|
249
297
|
},
|
|
250
298
|
|
|
251
299
|
sessions: {
|
|
@@ -287,6 +335,7 @@ export const api = {
|
|
|
287
335
|
await apiFetch(`${API_BASE}/sessions`, {
|
|
288
336
|
method: "POST",
|
|
289
337
|
headers: authHeaders(),
|
|
338
|
+
timeoutMs: SEND_TIMEOUT_MS,
|
|
290
339
|
body: JSON.stringify({
|
|
291
340
|
title,
|
|
292
341
|
...(opts.providerId ? { providerId: opts.providerId } : {}),
|
|
@@ -294,7 +343,16 @@ export const api = {
|
|
|
294
343
|
}),
|
|
295
344
|
}),
|
|
296
345
|
);
|
|
297
|
-
|
|
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]);
|
|
298
356
|
},
|
|
299
357
|
|
|
300
358
|
async update(sessionId: string, title: string): Promise<Session> {
|
|
@@ -323,15 +381,19 @@ export const api = {
|
|
|
323
381
|
);
|
|
324
382
|
},
|
|
325
383
|
|
|
326
|
-
async interrupt(sessionId: string): Promise<{
|
|
384
|
+
async interrupt(sessionId: string): Promise<{ interrupted: boolean }> {
|
|
327
385
|
if (runtimeConfig.useMockBackend) {
|
|
328
|
-
return {
|
|
386
|
+
return { interrupted: true };
|
|
329
387
|
}
|
|
330
|
-
|
|
331
|
-
|
|
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`, {
|
|
332
394
|
method: "POST",
|
|
333
395
|
headers: { ...authHeaders(), "Content-Type": "application/json" },
|
|
334
|
-
body: JSON.stringify({
|
|
396
|
+
body: JSON.stringify({}),
|
|
335
397
|
}),
|
|
336
398
|
);
|
|
337
399
|
},
|
|
@@ -348,6 +410,7 @@ export const api = {
|
|
|
348
410
|
await apiFetch(`${API_BASE}/sessions/${sessionId}/messages`, {
|
|
349
411
|
method: "POST",
|
|
350
412
|
headers: { ...authHeaders(), "Content-Type": "application/json" },
|
|
413
|
+
timeoutMs: SEND_TIMEOUT_MS,
|
|
351
414
|
body: JSON.stringify({
|
|
352
415
|
type: payload.type ?? "user_message",
|
|
353
416
|
content: payload.content,
|
|
@@ -419,6 +482,40 @@ export const api = {
|
|
|
419
482
|
return Array.isArray(raw.events) ? (raw.events as RawAgUiEvent[]) : [];
|
|
420
483
|
},
|
|
421
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
|
+
|
|
422
519
|
async state(sessionId: string): Promise<SessionStateSnapshot> {
|
|
423
520
|
if (runtimeConfig.useMockBackend) {
|
|
424
521
|
return mockBackend.state();
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Presentation helpers for the tool-activity block (#84).
|
|
3
|
+
*
|
|
4
|
+
* The runtime namespaces MCP tools as `mcp__<server>__<tool>` (see
|
|
5
|
+
* packages/runtime/src/mcp-bridge.ts) to avoid collisions, and tool
|
|
6
|
+
* args/results arrive as already-encoded JSON strings. Surfacing those raw in
|
|
7
|
+
* the chat UI reads like debug output:
|
|
8
|
+
* - `mcp__bp_skills__skills_tool` instead of a friendly name, and
|
|
9
|
+
* - payloads double-encoded into `\"key\": \"value\"` walls of backslashes.
|
|
10
|
+
*
|
|
11
|
+
* These helpers are display-only — the raw name/payload stays available for
|
|
12
|
+
* copying and debugging; nothing here touches the wire protocol.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Friendly tool name. `mcp__<server>__<tool>` collapses to `<server> · <tool>`;
|
|
17
|
+
* any other name (built-in tools, already-friendly names) is returned as-is.
|
|
18
|
+
*
|
|
19
|
+
* The MCP prefix split is intentionally lenient: a server or tool segment may
|
|
20
|
+
* itself contain single underscores, so we split on the literal `mcp__` prefix
|
|
21
|
+
* and the FIRST `__` separator after the server name.
|
|
22
|
+
*/
|
|
23
|
+
export function formatToolName(raw: string | undefined | null): string {
|
|
24
|
+
if (!raw) return "tool";
|
|
25
|
+
if (!raw.startsWith("mcp__")) return raw;
|
|
26
|
+
const rest = raw.slice("mcp__".length);
|
|
27
|
+
const sep = rest.indexOf("__");
|
|
28
|
+
if (sep <= 0 || sep >= rest.length - 2) {
|
|
29
|
+
// Malformed (no tool segment) — show the un-prefixed remainder rather than
|
|
30
|
+
// the raw mcp__ identifier.
|
|
31
|
+
return rest || raw;
|
|
32
|
+
}
|
|
33
|
+
const server = rest.slice(0, sep);
|
|
34
|
+
const tool = rest.slice(sep + 2);
|
|
35
|
+
return `${server} · ${tool}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Pretty-print a tool payload without double-escaping. Tool args/results are
|
|
40
|
+
* accumulated as JSON strings over TOOL_CALL_ARGS deltas; calling
|
|
41
|
+
* JSON.stringify on an already-stringified value yields a `\"`-littered wall.
|
|
42
|
+
*
|
|
43
|
+
* Strategy:
|
|
44
|
+
* - string that parses as JSON → parse, then pretty-print the value;
|
|
45
|
+
* - string that does NOT parse → return verbatim (plain text / partial);
|
|
46
|
+
* - anything else (object/etc.) → pretty-print directly.
|
|
47
|
+
*
|
|
48
|
+
* Returns "" for null/undefined so callers can skip empty <pre> blocks.
|
|
49
|
+
*/
|
|
50
|
+
export function formatPayload(value: unknown): string {
|
|
51
|
+
if (value === undefined || value === null) return "";
|
|
52
|
+
if (typeof value === "string") {
|
|
53
|
+
const trimmed = value.trim();
|
|
54
|
+
if (trimmed === "") return "";
|
|
55
|
+
// Only attempt a parse when it looks like JSON — avoids turning a bare
|
|
56
|
+
// number/quoted word into a reformatted value users didn't write.
|
|
57
|
+
const looksJson =
|
|
58
|
+
(trimmed.startsWith("{") && trimmed.endsWith("}")) ||
|
|
59
|
+
(trimmed.startsWith("[") && trimmed.endsWith("]"));
|
|
60
|
+
if (looksJson) {
|
|
61
|
+
try {
|
|
62
|
+
return JSON.stringify(JSON.parse(trimmed), null, 2);
|
|
63
|
+
} catch {
|
|
64
|
+
return value;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
return JSON.stringify(value, null, 2);
|
|
71
|
+
} catch {
|
|
72
|
+
return String(value);
|
|
73
|
+
}
|
|
74
|
+
}
|