@tea-agent/loop-agent 0.44.0-next.5 → 0.44.0-next.7

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 (111) hide show
  1. package/CHANGELOG.md +299 -445
  2. package/dist/build-stamp.json +3 -3
  3. package/dist/executors/pi-sdk-executor.js +7 -7
  4. package/dist/infrastructure/console/operation-store.js +46 -14
  5. package/dist/shared/git-workspace-status.js +2 -0
  6. package/dist/shared/wizard-local-telemetry.js +2 -1
  7. package/dist/worker/console/chat/assistant-content.js +5 -34
  8. package/dist/worker/console/chat/chat-event-store.js +27 -10
  9. package/dist/worker/console/chat/db-connections.js +72 -8
  10. package/dist/worker/console/chat/pi-runtime/custom-tools/sql-tools.js +52 -24
  11. package/dist/worker/console/chat/pi-runtime.js +251 -18
  12. package/dist/worker/console/chat/repo-browser.js +49 -0
  13. package/dist/worker/console/chat/routes.js +215 -66
  14. package/dist/worker/console/chat/session-store.js +147 -19
  15. package/dist/worker/console/chat/turn-process.js +69 -23
  16. package/dist/worker/console/chat/user-turn-outline.js +19 -0
  17. package/dist/worker/console/console-update-runtime.js +2 -2
  18. package/dist/worker/console/operator-user-error.js +28 -1
  19. package/dist/worker/console/pi-readiness.js +4 -4
  20. package/dist/worker/console/recovery-cta.js +14 -14
  21. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-BHuhGM1e.js → abnfDiagram-N423BO3Z-DTuku_-7.js} +1 -1
  22. package/dist/worker/console/static/assets/{arc-CKh5E_sr.js → arc-CQbCsSqb.js} +1 -1
  23. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DizhiBEv.js → architectureDiagram-T3A2C74G-C-xUntTR.js} +1 -1
  24. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-3Neea-Z-.js → blockDiagram-VBNYF7ZC-DhNlwGAl.js} +1 -1
  25. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BCP_Qrgi.js → c4Diagram-5PPSVZJV-B7j3nx8o.js} +1 -1
  26. package/dist/worker/console/static/assets/channel-5_os7t_x.js +1 -0
  27. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-C_wydftK.js → chunk-2GRJ4B5K-BCrcxEh2.js} +1 -1
  28. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-jRPXZbzx.js → chunk-2Q5K7J3B-CsaePDb3.js} +1 -1
  29. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BLQf2OYO.js → chunk-5RXB4S5H--SRQc1rN.js} +1 -1
  30. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-0qRq64rI.js → chunk-5VM5RSS4-C5BFY2Po.js} +1 -1
  31. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-ClMg8xmV.js → chunk-6Q2QTUOP-Caso6uN7.js} +1 -1
  32. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-Coop2FlL.js → chunk-GF5L2VYU-AY2ZSAx2.js} +1 -1
  33. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-CDdO-jPJ.js → chunk-JWPE2WC7-DiMqZQ_8.js} +1 -1
  34. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-D8Gkppjl.js → chunk-KBJHAD2P-gpvvriOp.js} +1 -1
  35. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DA9HIySf.js → chunk-RYQCIY6F-CUdyoJhI.js} +1 -1
  36. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-DZNDRX1k.js → chunk-XXDRQBXY-Bp1xW3T4.js} +1 -1
  37. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-loz0n18W.js +1 -0
  38. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-loz0n18W.js +1 -0
  39. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CZ1zGyjq.js → cose-bilkent-JH36ORCC-D-JIcOIT.js} +1 -1
  40. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-B8u1_FtM.js → cynefin-VYW2F7L2-C-NKFflx.js} +1 -1
  41. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-CPBB6lfo.js → cynefinDiagram-MW4NZA55-CSOcvOAA.js} +1 -1
  42. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CG3Eov3E.js → dagre-VZM6K2ZE-BQSBltMy.js} +1 -1
  43. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-ycI-dOjh.js → diagram-7IWD3JNH-B0LVC45G.js} +1 -1
  44. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-Bx_GyQ7Y.js → diagram-B4RE2ZJO-Cypg3nqy.js} +1 -1
  45. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-m0G6mP4d.js → diagram-LBJQPF4R-BBDU88XJ.js} +1 -1
  46. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-DTDJkqcJ.js → diagram-Q27KOJAE-BZusLqSb.js} +1 -1
  47. package/dist/worker/console/static/assets/{diagram-UB23O5K3-BCfFrG-p.js → diagram-UB23O5K3-Cfolotcr.js} +1 -1
  48. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BIkTNPpp.js → ebnfDiagram-BXEA7PRR-CjfyUy0m.js} +1 -1
  49. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DTcMWrd2.js → erDiagram-JOGREHBK-DJdz7txE.js} +1 -1
  50. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-BqzDaNQ7.js → flowDiagram-UKHOOZJN-CQqgPO6y.js} +1 -1
  51. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-BSl_vGFe.js → ganttDiagram-PKOTCBZU-BOpGfS2m.js} +1 -1
  52. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-DCA5HXM6.js → gitGraphDiagram-DS77QQ5N-C6UCu0qx.js} +1 -1
  53. package/dist/worker/console/static/assets/index-CUT440A8.css +1 -0
  54. package/dist/worker/console/static/assets/index-DWKejNVJ.js +486 -0
  55. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DvqT18ge.js → infoDiagram-6WML65LV-BWeaVEUB.js} +1 -1
  56. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-B6zGKrQ3.js → ishikawaDiagram-WSZJBQD7-C8gmlBbk.js} +1 -1
  57. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-BpLQPI9C.js → journeyDiagram-NVQOT4AX-DF3bm8Bp.js} +1 -1
  58. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CzIz_-Lt.js → kanban-definition-27J2QSJJ-D2fG2Vxi.js} +1 -1
  59. package/dist/worker/console/static/assets/{linear-CMQ8Jpp-.js → linear-BGKgMc2T.js} +1 -1
  60. package/dist/worker/console/static/assets/{mermaid.core-BdAMrTf7.js → mermaid.core-DF6mMPJ9.js} +5 -5
  61. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-hSwF6sOq.js → mindmap-definition-FAOFIHXS-AMhePheC.js} +1 -1
  62. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DZdv509B.js → pegDiagram-VL7TDLO6-DN1Cl0W0.js} +1 -1
  63. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-CmD4-b0K.js → pieDiagram-7S7Q4E2Y-Cx9ngUeN.js} +1 -1
  64. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-DtKxNxlS.js → quadrantDiagram-CIZ2JOQS-BcYPTQJb.js} +1 -1
  65. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-D48iIoiS.js → railroadDiagram-AXF67PYL-BqHUZWnH.js} +1 -1
  66. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-C3N0K-2p.js → requirementDiagram-LRYGKXZP-CmqWZ0-B.js} +1 -1
  67. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-CACWR-Xs.js → sankeyDiagram-W5VNT64P--4YetH1y.js} +1 -1
  68. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-g_QgugvZ.js → sequenceDiagram-SI44F4Z6-DGD4VMfD.js} +1 -1
  69. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DqLoRPGK.js → sizeCapture-X5ZJPWSS-QVw9q7rh.js} +1 -1
  70. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-ES59VgMG.js → stateDiagram-OKZ733FA-CuemV9pe.js} +1 -1
  71. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BdlGdIJw.js +1 -0
  72. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-fjyLVNWo.js → swimlanes-SLNWSIFB-C7oKzZKs.js} +2 -2
  73. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DjjbHbde.js +8 -0
  74. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-Cd5-biKv.js → timeline-definition-Z64GVDOM-C2Of60nT.js} +1 -1
  75. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-CZTu6mPf.js → vennDiagram-T6HMQDX7-DHtpQHyL.js} +1 -1
  76. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DR01jjbe.js → wardleyDiagram-T6FBY63Y-COi9OhSy.js} +1 -1
  77. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-iZcuGVGa.js → xychartDiagram-ELKLHX3M-DPHQfRrt.js} +1 -1
  78. package/dist/worker/console/static/index.html +2 -2
  79. package/dist/worker/console/static-src/app/console-types.js +9 -1
  80. package/dist/worker/console/static-src/operator-chat/chat-link.js +20 -0
  81. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +58 -3
  82. package/dist/worker/console/static-src/operator-chat/details-rail-surfaces.js +98 -0
  83. package/dist/worker/console/static-src/operator-chat/file-palette-nav.js +81 -0
  84. package/dist/worker/console/static-src/operator-chat/process-label.js +19 -6
  85. package/dist/worker/console/static-src/operator-chat/transcript-mode.js +21 -0
  86. package/dist/worker/console/static-src/operator-chat/turn-group-equality.js +2 -0
  87. package/dist/worker/console/static-src/operator-chat/turn-process-disclosure.js +13 -5
  88. package/dist/worker/console/static-src/operator-chat/turn-state-projection.js +166 -0
  89. package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +56 -2
  90. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +202 -41
  91. package/dist/worker/console/static-src/operator-chat/useChatStream.js +78 -23
  92. package/dist/worker/console/static-src/operator-chat/useChatThread.js +55 -18
  93. package/dist/worker/console/static-src/operator-chat/useComposer.js +80 -6
  94. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +13 -3
  95. package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +36 -17
  96. package/dist/worker/console/static-src/operator-chat/useRuntimeSnapshot.js +18 -0
  97. package/dist/worker/console/static-src/operator-chat/useWorkspaceBrowserGate.js +4 -0
  98. package/dist/worker/console/static-src/shell/console-update-reload.js +1 -1
  99. package/dist/worker/console/workspace-initialization.js +3 -3
  100. package/dist/worker/observability/read-model.js +23 -2
  101. package/dist/worker/scheduler/scheduled-goal-dispatch.js +3 -2
  102. package/dist/worker/scheduler/scheduled-goal-evidence.js +6 -1
  103. package/docs/templates/backend-test-dag.json +1 -0
  104. package/package.json +4 -4
  105. package/dist/worker/console/static/assets/channel-DxgzA1GF.js +0 -1
  106. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BZcm7fs6.js +0 -1
  107. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BZcm7fs6.js +0 -1
  108. package/dist/worker/console/static/assets/index-BAEDjTbD.css +0 -1
  109. package/dist/worker/console/static/assets/index-wKEJUaP5.js +0 -482
  110. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DyCIq6jx.js +0 -1
  111. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CsKtO34q.js +0 -8
@@ -9,6 +9,7 @@ import { taskContextUnchanged } from "./format.js";
9
9
  import { upsertCompactionMessage } from "./compaction-message.js";
10
10
  import { sameTurnGroupInputs } from "./turn-group-equality.js";
11
11
  import { conversationNodesFromEvents, } from "../../chat/conversation-nodes.js";
12
+ import { mergeTurnStateSummaries, setProjectedTurnState, } from "./turn-state-projection.js";
12
13
  const EMPTY_USAGE = {
13
14
  inputTokens: 0,
14
15
  outputTokens: 0,
@@ -141,8 +142,13 @@ export function useChatThread(params) {
141
142
  const [usage, setUsage] = useState(EMPTY_USAGE);
142
143
  const [sessionStats, setSessionStats] = useState(EMPTY_SESSION_STATS);
143
144
  const [turnTelemetry, setTurnTelemetry] = useState([]);
145
+ const [userTurnOutline, setUserTurnOutline] = useState([]);
144
146
  const [operatorActions, setOperatorActions] = useState([]);
145
147
  const [conversationNodes, setConversationNodes] = useState([]);
148
+ const [turnStatesById, setTurnStatesById] = useState({});
149
+ const reconcileTurnStates = useCallback((summaries) => {
150
+ setTurnStatesById((current) => mergeTurnStateSummaries(current, summaries));
151
+ }, []);
146
152
  const [sddSessionIdentity, setSddSessionIdentity] = useState(null);
147
153
  const [streaming, setStreaming] = useState(false);
148
154
  const [turnPhase, setTurnPhase] = useState("idle");
@@ -172,11 +178,12 @@ export function useChatThread(params) {
172
178
  setUsage(body.usage);
173
179
  setTurnTelemetry(body.turnTelemetry ?? []);
174
180
  setSddSessionIdentity(body.sddDataAlignment ?? null);
181
+ reconcileTurnStates(body.session?.turnStates);
175
182
  }
176
183
  catch {
177
184
  // Telemetry is supplementary; the normal event stream remains authoritative.
178
185
  }
179
- }, [origin, refs]);
186
+ }, [origin, refs, reconcileTurnStates]);
180
187
  // All inputs are stable (state setters + the memoized refs bag), so the
181
188
  // applier identity is stable across renders.
182
189
  const applyChatSseEvent = useMemo(() => createChatSseEventApplier({
@@ -195,6 +202,7 @@ export function useChatThread(params) {
195
202
  setGoal,
196
203
  setOperatorActions,
197
204
  setConversationNodes,
205
+ setTurnState: (turnId, state) => setTurnStatesById((current) => setProjectedTurnState(current, turnId, state)),
198
206
  setSddSessionIdentity,
199
207
  setStreamingAssistantId,
200
208
  setUnread,
@@ -334,6 +342,7 @@ export function useChatThread(params) {
334
342
  streaming,
335
343
  activeAssistantId: streamingAssistantId,
336
344
  conversationNodes,
345
+ turnStatesById,
337
346
  });
338
347
  // Preserve completed group objects across streaming tokens. Stable
339
348
  // historical groups keep their references so
@@ -353,11 +362,12 @@ export function useChatThread(params) {
353
362
  streaming,
354
363
  streamingAssistantId,
355
364
  conversationNodes,
365
+ turnStatesById,
356
366
  historyLimit,
357
367
  ]);
358
- const loadEarlierMessages = useCallback(async (beforePrepend) => {
368
+ const loadHistoryPages = useCallback(async (input) => {
359
369
  const targetSessionId = refs.sessionRef.current?.sessionId;
360
- const beforeId = historyNextCursor;
370
+ let beforeId = historyNextCursor;
361
371
  if (!targetSessionId || !historyHasMore || !beforeId || historyLoadingRef.current)
362
372
  return false;
363
373
  const generation = refs.sessionGenerationRef.current;
@@ -365,17 +375,29 @@ export function useChatThread(params) {
365
375
  historyLoadingRef.current = request;
366
376
  setHistoryLoading(true);
367
377
  try {
368
- const query = new URLSearchParams({ limit: "50", beforeId });
369
- const res = await fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(targetSessionId)}/messages?${query.toString()}`, { credentials: "include" });
370
- if (!res.ok || refs.sessionRef.current?.sessionId !== targetSessionId)
371
- return false;
372
- const body = (await res.json());
373
- if (refs.sessionRef.current?.sessionId !== targetSessionId ||
374
- refs.sessionGenerationRef.current !== generation ||
375
- historyLoadingRef.current !== request)
376
- return false;
377
- const page = hydrateSessionThread(body.messages ?? []);
378
- beforePrepend?.();
378
+ const collectedMessages = [];
379
+ const collectedTurnStates = [];
380
+ let hasMore = historyHasMore;
381
+ let found = false;
382
+ do {
383
+ const query = new URLSearchParams({ limit: "50", beforeId });
384
+ const res = await fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(targetSessionId)}/messages?${query.toString()}`, { credentials: "include" });
385
+ if (!res.ok || refs.sessionRef.current?.sessionId !== targetSessionId)
386
+ return false;
387
+ const body = (await res.json());
388
+ if (refs.sessionRef.current?.sessionId !== targetSessionId ||
389
+ refs.sessionGenerationRef.current !== generation ||
390
+ historyLoadingRef.current !== request)
391
+ return false;
392
+ const pageMessages = body.messages ?? [];
393
+ collectedMessages.unshift(...pageMessages);
394
+ collectedTurnStates.push(...(body.turnStates ?? []));
395
+ found = Boolean(input?.untilMessageId && pageMessages.some((message) => message.id === input.untilMessageId));
396
+ hasMore = body.hasMore === true;
397
+ beforeId = body.nextCursor ?? "";
398
+ } while (input?.untilMessageId && !found && hasMore && beforeId);
399
+ const page = hydrateSessionThread(collectedMessages);
400
+ input?.beforePrepend?.();
379
401
  setMessages((current) => {
380
402
  const seen = new Set();
381
403
  const merged = [...page.messages, ...current].filter((message) => {
@@ -392,10 +414,11 @@ export function useChatThread(params) {
392
414
  byId.set(tool.id, tool);
393
415
  return [...byId.values()].sort((a, b) => a.createdAt - b.createdAt);
394
416
  });
395
- setHistoryHasMore(body.hasMore === true);
396
- setHistoryNextCursor(body.nextCursor ?? null);
397
- setHistoryLimit((value) => value + 50);
398
- return true;
417
+ setTurnStatesById((current) => mergeTurnStateSummaries(current, collectedTurnStates));
418
+ setHistoryHasMore(hasMore);
419
+ setHistoryNextCursor(beforeId || null);
420
+ setHistoryLimit((value) => value + collectedMessages.length);
421
+ return input?.untilMessageId ? found : true;
399
422
  }
400
423
  catch (error) {
401
424
  if (refs.sessionRef.current?.sessionId === targetSessionId && refs.sessionGenerationRef.current === generation) {
@@ -410,6 +433,10 @@ export function useChatThread(params) {
410
433
  }
411
434
  }
412
435
  }, [historyHasMore, historyNextCursor, origin, refs, setError]);
436
+ const loadEarlierMessages = useCallback((beforePrepend) => loadHistoryPages({ beforePrepend }), [loadHistoryPages]);
437
+ const loadThroughMessage = useCallback((messageId) => messagesRef.current.some((message) => message.id === messageId)
438
+ ? Promise.resolve(true)
439
+ : loadHistoryPages({ untilMessageId: messageId }), [loadHistoryPages]);
413
440
  const visibleMessages = useMemo(() => {
414
441
  const next = [];
415
442
  for (const group of turnGroups) {
@@ -431,8 +458,10 @@ export function useChatThread(params) {
431
458
  setUsage(EMPTY_USAGE);
432
459
  setSessionStats(EMPTY_SESSION_STATS);
433
460
  setTurnTelemetry([]);
461
+ setUserTurnOutline([]);
434
462
  setOperatorActions([]);
435
463
  setConversationNodes([]);
464
+ setTurnStatesById({});
436
465
  setSddSessionIdentity(null);
437
466
  setStreaming(false);
438
467
  setTurnPhase("idle");
@@ -483,6 +512,7 @@ export function useChatThread(params) {
483
512
  : undefined));
484
513
  setToolCalls(next.toolCalls);
485
514
  setConversationNodes(conversationNodesFromEvents(next.conversationEvents ?? []));
515
+ setTurnStatesById(mergeTurnStateSummaries({}, next.turnStates));
486
516
  setOperationCards([]);
487
517
  setHumanGateCards([]);
488
518
  setArtifactCards([]);
@@ -491,6 +521,7 @@ export function useChatThread(params) {
491
521
  setUsage(next.usage ?? EMPTY_USAGE);
492
522
  setSessionStats(next.sessionStats ?? EMPTY_SESSION_STATS);
493
523
  setTurnTelemetry(next.turnTelemetry ?? []);
524
+ setUserTurnOutline(next.userTurnOutline ?? []);
494
525
  setOperatorActions([]);
495
526
  setSddSessionIdentity(next.sddSessionIdentity ?? null);
496
527
  refs.usageResponseKeysRef.current = new Set();
@@ -544,6 +575,7 @@ export function useChatThread(params) {
544
575
  if (body.usage)
545
576
  setUsage(body.usage);
546
577
  setTurnTelemetry(body.turnTelemetry ?? []);
578
+ setUserTurnOutline(body.userTurnOutline ?? []);
547
579
  setSddSessionIdentity(body.sddDataAlignment ?? null);
548
580
  // Resume the events stream from the snapshot tail so the next
549
581
  // reconnect never immediately re-expires (AC-003). The refs below
@@ -554,6 +586,7 @@ export function useChatThread(params) {
554
586
  refs.sealedAssistantIdsRef.current = new Set();
555
587
  const hydrated = hydrateSessionThread(body.session.messages ?? []);
556
588
  setConversationNodes(conversationNodesFromEvents(body.conversationEvents ?? []));
589
+ setTurnStatesById((current) => mergeTurnStateSummaries(current, body.session?.turnStates));
557
590
  // Recovery is addressed to targetSessionId, so an optimistic
558
591
  // submission from another session must never be projected into
559
592
  // this snapshot (the activation path enforces the same owner gate).
@@ -616,6 +649,7 @@ export function useChatThread(params) {
616
649
  historyNextCursor,
617
650
  historyLoading,
618
651
  loadEarlierMessages,
652
+ loadThroughMessage,
619
653
  unread,
620
654
  humanGateCards,
621
655
  interview,
@@ -625,8 +659,11 @@ export function useChatThread(params) {
625
659
  usage,
626
660
  sessionStats,
627
661
  turnTelemetry,
662
+ userTurnOutline,
628
663
  operatorActions,
629
664
  conversationNodes,
665
+ turnStatesById,
666
+ reconcileTurnStates,
630
667
  sddSessionIdentity,
631
668
  setSddSessionIdentity,
632
669
  streaming,
@@ -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);
@@ -91,9 +94,11 @@ export function useRepoBrowser(params) {
91
94
  document.addEventListener("visibilitychange", onVisibility);
92
95
  return () => document.removeEventListener("visibilitychange", onVisibility);
93
96
  }, [refreshTree]);
94
- const openPreview = useCallback(async (file) => {
97
+ const openPreview = useCallback(async (file, focusLine) => {
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 {
@@ -103,7 +108,10 @@ export function useRepoBrowser(params) {
103
108
  return;
104
109
  if (!res.ok || !body.data)
105
110
  throw new Error(body.error?.message ?? "无法预览文件");
106
- const loaded = withHtmlPreviewUrl(body.data, origin, workspaceId);
111
+ const loaded = {
112
+ ...withHtmlPreviewUrl(body.data, origin, workspaceId),
113
+ ...(focusLine ? { focusLine } : {}),
114
+ };
107
115
  setPreviews((current) => {
108
116
  const next = [
109
117
  ...current.filter((item) => item.path !== loaded.path),
@@ -177,6 +185,8 @@ export function useRepoBrowser(params) {
177
185
  }
178
186
  if (!paths.length)
179
187
  return;
188
+ if (!workspaceScopeReady(workspaceId))
189
+ return;
180
190
  void Promise.all(paths.map(async (file) => {
181
191
  const res = await fetch(`${origin}/api/operator/v1/chat/repo/file?path=${encodeURIComponent(file)}${workspaceQuery}`, { credentials: "include" });
182
192
  if (!res.ok)
@@ -192,7 +202,7 @@ export function useRepoBrowser(params) {
192
202
  setPreviews(available);
193
203
  setActivePreviewPath(available.at(-1)?.path ?? null);
194
204
  });
195
- }, [origin, workspaceQuery, sessionId, refs]);
205
+ }, [origin, workspaceQuery, workspaceId, sessionId, refs]);
196
206
  const toggleDir = useCallback((dir) => {
197
207
  setExpandedDirs((current) => {
198
208
  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")