@brainpilot/web 0.0.8 → 0.0.10
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-D63mUJxx.js +450 -0
- package/dist/assets/index-D8J9Cnup.css +1 -0
- package/dist/index.html +2 -2
- package/package.json +2 -2
- package/src/__tests__/api.test.ts +90 -1
- package/src/__tests__/demoTruncatedExport.test.ts +104 -0
- package/src/__tests__/rehydrateMerge.test.ts +40 -0
- package/src/__tests__/runningScripts.test.ts +139 -0
- package/src/__tests__/timelineBounds.test.ts +51 -0
- package/src/components/chat/MessageStream.tsx +1 -11
- package/src/components/chat/PromptComposer.tsx +118 -16
- package/src/components/chat/RunningScriptsPanel.tsx +118 -0
- package/src/components/chat/runningScripts.ts +88 -0
- package/src/components/demo/demoBundle.ts +8 -3
- package/src/components/files/FileSidebar.tsx +82 -11
- package/src/components/session/AgentNetwork.tsx +1 -0
- package/src/components/session/TimelineTab.tsx +39 -9
- package/src/components/settings/KnowledgeBasePanel.tsx +594 -0
- package/src/components/settings/SettingsDialog.tsx +12 -4
- package/src/contexts/SessionContext.tsx +31 -7
- package/src/contexts/messageReducer.ts +19 -0
- package/src/contracts/backend.ts +8 -1
- package/src/i18n/messages/chat.ts +4 -0
- package/src/i18n/messages/files.ts +2 -0
- package/src/i18n/messages/settings.ts +57 -0
- package/src/styles/global.css +139 -1
- package/src/utils/api.ts +139 -3
- package/src/utils/format.ts +9 -0
- package/dist/assets/index-162Pskp8.js +0 -438
- package/dist/assets/index-DWOsU22G.css +0 -1
|
@@ -13,9 +13,9 @@ import {
|
|
|
13
13
|
X,
|
|
14
14
|
} from "lucide-react";
|
|
15
15
|
import { FileContent, FileEntry } from "../../contracts/backend";
|
|
16
|
+
import { runtimeConfig } from "../../config";
|
|
16
17
|
import { useSandbox } from "../../contexts/SandboxContext";
|
|
17
18
|
import { useSessions } from "../../contexts/SessionContext";
|
|
18
|
-
import { runtimeConfig } from "../../config";
|
|
19
19
|
import { useT } from "../../i18n/useT";
|
|
20
20
|
import { api } from "../../utils/api";
|
|
21
21
|
import { downloadBlob } from "../../utils/download";
|
|
@@ -128,10 +128,14 @@ function findNode(root: FileNode, path: string | null): FileNode | null {
|
|
|
128
128
|
export function FileSidebar({ isOpen, onClose, onResize, onResizeEnd, onResizeStart, width }: FileSidebarProps) {
|
|
129
129
|
const { currentSandbox } = useSandbox();
|
|
130
130
|
const { currentSession } = useSessions();
|
|
131
|
-
//
|
|
132
|
-
// id
|
|
133
|
-
//
|
|
134
|
-
|
|
131
|
+
// The runtime always addresses a workspace by session id (workspaces/<sid>/),
|
|
132
|
+
// never by container id — in both local and remote mode. A container can host
|
|
133
|
+
// several sessions, and the file tree shows the *current session's* workspace.
|
|
134
|
+
// (#168) `currentSandbox.status` still gates whether files are live; the
|
|
135
|
+
// variable name stays `sandboxId` only because the call sites/sub-component
|
|
136
|
+
// prop are named that way — it has always carried the session id in local
|
|
137
|
+
// mode. A full rename rides with the planned session-management cleanup.
|
|
138
|
+
const sandboxId = currentSession?.id ?? null;
|
|
135
139
|
const t = useT();
|
|
136
140
|
const [tree, setTree] = useState<FileNode>(rootNode);
|
|
137
141
|
const [expandedPaths, setExpandedPaths] = useState<Set<string>>(() => new Set(["/workspace"]));
|
|
@@ -144,18 +148,82 @@ export function FileSidebar({ isOpen, onClose, onResize, onResizeEnd, onResizeSt
|
|
|
144
148
|
const [isPreviewMaximized, setIsPreviewMaximized] = useState(false);
|
|
145
149
|
const resizeStartRef = useRef<{ pointerX: number; width: number } | null>(null);
|
|
146
150
|
|
|
151
|
+
// #156: in local mode, surface the real on-disk workspace dir so users know
|
|
152
|
+
// which directory the agent writes into. `workspacesRoot` comes from the
|
|
153
|
+
// backend (gated to local mode there too); the per-session dir is
|
|
154
|
+
// `<workspacesRoot>/<sessionId>`. Null in hosted mode → keep showing the
|
|
155
|
+
// virtual `/workspace` and never disclose a host path.
|
|
156
|
+
const [workspacesRoot, setWorkspacesRoot] = useState<string | null>(null);
|
|
157
|
+
useEffect(() => {
|
|
158
|
+
if (!runtimeConfig.localMode) return;
|
|
159
|
+
let cancelled = false;
|
|
160
|
+
void api.getInfo().then((info) => {
|
|
161
|
+
if (!cancelled && info.localMode && info.workspacesRoot) {
|
|
162
|
+
setWorkspacesRoot(info.workspacesRoot);
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
return () => {
|
|
166
|
+
cancelled = true;
|
|
167
|
+
};
|
|
168
|
+
}, []);
|
|
169
|
+
|
|
170
|
+
// Join with the platform's separator: a Windows root contains "\", a POSIX
|
|
171
|
+
// root "/". Detect from the root itself rather than assuming the host.
|
|
172
|
+
const realWorkspacePath = useMemo(() => {
|
|
173
|
+
if (!workspacesRoot || !currentSession?.id) return null;
|
|
174
|
+
const sepChar = workspacesRoot.includes("\\") && !workspacesRoot.includes("/") ? "\\" : "/";
|
|
175
|
+
return `${workspacesRoot.replace(/[\\/]$/, "")}${sepChar}${currentSession.id}`;
|
|
176
|
+
}, [workspacesRoot, currentSession?.id]);
|
|
177
|
+
|
|
178
|
+
// Map a virtual `/workspace[/...]` path to its real on-disk equivalent for
|
|
179
|
+
// display. Returns the original virtual path when no real root is known.
|
|
180
|
+
const toDisplayPath = useCallback(
|
|
181
|
+
(virtualPath: string): string => {
|
|
182
|
+
if (!realWorkspacePath) return virtualPath;
|
|
183
|
+
const sepChar = realWorkspacePath.includes("\\") && !realWorkspacePath.includes("/") ? "\\" : "/";
|
|
184
|
+
if (virtualPath === "/workspace") return realWorkspacePath;
|
|
185
|
+
if (virtualPath.startsWith("/workspace/")) {
|
|
186
|
+
const rel = virtualPath.slice("/workspace/".length).split("/").join(sepChar);
|
|
187
|
+
return `${realWorkspacePath}${sepChar}${rel}`;
|
|
188
|
+
}
|
|
189
|
+
return virtualPath;
|
|
190
|
+
},
|
|
191
|
+
[realWorkspacePath],
|
|
192
|
+
);
|
|
193
|
+
|
|
147
194
|
const loadDirectory = useCallback(
|
|
148
195
|
async (path: string) => {
|
|
149
196
|
if (!currentSandbox || currentSandbox.status !== "running" || !sandboxId) {
|
|
197
|
+
// #193 diagnostics: distinguish "panel gated off" from "listed but empty".
|
|
198
|
+
// Logs the exact reason the gate blocked the load so a user (esp. on
|
|
199
|
+
// Windows, where the empty-panel report originates) can paste it back.
|
|
200
|
+
console.warn("[FileSidebar] load skipped — sandbox not ready", {
|
|
201
|
+
path,
|
|
202
|
+
sandboxId,
|
|
203
|
+
hasSandbox: !!currentSandbox,
|
|
204
|
+
sandboxStatus: currentSandbox?.status ?? null,
|
|
205
|
+
});
|
|
150
206
|
setError(t("files.error.notRunning"));
|
|
151
207
|
return;
|
|
152
208
|
}
|
|
153
209
|
setError(null);
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
210
|
+
try {
|
|
211
|
+
// #193 diagnostics: log the exact request being addressed so an empty or
|
|
212
|
+
// failing listing can be traced to the real sandboxId + path on the wire.
|
|
213
|
+
console.debug("[FileSidebar] listFiles", { sandboxId, path });
|
|
214
|
+
const entries = await api.sandbox.listFiles(sandboxId, path);
|
|
215
|
+
console.debug("[FileSidebar] listFiles ok", { sandboxId, path, count: entries.length });
|
|
216
|
+
const children = entries.map((entry) => ({ ...entry, path: joinPath(path, entry.name) }));
|
|
217
|
+
setTree((current) => updateNode(current, path, (node) => ({ ...node, children, loaded: true })));
|
|
218
|
+
} catch (err) {
|
|
219
|
+
// The runtime now returns a distinct error (instead of an empty array)
|
|
220
|
+
// when readdir fails for a reason other than ENOENT (#193). Surface it
|
|
221
|
+
// rather than leaving the panel stuck loading with no feedback.
|
|
222
|
+
console.error("[FileSidebar] listFiles failed", { sandboxId, path, error: err });
|
|
223
|
+
setError(err instanceof Error ? err.message : t("files.error.loadFailed"));
|
|
224
|
+
}
|
|
157
225
|
},
|
|
158
|
-
[currentSandbox, sandboxId],
|
|
226
|
+
[currentSandbox, sandboxId, t],
|
|
159
227
|
);
|
|
160
228
|
|
|
161
229
|
useEffect(() => {
|
|
@@ -443,7 +511,7 @@ export function FileSidebar({ isOpen, onClose, onResize, onResizeEnd, onResizeSt
|
|
|
443
511
|
</header>
|
|
444
512
|
|
|
445
513
|
<div className="file-sidebar__path">
|
|
446
|
-
<span
|
|
514
|
+
<span title={realWorkspacePath ?? "/workspace"}>{realWorkspacePath ?? "/workspace"}</span>
|
|
447
515
|
<small>{currentSandbox?.status === "running" ? t("files.live") : t("files.offline")}</small>
|
|
448
516
|
</div>
|
|
449
517
|
|
|
@@ -463,6 +531,7 @@ export function FileSidebar({ isOpen, onClose, onResize, onResizeEnd, onResizeSt
|
|
|
463
531
|
setIsPreviewMaximized(false);
|
|
464
532
|
}}
|
|
465
533
|
sandboxId={sandboxId}
|
|
534
|
+
toDisplayPath={toDisplayPath}
|
|
466
535
|
onToggleMaximize={() => setIsPreviewMaximized((current) => !current)}
|
|
467
536
|
/>
|
|
468
537
|
</>
|
|
@@ -475,6 +544,7 @@ function FilePreviewPanel({
|
|
|
475
544
|
isMaximized,
|
|
476
545
|
onClose,
|
|
477
546
|
sandboxId,
|
|
547
|
+
toDisplayPath,
|
|
478
548
|
onToggleMaximize,
|
|
479
549
|
}: {
|
|
480
550
|
file: FileNode | null;
|
|
@@ -482,6 +552,7 @@ function FilePreviewPanel({
|
|
|
482
552
|
isMaximized: boolean;
|
|
483
553
|
onClose: () => void;
|
|
484
554
|
sandboxId: string | null;
|
|
555
|
+
toDisplayPath: (virtualPath: string) => string;
|
|
485
556
|
onToggleMaximize: () => void;
|
|
486
557
|
}) {
|
|
487
558
|
const t = useT();
|
|
@@ -634,7 +705,7 @@ function FilePreviewPanel({
|
|
|
634
705
|
<dl className="file-preview__meta">
|
|
635
706
|
<div>
|
|
636
707
|
<dt>{t("files.preview.path")}</dt>
|
|
637
|
-
<dd>{file.path}</dd>
|
|
708
|
+
<dd>{toDisplayPath(file.path)}</dd>
|
|
638
709
|
</div>
|
|
639
710
|
<div>
|
|
640
711
|
<dt>{t("files.preview.size")}</dt>
|
|
@@ -15,6 +15,13 @@ import { getMessageEdge, msgTypeKind } from "./agentNetworkShared";
|
|
|
15
15
|
interface TimelineTabProps {
|
|
16
16
|
messages: ChatMessage[];
|
|
17
17
|
now: number;
|
|
18
|
+
/**
|
|
19
|
+
* Whether the session is actively running (≥1 agent in a running state).
|
|
20
|
+
* Only while running does the axis track wall-clock `now` and show the
|
|
21
|
+
* live "now" marker; a finished session freezes the axis at the last
|
|
22
|
+
* message so the plot doesn't grow a blank right gutter over time (#166).
|
|
23
|
+
*/
|
|
24
|
+
isRunning?: boolean;
|
|
18
25
|
/** Click a dot → caller selects that agent (and flips to Detail tab). */
|
|
19
26
|
onSelectMessage: (agentName: string) => void;
|
|
20
27
|
}
|
|
@@ -33,7 +40,30 @@ const LABEL_W = 88;
|
|
|
33
40
|
const PAD_TOP = 28;
|
|
34
41
|
const TICK_COUNT = 6;
|
|
35
42
|
|
|
36
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Compute the timeline's [start, end] axis bounds (#166).
|
|
45
|
+
*
|
|
46
|
+
* The axis always starts at the first message. It ends at the LAST message,
|
|
47
|
+
* and only extends to wall-clock `now` while the session is actively running.
|
|
48
|
+
* A finished session therefore freezes its right edge at the last event
|
|
49
|
+
* instead of accreting blank space as real time marches on.
|
|
50
|
+
*
|
|
51
|
+
* `tsList` must be ascending (as produced by the sorted `dots`).
|
|
52
|
+
*/
|
|
53
|
+
export function computeTimeBounds(
|
|
54
|
+
tsList: number[],
|
|
55
|
+
now: number,
|
|
56
|
+
isRunning: boolean,
|
|
57
|
+
): { start: number; end: number } {
|
|
58
|
+
if (tsList.length === 0) return { start: now - 60_000, end: now };
|
|
59
|
+
const start = tsList[0];
|
|
60
|
+
const lastTs = tsList[tsList.length - 1];
|
|
61
|
+
const end = isRunning ? Math.max(now, lastTs) : lastTs;
|
|
62
|
+
// Degenerate span (single dot / identical timestamps): give it a minute.
|
|
63
|
+
return { start, end: end === start ? start + 60_000 : end };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function TimelineTab({ messages, now, isRunning = false, onSelectMessage }: TimelineTabProps) {
|
|
37
67
|
const t = useT();
|
|
38
68
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
39
69
|
const [zoom, setZoom] = useState(1);
|
|
@@ -78,12 +108,10 @@ export function TimelineTab({ messages, now, onSelectMessage }: TimelineTabProps
|
|
|
78
108
|
return names;
|
|
79
109
|
}, [dots]);
|
|
80
110
|
|
|
81
|
-
const timeBounds = useMemo(
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
return { start, end: end === start ? start + 60_000 : end };
|
|
86
|
-
}, [dots, now]);
|
|
111
|
+
const timeBounds = useMemo(
|
|
112
|
+
() => computeTimeBounds(dots.map((d) => d.ts), now, isRunning),
|
|
113
|
+
[dots, now, isRunning],
|
|
114
|
+
);
|
|
87
115
|
|
|
88
116
|
if (dots.length === 0) {
|
|
89
117
|
return (
|
|
@@ -243,8 +271,10 @@ export function TimelineTab({ messages, now, onSelectMessage }: TimelineTabProps
|
|
|
243
271
|
</g>
|
|
244
272
|
))}
|
|
245
273
|
|
|
246
|
-
{/* "now" marker */}
|
|
247
|
-
|
|
274
|
+
{/* "now" marker — only meaningful while the session is live (#166) */}
|
|
275
|
+
{isRunning && (
|
|
276
|
+
<line x1={xOf(now)} x2={xOf(now)} y1={PAD_TOP - 6} y2={svgH - 4} className="agent-timeline__now" />
|
|
277
|
+
)}
|
|
248
278
|
|
|
249
279
|
{/* delegate→result arcs */}
|
|
250
280
|
{arcs.map((a) => {
|