@tea-agent/loop-agent 0.44.0-next.4 → 0.44.0-next.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/CHANGELOG.md +29 -1
- package/dist/build-stamp.json +3 -3
- package/dist/executors/pi-sdk-executor.js +7 -7
- package/dist/executors/shell-write-guard.js +26 -3
- package/dist/infrastructure/console/operation-store.js +46 -14
- package/dist/shared/file-identity.js +23 -0
- package/dist/shared/git-workspace-status.js +480 -0
- package/dist/shared/path-safety.js +2 -2
- package/dist/shared/wizard-local-telemetry.js +2 -1
- package/dist/worker/console/chat/context-insights.js +723 -0
- package/dist/worker/console/chat/pi-runtime.js +217 -18
- package/dist/worker/console/chat/repo-browser.js +52 -4
- package/dist/worker/console/chat/routes.js +150 -58
- package/dist/worker/console/chat/shortcuts.js +7 -0
- package/dist/worker/console/console-update-runtime.js +2 -2
- package/dist/worker/console/operator-user-error.js +28 -1
- package/dist/worker/console/pi-readiness.js +4 -4
- package/dist/worker/console/recovery-cta.js +14 -14
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-B_lmJjOm.js → abnfDiagram-N423BO3Z-Brn2sPzL.js} +1 -1
- package/dist/worker/console/static/assets/{arc-pymM4Qeh.js → arc-BIMNTG0W.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-Bw9EPAhi.js → architectureDiagram-T3A2C74G--WtcgR_A.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-C7_oXJp2.js → blockDiagram-VBNYF7ZC-BT_Ztog1.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-LnLWDbh8.js → c4Diagram-5PPSVZJV-C5hbVr-l.js} +1 -1
- package/dist/worker/console/static/assets/channel-DNWB4hpR.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-BBiu3sCW.js → chunk-2GRJ4B5K-BKKnyR9Z.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-DiXqDQml.js → chunk-2Q5K7J3B-BHs6biqA.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-lmNq9nYk.js → chunk-5RXB4S5H-BdSFy_vP.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-boBu74dU.js → chunk-5VM5RSS4-BpJIdTKV.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-CjxqoJLQ.js → chunk-6Q2QTUOP-C_iWJhkT.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-DL0vQ9MG.js → chunk-GF5L2VYU-BTbmx3-k.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-DGPqE-O0.js → chunk-JWPE2WC7-D4Ee6DmP.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-v4BbT_r2.js → chunk-KBJHAD2P-B487SORR.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-Ci0ynvyL.js → chunk-RYQCIY6F-BKYHjNrr.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-CGsbJoTV.js → chunk-XXDRQBXY-DrTdoYZG.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-COhFgJji.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-COhFgJji.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CzoRDEI7.js → cose-bilkent-JH36ORCC-CttUY_Jo.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BOwFwbX4.js → cynefin-VYW2F7L2-B4Rm5gnR.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-2Cfg451l.js → cynefinDiagram-MW4NZA55-ByxHBq_S.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-DbT0D6Y5.js → dagre-VZM6K2ZE-DGrfNFA0.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Dfd_JoMs.js → diagram-7IWD3JNH-KjrUPquv.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-DaGm82kh.js → diagram-B4RE2ZJO-VS4mlm75.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DMoI8Ibc.js → diagram-LBJQPF4R-BLXYmFTM.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BtIFA3aY.js → diagram-Q27KOJAE-C6xoFAqH.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-D_2X1DTZ.js → diagram-UB23O5K3-C478dCe7.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-yFW5ZYlZ.js → ebnfDiagram-BXEA7PRR-DHEpW5zN.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DBXh5Svj.js → erDiagram-JOGREHBK-CA5GKfo9.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-C4ycm8KM.js → flowDiagram-UKHOOZJN-zysi0Fl-.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-qr-_Pekr.js → ganttDiagram-PKOTCBZU-DGzcKA8Y.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-DzOSLv_A.js → gitGraphDiagram-DS77QQ5N-CHsuIG1w.js} +1 -1
- package/dist/worker/console/static/assets/index-CJh6pVLm.js +482 -0
- package/dist/worker/console/static/assets/index-C_kvKmFM.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-C7Hl8D7l.js → infoDiagram-6WML65LV-BexZHPCp.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-fBxVlr6t.js → ishikawaDiagram-WSZJBQD7-Gkx8q-DB.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-CoiJEVCl.js → journeyDiagram-NVQOT4AX-XDW3qrJ7.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-TJXpxNuV.js → kanban-definition-27J2QSJJ-DJ07eEaN.js} +1 -1
- package/dist/worker/console/static/assets/{linear-CjQWz1pG.js → linear-DL-zeqOn.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-DY9_apIE.js → mermaid.core-C0HdXtOf.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-HB0Id8gM.js → mindmap-definition-FAOFIHXS-CqRX0lrt.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-0OSWgr6v.js → pegDiagram-VL7TDLO6-BVM9JZ2H.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-Cei1GGC2.js → pieDiagram-7S7Q4E2Y-BA3ozdKk.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CDy0Adfu.js → quadrantDiagram-CIZ2JOQS-B-0_p9_4.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BwqzUWwN.js → railroadDiagram-AXF67PYL-DHRzPWFc.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-Bif2IBRL.js → requirementDiagram-LRYGKXZP-9BEC_PcW.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-CYIoNMLg.js → sankeyDiagram-W5VNT64P-DMxeW4rN.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BDS7Y0aQ.js → sequenceDiagram-SI44F4Z6-DixP6LIU.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-lZic-_Bp.js → sizeCapture-X5ZJPWSS-B_EifJBo.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-WCVlCqSd.js → stateDiagram-OKZ733FA-BwikwWbX.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-S2ZHMo1c.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-pGccxTw_.js → swimlanes-SLNWSIFB-C162l9o1.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BZm4ByfV.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-D29RlrQA.js → timeline-definition-Z64GVDOM-mpe_u1i4.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-01fEM1NU.js → vennDiagram-T6HMQDX7-DyUk54HX.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-C4GeB6dq.js → wardleyDiagram-T6FBY63Y-HYyMkzfm.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-BXHrXHps.js → xychartDiagram-ELKLHX3M-DRoh7skR.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/app/console-types.js +9 -1
- package/dist/worker/console/static-src/operator-chat/context-insights-store.js +64 -0
- package/dist/worker/console/static-src/operator-chat/details-rail-surfaces.js +98 -0
- package/dist/worker/console/static-src/operator-chat/file-palette-nav.js +81 -0
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +4 -2
- package/dist/worker/console/static-src/operator-chat/useComposer.js +80 -6
- package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +8 -1
- package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +36 -17
- package/dist/worker/console/static-src/operator-chat/useRuntimeSnapshot.js +18 -0
- package/dist/worker/console/static-src/operator-chat/useWorkspaceBrowserGate.js +4 -0
- package/dist/worker/console/static-src/shell/console-update-reload.js +1 -1
- package/dist/worker/console/workspace-initialization.js +3 -3
- package/dist/worker/observability/read-model.js +23 -2
- package/dist/worker/observe/node-context-insights.js +479 -0
- package/docs/README.md +1 -1
- package/docs/templates/backend-test-dag.json +1 -0
- package/package.json +4 -4
- package/dist/worker/console/static/assets/channel-Ym13M3Lb.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BeFR1wGo.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BeFR1wGo.js +0 -1
- package/dist/worker/console/static/assets/index-PFjB6g6l.js +0 -482
- package/dist/worker/console/static/assets/index-ZtyB6Ol-.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-cj5UUB6m.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-C_Qp6bnh.js +0 -8
|
@@ -43,6 +43,9 @@ export function useComposer(params) {
|
|
|
43
43
|
const input = activeSnapshot.draft;
|
|
44
44
|
const pendingImages = activeSnapshot.images;
|
|
45
45
|
const [fileOptions, setFileOptions] = useState([]);
|
|
46
|
+
const [fileIndex, setFileIndex] = useState(0);
|
|
47
|
+
const [fileDrilled, setFileDrilled] = useState(false);
|
|
48
|
+
const [fileDismissedQuery, setFileDismissedQuery] = useState(null);
|
|
46
49
|
const [shortcutOpen, setShortcutOpen] = useState(false);
|
|
47
50
|
/** Any composer text that starts with `/` should reveal the palette,
|
|
48
51
|
* including draft restore. Escape can still close it without clearing. */
|
|
@@ -123,6 +126,10 @@ export function useComposer(params) {
|
|
|
123
126
|
forceRender();
|
|
124
127
|
setShortcutOpen(false);
|
|
125
128
|
setShortcutIndex(0);
|
|
129
|
+
setFileOptions([]);
|
|
130
|
+
setFileDrilled(false);
|
|
131
|
+
setFileDismissedQuery(null);
|
|
132
|
+
setFileIndex(0);
|
|
126
133
|
return attempt;
|
|
127
134
|
}, [ledger, refs]);
|
|
128
135
|
const claimSessionSubmission = useCallback((identity, text, images = []) => {
|
|
@@ -259,6 +266,14 @@ export function useComposer(params) {
|
|
|
259
266
|
.slice(0, textareaRef.current?.selectionStart ?? input.length)
|
|
260
267
|
.match(/(?:^|\s)@([^\s@]*)$/);
|
|
261
268
|
if (!match) {
|
|
269
|
+
setFileOptions([]);
|
|
270
|
+
setFileDrilled(false);
|
|
271
|
+
setFileDismissedQuery(null);
|
|
272
|
+
setFileIndex(0);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
const query = match[1] ?? "";
|
|
276
|
+
if (fileDismissedQuery === query) {
|
|
262
277
|
setFileOptions([]);
|
|
263
278
|
return;
|
|
264
279
|
}
|
|
@@ -267,7 +282,7 @@ export function useComposer(params) {
|
|
|
267
282
|
const generation = refs.sessionGenerationRef.current;
|
|
268
283
|
const controller = new AbortController();
|
|
269
284
|
const timer = window.setTimeout(() => {
|
|
270
|
-
void fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(sessionId)}/files?q=${encodeURIComponent(
|
|
285
|
+
void fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(sessionId)}/files?q=${encodeURIComponent(query)}`, { signal: controller.signal })
|
|
271
286
|
.then((r) => (r.ok ? r.json() : null))
|
|
272
287
|
.then((body) => {
|
|
273
288
|
const active = activeIdentityRef.current;
|
|
@@ -276,7 +291,14 @@ export function useComposer(params) {
|
|
|
276
291
|
composerSessionKey(active) !== ownerKey ||
|
|
277
292
|
refs.sessionGenerationRef.current !== generation)
|
|
278
293
|
return;
|
|
279
|
-
|
|
294
|
+
const files = (body?.files ?? []);
|
|
295
|
+
setFileOptions(files
|
|
296
|
+
.filter((item) => typeof item.path === "string" && item.path.length > 0)
|
|
297
|
+
.map((item) => ({
|
|
298
|
+
path: item.path,
|
|
299
|
+
isDir: item.isDir === true || item.path.endsWith("/"),
|
|
300
|
+
})));
|
|
301
|
+
setFileIndex(0);
|
|
280
302
|
})
|
|
281
303
|
.catch(() => undefined);
|
|
282
304
|
}, 150);
|
|
@@ -284,16 +306,58 @@ export function useComposer(params) {
|
|
|
284
306
|
window.clearTimeout(timer);
|
|
285
307
|
controller.abort();
|
|
286
308
|
};
|
|
287
|
-
}, [
|
|
288
|
-
|
|
309
|
+
}, [
|
|
310
|
+
origin,
|
|
311
|
+
sessionId,
|
|
312
|
+
sessionIdentity,
|
|
313
|
+
sessionReady,
|
|
314
|
+
input,
|
|
315
|
+
refs,
|
|
316
|
+
fileDismissedQuery,
|
|
317
|
+
]);
|
|
318
|
+
const replaceAtToken = useCallback((nextPath, close) => {
|
|
289
319
|
const caret = textareaRef.current?.selectionStart ?? input.length;
|
|
290
320
|
const before = input.slice(0, caret);
|
|
291
321
|
const start = before.search(/@[^\s@]*$/);
|
|
292
322
|
if (start < 0)
|
|
323
|
+
return false;
|
|
324
|
+
const suffix = close ? " " : "";
|
|
325
|
+
setInput(`${input.slice(0, start)}@${nextPath}${suffix}${input.slice(caret)}`);
|
|
326
|
+
return true;
|
|
327
|
+
}, [input, setInput]);
|
|
328
|
+
const insertFileReference = useCallback((file) => {
|
|
329
|
+
if (!replaceAtToken(file, true))
|
|
293
330
|
return;
|
|
294
|
-
setInput(`${input.slice(0, start)}@${file} ${input.slice(caret)}`);
|
|
295
331
|
setFileOptions([]);
|
|
296
|
-
|
|
332
|
+
setFileDrilled(false);
|
|
333
|
+
setFileDismissedQuery(null);
|
|
334
|
+
setFileIndex(0);
|
|
335
|
+
}, [replaceAtToken]);
|
|
336
|
+
const drillFileReference = useCallback((file) => {
|
|
337
|
+
if (!replaceAtToken(file, false))
|
|
338
|
+
return;
|
|
339
|
+
setFileDrilled(true);
|
|
340
|
+
setFileDismissedQuery(null);
|
|
341
|
+
setFileIndex(0);
|
|
342
|
+
}, [replaceAtToken]);
|
|
343
|
+
const pickFileCrumb = useCallback((value) => {
|
|
344
|
+
if (!replaceAtToken(value, false))
|
|
345
|
+
return;
|
|
346
|
+
setFileDrilled(true);
|
|
347
|
+
setFileDismissedQuery(null);
|
|
348
|
+
setFileIndex(0);
|
|
349
|
+
}, [replaceAtToken]);
|
|
350
|
+
const dismissFilePalette = useCallback(() => {
|
|
351
|
+
const query = input
|
|
352
|
+
.slice(0, textareaRef.current?.selectionStart ?? input.length)
|
|
353
|
+
.match(/(?:^|\s)@([^\s@]*)$/)?.[1];
|
|
354
|
+
if (query === undefined) {
|
|
355
|
+
setFileOptions([]);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
setFileDismissedQuery(query);
|
|
359
|
+
setFileOptions([]);
|
|
360
|
+
}, [input]);
|
|
297
361
|
const addImagesFromFiles = useCallback((files) => {
|
|
298
362
|
const owner = activeIdentityRef.current;
|
|
299
363
|
if (!owner)
|
|
@@ -364,6 +428,9 @@ export function useComposer(params) {
|
|
|
364
428
|
useEffect(() => {
|
|
365
429
|
setShortcutIndex((index) => clampShortcutIndex(index, filteredShortcuts.length));
|
|
366
430
|
}, [filteredShortcuts.length]);
|
|
431
|
+
useEffect(() => {
|
|
432
|
+
setFileIndex((index) => clampShortcutIndex(index, fileOptions.length));
|
|
433
|
+
}, [fileOptions.length]);
|
|
367
434
|
const onCompositionStart = useCallback(() => {
|
|
368
435
|
refs.isComposingRef.current = true;
|
|
369
436
|
}, [refs]);
|
|
@@ -388,6 +455,7 @@ export function useComposer(params) {
|
|
|
388
455
|
sessionId: sessionReady ? sessionId : undefined,
|
|
389
456
|
refs,
|
|
390
457
|
onUnrecoverableSession,
|
|
458
|
+
deferInitialFetch: true,
|
|
391
459
|
});
|
|
392
460
|
}, [origin, sessionId, sessionReady, refs, onUnrecoverableSession]);
|
|
393
461
|
return {
|
|
@@ -404,6 +472,9 @@ export function useComposer(params) {
|
|
|
404
472
|
replaceSessionDraft,
|
|
405
473
|
releaseSession,
|
|
406
474
|
fileOptions,
|
|
475
|
+
fileIndex,
|
|
476
|
+
setFileIndex,
|
|
477
|
+
fileDrilled,
|
|
407
478
|
pendingImages,
|
|
408
479
|
setPendingImages,
|
|
409
480
|
addImagesFromFiles,
|
|
@@ -417,6 +488,9 @@ export function useComposer(params) {
|
|
|
417
488
|
dynamicCommandsError,
|
|
418
489
|
retryDynamicCommands,
|
|
419
490
|
insertFileReference,
|
|
491
|
+
drillFileReference,
|
|
492
|
+
pickFileCrumb,
|
|
493
|
+
dismissFilePalette,
|
|
420
494
|
textareaRef,
|
|
421
495
|
onCompositionStart,
|
|
422
496
|
onCompositionEnd,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
2
|
import { ACTIVE_CHAT_SESSION_STORAGE_KEY } from "../../chat/workspace-landing.js";
|
|
3
|
+
import { workspaceScopeReady } from "./useWorkspaceBrowserGate.js";
|
|
3
4
|
const REPO_HTML_PREVIEW_ROUTE_PREFIX = "/api/operator/v1/chat/repo/html/";
|
|
4
5
|
/** DSH-style path-encoded preview URL: relative HTML assets resolve under the
|
|
5
6
|
* same workspace-scoped route instead of losing a query-string workspaceId. */
|
|
@@ -46,6 +47,8 @@ export function useRepoBrowser(params) {
|
|
|
46
47
|
const loadTree = useCallback(async (directory = "") => {
|
|
47
48
|
if (blocked)
|
|
48
49
|
return; // AC-R5-002: fail-closed, zero repo requests
|
|
50
|
+
if (!workspaceScopeReady(workspaceId))
|
|
51
|
+
return;
|
|
49
52
|
const requestWorkspaceId = workspaceId ?? null;
|
|
50
53
|
try {
|
|
51
54
|
setTreeError(null);
|
|
@@ -94,6 +97,8 @@ export function useRepoBrowser(params) {
|
|
|
94
97
|
const openPreview = useCallback(async (file) => {
|
|
95
98
|
if (blocked)
|
|
96
99
|
return; // AC-R5-002: fail-closed
|
|
100
|
+
if (!workspaceScopeReady(workspaceId))
|
|
101
|
+
return;
|
|
97
102
|
const requestWorkspaceId = workspaceId ?? null;
|
|
98
103
|
setPreviewLoading(true);
|
|
99
104
|
try {
|
|
@@ -177,6 +182,8 @@ export function useRepoBrowser(params) {
|
|
|
177
182
|
}
|
|
178
183
|
if (!paths.length)
|
|
179
184
|
return;
|
|
185
|
+
if (!workspaceScopeReady(workspaceId))
|
|
186
|
+
return;
|
|
180
187
|
void Promise.all(paths.map(async (file) => {
|
|
181
188
|
const res = await fetch(`${origin}/api/operator/v1/chat/repo/file?path=${encodeURIComponent(file)}${workspaceQuery}`, { credentials: "include" });
|
|
182
189
|
if (!res.ok)
|
|
@@ -192,7 +199,7 @@ export function useRepoBrowser(params) {
|
|
|
192
199
|
setPreviews(available);
|
|
193
200
|
setActivePreviewPath(available.at(-1)?.path ?? null);
|
|
194
201
|
});
|
|
195
|
-
}, [origin, workspaceQuery, sessionId, refs]);
|
|
202
|
+
}, [origin, workspaceQuery, workspaceId, sessionId, refs]);
|
|
196
203
|
const toggleDir = useCallback((dir) => {
|
|
197
204
|
setExpandedDirs((current) => {
|
|
198
205
|
const next = new Set(current);
|
|
@@ -3,7 +3,8 @@ import { formatModelRef, mergeSessionRuntime, sessionRuntimeMatches, } from "../
|
|
|
3
3
|
import { confirmationToken } from "./format.js";
|
|
4
4
|
import { isLostChatSessionError, mergeRuntimeContextWithSnapshot, runtimeSelectionFromSnapshot, } from "./runtime-snapshot-store.js";
|
|
5
5
|
import { buildToolCatalog } from "./tools-catalog.js";
|
|
6
|
-
import { adoptMutationSnapshot, dispatchRuntimeSnapshot, getRuntimeSnapshotState, refreshRuntimeSnapshot, settleMutationLifecycle, useRuntimeSnapshot, } from "./useRuntimeSnapshot.js";
|
|
6
|
+
import { adoptMutationSnapshot, dispatchRuntimeSnapshot, getRuntimeSnapshotState, refreshRuntimeSnapshot, releaseDeferredRuntimeSnapshotFetch, settleMutationLifecycle, useRuntimeSnapshot, } from "./useRuntimeSnapshot.js";
|
|
7
|
+
import { workspaceScopeReady } from "./useWorkspaceBrowserGate.js";
|
|
7
8
|
/**
|
|
8
9
|
* Monotonic mutation sequence shared across switchRuntime calls (RF-04). Each
|
|
9
10
|
* mutation captures its own token; only the LATEST started token may settle
|
|
@@ -19,6 +20,7 @@ export function useRuntimeControls(params) {
|
|
|
19
20
|
const [models, setModels] = useState([]);
|
|
20
21
|
const [modelsStatus, setModelsStatus] = useState("idle");
|
|
21
22
|
const [modelsError, setModelsError] = useState(null);
|
|
23
|
+
const [modelsForSessionId, setModelsForSessionId] = useState(null);
|
|
22
24
|
const [thinkingLevels, setThinkingLevels] = useState([]);
|
|
23
25
|
const [runtimeContext, setRuntimeContext] = useState(null);
|
|
24
26
|
const [runtimeContextError, setRuntimeContextError] = useState(null);
|
|
@@ -44,13 +46,14 @@ export function useRuntimeControls(params) {
|
|
|
44
46
|
const loadCapabilities = useCallback(async () => {
|
|
45
47
|
try {
|
|
46
48
|
// Workspace-scoped: the server rejects a bare capabilities call with
|
|
47
|
-
// WORKSPACE_REQUIRED
|
|
48
|
-
//
|
|
49
|
-
const scope = session?.workspaceId ??
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
49
|
+
// WORKSPACE_REQUIRED. Wait for a selected workspace instead of firing
|
|
50
|
+
// an unscoped request that shows up as a 400 in the browser Network panel.
|
|
51
|
+
const scope = (session?.workspaceId ??
|
|
52
|
+
params.workspaceId ??
|
|
53
|
+
"").trim();
|
|
54
|
+
if (!workspaceScopeReady(scope))
|
|
55
|
+
return;
|
|
56
|
+
const res = await fetch(`${origin}/api/operator/v1/chat/capabilities?workspaceId=${encodeURIComponent(scope)}`);
|
|
54
57
|
if (!res.ok)
|
|
55
58
|
return;
|
|
56
59
|
const body = (await res.json());
|
|
@@ -63,16 +66,17 @@ export function useRuntimeControls(params) {
|
|
|
63
66
|
useEffect(() => {
|
|
64
67
|
void loadCapabilities();
|
|
65
68
|
}, [loadCapabilities]);
|
|
66
|
-
// Models + thinking levels:
|
|
67
|
-
// session
|
|
68
|
-
// never silently folded into an empty list.
|
|
69
|
+
// Models + thinking levels: GET lists the workspace catalog and does not
|
|
70
|
+
// wait for historical Pi session reopen. Failure is a STRUCTURED error
|
|
71
|
+
// (with retry), never silently folded into an empty list.
|
|
72
|
+
const sessionId = session?.sessionId ?? null;
|
|
69
73
|
const loadModels = useCallback(async () => {
|
|
70
|
-
if (!
|
|
74
|
+
if (!sessionId) {
|
|
71
75
|
setModels([]);
|
|
72
76
|
setModelsStatus("idle");
|
|
77
|
+
setModelsForSessionId(null);
|
|
73
78
|
return;
|
|
74
79
|
}
|
|
75
|
-
const sessionId = session.sessionId;
|
|
76
80
|
setModelsStatus("loading");
|
|
77
81
|
setModelsError(null);
|
|
78
82
|
try {
|
|
@@ -93,27 +97,36 @@ export function useRuntimeControls(params) {
|
|
|
93
97
|
})) {
|
|
94
98
|
setModelsStatus("idle");
|
|
95
99
|
setModelsError(null);
|
|
100
|
+
setModelsForSessionId(sessionId);
|
|
96
101
|
onUnrecoverableSession?.(sessionId);
|
|
97
102
|
return;
|
|
98
103
|
}
|
|
99
104
|
setModelsStatus("error");
|
|
100
105
|
setModelsError(error?.message ?? `模型列表读取失败(HTTP ${res.status})`);
|
|
106
|
+
setModelsForSessionId(sessionId);
|
|
101
107
|
return;
|
|
102
108
|
}
|
|
103
109
|
setModels(body.models ?? []);
|
|
104
110
|
setThinkingLevels(body.thinkingLevels ?? []);
|
|
105
111
|
setModelsStatus("ready");
|
|
112
|
+
setModelsForSessionId(sessionId);
|
|
106
113
|
}
|
|
107
114
|
catch (reason) {
|
|
108
115
|
if (refs.sessionRef.current?.sessionId !== sessionId)
|
|
109
116
|
return;
|
|
110
117
|
setModelsStatus("error");
|
|
111
118
|
setModelsError(reason instanceof Error ? reason.message : String(reason));
|
|
119
|
+
setModelsForSessionId(sessionId);
|
|
112
120
|
}
|
|
113
|
-
}, [origin,
|
|
121
|
+
}, [origin, sessionId, refs, onUnrecoverableSession]);
|
|
114
122
|
useEffect(() => {
|
|
115
123
|
void loadModels();
|
|
116
124
|
}, [loadModels]);
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
if (!session || modelsForSessionId !== session.sessionId)
|
|
127
|
+
return;
|
|
128
|
+
releaseDeferredRuntimeSnapshotFetch(session.sessionId);
|
|
129
|
+
}, [session, modelsForSessionId]);
|
|
117
130
|
useEffect(() => {
|
|
118
131
|
if (!session) {
|
|
119
132
|
setRuntimeContext(null);
|
|
@@ -121,6 +134,12 @@ export function useRuntimeControls(params) {
|
|
|
121
134
|
setRuntimeContextLoading(false);
|
|
122
135
|
return;
|
|
123
136
|
}
|
|
137
|
+
if (modelsForSessionId !== session.sessionId) {
|
|
138
|
+
setRuntimeContext(null);
|
|
139
|
+
setRuntimeContextError(null);
|
|
140
|
+
setRuntimeContextLoading(true);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
124
143
|
const sessionId = session.sessionId;
|
|
125
144
|
const base = `${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(sessionId)}`;
|
|
126
145
|
setRuntimeContextError(null);
|
|
@@ -181,7 +200,7 @@ export function useRuntimeControls(params) {
|
|
|
181
200
|
return;
|
|
182
201
|
setRuntimeContextError("注入上下文读取失败,请重试");
|
|
183
202
|
});
|
|
184
|
-
}, [origin, session?.sessionId, refs, onUnrecoverableSession]);
|
|
203
|
+
}, [origin, session?.sessionId, modelsForSessionId, refs, onUnrecoverableSession]);
|
|
185
204
|
// AC-01/RF3-01: hydrate the Composer from the unified snapshot — real
|
|
186
205
|
// model and thinking selection; `auto` displays the Pi actual effective
|
|
187
206
|
// level. Derived session/runtimeContext state ONLY comes from a snapshot
|
|
@@ -348,8 +367,8 @@ export function useRuntimeControls(params) {
|
|
|
348
367
|
capabilities,
|
|
349
368
|
]);
|
|
350
369
|
// Snapshot is the single source of truth when ready; the /models fetch
|
|
351
|
-
// result is the fallback
|
|
352
|
-
const effectiveModels = snapshot
|
|
370
|
+
// result is the fallback so the picker is usable before Pi session reopen.
|
|
371
|
+
const effectiveModels = snapshot && snapshotState.status === "ready"
|
|
353
372
|
? (snapshot.models ?? []).map((model) => ({
|
|
354
373
|
provider: model.provider,
|
|
355
374
|
id: model.id,
|
|
@@ -14,6 +14,7 @@ import { confirmationToken } from "./format.js";
|
|
|
14
14
|
import { createRuntimeSnapshotStore, isLostChatSessionError, parseRuntimeSnapshotError, } from "./runtime-snapshot-store.js";
|
|
15
15
|
const store = createRuntimeSnapshotStore();
|
|
16
16
|
let controller;
|
|
17
|
+
let snapshotFetchStartedFor;
|
|
17
18
|
function snapshotUrl(sessionId) {
|
|
18
19
|
return `/api/operator/v1/chat/sessions/${encodeURIComponent(sessionId)}`;
|
|
19
20
|
}
|
|
@@ -36,12 +37,28 @@ export function attachRuntimeSnapshotController(input) {
|
|
|
36
37
|
store.dispatch({ type: "mutation-reset" });
|
|
37
38
|
}
|
|
38
39
|
if (!input.sessionId) {
|
|
40
|
+
snapshotFetchStartedFor = undefined;
|
|
39
41
|
store.dispatch({ type: "reset" });
|
|
40
42
|
return;
|
|
41
43
|
}
|
|
42
44
|
store.dispatch({ type: "load-start", sessionId: input.sessionId });
|
|
45
|
+
if (input.deferInitialFetch) {
|
|
46
|
+
snapshotFetchStartedFor = undefined;
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
snapshotFetchStartedFor = input.sessionId;
|
|
43
50
|
void fetchSnapshot(input.sessionId, input.origin);
|
|
44
51
|
}
|
|
52
|
+
/** Start the deferred GET /runtime-snapshot after the workspace model catalog settles. */
|
|
53
|
+
export function releaseDeferredRuntimeSnapshotFetch(sessionId) {
|
|
54
|
+
const ctrl = controller;
|
|
55
|
+
if (!ctrl?.sessionId || ctrl.sessionId !== sessionId)
|
|
56
|
+
return;
|
|
57
|
+
if (snapshotFetchStartedFor === sessionId)
|
|
58
|
+
return;
|
|
59
|
+
snapshotFetchStartedFor = sessionId;
|
|
60
|
+
void fetchSnapshot(sessionId, ctrl.origin);
|
|
61
|
+
}
|
|
45
62
|
async function fetchSnapshot(sessionId, origin) {
|
|
46
63
|
try {
|
|
47
64
|
const res = await fetch(`${origin}${snapshotUrl(sessionId)}/runtime-snapshot`, { credentials: "include" });
|
|
@@ -144,6 +161,7 @@ export async function refreshRuntimeSnapshot() {
|
|
|
144
161
|
return false;
|
|
145
162
|
const sessionId = ctrl.sessionId;
|
|
146
163
|
store.dispatch({ type: "load-start", sessionId });
|
|
164
|
+
snapshotFetchStartedFor = sessionId;
|
|
147
165
|
void fetchSnapshot(sessionId, ctrl.origin);
|
|
148
166
|
return true;
|
|
149
167
|
}
|
|
@@ -2,3 +2,7 @@ export const WORKSPACE_BROWSER_OK = { status: "ok" };
|
|
|
2
2
|
export function workspaceBrowserGateActive(gate) {
|
|
3
3
|
return gate.status !== "ok";
|
|
4
4
|
}
|
|
5
|
+
/** Non-session chat APIs require a selected workspace; empty/whitespace is unset. */
|
|
6
|
+
export function workspaceScopeReady(workspaceId) {
|
|
7
|
+
return Boolean(workspaceId?.trim());
|
|
8
|
+
}
|
|
@@ -22,7 +22,7 @@ export async function reloadWhenConsoleReady(options = {}) {
|
|
|
22
22
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
23
23
|
const reload = options.reload ?? (() => window.location.reload());
|
|
24
24
|
const started = Date.now();
|
|
25
|
-
let previousConsoleInstanceId = null;
|
|
25
|
+
let previousConsoleInstanceId = options.previousConsoleInstanceId?.trim() || null;
|
|
26
26
|
while (Date.now() - started < timeoutMs) {
|
|
27
27
|
try {
|
|
28
28
|
const res = await fetchImpl(`/api/health?ts=${Date.now()}`, {
|
|
@@ -2,7 +2,7 @@ import { lstat, readFile } from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { acquireUpdateLock, updateLockPath } from "../../shared/update/state.js";
|
|
4
4
|
import os from "node:os";
|
|
5
|
-
import {
|
|
5
|
+
import { isOperationOccupied } from "../../infrastructure/console/operation-store.js";
|
|
6
6
|
import { readInitRuntimeActivity } from "../observability/init-runtime-activity.js";
|
|
7
7
|
const record = (value) => {
|
|
8
8
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
@@ -138,7 +138,7 @@ export class WorkspaceInitializationService {
|
|
|
138
138
|
}
|
|
139
139
|
async check(refresh = false) {
|
|
140
140
|
const latest = (await this.options.operations.list()).filter(op => op.action === "workspaceInitialize").at(-1);
|
|
141
|
-
const running = latest &&
|
|
141
|
+
const running = latest && isOperationOccupied(latest.state);
|
|
142
142
|
if (!running && (refresh || !this.cached || Date.now() - this.cached.at > 30_000)) {
|
|
143
143
|
const generation = this.generation;
|
|
144
144
|
this.pending ??= this.inspect().then(value => { if (generation === this.generation)
|
|
@@ -174,7 +174,7 @@ export class WorkspaceInitializationService {
|
|
|
174
174
|
if (activity.workerProjectionError || activity.activeDagRunIds.length || activity.activeWorkerTasks || activity.activeWorkerBatches || this.options.activeChatTurnCount())
|
|
175
175
|
throw new Error("当前工作区有任务或对话正在运行,请结束后重试");
|
|
176
176
|
const operations = await this.options.operations.list();
|
|
177
|
-
if (operations.some(op => op.operationId !== operationId &&
|
|
177
|
+
if (operations.some(op => op.operationId !== operationId && isOperationOccupied(op.state)))
|
|
178
178
|
throw new Error("当前工作区有其他操作正在运行,请稍后重试");
|
|
179
179
|
const before = await this.inspect();
|
|
180
180
|
if (before.state === "error")
|
|
@@ -2176,6 +2176,12 @@ function isExecutedNodeStatus(status) {
|
|
|
2176
2176
|
return false;
|
|
2177
2177
|
return EXECUTED_NODE_STATUSES.has(status.toLowerCase());
|
|
2178
2178
|
}
|
|
2179
|
+
/**
|
|
2180
|
+
* Node statuses that have not reached a terminal outcome. A live node's last
|
|
2181
|
+
* recorded attempt can be a failure that is already being retried, so history
|
|
2182
|
+
* copy must not present it as the final result.
|
|
2183
|
+
*/
|
|
2184
|
+
const LIVE_NODE_STATUSES = new Set(["running", "started", "paused"]);
|
|
2179
2185
|
const PASS_SUCCESS_STATUSES = new Set([
|
|
2180
2186
|
"finished",
|
|
2181
2187
|
"completed",
|
|
@@ -2221,12 +2227,21 @@ export function projectExecutionSummary(options) {
|
|
|
2221
2227
|
const recovered = totalAttemptCount > 1 &&
|
|
2222
2228
|
firstOutcome === "failure" &&
|
|
2223
2229
|
finalOutcome === "success";
|
|
2230
|
+
// A node that is still live (running/started/paused) has not reached a
|
|
2231
|
+
// final outcome: its last *recorded* attempt may have failed while a
|
|
2232
|
+
// retry is already queued. Labelling that "最终失败" told the operator
|
|
2233
|
+
// the run was over when it was still working, so keep the attempt count
|
|
2234
|
+
// and mark the last attempt instead of the node.
|
|
2235
|
+
const stillRunning = options.status !== undefined &&
|
|
2236
|
+
LIVE_NODE_STATUSES.has(options.status.toLowerCase());
|
|
2224
2237
|
// 一次就成功/失败:卡片不展示 historyLabel;Inspector 仍可拿 attempts 元数据。
|
|
2225
2238
|
const historyLabel = totalAttemptCount <= 1
|
|
2226
2239
|
? undefined
|
|
2227
2240
|
: recovered
|
|
2228
2241
|
? `${totalAttemptCount} 次 · 先失败后成功`
|
|
2229
|
-
:
|
|
2242
|
+
: stillRunning && finalOutcome === "failure"
|
|
2243
|
+
? `${totalAttemptCount} 次 · 最近一次失败,仍在重试`
|
|
2244
|
+
: `${totalAttemptCount} 次 · 最终${finalOutcome === "success" ? "成功" : "失败"}`;
|
|
2230
2245
|
return {
|
|
2231
2246
|
totalAttemptCount,
|
|
2232
2247
|
...(recovered ? { recovered: true } : {}),
|
|
@@ -2266,12 +2281,18 @@ export function projectExecutionSummary(options) {
|
|
|
2266
2281
|
}
|
|
2267
2282
|
// 仅一轮 pass 时不在卡片展示(无叙事价值);多轮才展示中文「修复轮次 N」
|
|
2268
2283
|
// 主文案,裸 P1/P2 编号不再作为主标题(原始编号仅 hover/title/诊断)。
|
|
2284
|
+
// 与 attempt 分支同理:节点仍在运行时,最后一个已记录 pass 的失败只是
|
|
2285
|
+
// 阶段性事实,不能当作最终结论。
|
|
2286
|
+
const livePasses = options.status !== undefined &&
|
|
2287
|
+
LIVE_NODE_STATUSES.has(options.status.toLowerCase());
|
|
2269
2288
|
const historyLabel = passes.length <= 1
|
|
2270
2289
|
? undefined
|
|
2271
2290
|
: `修复轮次 ${passes.join(",")}${finalOutcome === "success"
|
|
2272
2291
|
? " · 最终成功"
|
|
2273
2292
|
: finalOutcome === "failure"
|
|
2274
|
-
?
|
|
2293
|
+
? livePasses
|
|
2294
|
+
? " · 最近一轮失败,仍在重试"
|
|
2295
|
+
: " · 最终失败"
|
|
2275
2296
|
: ""}`;
|
|
2276
2297
|
return {
|
|
2277
2298
|
totalAttemptCount: passes.length,
|