@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.
Files changed (100) hide show
  1. package/CHANGELOG.md +29 -1
  2. package/dist/build-stamp.json +3 -3
  3. package/dist/executors/pi-sdk-executor.js +7 -7
  4. package/dist/executors/shell-write-guard.js +26 -3
  5. package/dist/infrastructure/console/operation-store.js +46 -14
  6. package/dist/shared/file-identity.js +23 -0
  7. package/dist/shared/git-workspace-status.js +480 -0
  8. package/dist/shared/path-safety.js +2 -2
  9. package/dist/shared/wizard-local-telemetry.js +2 -1
  10. package/dist/worker/console/chat/context-insights.js +723 -0
  11. package/dist/worker/console/chat/pi-runtime.js +217 -18
  12. package/dist/worker/console/chat/repo-browser.js +52 -4
  13. package/dist/worker/console/chat/routes.js +150 -58
  14. package/dist/worker/console/chat/shortcuts.js +7 -0
  15. package/dist/worker/console/console-update-runtime.js +2 -2
  16. package/dist/worker/console/operator-user-error.js +28 -1
  17. package/dist/worker/console/pi-readiness.js +4 -4
  18. package/dist/worker/console/recovery-cta.js +14 -14
  19. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-B_lmJjOm.js → abnfDiagram-N423BO3Z-Brn2sPzL.js} +1 -1
  20. package/dist/worker/console/static/assets/{arc-pymM4Qeh.js → arc-BIMNTG0W.js} +1 -1
  21. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-Bw9EPAhi.js → architectureDiagram-T3A2C74G--WtcgR_A.js} +1 -1
  22. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-C7_oXJp2.js → blockDiagram-VBNYF7ZC-BT_Ztog1.js} +1 -1
  23. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-LnLWDbh8.js → c4Diagram-5PPSVZJV-C5hbVr-l.js} +1 -1
  24. package/dist/worker/console/static/assets/channel-DNWB4hpR.js +1 -0
  25. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-BBiu3sCW.js → chunk-2GRJ4B5K-BKKnyR9Z.js} +1 -1
  26. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-DiXqDQml.js → chunk-2Q5K7J3B-BHs6biqA.js} +1 -1
  27. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-lmNq9nYk.js → chunk-5RXB4S5H-BdSFy_vP.js} +1 -1
  28. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-boBu74dU.js → chunk-5VM5RSS4-BpJIdTKV.js} +1 -1
  29. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-CjxqoJLQ.js → chunk-6Q2QTUOP-C_iWJhkT.js} +1 -1
  30. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-DL0vQ9MG.js → chunk-GF5L2VYU-BTbmx3-k.js} +1 -1
  31. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-DGPqE-O0.js → chunk-JWPE2WC7-D4Ee6DmP.js} +1 -1
  32. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-v4BbT_r2.js → chunk-KBJHAD2P-B487SORR.js} +1 -1
  33. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-Ci0ynvyL.js → chunk-RYQCIY6F-BKYHjNrr.js} +1 -1
  34. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-CGsbJoTV.js → chunk-XXDRQBXY-DrTdoYZG.js} +1 -1
  35. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-COhFgJji.js +1 -0
  36. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-COhFgJji.js +1 -0
  37. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CzoRDEI7.js → cose-bilkent-JH36ORCC-CttUY_Jo.js} +1 -1
  38. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BOwFwbX4.js → cynefin-VYW2F7L2-B4Rm5gnR.js} +1 -1
  39. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-2Cfg451l.js → cynefinDiagram-MW4NZA55-ByxHBq_S.js} +1 -1
  40. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-DbT0D6Y5.js → dagre-VZM6K2ZE-DGrfNFA0.js} +1 -1
  41. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Dfd_JoMs.js → diagram-7IWD3JNH-KjrUPquv.js} +1 -1
  42. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-DaGm82kh.js → diagram-B4RE2ZJO-VS4mlm75.js} +1 -1
  43. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DMoI8Ibc.js → diagram-LBJQPF4R-BLXYmFTM.js} +1 -1
  44. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BtIFA3aY.js → diagram-Q27KOJAE-C6xoFAqH.js} +1 -1
  45. package/dist/worker/console/static/assets/{diagram-UB23O5K3-D_2X1DTZ.js → diagram-UB23O5K3-C478dCe7.js} +1 -1
  46. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-yFW5ZYlZ.js → ebnfDiagram-BXEA7PRR-DHEpW5zN.js} +1 -1
  47. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DBXh5Svj.js → erDiagram-JOGREHBK-CA5GKfo9.js} +1 -1
  48. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-C4ycm8KM.js → flowDiagram-UKHOOZJN-zysi0Fl-.js} +1 -1
  49. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-qr-_Pekr.js → ganttDiagram-PKOTCBZU-DGzcKA8Y.js} +1 -1
  50. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-DzOSLv_A.js → gitGraphDiagram-DS77QQ5N-CHsuIG1w.js} +1 -1
  51. package/dist/worker/console/static/assets/index-CJh6pVLm.js +482 -0
  52. package/dist/worker/console/static/assets/index-C_kvKmFM.css +1 -0
  53. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-C7Hl8D7l.js → infoDiagram-6WML65LV-BexZHPCp.js} +1 -1
  54. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-fBxVlr6t.js → ishikawaDiagram-WSZJBQD7-Gkx8q-DB.js} +1 -1
  55. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-CoiJEVCl.js → journeyDiagram-NVQOT4AX-XDW3qrJ7.js} +1 -1
  56. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-TJXpxNuV.js → kanban-definition-27J2QSJJ-DJ07eEaN.js} +1 -1
  57. package/dist/worker/console/static/assets/{linear-CjQWz1pG.js → linear-DL-zeqOn.js} +1 -1
  58. package/dist/worker/console/static/assets/{mermaid.core-DY9_apIE.js → mermaid.core-C0HdXtOf.js} +5 -5
  59. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-HB0Id8gM.js → mindmap-definition-FAOFIHXS-CqRX0lrt.js} +1 -1
  60. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-0OSWgr6v.js → pegDiagram-VL7TDLO6-BVM9JZ2H.js} +1 -1
  61. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-Cei1GGC2.js → pieDiagram-7S7Q4E2Y-BA3ozdKk.js} +1 -1
  62. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CDy0Adfu.js → quadrantDiagram-CIZ2JOQS-B-0_p9_4.js} +1 -1
  63. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BwqzUWwN.js → railroadDiagram-AXF67PYL-DHRzPWFc.js} +1 -1
  64. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-Bif2IBRL.js → requirementDiagram-LRYGKXZP-9BEC_PcW.js} +1 -1
  65. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-CYIoNMLg.js → sankeyDiagram-W5VNT64P-DMxeW4rN.js} +1 -1
  66. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BDS7Y0aQ.js → sequenceDiagram-SI44F4Z6-DixP6LIU.js} +1 -1
  67. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-lZic-_Bp.js → sizeCapture-X5ZJPWSS-B_EifJBo.js} +1 -1
  68. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-WCVlCqSd.js → stateDiagram-OKZ733FA-BwikwWbX.js} +1 -1
  69. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-S2ZHMo1c.js +1 -0
  70. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-pGccxTw_.js → swimlanes-SLNWSIFB-C162l9o1.js} +2 -2
  71. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BZm4ByfV.js +8 -0
  72. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-D29RlrQA.js → timeline-definition-Z64GVDOM-mpe_u1i4.js} +1 -1
  73. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-01fEM1NU.js → vennDiagram-T6HMQDX7-DyUk54HX.js} +1 -1
  74. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-C4GeB6dq.js → wardleyDiagram-T6FBY63Y-HYyMkzfm.js} +1 -1
  75. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-BXHrXHps.js → xychartDiagram-ELKLHX3M-DRoh7skR.js} +1 -1
  76. package/dist/worker/console/static/index.html +2 -2
  77. package/dist/worker/console/static-src/app/console-types.js +9 -1
  78. package/dist/worker/console/static-src/operator-chat/context-insights-store.js +64 -0
  79. package/dist/worker/console/static-src/operator-chat/details-rail-surfaces.js +98 -0
  80. package/dist/worker/console/static-src/operator-chat/file-palette-nav.js +81 -0
  81. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +4 -2
  82. package/dist/worker/console/static-src/operator-chat/useComposer.js +80 -6
  83. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +8 -1
  84. package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +36 -17
  85. package/dist/worker/console/static-src/operator-chat/useRuntimeSnapshot.js +18 -0
  86. package/dist/worker/console/static-src/operator-chat/useWorkspaceBrowserGate.js +4 -0
  87. package/dist/worker/console/static-src/shell/console-update-reload.js +1 -1
  88. package/dist/worker/console/workspace-initialization.js +3 -3
  89. package/dist/worker/observability/read-model.js +23 -2
  90. package/dist/worker/observe/node-context-insights.js +479 -0
  91. package/docs/README.md +1 -1
  92. package/docs/templates/backend-test-dag.json +1 -0
  93. package/package.json +4 -4
  94. package/dist/worker/console/static/assets/channel-Ym13M3Lb.js +0 -1
  95. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BeFR1wGo.js +0 -1
  96. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BeFR1wGo.js +0 -1
  97. package/dist/worker/console/static/assets/index-PFjB6g6l.js +0 -482
  98. package/dist/worker/console/static/assets/index-ZtyB6Ol-.css +0 -1
  99. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-cj5UUB6m.js +0 -1
  100. 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(match[1] ?? "")}`, { signal: controller.signal })
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
- setFileOptions((body?.files ?? []).map((item) => item.path));
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
- }, [origin, sessionId, sessionIdentity, sessionReady, input, refs]);
288
- const insertFileReference = useCallback((file) => {
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
- }, [input, setInput]);
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 in multi-workspace consoles, which would silently
48
- // null the tool catalog (trajectory injection section included).
49
- const scope = session?.workspaceId ?? params.workspaceId ?? undefined;
50
- const query = scope
51
- ? `?workspaceId=${encodeURIComponent(scope)}`
52
- : "";
53
- const res = await fetch(`${origin}/api/operator/v1/chat/capabilities${query}`);
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: AC-01/AC-06 — the GET may reopen a historical
67
- // session server-side; a failure is a STRUCTURED error state (with retry),
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 (!session) {
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, session, refs, onUnrecoverableSession]);
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 (both hit the same backend, never fake-empty).
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 { TERMINAL_OPERATION_STATES } from "../../infrastructure/console/operation-store.js";
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 && !TERMINAL_OPERATION_STATES.has(latest.state) && latest.state !== "needs-reconcile";
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 && !TERMINAL_OPERATION_STATES.has(op.state) && op.state !== "needs-reconcile"))
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
- : `${totalAttemptCount} 次 · 最终${finalOutcome === "success" ? "成功" : "失败"}`;
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,