@tea-agent/loop-agent 0.43.0 → 0.44.0-next.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (226) hide show
  1. package/CHANGELOG.md +408 -367
  2. package/dist/application/evaluation/budget.js +21 -0
  3. package/dist/application/evaluation/corpus-hash.js +10 -15
  4. package/dist/application/evaluation/corpus.js +2 -1
  5. package/dist/application/evaluation/frontend-browser-acceptance.js +77 -0
  6. package/dist/application/evaluation/frontend-corpus-browser-acceptance.js +175 -0
  7. package/dist/application/evaluation/frontend-gateway-evidence.js +226 -0
  8. package/dist/application/evaluation/frontend-pair-registration.js +143 -0
  9. package/dist/application/evaluation/frontend-paired-summary.js +150 -0
  10. package/dist/application/evaluation/frontend-run-observation.js +144 -0
  11. package/dist/application/evaluation/frontend-shared-evidence.js +105 -0
  12. package/dist/application/evaluation/types.js +45 -12
  13. package/dist/application/task-lifecycle/observe.js +26 -1
  14. package/dist/build-stamp.json +3 -3
  15. package/dist/cli/command-definitions.js +11 -0
  16. package/dist/cli/program.js +4 -0
  17. package/dist/commands/task-advance.js +3 -5
  18. package/dist/commands/task-source-prepare.js +13 -1
  19. package/dist/executors/dag-pi/guards.js +4 -0
  20. package/dist/executors/dag-pi/model-config.js +30 -0
  21. package/dist/executors/dag-pi/plan/fact-adoption.js +55 -0
  22. package/dist/{workflows/dag/frontend-plan-completeness.js → executors/dag-pi/plan/facts.js} +22 -28
  23. package/dist/executors/dag-pi/plan/finalize-tools.js +259 -0
  24. package/dist/executors/dag-pi/plan/ledger-contract.js +55 -0
  25. package/dist/executors/dag-pi/plan/record-tools.js +861 -0
  26. package/dist/executors/dag-pi/plan/schema.js +129 -0
  27. package/dist/executors/dag-pi/plan/session.js +1 -0
  28. package/dist/executors/dag-pi/plan/tool-helpers.js +16 -0
  29. package/dist/executors/dag-pi/tools/contract-tools.js +515 -0
  30. package/dist/executors/dag-pi/tools/design-terminal-tools.js +159 -0
  31. package/dist/executors/dag-pi/tools/plan-decision-tools.js +529 -0
  32. package/dist/executors/dag-pi/tools/review-terminal-tools.js +159 -0
  33. package/dist/executors/dag-pi/tools/scout-evidence-tools.js +270 -0
  34. package/dist/executors/dag-pi-executor.js +398 -3384
  35. package/dist/executors/pi-executor.js +6 -1
  36. package/dist/executors/pi-sdk-executor.js +112 -19
  37. package/dist/executors/shell-executor.js +88 -12
  38. package/dist/executors/shell-presets.js +1 -1
  39. package/dist/executors/shell-write-guard.js +26 -3
  40. package/dist/infrastructure/console/operation-store.js +48 -14
  41. package/dist/infrastructure/evaluation/corpus-store.js +37 -12
  42. package/dist/shared/file-identity.js +23 -0
  43. package/dist/shared/git-workspace-status.js +482 -0
  44. package/dist/shared/operator/capabilities.js +10 -5
  45. package/dist/shared/path-safety.js +2 -2
  46. package/dist/shared/wizard-local-telemetry.js +51 -0
  47. package/dist/task/config-types.js +22 -8
  48. package/dist/task/contract/project.js +13 -0
  49. package/dist/task/contract/schema.js +2 -7
  50. package/dist/task/frontend-preflight.js +1 -1
  51. package/dist/task/source-prepare/parse-intent.js +124 -5
  52. package/dist/task/source-prepare/prepare.js +6 -1
  53. package/dist/task/source-prepare/semantic-intake.js +18 -3
  54. package/dist/task/source-prepare/source-execution.js +65 -0
  55. package/dist/task/source-prepare/source-fidelity-pi.js +22 -9
  56. package/dist/task/source-prepare/source-provider-budget.js +290 -0
  57. package/dist/worker/console/chat/assistant-content.js +5 -34
  58. package/dist/worker/console/chat/chat-event-store.js +62 -10
  59. package/dist/worker/console/chat/context-insights.js +723 -0
  60. package/dist/worker/console/chat/conversation-nodes.js +383 -0
  61. package/dist/worker/console/chat/db-connections.js +854 -0
  62. package/dist/worker/console/chat/pi-runtime/custom-tools/operator-tools.js +1 -1
  63. package/dist/worker/console/chat/pi-runtime/custom-tools/sql-tools.js +655 -0
  64. package/dist/worker/console/chat/pi-runtime.js +445 -36
  65. package/dist/worker/console/chat/provider-error.js +0 -12
  66. package/dist/worker/console/chat/repo-browser.js +52 -4
  67. package/dist/worker/console/chat/routes.js +577 -72
  68. package/dist/worker/console/chat/sdd-data-alignment.js +8 -31
  69. package/dist/worker/console/chat/session-store.js +147 -19
  70. package/dist/worker/console/chat/shortcuts.js +7 -0
  71. package/dist/worker/console/chat/timeline-message-order.js +22 -0
  72. package/dist/worker/console/chat/tools.js +34 -0
  73. package/dist/worker/console/chat/turn-process.js +237 -66
  74. package/dist/worker/console/chat/user-turn-outline.js +19 -0
  75. package/dist/worker/console/console-update-runtime.js +2 -2
  76. package/dist/worker/console/dag-confirmation.js +3 -0
  77. package/dist/worker/console/dag-execution-receipt.js +3 -0
  78. package/dist/worker/console/operator-actions.js +32 -3
  79. package/dist/worker/console/operator-user-error.js +28 -1
  80. package/dist/worker/console/pi-readiness.js +4 -4
  81. package/dist/worker/console/prd-intake-bridge.js +5 -12
  82. package/dist/worker/console/recovery-cta.js +14 -14
  83. package/dist/worker/console/server.js +5 -1
  84. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-8-j6y-sd.js → abnfDiagram-N423BO3Z-r1NAPU_5.js} +1 -1
  85. package/dist/worker/console/static/assets/{arc-eoQiMvuk.js → arc-dT5T4sVL.js} +1 -1
  86. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-C4A3uMcI.js → architectureDiagram-T3A2C74G-CP_cOq46.js} +1 -1
  87. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-D4zD2F-Q.js → blockDiagram-VBNYF7ZC-sToEZxEf.js} +1 -1
  88. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-j1RkJziL.js → c4Diagram-5PPSVZJV-DSxwwi2m.js} +1 -1
  89. package/dist/worker/console/static/assets/channel-CyBY_yGk.js +1 -0
  90. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-YBHmhik1.js → chunk-2GRJ4B5K-B9fh0IkF.js} +1 -1
  91. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-COfWyo9P.js → chunk-2Q5K7J3B-CAUNl-A6.js} +1 -1
  92. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-I99OUkHY.js → chunk-5RXB4S5H-BomeqbNX.js} +1 -1
  93. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-XKxoJNJ4.js → chunk-5VM5RSS4-BNAuTOLE.js} +1 -1
  94. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DLT_cYx1.js → chunk-6Q2QTUOP-DfzjRD5y.js} +1 -1
  95. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CxcKZ9mV.js → chunk-GF5L2VYU-C3uE-yc2.js} +1 -1
  96. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-V1EqXdjY.js → chunk-JWPE2WC7-C3sQMMYr.js} +1 -1
  97. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DebTtdFp.js → chunk-KBJHAD2P-D_AtEYQL.js} +1 -1
  98. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-B-ivNRDf.js → chunk-RYQCIY6F-DW4TwhKv.js} +1 -1
  99. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-DC_Ds11b.js → chunk-XXDRQBXY-CTQDmJcp.js} +1 -1
  100. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-D5lN4E_E.js +1 -0
  101. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-D5lN4E_E.js +1 -0
  102. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-sWEqKwIb.js → cose-bilkent-JH36ORCC-iMqPuBsL.js} +1 -1
  103. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BXa_dcu4.js → cynefin-VYW2F7L2-DktIuDWs.js} +1 -1
  104. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-C-Mle84F.js → cynefinDiagram-MW4NZA55-BgVnjyEs.js} +1 -1
  105. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CXPDBITe.js → dagre-VZM6K2ZE-CR25E4bu.js} +1 -1
  106. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-CC-WJQfa.js → diagram-7IWD3JNH-CPM2h5HY.js} +1 -1
  107. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-CDAV5Vs4.js → diagram-B4RE2ZJO-DqF0x2g1.js} +1 -1
  108. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-CmFiAcNz.js → diagram-LBJQPF4R-G8BYp2eb.js} +1 -1
  109. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-DPBZHuyn.js → diagram-Q27KOJAE-BbC4kT_D.js} +1 -1
  110. package/dist/worker/console/static/assets/{diagram-UB23O5K3-jUlm_Ds3.js → diagram-UB23O5K3-CKZwZ-G5.js} +1 -1
  111. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-DWhQ3mfY.js → ebnfDiagram-BXEA7PRR-kJDmheia.js} +1 -1
  112. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-Tr2gMqet.js → erDiagram-JOGREHBK-DFTVHyxB.js} +1 -1
  113. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-DJjVQHPA.js → flowDiagram-UKHOOZJN-BqdN5_os.js} +1 -1
  114. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-D74dQ4u0.js → ganttDiagram-PKOTCBZU-SYDjVakh.js} +1 -1
  115. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-DwW0tZ0X.js → gitGraphDiagram-DS77QQ5N-Bf8bNpGA.js} +1 -1
  116. package/dist/worker/console/static/assets/index-B28Onmfy.css +1 -0
  117. package/dist/worker/console/static/assets/index-CTMZsDmh.js +486 -0
  118. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-ILCbxJyb.js → infoDiagram-6WML65LV-DIeAmJ45.js} +1 -1
  119. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-DeuWBQ89.js → ishikawaDiagram-WSZJBQD7-oikqUAlo.js} +1 -1
  120. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-CF5ih8Fk.js → journeyDiagram-NVQOT4AX-mKEF_xYT.js} +1 -1
  121. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-C7yOSuRO.js → kanban-definition-27J2QSJJ-BdWJHPnN.js} +1 -1
  122. package/dist/worker/console/static/assets/{linear-BDZ9riWi.js → linear-DGRlieMH.js} +1 -1
  123. package/dist/worker/console/static/assets/{mermaid.core-7pKqYtpZ.js → mermaid.core-4P9yPC9c.js} +5 -5
  124. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-LeJDybSU.js → mindmap-definition-FAOFIHXS-5PTSkos6.js} +1 -1
  125. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BJvT3pMD.js → pegDiagram-VL7TDLO6-DONIFpG5.js} +1 -1
  126. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-_rGqpMan.js → pieDiagram-7S7Q4E2Y-D_Z4S70M.js} +1 -1
  127. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-DPcSv5aA.js → quadrantDiagram-CIZ2JOQS-CAsjh8iY.js} +1 -1
  128. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Drxx4hkJ.js → railroadDiagram-AXF67PYL-Cx4Dd6ac.js} +1 -1
  129. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-BCTUdU4z.js → requirementDiagram-LRYGKXZP-cQb7mwyH.js} +1 -1
  130. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-B6wzbZmB.js → sankeyDiagram-W5VNT64P-Cd-DhUdf.js} +1 -1
  131. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BGt8d3QQ.js → sequenceDiagram-SI44F4Z6-AlKYRgIk.js} +1 -1
  132. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-7nlhJKo7.js → sizeCapture-X5ZJPWSS-CUy_ET4e.js} +1 -1
  133. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Cv2stqMA.js → stateDiagram-OKZ733FA-CTuVe6Sa.js} +1 -1
  134. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BsWhs8O7.js +1 -0
  135. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-qDAo4Yc1.js → swimlanes-SLNWSIFB-ChFLJb-v.js} +2 -2
  136. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CJcioYGH.js +8 -0
  137. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CKbDNKTr.js → timeline-definition-Z64GVDOM-DZZeiAPr.js} +1 -1
  138. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DI-9EHic.js → vennDiagram-T6HMQDX7-t4O91cZU.js} +1 -1
  139. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-BrzzRDpX.js → wardleyDiagram-T6FBY63Y-CV5qGHZu.js} +1 -1
  140. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-BclH5hGh.js → xychartDiagram-ELKLHX3M-DyXbytUI.js} +1 -1
  141. package/dist/worker/console/static/index.html +2 -2
  142. package/dist/worker/console/static-src/app/console-types.js +9 -1
  143. package/dist/worker/console/static-src/operator-chat/animation-frame-batcher.js +38 -0
  144. package/dist/worker/console/static-src/operator-chat/chat-link.js +20 -0
  145. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +186 -5
  146. package/dist/worker/console/static-src/operator-chat/chat-stream-continuity.js +101 -0
  147. package/dist/worker/console/static-src/operator-chat/compaction-message.js +18 -3
  148. package/dist/worker/console/static-src/operator-chat/context-insights-store.js +64 -0
  149. package/dist/worker/console/static-src/operator-chat/details-rail-surfaces.js +98 -0
  150. package/dist/worker/console/static-src/operator-chat/file-palette-nav.js +81 -0
  151. package/dist/worker/console/static-src/operator-chat/pending-user-message.js +17 -3
  152. package/dist/worker/console/static-src/operator-chat/process-label.js +27 -5
  153. package/dist/worker/console/static-src/operator-chat/tools-catalog.js +16 -7
  154. package/dist/worker/console/static-src/operator-chat/transcript-mode.js +21 -0
  155. package/dist/worker/console/static-src/operator-chat/turn-group-equality.js +4 -1
  156. package/dist/worker/console/static-src/operator-chat/turn-process-disclosure.js +13 -5
  157. package/dist/worker/console/static-src/operator-chat/turn-state-projection.js +166 -0
  158. package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +56 -2
  159. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +211 -40
  160. package/dist/worker/console/static-src/operator-chat/useChatStream.js +124 -27
  161. package/dist/worker/console/static-src/operator-chat/useChatThread.js +88 -20
  162. package/dist/worker/console/static-src/operator-chat/useComposer.js +80 -6
  163. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +13 -3
  164. package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +36 -17
  165. package/dist/worker/console/static-src/operator-chat/useRuntimeSnapshot.js +18 -0
  166. package/dist/worker/console/static-src/operator-chat/useWorkspaceBrowserGate.js +4 -0
  167. package/dist/worker/console/static-src/shell/console-update-reload.js +1 -1
  168. package/dist/worker/console/workspace-context.js +2 -0
  169. package/dist/worker/console/workspace-initialization.js +3 -3
  170. package/dist/worker/observability/read-model.js +23 -2
  171. package/dist/worker/observe/node-context-insights.js +479 -0
  172. package/dist/worker/observe/static/console-theme.css +174 -0
  173. package/dist/worker/observe/static/console-theme.js +36 -0
  174. package/dist/worker/observe/static/operator-chrome.css +85 -0
  175. package/dist/worker/observe/static/operator-chrome.js +51 -0
  176. package/dist/worker/scheduler/scheduled-goal-dispatch.js +3 -2
  177. package/dist/worker/scheduler/scheduled-goal-evidence.js +66 -3
  178. package/dist/worker/scheduler/scheduled-goal-store.js +10 -0
  179. package/dist/workflows/dag/budget-enforcement.js +31 -2
  180. package/dist/workflows/dag/frontend-closeout.js +3 -1
  181. package/dist/workflows/dag/frontend-contract-facts.js +11 -8
  182. package/dist/workflows/dag/frontend-design-policy.js +6 -6
  183. package/dist/workflows/dag/frontend-durable-tools.js +90 -70
  184. package/dist/workflows/dag/frontend-implementation-contract.js +146 -57
  185. package/dist/workflows/dag/frontend-plan-canary.js +66 -16
  186. package/dist/workflows/dag/frontend-plan-decision-contract.js +13 -21
  187. package/dist/workflows/dag/frontend-plan-progress.js +92 -0
  188. package/dist/workflows/dag/frontend-recovery-plan.js +10 -5
  189. package/dist/workflows/dag/frontend-recovery-run.js +51 -46
  190. package/dist/workflows/dag/frontend-repair-assertions.js +36 -0
  191. package/dist/workflows/dag/frontend-review-context.js +29 -1
  192. package/dist/workflows/dag/frontend-review-scopes.js +92 -2
  193. package/dist/workflows/dag/frontend-risk.js +8 -3
  194. package/dist/workflows/dag/frontend-root-observation.js +261 -0
  195. package/dist/workflows/dag/frontend-session-budget.js +52 -39
  196. package/dist/workflows/dag/frontend-session-context.js +10 -0
  197. package/dist/workflows/dag/frontend-test-execution-evidence.js +206 -48
  198. package/dist/workflows/dag/frontend-typed-event-store.js +11 -1
  199. package/dist/workflows/dag/frontend-verification-trace.js +6 -0
  200. package/dist/workflows/dag/frontend-writer-admission.js +3 -1
  201. package/dist/workflows/dag/init-hybrid.js +108 -37
  202. package/dist/workflows/dag/node-execution.js +11 -11
  203. package/dist/workflows/dag/output-protocol.js +1 -0
  204. package/dist/workflows/dag/rerun-feedback.js +35 -4
  205. package/dist/workflows/dag/rerun-task.js +62 -66
  206. package/dist/workflows/dag/runner.js +58 -12
  207. package/dist/workflows/dag/types.js +15 -8
  208. package/dist/workflows/loop/benchmark.js +11 -8
  209. package/docs/README.md +1 -1
  210. package/docs/architecture/runtime-boundaries.md +1 -1
  211. package/docs/governance/harness-methodology-verification.md +11 -0
  212. package/docs/templates/backend-test-dag.json +5 -4
  213. package/docs/templates/frontend-implementation-contract.schema.json +31 -11
  214. package/package.json +6 -4
  215. package/skills/frontend-design-review/SKILL.md +8 -11
  216. package/skills/frontend-design-review/references/review-checklist.md +3 -4
  217. package/skills/frontend-review/SKILL.md +5 -1
  218. package/skills/loop-agent/references/command-reference.md +8 -0
  219. package/skills/loop-agent/references/hybrid-dag.md +1 -1
  220. package/dist/worker/console/static/assets/channel-ChE7y-cx.js +0 -1
  221. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C01TCf2X.js +0 -1
  222. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C01TCf2X.js +0 -1
  223. package/dist/worker/console/static/assets/index-BWkIfcrK.css +0 -1
  224. package/dist/worker/console/static/assets/index-Cdkvw_H6.js +0 -469
  225. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DC7V5vcq.js +0 -1
  226. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Bwy4QUTO.js +0 -8
@@ -8,6 +8,8 @@ import { mergePendingSubmissionOnHydration } from "./pending-user-message.js";
8
8
  import { taskContextUnchanged } from "./format.js";
9
9
  import { upsertCompactionMessage } from "./compaction-message.js";
10
10
  import { sameTurnGroupInputs } from "./turn-group-equality.js";
11
+ import { conversationNodesFromEvents, } from "../../chat/conversation-nodes.js";
12
+ import { mergeTurnStateSummaries, setProjectedTurnState, } from "./turn-state-projection.js";
11
13
  const EMPTY_USAGE = {
12
14
  inputTokens: 0,
13
15
  outputTokens: 0,
@@ -140,6 +142,14 @@ export function useChatThread(params) {
140
142
  const [usage, setUsage] = useState(EMPTY_USAGE);
141
143
  const [sessionStats, setSessionStats] = useState(EMPTY_SESSION_STATS);
142
144
  const [turnTelemetry, setTurnTelemetry] = useState([]);
145
+ const [userTurnOutline, setUserTurnOutline] = useState([]);
146
+ const [operatorActions, setOperatorActions] = useState([]);
147
+ const [conversationNodes, setConversationNodes] = useState([]);
148
+ const [turnStatesById, setTurnStatesById] = useState({});
149
+ const reconcileTurnStates = useCallback((summaries) => {
150
+ setTurnStatesById((current) => mergeTurnStateSummaries(current, summaries));
151
+ }, []);
152
+ const [sddSessionIdentity, setSddSessionIdentity] = useState(null);
143
153
  const [streaming, setStreaming] = useState(false);
144
154
  const [turnPhase, setTurnPhase] = useState("idle");
145
155
  const [deferredTurn, setDeferredTurn] = useState(null);
@@ -167,11 +177,13 @@ export function useChatThread(params) {
167
177
  if (body.usage)
168
178
  setUsage(body.usage);
169
179
  setTurnTelemetry(body.turnTelemetry ?? []);
180
+ setSddSessionIdentity(body.sddDataAlignment ?? null);
181
+ reconcileTurnStates(body.session?.turnStates);
170
182
  }
171
183
  catch {
172
184
  // Telemetry is supplementary; the normal event stream remains authoritative.
173
185
  }
174
- }, [origin, refs]);
186
+ }, [origin, refs, reconcileTurnStates]);
175
187
  // All inputs are stable (state setters + the memoized refs bag), so the
176
188
  // applier identity is stable across renders.
177
189
  const applyChatSseEvent = useMemo(() => createChatSseEventApplier({
@@ -188,6 +200,10 @@ export function useChatThread(params) {
188
200
  setPendingQuestion,
189
201
  setTodos,
190
202
  setGoal,
203
+ setOperatorActions,
204
+ setConversationNodes,
205
+ setTurnState: (turnId, state) => setTurnStatesById((current) => setProjectedTurnState(current, turnId, state)),
206
+ setSddSessionIdentity,
191
207
  setStreamingAssistantId,
192
208
  setUnread,
193
209
  setOperationCards,
@@ -325,6 +341,8 @@ export function useChatThread(params) {
325
341
  toolCalls,
326
342
  streaming,
327
343
  activeAssistantId: streamingAssistantId,
344
+ conversationNodes,
345
+ turnStatesById,
328
346
  });
329
347
  // Preserve completed group objects across streaming tokens. Stable
330
348
  // historical groups keep their references so
@@ -343,11 +361,13 @@ export function useChatThread(params) {
343
361
  toolCalls,
344
362
  streaming,
345
363
  streamingAssistantId,
364
+ conversationNodes,
365
+ turnStatesById,
346
366
  historyLimit,
347
367
  ]);
348
- const loadEarlierMessages = useCallback(async (beforePrepend) => {
368
+ const loadHistoryPages = useCallback(async (input) => {
349
369
  const targetSessionId = refs.sessionRef.current?.sessionId;
350
- const beforeId = historyNextCursor;
370
+ let beforeId = historyNextCursor;
351
371
  if (!targetSessionId || !historyHasMore || !beforeId || historyLoadingRef.current)
352
372
  return false;
353
373
  const generation = refs.sessionGenerationRef.current;
@@ -355,17 +375,29 @@ export function useChatThread(params) {
355
375
  historyLoadingRef.current = request;
356
376
  setHistoryLoading(true);
357
377
  try {
358
- const query = new URLSearchParams({ limit: "50", beforeId });
359
- const res = await fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(targetSessionId)}/messages?${query.toString()}`, { credentials: "include" });
360
- if (!res.ok || refs.sessionRef.current?.sessionId !== targetSessionId)
361
- return false;
362
- const body = (await res.json());
363
- if (refs.sessionRef.current?.sessionId !== targetSessionId ||
364
- refs.sessionGenerationRef.current !== generation ||
365
- historyLoadingRef.current !== request)
366
- return false;
367
- const page = hydrateSessionThread(body.messages ?? []);
368
- 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?.();
369
401
  setMessages((current) => {
370
402
  const seen = new Set();
371
403
  const merged = [...page.messages, ...current].filter((message) => {
@@ -382,10 +414,11 @@ export function useChatThread(params) {
382
414
  byId.set(tool.id, tool);
383
415
  return [...byId.values()].sort((a, b) => a.createdAt - b.createdAt);
384
416
  });
385
- setHistoryHasMore(body.hasMore === true);
386
- setHistoryNextCursor(body.nextCursor ?? null);
387
- setHistoryLimit((value) => value + 50);
388
- 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;
389
422
  }
390
423
  catch (error) {
391
424
  if (refs.sessionRef.current?.sessionId === targetSessionId && refs.sessionGenerationRef.current === generation) {
@@ -400,6 +433,10 @@ export function useChatThread(params) {
400
433
  }
401
434
  }
402
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]);
403
440
  const visibleMessages = useMemo(() => {
404
441
  const next = [];
405
442
  for (const group of turnGroups) {
@@ -421,6 +458,11 @@ export function useChatThread(params) {
421
458
  setUsage(EMPTY_USAGE);
422
459
  setSessionStats(EMPTY_SESSION_STATS);
423
460
  setTurnTelemetry([]);
461
+ setUserTurnOutline([]);
462
+ setOperatorActions([]);
463
+ setConversationNodes([]);
464
+ setTurnStatesById({});
465
+ setSddSessionIdentity(null);
424
466
  setStreaming(false);
425
467
  setTurnPhase("idle");
426
468
  setDeferredTurn(null);
@@ -469,6 +511,8 @@ export function useChatThread(params) {
469
511
  }
470
512
  : undefined));
471
513
  setToolCalls(next.toolCalls);
514
+ setConversationNodes(conversationNodesFromEvents(next.conversationEvents ?? []));
515
+ setTurnStatesById(mergeTurnStateSummaries({}, next.turnStates));
472
516
  setOperationCards([]);
473
517
  setHumanGateCards([]);
474
518
  setArtifactCards([]);
@@ -477,6 +521,9 @@ export function useChatThread(params) {
477
521
  setUsage(next.usage ?? EMPTY_USAGE);
478
522
  setSessionStats(next.sessionStats ?? EMPTY_SESSION_STATS);
479
523
  setTurnTelemetry(next.turnTelemetry ?? []);
524
+ setUserTurnOutline(next.userTurnOutline ?? []);
525
+ setOperatorActions([]);
526
+ setSddSessionIdentity(next.sddSessionIdentity ?? null);
480
527
  refs.usageResponseKeysRef.current = new Set();
481
528
  setStreaming(visuallyStopped ? false : next.streaming);
482
529
  setTurnPhase(visuallyStopped
@@ -500,13 +547,15 @@ export function useChatThread(params) {
500
547
  * operation/human-gate/artifact/interview cards, which the reconnected tail
501
548
  * stream will not replay (they live in the ring, already consumed). */
502
549
  const recoverSession = useCallback(async (targetSessionId) => {
550
+ const targetGeneration = refs.sessionGenerationRef.current;
503
551
  try {
504
552
  const res = await fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(targetSessionId)}`, { credentials: "include" });
505
553
  if (!res.ok)
506
554
  return false;
507
555
  const body = (await res.json());
508
556
  if (!body.session ||
509
- refs.sessionRef.current?.sessionId !== targetSessionId)
557
+ refs.sessionRef.current?.sessionId !== targetSessionId ||
558
+ refs.sessionGenerationRef.current !== targetGeneration)
510
559
  return false;
511
560
  const activeTurnId = body.session.activeTurnId ?? null;
512
561
  const visuallyStopped = body.session.turnPhase === "stopping";
@@ -526,6 +575,8 @@ export function useChatThread(params) {
526
575
  if (body.usage)
527
576
  setUsage(body.usage);
528
577
  setTurnTelemetry(body.turnTelemetry ?? []);
578
+ setUserTurnOutline(body.userTurnOutline ?? []);
579
+ setSddSessionIdentity(body.sddDataAlignment ?? null);
529
580
  // Resume the events stream from the snapshot tail so the next
530
581
  // reconnect never immediately re-expires (AC-003). The refs below
531
582
  // are reset so any follow-up tail replay re-applies cleanly.
@@ -534,7 +585,15 @@ export function useChatThread(params) {
534
585
  refs.turnAssistantIdsRef.current = new Map();
535
586
  refs.sealedAssistantIdsRef.current = new Set();
536
587
  const hydrated = hydrateSessionThread(body.session.messages ?? []);
537
- const pending = refs.pendingSubmissionRef.current;
588
+ setConversationNodes(conversationNodesFromEvents(body.conversationEvents ?? []));
589
+ setTurnStatesById((current) => mergeTurnStateSummaries(current, body.session?.turnStates));
590
+ // Recovery is addressed to targetSessionId, so an optimistic
591
+ // submission from another session must never be projected into
592
+ // this snapshot (the activation path enforces the same owner gate).
593
+ const pending = refs.pendingSubmissionRef.current?.sessionId === targetSessionId &&
594
+ refs.pendingSubmissionRef.current.sessionGeneration === targetGeneration
595
+ ? refs.pendingSubmissionRef.current
596
+ : undefined;
538
597
  const recovered = mergePendingSubmissionOnHydration(hydrated.messages, pending
539
598
  ? {
540
599
  clientRequestId: pending.clientRequestId,
@@ -590,6 +649,7 @@ export function useChatThread(params) {
590
649
  historyNextCursor,
591
650
  historyLoading,
592
651
  loadEarlierMessages,
652
+ loadThroughMessage,
593
653
  unread,
594
654
  humanGateCards,
595
655
  interview,
@@ -599,6 +659,13 @@ export function useChatThread(params) {
599
659
  usage,
600
660
  sessionStats,
601
661
  turnTelemetry,
662
+ userTurnOutline,
663
+ operatorActions,
664
+ conversationNodes,
665
+ turnStatesById,
666
+ reconcileTurnStates,
667
+ sddSessionIdentity,
668
+ setSddSessionIdentity,
602
669
  streaming,
603
670
  setStreaming,
604
671
  turnPhase,
@@ -621,6 +688,7 @@ export function useChatThread(params) {
621
688
  applySseBlock,
622
689
  loadTaskContext,
623
690
  applyOperationLinkMutation,
691
+ refreshSessionTelemetry,
624
692
  resetForNewSession,
625
693
  applyActivatedSession,
626
694
  recoverSession,
@@ -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()}`, {