@tea-agent/loop-agent 0.39.0-next.3 → 0.39.0-next.31

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 (238) hide show
  1. package/AGENTS.md +4 -1
  2. package/CHANGELOG.md +270 -65
  3. package/README.md +11 -5
  4. package/bin/loop-agent.js +7 -3
  5. package/dist/application/dag/generate-task-dag.js +9 -0
  6. package/dist/application/task-lifecycle/advance.js +4 -0
  7. package/dist/build-stamp.json +3 -3
  8. package/dist/cli/command-definitions.js +16 -5
  9. package/dist/cli/program.js +16 -2
  10. package/dist/cli/update/notifier.js +2 -2
  11. package/dist/cli.js +2 -1
  12. package/dist/commands/dag-rerun.js +55 -1
  13. package/dist/commands/init-model-catalog.js +464 -0
  14. package/dist/commands/init-upgrade.js +265 -97
  15. package/dist/commands/init.js +428 -27
  16. package/dist/commands/inspect-next.js +8 -0
  17. package/dist/commands/task-advance.js +31 -0
  18. package/dist/executors/dag-pi-executor.js +118 -14
  19. package/dist/executors/pi-event-serializer.js +42 -1
  20. package/dist/executors/pi-executor.js +51 -0
  21. package/dist/executors/pi-extension-resolver.js +233 -0
  22. package/dist/executors/pi-playwright-cli-tool.js +51 -12
  23. package/dist/executors/pi-sdk-executor.js +187 -59
  24. package/dist/executors/shell-executor.js +533 -29
  25. package/dist/executors/shell-write-guard.js +7 -0
  26. package/dist/shared/dag-prompt-override.js +28 -0
  27. package/dist/shared/operator/capabilities.js +181 -27
  28. package/dist/shared/playwright-cli-command-policy.js +15 -0
  29. package/dist/shared/update/console-notifier.js +100 -0
  30. package/dist/{cli → shared}/update/npm-client.js +41 -8
  31. package/dist/{cli → shared}/update/state.js +41 -13
  32. package/dist/task/config-types.js +22 -0
  33. package/dist/task/contract/constants.js +1 -0
  34. package/dist/task/contract/diff.js +26 -0
  35. package/dist/task/contract/project.js +5 -0
  36. package/dist/task/contract/schema.js +6 -1
  37. package/dist/task/source-prepare/build-draft.js +15 -0
  38. package/dist/task/source-prepare/parse-intent.js +24 -4
  39. package/dist/worker/cli.js +31 -1
  40. package/dist/worker/console/chat/deferred-turn.js +91 -0
  41. package/dist/worker/console/chat/mcp-inventory.js +221 -0
  42. package/dist/worker/console/chat/pi-runtime.js +165 -83
  43. package/dist/worker/console/chat/repo-walk.js +10 -1
  44. package/dist/worker/console/chat/routes.js +478 -55
  45. package/dist/worker/console/chat/semantic-activity.js +20 -8
  46. package/dist/worker/console/chat/session-store.js +146 -2
  47. package/dist/worker/console/chat/turn-process.js +17 -30
  48. package/dist/worker/console/dag-execution-receipt.js +33 -0
  49. package/dist/worker/console/harness-pi-model-matrix.js +137 -0
  50. package/dist/worker/console/operator-actions.js +244 -4
  51. package/dist/worker/console/operator-user-error.js +20 -1
  52. package/dist/worker/console/pi-readiness.js +12 -5
  53. package/dist/worker/console/recovery-cta.js +4 -4
  54. package/dist/worker/console/recovery-error-copy.js +4 -4
  55. package/dist/worker/console/routes.js +43 -2
  56. package/dist/worker/console/security.js +83 -10
  57. package/dist/worker/console/server.js +42 -6
  58. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-DSvF6RXQ.js → abnfDiagram-N423BO3Z-gauhD9kD.js} +1 -1
  59. package/dist/worker/console/static/assets/{arc-DuI4ogtJ.js → arc-pm3c2Rsl.js} +1 -1
  60. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DdBbgD4y.js → architectureDiagram-T3A2C74G-D_JAg3wH.js} +1 -1
  61. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-xQC2HELm.js → blockDiagram-VBNYF7ZC-Dk7-ex52.js} +1 -1
  62. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DPMSgwIZ.js → c4Diagram-5PPSVZJV-Uval9oRI.js} +1 -1
  63. package/dist/worker/console/static/assets/channel-CVrnUdZl.js +1 -0
  64. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-WTEKTxin.js → chunk-2GRJ4B5K-BSYjHs-v.js} +1 -1
  65. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-B6vZ0Pv_.js → chunk-2Q5K7J3B-KGUuBXI8.js} +1 -1
  66. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BrerNAix.js → chunk-5RXB4S5H-JqAtxD9N.js} +1 -1
  67. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-D_ch2aaI.js → chunk-5VM5RSS4-D6EJOVQK.js} +1 -1
  68. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-VvfacNrq.js → chunk-6Q2QTUOP-B1JriSjm.js} +1 -1
  69. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-B-uGFIg2.js → chunk-GF5L2VYU-D62b96Jd.js} +1 -1
  70. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BlysCtod.js → chunk-JWPE2WC7-BsNKEC_r.js} +1 -1
  71. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BjAUAfcT.js → chunk-KBJHAD2P-B56v6-ZP.js} +1 -1
  72. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-BeZnBmb-.js → chunk-RYQCIY6F-D2KPnd8K.js} +1 -1
  73. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-Bbfe3UJG.js → chunk-XXDRQBXY-kzKSCaaL.js} +1 -1
  74. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-aX91WTya.js +1 -0
  75. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-aX91WTya.js +1 -0
  76. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BKJQnYVM.js → cose-bilkent-JH36ORCC-C6N6KNJd.js} +1 -1
  77. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-B1wtI3hU.js → cynefin-VYW2F7L2-DiyymT2D.js} +1 -1
  78. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-DCF6DBSu.js → cynefinDiagram-MW4NZA55-BeKAXvf3.js} +1 -1
  79. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BJyNGRlP.js → dagre-VZM6K2ZE-TkIdXfPF.js} +1 -1
  80. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-B7j1-VsZ.js → diagram-7IWD3JNH-BFmSG3ks.js} +1 -1
  81. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BREJ140a.js → diagram-B4RE2ZJO-DqpJT2Fs.js} +1 -1
  82. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-CDPHmHGH.js → diagram-LBJQPF4R-r5ATN43t.js} +1 -1
  83. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BtZLrez1.js → diagram-Q27KOJAE-DEcSCMuR.js} +1 -1
  84. package/dist/worker/console/static/assets/{diagram-UB23O5K3-mzybu8oU.js → diagram-UB23O5K3-VOvYRLVc.js} +1 -1
  85. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Dk3I2uO6.js → ebnfDiagram-BXEA7PRR-D7f4Cb1u.js} +1 -1
  86. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-BT97u_tQ.js → erDiagram-JOGREHBK-ziUf3U0G.js} +1 -1
  87. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CMOtYzui.js → flowDiagram-UKHOOZJN-BoRdQ-1y.js} +1 -1
  88. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DiNydkuY.js → ganttDiagram-PKOTCBZU-B1-GncNm.js} +1 -1
  89. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Cwt6fLkl.js → gitGraphDiagram-DS77QQ5N-CTW-ph8W.js} +1 -1
  90. package/dist/worker/console/static/assets/index-C8rOIAJr.css +1 -0
  91. package/dist/worker/console/static/assets/index-DEbyrEfW.js +404 -0
  92. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-AsGIGKKB.js → infoDiagram-6WML65LV-BFA337tZ.js} +1 -1
  93. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-BL8ITZh3.js → ishikawaDiagram-WSZJBQD7-BtEujgRR.js} +1 -1
  94. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-VPKAUX8M.js → journeyDiagram-NVQOT4AX-C8ZwD2Jp.js} +1 -1
  95. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BL56hfGp.js → kanban-definition-27J2QSJJ-Ck-z8B11.js} +1 -1
  96. package/dist/worker/console/static/assets/{linear-BmSG3nIZ.js → linear-CAyXjQjN.js} +1 -1
  97. package/dist/worker/console/static/assets/{mermaid.core-DWGK9kwP.js → mermaid.core-CcWKn6tS.js} +5 -5
  98. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS--vvKLjo0.js → mindmap-definition-FAOFIHXS-C2NuDO7E.js} +1 -1
  99. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-B41mxb2-.js → pegDiagram-VL7TDLO6-BFub3PDz.js} +1 -1
  100. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DTlu77sw.js → pieDiagram-7S7Q4E2Y-BJx5THZ5.js} +1 -1
  101. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-Bk5A7alv.js → quadrantDiagram-CIZ2JOQS-DhalkDWV.js} +1 -1
  102. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Csf9jzQy.js → railroadDiagram-AXF67PYL-CVcBM36o.js} +1 -1
  103. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CeptoSNx.js → requirementDiagram-LRYGKXZP-D_vy4n0w.js} +1 -1
  104. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-A-DLCLer.js → sankeyDiagram-W5VNT64P-DGYdJL1D.js} +1 -1
  105. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-22VbPdOI.js → sequenceDiagram-SI44F4Z6-C1xJFg3k.js} +1 -1
  106. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DWvAyBC9.js → sizeCapture-X5ZJPWSS-D_Fc43df.js} +1 -1
  107. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-qICZ75yT.js → stateDiagram-OKZ733FA-BhGbIjbu.js} +1 -1
  108. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DUt1lyFL.js +1 -0
  109. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-xi7lcMyp.js → swimlanes-SLNWSIFB-DVkNcO-V.js} +2 -2
  110. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BM6SGr7j.js +8 -0
  111. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-Bu6Nl6O2.js → timeline-definition-Z64GVDOM-BQU9ufV0.js} +1 -1
  112. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BzMz4Bn9.js → vennDiagram-T6HMQDX7-BCQhRmYB.js} +1 -1
  113. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CxaJnzc5.js → wardleyDiagram-T6FBY63Y-DWerOyHO.js} +1 -1
  114. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-5JK-omQK.js → xychartDiagram-ELKLHX3M-DxLeebQU.js} +1 -1
  115. package/dist/worker/console/static/index.html +2 -2
  116. package/dist/worker/console/static-src/app/console-types.js +39 -0
  117. package/dist/worker/console/static-src/app/useOperatorActions.js +1 -1
  118. package/dist/worker/console/static-src/app/useRecoveryActions.js +2 -2
  119. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +82 -22
  120. package/dist/worker/console/static-src/operator-chat/mutation-gate.js +45 -0
  121. package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +328 -0
  122. package/dist/worker/console/static-src/operator-chat/resource-diagnostics.js +204 -0
  123. package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +8 -1
  124. package/dist/worker/console/static-src/operator-chat/turn-submission.js +42 -0
  125. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +18 -0
  126. package/dist/worker/console/static-src/operator-chat/useChatStream.js +216 -10
  127. package/dist/worker/console/static-src/operator-chat/useChatThread.js +50 -22
  128. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +26 -1
  129. package/dist/worker/loop-agent/loop-agent-client.js +7 -3
  130. package/dist/worker/observe/node-input.js +74 -3
  131. package/dist/worker/observe/node-process.js +377 -0
  132. package/dist/worker/observe/routes.js +36 -3
  133. package/dist/worker/observe/shell-handler-keys.js +34 -0
  134. package/dist/worker/observe/static/api.js +66 -0
  135. package/dist/worker/observe/static/constants.js +8 -1
  136. package/dist/worker/observe/static/dag-history-labels.js +4 -0
  137. package/dist/worker/observe/static/format.js +23 -0
  138. package/dist/worker/observe/static/markdown-render.js +20 -1
  139. package/dist/worker/observe/static/operator-chrome.js +4 -0
  140. package/dist/worker/observe/static/state.js +13 -2
  141. package/dist/worker/observe/static/styles.css +73 -0
  142. package/dist/worker/observe/static/views/dag-inspector.js +612 -8
  143. package/dist/worker/observe/static/views/dag.js +8 -1
  144. package/dist/worker/observe/static/views/failures.js +5 -2
  145. package/dist/worker/observe/static/views/session-timeline.js +78 -9
  146. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +827 -53
  147. package/dist/workflows/dag/backend-test-case-manifest.js +4 -0
  148. package/dist/workflows/dag/backend-test-execution-contract.js +18 -9
  149. package/dist/workflows/dag/backend-test-gap-fill.js +205 -0
  150. package/dist/workflows/dag/backend-test-intake-context.js +32 -0
  151. package/dist/workflows/dag/backend-test-layout.js +133 -0
  152. package/dist/workflows/dag/backend-test-markdown-workflow.js +167 -32
  153. package/dist/workflows/dag/backend-test-module-stem.js +5 -0
  154. package/dist/workflows/dag/backend-test-pytest-collection.js +368 -33
  155. package/dist/workflows/dag/backend-test-result-contract.js +1 -1
  156. package/dist/workflows/dag/backend-test-scenario-param.js +884 -140
  157. package/dist/workflows/dag/backend-test-scenario-partitions.js +321 -0
  158. package/dist/workflows/dag/backend-test-writer-completeness.js +100 -56
  159. package/dist/workflows/dag/contract-output-registry.js +15 -0
  160. package/dist/workflows/dag/contract-validator-registrations.js +3 -1
  161. package/dist/workflows/dag/convergence/controller.js +141 -0
  162. package/dist/workflows/dag/dynamic-runtime/map.js +24 -8
  163. package/dist/workflows/dag/dynamic-runtime/shared.js +1 -1
  164. package/dist/workflows/dag/failure-category.js +12 -0
  165. package/dist/workflows/dag/frontend-implementation-contract.js +493 -2
  166. package/dist/workflows/dag/frontend-plan-render.js +90 -0
  167. package/dist/workflows/dag/frontend-prewrite-gate.js +221 -31
  168. package/dist/workflows/dag/frontend-repair.js +14 -0
  169. package/dist/workflows/dag/frontend-risk.js +15 -2
  170. package/dist/workflows/dag/frontend-test-case-checklist.js +26 -2
  171. package/dist/workflows/dag/frontend-test-case-manifest.js +5 -0
  172. package/dist/workflows/dag/frontend-test-case-quality.js +5 -13
  173. package/dist/workflows/dag/frontend-test-environment-probe.js +227 -0
  174. package/dist/workflows/dag/frontend-test-markdown.js +61 -0
  175. package/dist/workflows/dag/frontend-test-result-contract.js +160 -28
  176. package/dist/workflows/dag/frontend-test-standard-scenarios.js +68 -0
  177. package/dist/workflows/dag/frontend-verification-trace.js +29 -4
  178. package/dist/workflows/dag/init-hybrid.js +895 -127
  179. package/dist/workflows/dag/node-execution.js +194 -6
  180. package/dist/workflows/dag/prompt.js +60 -1
  181. package/dist/workflows/dag/rerun-feedback.js +212 -0
  182. package/dist/workflows/dag/rerun-plan.js +224 -27
  183. package/dist/workflows/dag/rerun-run.js +61 -6
  184. package/dist/workflows/dag/rerun-task.js +61 -2
  185. package/dist/workflows/dag/retry-policy.js +113 -0
  186. package/dist/workflows/dag/structured-output-repair.js +712 -0
  187. package/dist/workflows/dag/types.js +157 -1
  188. package/dist/workflows/dag/validate.js +41 -1
  189. package/docs/init-surface.manifest.json +5 -0
  190. package/docs/operations/README.md +1 -1
  191. package/docs/operations/local-development-environment.md +1 -5
  192. package/docs/skills/vetted-skill-registry.md +14 -0
  193. package/docs/templates/README.md +1 -1
  194. package/docs/templates/agent-dag.schema.json +44 -0
  195. package/docs/templates/backend-test-dag.json +60 -35
  196. package/docs/templates/frontend-implementation-contract.schema.json +9 -2
  197. package/docs/templates/frontend-test-dag.json +8 -10
  198. package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +1 -1
  199. package/docs/templates/init-managed-agents.md +13 -3
  200. package/harness.json +5 -3
  201. package/package.json +12 -4
  202. package/scripts/next-info.mjs +356 -0
  203. package/scripts/next-publish-gate.mjs +238 -0
  204. package/scripts/release-source-binding.mjs +251 -0
  205. package/skills/analyze-product-requirements/SKILL.md +8 -5
  206. package/skills/analyze-product-requirements/references/acceptance-criteria.md +1 -1
  207. package/skills/analyze-product-requirements/references/clarification-and-knowledge.md +10 -6
  208. package/skills/analyze-product-requirements/references/example.md +50 -0
  209. package/skills/analyze-product-requirements/references/forward-test-cases.md +11 -7
  210. package/skills/analyze-product-requirements/references/kb-integration.md +5 -5
  211. package/skills/analyze-product-requirements/references/product-analysis-schema.md +8 -4
  212. package/skills/analyze-product-requirements/references/product-requirement-schema.md +2 -2
  213. package/skills/analyze-product-requirements/references/requirement-clarification-schema.md +1 -1
  214. package/skills/analyze-product-requirements/scripts/test-validators.mjs +11 -1
  215. package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +20 -1
  216. package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +9 -0
  217. package/skills/codebase-scout/SKILL.md +1 -1
  218. package/skills/fe-test-ui-scout/SKILL.md +65 -0
  219. package/skills/fe-test-ui-scout/references/ledger-schema.md +62 -0
  220. package/skills/fe-test-ui-scout/references/recon-protocol.md +54 -0
  221. package/skills/frontend-design-review/references/review-checklist.md +2 -0
  222. package/skills/frontend-implementation/references/design-spec.md +16 -19
  223. package/skills/frontend-implementation/references/node-contracts.md +9 -6
  224. package/skills/improve-codebase-architecture/SKILL.md +81 -0
  225. package/skills/improve-codebase-architecture/deepening.md +37 -0
  226. package/skills/improve-codebase-architecture/html-report.md +123 -0
  227. package/skills/improve-codebase-architecture/interface-design.md +44 -0
  228. package/skills/improve-codebase-architecture/language.md +53 -0
  229. package/skills/loop-agent/references/command-reference.md +11 -4
  230. package/skills/playwright-cli/SKILL.md +22 -0
  231. package/skills/playwright-cli-case-generator/SKILL.md +20 -1
  232. package/dist/worker/console/static/assets/channel-DLVz9TYW.js +0 -1
  233. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BeAuGcO9.js +0 -1
  234. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BeAuGcO9.js +0 -1
  235. package/dist/worker/console/static/assets/index-DQxiso1w.js +0 -325
  236. package/dist/worker/console/static/assets/index-_j0Hzo69.css +0 -1
  237. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DzKvfHTX.js +0 -1
  238. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Cxqr_qM1.js +0 -8
@@ -1,8 +1,8 @@
1
1
  import { useCallback, useEffect } from "react";
2
2
  import { formatOperatorUserError } from "../../operator-user-error.js";
3
3
  import { confirmationToken, requestId, sleep } from "./format.js";
4
- import { classifyCreateTurnNetworkFailure, classifyCreateTurnResponse, createPendingSubmission, isRuntimeBusyUnion, isSubmissionCurrent, isTerminalTurnState, keepAmbiguousAfterRetry, reconcilePendingSubmissionFromState, resolveAmbiguousFromState, } from "./turn-submission.js";
5
- import { applyAbortDecision, applyPendingReconcileDecision, classifyAbortResponse, markStopRequested, operatorChatAbortUrl, operatorChatEventsUrl, operatorChatStateUrl, operatorChatTurnsUrl, releaseOwnershipOnTerminalEvidence, releaseSubmitOwnership as releaseOwnershipBag, requestStopOnOwnership, settleCreateTurnOutcome, } from "./turn-stream-controller.js";
4
+ import { classifyCreateTurnNetworkFailure, classifyCreateTurnResponse, createPendingSubmission, decideSubmissionRoute, isRuntimeBusyUnion, isSubmissionCurrent, isTerminalTurnState, keepAmbiguousAfterRetry, reconcileDeferredSubmissionFromState, reconcilePendingSubmissionFromState, resolveAmbiguousFromState, } from "./turn-submission.js";
5
+ import { applyAbortDecision, applyPendingReconcileDecision, classifyAbortResponse, markStopRequested, operatorChatAbortUrl, operatorChatEventsUrl, operatorChatStateUrl, operatorChatTurnsUrl, releaseOwnershipOnTerminalEvidence, releaseSubmitOwnership as releaseOwnershipBag, requestStopOnOwnership, settleCreateTurnOutcome, turnPhaseFromOwnership, } from "./turn-stream-controller.js";
6
6
  /** Project ChatRefs into the executable ownership bag used by the controller. */
7
7
  function ownershipBagFromRefs(refs) {
8
8
  return {
@@ -57,7 +57,7 @@ function clearEmptyAssistant(setMessages, assistantId) {
57
57
  * detached Turn resource only; it never opens a second inline SSE consumer. */
58
58
  export function useChatStream(params) {
59
59
  const { origin, refs, session, thread, composer, setError, setAutoScroll, setNotice, recoveryVersion = 0, watchSessionTitle, } = params;
60
- const { applySseBlock, setMessages, setStreaming, setStreamingAssistantId, streaming, } = thread;
60
+ const { applySseBlock, setMessages, setStreaming, setStreamingAssistantId, streaming, setTurnPhase, setDeferredTurn, } = thread;
61
61
  const { setInput, setPendingImages } = composer;
62
62
  const showNotice = useCallback((message) => {
63
63
  if (!setNotice)
@@ -78,9 +78,10 @@ export function useChatStream(params) {
78
78
  refs.assistantIdRef.current = null;
79
79
  refs.stoppingTurnIdRef.current = null;
80
80
  setStreaming(false);
81
+ setTurnPhase("idle");
81
82
  setStreamingAssistantId(null);
82
83
  clearEmptyAssistant(setMessages, assistantId);
83
- }, [refs, setStreaming, setStreamingAssistantId, setMessages]);
84
+ }, [refs, setStreaming, setTurnPhase, setStreamingAssistantId, setMessages]);
84
85
  /** Follow one session's event stream until it is explicitly aborted. The
85
86
  * server's `stream_ready` marker separates replay from live events; this
86
87
  * means replay cannot render duplicate messages while live text remains
@@ -242,9 +243,13 @@ export function useChatStream(params) {
242
243
  /** Issue real POST /abort for a known turnId (Stop never disposes Session). */
243
244
  const postAbortForTurn = useCallback(async (sessionId, generation, turnId) => {
244
245
  refs.stoppingTurnIdRef.current = turnId;
245
- // Keep streaming/busy while abort is in flight (UIS-stopping).
246
- refs.streamingRef.current = true;
247
- setStreaming(true);
246
+ // Optimistic visual stop: remove the running spinner immediately, but
247
+ // retain ownership as `stopping` until the server proves a terminal state.
248
+ // This keeps the composer responsive without falsely advertising idle.
249
+ refs.streamingRef.current = false;
250
+ setStreaming(false);
251
+ setTurnPhase("stopping");
252
+ setStreamingAssistantId(null);
248
253
  try {
249
254
  const res = await fetch(operatorChatAbortUrl(origin, sessionId, turnId), {
250
255
  method: "POST",
@@ -278,6 +283,7 @@ export function useChatStream(params) {
278
283
  markIdle: () => markIdleIfCurrent(sessionId, generation, refs.assistantIdRef.current),
279
284
  });
280
285
  applyOwnershipBagToRefs(refs, bag);
286
+ setTurnPhase(turnPhaseFromOwnership(bag));
281
287
  return;
282
288
  }
283
289
  applyAbortDecision(bag, decision, noticeSink(), {
@@ -285,6 +291,11 @@ export function useChatStream(params) {
285
291
  markIdle: () => markIdleIfCurrent(sessionId, generation, refs.assistantIdRef.current),
286
292
  });
287
293
  applyOwnershipBagToRefs(refs, bag);
294
+ setTurnPhase(turnPhaseFromOwnership(bag));
295
+ // Sync the post-decision busy state back to React: optimistic stop
296
+ // flipped it idle up front; pending/keep-busy outcomes restore it.
297
+ if (bag.streaming)
298
+ setStreaming(true);
288
299
  }
289
300
  catch {
290
301
  // Abort network failure: keep busy (UIS-abort-network-fail).
@@ -297,11 +308,17 @@ export function useChatStream(params) {
297
308
  networkError: true,
298
309
  }), noticeSink());
299
310
  applyOwnershipBagToRefs(refs, bag);
311
+ // Network failure: the optimistic idle was premature — restore busy
312
+ // so the turn is not silently dropped; /state reconcile converges.
313
+ refs.streamingRef.current = true;
314
+ setStreaming(true);
315
+ setTurnPhase("stopping");
300
316
  }
301
317
  }, [
302
318
  origin,
303
319
  refs,
304
320
  setStreaming,
321
+ setTurnPhase,
305
322
  markIdleIfCurrent,
306
323
  fetchSessionState,
307
324
  noticeSink,
@@ -351,10 +368,12 @@ export function useChatStream(params) {
351
368
  },
352
369
  });
353
370
  applyOwnershipBagToRefs(refs, bag);
371
+ setTurnPhase(turnPhaseFromOwnership(bag));
354
372
  }, [
355
373
  refs,
356
374
  noticeSink,
357
375
  setStreaming,
376
+ setTurnPhase,
358
377
  setStreamingAssistantId,
359
378
  setPendingImages,
360
379
  watchSessionTitle,
@@ -363,20 +382,199 @@ export function useChatStream(params) {
363
382
  setError,
364
383
  postAbortForTurn,
365
384
  ]);
385
+ const queueDeferredTurn = useCallback(async (input) => {
386
+ const activeSession = refs.sessionRef.current;
387
+ if (!activeSession || (!input.text.trim() && input.images.length === 0))
388
+ return { action: "handled" };
389
+ const existing = thread.deferredTurn;
390
+ showNotice("正在排队…");
391
+ const acceptDeferred = (deferredTurn) => {
392
+ setDeferredTurn(deferredTurn);
393
+ setInput("");
394
+ setPendingImages([]);
395
+ setError(null);
396
+ showNotice(existing ? "已更新停止后发送的消息" : "已排队,将在上一轮停止后自动发送");
397
+ };
398
+ const acceptDispatched = () => {
399
+ setDeferredTurn(null);
400
+ setInput("");
401
+ setPendingImages([]);
402
+ setError(null);
403
+ showNotice("消息已进入发送");
404
+ };
405
+ const putOnce = async () => {
406
+ const response = await fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(activeSession.sessionId)}/deferred-turn`, {
407
+ method: "PUT",
408
+ headers: {
409
+ "Content-Type": "application/json",
410
+ "x-loop-console-confirmation": confirmationToken(),
411
+ },
412
+ credentials: "include",
413
+ body: JSON.stringify({
414
+ clientRequestId: input.clientRequestId,
415
+ blockedByTurnId: input.blockedByTurnId,
416
+ text: input.text,
417
+ images: input.images,
418
+ ...(existing ? { expectedRevision: existing.revision } : {}),
419
+ }),
420
+ });
421
+ const body = (await response.json().catch(() => ({})));
422
+ return { response, body };
423
+ };
424
+ try {
425
+ for (let attempt = 0; attempt < 2; attempt += 1) {
426
+ const { response, body } = await putOnce();
427
+ if (response.ok && body.accepted && body.deferredTurn) {
428
+ acceptDeferred(body.deferredTurn);
429
+ return { action: "handled" };
430
+ }
431
+ if (body.deferredTurn)
432
+ setDeferredTurn(body.deferredTurn);
433
+ if (body.error?.code !== "TURN_NOT_STOPPING") {
434
+ setError(body.error?.message ??
435
+ `待发送消息未被服务端接受(HTTP ${response.status})`);
436
+ return { action: "handled" };
437
+ }
438
+ const canonical = await fetchSessionState(activeSession.sessionId);
439
+ const decision = reconcileDeferredSubmissionFromState({
440
+ clientRequestId: input.clientRequestId,
441
+ blockedByTurnId: input.blockedByTurnId,
442
+ state: canonical ?? undefined,
443
+ });
444
+ if (decision.action === "deferred") {
445
+ acceptDeferred(decision.deferredTurn);
446
+ return { action: "handled" };
447
+ }
448
+ if (decision.action === "accepted") {
449
+ acceptDispatched();
450
+ return { action: "handled" };
451
+ }
452
+ if (decision.action === "retry-deferred" && attempt === 0)
453
+ continue;
454
+ if (decision.action === "send-now" && !existing) {
455
+ showNotice("上一轮已停止,正在发送当前消息…");
456
+ return { action: "send-now" };
457
+ }
458
+ setError(body.error?.message ?? "停止状态已变化,当前消息仍保留在输入框");
459
+ return { action: "handled" };
460
+ }
461
+ return { action: "handled" };
462
+ }
463
+ catch (error) {
464
+ const canonical = await fetchSessionState(activeSession.sessionId);
465
+ const decision = reconcileDeferredSubmissionFromState({
466
+ clientRequestId: input.clientRequestId,
467
+ blockedByTurnId: input.blockedByTurnId,
468
+ state: canonical ?? undefined,
469
+ });
470
+ if (decision.action === "deferred") {
471
+ acceptDeferred(decision.deferredTurn);
472
+ return { action: "handled" };
473
+ }
474
+ if (decision.action === "accepted") {
475
+ acceptDispatched();
476
+ return { action: "handled" };
477
+ }
478
+ if (decision.action === "send-now" && !existing) {
479
+ showNotice("上一轮已停止,正在发送当前消息…");
480
+ return { action: "send-now" };
481
+ }
482
+ setError(error instanceof Error
483
+ ? `无法确认消息是否已排队:${error.message}`
484
+ : "无法确认消息是否已排队,请刷新后查看服务端状态");
485
+ return { action: "handled" };
486
+ }
487
+ }, [
488
+ origin,
489
+ refs,
490
+ thread.deferredTurn,
491
+ setDeferredTurn,
492
+ setInput,
493
+ setPendingImages,
494
+ setError,
495
+ showNotice,
496
+ fetchSessionState,
497
+ ]);
498
+ const cancelDeferredTurn = useCallback(async () => {
499
+ const activeSession = refs.sessionRef.current;
500
+ const deferred = thread.deferredTurn;
501
+ if (!activeSession || !deferred)
502
+ return;
503
+ try {
504
+ const response = await fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(activeSession.sessionId)}/deferred-turn`, {
505
+ method: "DELETE",
506
+ headers: {
507
+ "Content-Type": "application/json",
508
+ "x-loop-console-confirmation": confirmationToken(),
509
+ },
510
+ credentials: "include",
511
+ body: JSON.stringify({
512
+ clientRequestId: deferred.clientRequestId,
513
+ expectedRevision: deferred.revision,
514
+ }),
515
+ });
516
+ const body = (await response.json().catch(() => ({})));
517
+ if (!response.ok || !body.cancelled) {
518
+ setError(body.error?.message ?? "待发送消息撤回失败");
519
+ return;
520
+ }
521
+ setDeferredTurn(null);
522
+ setInput(body.deferredTurn?.text ?? "");
523
+ setPendingImages(body.deferredTurn?.images ?? []);
524
+ setError(null);
525
+ showNotice("待发送消息已移回输入框");
526
+ }
527
+ catch (error) {
528
+ setError(error instanceof Error ? error.message : String(error));
529
+ }
530
+ }, [
531
+ origin,
532
+ refs,
533
+ thread.deferredTurn,
534
+ setDeferredTurn,
535
+ setInput,
536
+ setPendingImages,
537
+ setError,
538
+ showNotice,
539
+ ]);
366
540
  const sendPrompt = useCallback(async (text, images) => {
541
+ const initialSession = refs.sessionRef.current;
542
+ if (!initialSession || (!text.trim() && images.length === 0))
543
+ return;
544
+ const ownership = ownershipBagFromRefs(refs);
545
+ const route = decideSubmissionRoute({
546
+ turnPhase: turnPhaseFromOwnership(ownership),
547
+ blockedByTurnId: ownership.stoppingTurnId ?? ownership.activeTurnId,
548
+ });
549
+ const clientRequestId = requestId("chat-submit");
550
+ if (route.action === "hold") {
551
+ if (turnPhaseFromOwnership(ownership) === "stopping")
552
+ showNotice("正在确认上一轮停止状态,当前消息已保留");
553
+ return;
554
+ }
555
+ if (route.action === "queue-deferred") {
556
+ const deferred = await queueDeferredTurn({
557
+ text,
558
+ images,
559
+ clientRequestId,
560
+ blockedByTurnId: route.blockedByTurnId,
561
+ });
562
+ if (deferred.action !== "send-now")
563
+ return;
564
+ }
367
565
  const activeSession = refs.sessionRef.current;
368
566
  const generation = refs.sessionGenerationRef.current;
369
567
  if (!activeSession ||
568
+ activeSession.sessionId !== initialSession.sessionId ||
370
569
  (!text.trim() && images.length === 0) ||
371
570
  refs.streamingRef.current ||
372
571
  refs.submitInFlightRef.current ||
373
572
  // AC-R5-002: ownership lease blocks send until terminal release.
374
- Boolean(refs.pendingSubmissionRef.current))
573
+ refs.pendingSubmissionRef.current)
375
574
  return;
376
575
  // AC-001: acquire synchronous submit ownership before the first await.
377
576
  refs.submitInFlightRef.current = true;
378
577
  const submitGeneration = ++refs.submitGenerationRef.current;
379
- const clientRequestId = requestId("chat-submit");
380
578
  const userMessageId = requestId("u");
381
579
  const assistantId = requestId("a");
382
580
  const draftText = text.trim();
@@ -424,6 +622,7 @@ export function useChatStream(params) {
424
622
  setAutoScroll(true);
425
623
  setError(null);
426
624
  refs.streamingRef.current = true;
625
+ setTurnPhase("running");
427
626
  refs.assistantIdRef.current = assistantId;
428
627
  setStreaming(true);
429
628
  setStreamingAssistantId(assistantId);
@@ -519,7 +718,9 @@ export function useChatStream(params) {
519
718
  setAutoScroll,
520
719
  setError,
521
720
  setStreaming,
721
+ setTurnPhase,
522
722
  setStreamingAssistantId,
723
+ queueDeferredTurn,
523
724
  ]);
524
725
  const handleStop = useCallback(async () => {
525
726
  // AC-R5-001: durable stopRequested is set synchronously before any await so
@@ -535,6 +736,7 @@ export function useChatStream(params) {
535
736
  notice: noticeSink(),
536
737
  });
537
738
  applyOwnershipBagToRefs(refs, bag);
739
+ setTurnPhase("stopping");
538
740
  // Immediate abort already scheduled for a known turnId — coalesce via
539
741
  // abortPostedTurnIds; stopInFlight only gates concurrent in-flight POSTs.
540
742
  if (stopResult.abortedTurnId) {
@@ -643,6 +845,9 @@ export function useChatStream(params) {
643
845
  },
644
846
  });
645
847
  applyOwnershipBagToRefs(refs, bag);
848
+ setTurnPhase(turnPhaseFromOwnership(bag));
849
+ if (state && "deferredTurn" in state)
850
+ setDeferredTurn(state.deferredTurn ?? null);
646
851
  if (result === "bound") {
647
852
  // US-BUSY-ACTIVE-BOUND: keep streaming/busy with same id.
648
853
  setStreaming(true);
@@ -742,6 +947,7 @@ export function useChatStream(params) {
742
947
  refs,
743
948
  session,
744
949
  streaming,
950
+ setTurnPhase,
745
951
  markIdleIfCurrent,
746
952
  releaseSubmitOwnership,
747
953
  postAbortForTurn,
@@ -763,5 +969,5 @@ export function useChatStream(params) {
763
969
  }
764
970
  };
765
971
  }, [session, recoveryVersion, refs, startEventsStream]);
766
- return { sendPrompt, handleStop, openEventsStream };
972
+ return { sendPrompt, handleStop, cancelDeferredTurn, openEventsStream };
767
973
  }
@@ -1,6 +1,5 @@
1
1
  import { useCallback, useEffect, useMemo, useState } from "react";
2
- import { parseAssistantContent } from "../../chat/assistant-content.js";
3
- import { buildTurnProcessGroups } from "../../chat/turn-process.js";
2
+ import { buildTurnProcessGroups, hydrateSessionThread, selectRecentTurnGroups, } from "../../chat/turn-process.js";
4
3
  import { createChatSseEventApplier, } from "./chat-sse-events.js";
5
4
  import { taskContextUnchanged } from "./format.js";
6
5
  const EMPTY_USAGE = {
@@ -32,6 +31,8 @@ export function useChatThread(params) {
32
31
  const [contextLoading, setContextLoading] = useState(false);
33
32
  const [usage, setUsage] = useState(EMPTY_USAGE);
34
33
  const [streaming, setStreaming] = useState(false);
34
+ const [turnPhase, setTurnPhase] = useState("idle");
35
+ const [deferredTurn, setDeferredTurn] = useState(null);
35
36
  /** Assistant bubble id for the in-flight turn; only this row shows the streaming cursor. */
36
37
  const [streamingAssistantId, setStreamingAssistantId] = useState(null);
37
38
  const [showSystemInMessages] = useState(false);
@@ -45,6 +46,8 @@ export function useChatThread(params) {
45
46
  setHumanGateCards,
46
47
  setArtifactCards,
47
48
  setStreaming,
49
+ setTurnPhase,
50
+ setDeferredTurn,
48
51
  setStreamingAssistantId,
49
52
  setUnread,
50
53
  setOperationCards,
@@ -121,9 +124,7 @@ export function useChatThread(params) {
121
124
  }, 350);
122
125
  return () => window.clearTimeout(timer);
123
126
  }, [sessionId, operationCards, humanGateCards, interview, loadTaskContext]);
124
- const visibleMessages = useMemo(() => messages
125
- .filter((m) => m.role !== "system" || showSystemInMessages)
126
- .slice(-historyLimit), [messages, showSystemInMessages, historyLimit]);
127
+ const timelineMessages = useMemo(() => messages.filter((m) => m.role !== "system" || showSystemInMessages), [messages, showSystemInMessages]);
127
128
  useEffect(() => {
128
129
  const onVisibility = () => {
129
130
  if (!document.hidden)
@@ -158,12 +159,30 @@ export function useChatThread(params) {
158
159
  statusKind,
159
160
  };
160
161
  }, [toolCalls]);
161
- const turnGroups = useMemo(() => buildTurnProcessGroups({
162
- messages: visibleMessages,
162
+ const turnGroups = useMemo(() => {
163
+ const all = buildTurnProcessGroups({
164
+ messages: timelineMessages,
165
+ toolCalls,
166
+ streaming,
167
+ activeAssistantId: streamingAssistantId,
168
+ });
169
+ return selectRecentTurnGroups(all, historyLimit);
170
+ }, [
171
+ timelineMessages,
163
172
  toolCalls,
164
173
  streaming,
165
- activeAssistantId: streamingAssistantId,
166
- }), [visibleMessages, toolCalls, streaming, streamingAssistantId]);
174
+ streamingAssistantId,
175
+ historyLimit,
176
+ ]);
177
+ const visibleMessages = useMemo(() => {
178
+ const next = [];
179
+ for (const group of turnGroups) {
180
+ if (group.user)
181
+ next.push(group.user);
182
+ next.push(...group.assistants);
183
+ }
184
+ return next;
185
+ }, [turnGroups]);
167
186
  /** Reset all session-scoped projections before adopting a fresh session. */
168
187
  const resetForNewSession = useCallback(() => {
169
188
  setMessages([]);
@@ -175,6 +194,8 @@ export function useChatThread(params) {
175
194
  setTaskContext(null);
176
195
  setUsage(EMPTY_USAGE);
177
196
  setStreaming(false);
197
+ setTurnPhase("idle");
198
+ setDeferredTurn(null);
178
199
  setStreamingAssistantId(null);
179
200
  setUnread(0);
180
201
  setHistoryLimit(50);
@@ -192,6 +213,8 @@ export function useChatThread(params) {
192
213
  setUsage(EMPTY_USAGE);
193
214
  refs.usageResponseKeysRef.current = new Set();
194
215
  setStreaming(next.streaming);
216
+ setTurnPhase(next.turnPhase ?? (next.streaming ? "running" : "idle"));
217
+ setDeferredTurn(next.deferredTurn ?? null);
195
218
  setStreamingAssistantId(null);
196
219
  setUnread(0);
197
220
  setHistoryLimit(50);
@@ -213,6 +236,8 @@ export function useChatThread(params) {
213
236
  const activeTurnId = body.session.activeTurnId ?? null;
214
237
  refs.activeTurnIdRef.current = activeTurnId;
215
238
  refs.streamingRef.current = Boolean(activeTurnId);
239
+ setTurnPhase(body.session.turnPhase ?? (activeTurnId ? "running" : "idle"));
240
+ setDeferredTurn(body.session.deferredTurn ?? null);
216
241
  // Resume the events stream from the snapshot tail so the next
217
242
  // reconnect never immediately re-expires (AC-003). The refs below
218
243
  // are reset so any follow-up tail replay re-applies cleanly.
@@ -220,18 +245,9 @@ export function useChatThread(params) {
220
245
  refs.seenEventIdsRef.current = new Set();
221
246
  refs.turnAssistantIdsRef.current = new Map();
222
247
  refs.sealedAssistantIdsRef.current = new Set();
223
- setMessages((body.session.messages ?? [])
224
- .filter((message) => message.role === "user" || message.role === "assistant")
225
- .map((message) => {
226
- const content = parseAssistantContent(message.content);
227
- return {
228
- id: message.id,
229
- role: message.role,
230
- text: message.text,
231
- createdAt: Date.parse(message.at) || Date.now(),
232
- ...(content && content.length > 0 ? { content } : {}),
233
- };
234
- }));
248
+ const hydrated = hydrateSessionThread(body.session.messages ?? []);
249
+ setMessages(hydrated.messages);
250
+ setToolCalls(hydrated.toolCalls);
235
251
  setStreamingAssistantId(null);
236
252
  return true;
237
253
  }
@@ -240,7 +256,15 @@ export function useChatThread(params) {
240
256
  // trigger another reconcile and retry (best-effort recovery).
241
257
  return false;
242
258
  }
243
- }, [origin, refs, setMessages, setStreamingAssistantId]);
259
+ }, [
260
+ origin,
261
+ refs,
262
+ setMessages,
263
+ setToolCalls,
264
+ setStreamingAssistantId,
265
+ setTurnPhase,
266
+ setDeferredTurn,
267
+ ]);
244
268
  return {
245
269
  messages,
246
270
  setMessages,
@@ -259,6 +283,10 @@ export function useChatThread(params) {
259
283
  usage,
260
284
  streaming,
261
285
  setStreaming,
286
+ turnPhase,
287
+ setTurnPhase,
288
+ deferredTurn,
289
+ setDeferredTurn,
262
290
  streamingAssistantId,
263
291
  setStreamingAssistantId,
264
292
  visibleMessages,
@@ -1,4 +1,4 @@
1
- import { useCallback, useEffect, useState } from "react";
1
+ import { useCallback, useEffect, useRef, useState } from "react";
2
2
  import { ACTIVE_CHAT_SESSION_STORAGE_KEY } from "../../chat/workspace-landing.js";
3
3
  /** Left-rail repo tree + file preview tabs state. */
4
4
  export function useRepoBrowser(params) {
@@ -9,6 +9,8 @@ export function useRepoBrowser(params) {
9
9
  const [previews, setPreviews] = useState([]);
10
10
  const [activePreviewPath, setActivePreviewPath] = useState(null);
11
11
  const [previewLoading, setPreviewLoading] = useState(false);
12
+ const expandedDirsRef = useRef(new Set());
13
+ expandedDirsRef.current = expandedDirs;
12
14
  const loadTree = useCallback(async (directory = "") => {
13
15
  try {
14
16
  setTreeError(null);
@@ -28,6 +30,28 @@ export function useRepoBrowser(params) {
28
30
  useEffect(() => {
29
31
  void loadTree();
30
32
  }, [loadTree]);
33
+ /** pi-web parity: re-fetch the root + every expanded directory. Bump
34
+ * `refreshTick` to trigger; cached entries are NOT reused, so newly
35
+ * created/deleted files appear without a manual refresh. */
36
+ const [refreshTick, setRefreshTick] = useState(0);
37
+ const refreshTree = useCallback(() => {
38
+ setRefreshTick((tick) => tick + 1);
39
+ }, []);
40
+ useEffect(() => {
41
+ const dirs = ["", ...expandedDirsRef.current];
42
+ for (const dir of dirs)
43
+ void loadTree(dir);
44
+ }, [loadTree, refreshTick]);
45
+ // Also refresh when the tab regains focus: external tools (editor, git,
46
+ // CLI) may have changed the tree while the console was hidden.
47
+ useEffect(() => {
48
+ const onVisibility = () => {
49
+ if (!document.hidden)
50
+ refreshTree();
51
+ };
52
+ document.addEventListener("visibilitychange", onVisibility);
53
+ return () => document.removeEventListener("visibilitychange", onVisibility);
54
+ }, [refreshTree]);
31
55
  const openPreview = useCallback(async (file) => {
32
56
  setPreviewLoading(true);
33
57
  try {
@@ -116,6 +140,7 @@ export function useRepoBrowser(params) {
116
140
  previewLoading,
117
141
  setPreviews,
118
142
  loadTree,
143
+ refreshTree,
119
144
  openPreview,
120
145
  closePreview,
121
146
  toggleDir,
@@ -292,9 +292,13 @@ function assertControllerIdentityUnchangedBeforeSpawn(identity) {
292
292
  }
293
293
  }
294
294
  function controllerIdentityDriftError(message, cause) {
295
- return new Error(`controller identity changed before spawn: ${message}`, {
296
- cause,
297
- });
295
+ // Preserve the raw drift detail verbatim; the actionable suffix serves
296
+ // every consumer surface — operation errorMessage, SSE events, and agent/
297
+ // API callers that never reach the UI translation layer — with the exact
298
+ // recovery step: the running Console holds a stale controller fingerprint
299
+ // after rebuilds or commits touching fingerprint-scoped paths
300
+ // (package.json, bin/**, dist/**, skills/**).
301
+ return new Error(`controller identity changed before spawn: ${message}. Fix: restart the Console process so it re-resolves the current build, then retry the operation; avoid rebuilding or committing fingerprint-scoped files (package.json, bin/**, dist/**, skills/**) mid-session.`, { cause });
298
302
  }
299
303
  /**
300
304
  * Detect whether loopAgentBin is a Node-like executable and baseArgs