@tea-agent/loop-agent 0.39.0-next.9 → 0.39.1

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 (288) hide show
  1. package/AGENTS.md +4 -1
  2. package/CHANGELOG.md +269 -69
  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 +16 -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/update/runtime-activity.js +1 -29
  12. package/dist/cli.js +2 -1
  13. package/dist/commands/dag-rerun.js +55 -1
  14. package/dist/commands/init-model-catalog.js +464 -0
  15. package/dist/commands/init-upgrade.js +265 -97
  16. package/dist/commands/init.js +428 -27
  17. package/dist/commands/inspect-next.js +8 -0
  18. package/dist/commands/task-advance.js +31 -0
  19. package/dist/executors/dag-pi-executor.js +124 -18
  20. package/dist/executors/pi-event-serializer.js +42 -1
  21. package/dist/executors/pi-executor.js +51 -0
  22. package/dist/executors/pi-extension-resolver.js +233 -0
  23. package/dist/executors/pi-playwright-cli-tool.js +74 -28
  24. package/dist/executors/pi-sdk-executor.js +187 -59
  25. package/dist/executors/shell-executor.js +555 -34
  26. package/dist/executors/shell-write-guard.js +7 -0
  27. package/dist/shared/dag-failure-category.js +138 -0
  28. package/dist/shared/dag-prompt-override.js +27 -0
  29. package/dist/shared/operator/capabilities.js +181 -27
  30. package/dist/shared/playwright-cli-command-policy.js +15 -0
  31. package/dist/shared/update/console-notifier.js +100 -0
  32. package/dist/{cli → shared}/update/npm-client.js +70 -15
  33. package/dist/{cli → shared}/update/state.js +41 -13
  34. package/dist/task/config-types.js +28 -0
  35. package/dist/task/contract/apply.js +11 -1
  36. package/dist/task/contract/constants.js +3 -0
  37. package/dist/task/contract/diff.js +26 -0
  38. package/dist/task/contract/hash.js +3 -0
  39. package/dist/task/contract/observe.js +25 -4
  40. package/dist/task/contract/paths.js +2 -1
  41. package/dist/task/contract/project.js +20 -1
  42. package/dist/task/contract/schema.js +8 -1
  43. package/dist/task/contract/transaction.js +12 -0
  44. package/dist/task/source-prepare/build-draft.js +15 -0
  45. package/dist/task/source-prepare/completeness.js +188 -0
  46. package/dist/task/source-prepare/fragment-inventory.js +474 -0
  47. package/dist/task/source-prepare/index.js +3 -0
  48. package/dist/task/source-prepare/ledger-reconciliation.js +127 -0
  49. package/dist/task/source-prepare/ledger-review.js +214 -0
  50. package/dist/task/source-prepare/ledger.js +545 -0
  51. package/dist/task/source-prepare/parse-intent.js +24 -4
  52. package/dist/task/source-prepare/prepare.js +262 -1
  53. package/dist/task/source-prepare/semantic-intake.js +19 -3
  54. package/dist/task/source-prepare/source-fidelity-pi.js +384 -0
  55. package/dist/task/source-prepare/types.js +22 -0
  56. package/dist/worker/cli.js +18 -2
  57. package/dist/worker/console/chat/deferred-turn.js +91 -0
  58. package/dist/worker/console/chat/mcp-inventory.js +221 -0
  59. package/dist/worker/console/chat/operation-card.js +8 -1
  60. package/dist/worker/console/chat/pi-console-config.js +81 -10
  61. package/dist/worker/console/chat/pi-runtime.js +165 -83
  62. package/dist/worker/console/chat/repo-walk.js +10 -1
  63. package/dist/worker/console/chat/routes.js +480 -55
  64. package/dist/worker/console/chat/semantic-activity.js +20 -8
  65. package/dist/worker/console/chat/session-store.js +146 -2
  66. package/dist/worker/console/chat/turn-process.js +10 -10
  67. package/dist/worker/console/console-handoff.js +82 -0
  68. package/dist/worker/console/console-update-and-init.js +115 -0
  69. package/dist/worker/console/console-update-runtime.js +84 -0
  70. package/dist/worker/console/dag-execution-receipt.js +33 -0
  71. package/dist/worker/console/draft-store.js +49 -0
  72. package/dist/worker/console/inspect-split.js +18 -0
  73. package/dist/worker/console/operation-run-facts.js +19 -3
  74. package/dist/worker/console/operation-store.js +1 -1
  75. package/dist/worker/console/operator-actions.js +320 -4
  76. package/dist/worker/console/operator-surface-health.js +1 -0
  77. package/dist/worker/console/operator-user-error.js +2 -2
  78. package/dist/worker/console/pi-readiness.js +5 -4
  79. package/dist/worker/console/prd-intake-bridge.js +13 -0
  80. package/dist/worker/console/recovery-cta.js +4 -4
  81. package/dist/worker/console/recovery-error-copy.js +4 -4
  82. package/dist/worker/console/routes.js +46 -2
  83. package/dist/worker/console/security.js +63 -9
  84. package/dist/worker/console/server.js +111 -7
  85. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-BLXFe4qI.js → abnfDiagram-N423BO3Z-CTx1TrNa.js} +1 -1
  86. package/dist/worker/console/static/assets/{arc-7iOJeqho.js → arc-B4i9cVxX.js} +1 -1
  87. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CYoyj5K7.js → architectureDiagram-T3A2C74G-BAXE8_Rl.js} +1 -1
  88. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-fK01JNlR.js → blockDiagram-VBNYF7ZC-C5Xh59at.js} +1 -1
  89. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-bzbw5D_Z.js → c4Diagram-5PPSVZJV-B_asOzAu.js} +1 -1
  90. package/dist/worker/console/static/assets/channel-Dj_4Moei.js +1 -0
  91. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-Cf6hJFFG.js → chunk-2GRJ4B5K-wRO_iRNc.js} +1 -1
  92. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-DSFXR-et.js → chunk-2Q5K7J3B-ByOaSh_G.js} +1 -1
  93. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BagJechl.js → chunk-5RXB4S5H-DjLzudan.js} +1 -1
  94. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-E7QNMFaW.js → chunk-5VM5RSS4-JCNGkXYT.js} +1 -1
  95. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BG_eOBrC.js → chunk-6Q2QTUOP-BhrPXXMZ.js} +1 -1
  96. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-Bo42dqol.js → chunk-GF5L2VYU-TJ5JuEBT.js} +1 -1
  97. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-dibM-87v.js → chunk-JWPE2WC7-VVthn_mD.js} +1 -1
  98. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-Bytrp3Oe.js → chunk-KBJHAD2P-D60-Mny4.js} +1 -1
  99. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DBABILrr.js → chunk-RYQCIY6F-zXv7aVye.js} +1 -1
  100. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-DCUk5Pf9.js → chunk-XXDRQBXY-By9VQoi1.js} +1 -1
  101. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C0YYAyCa.js +1 -0
  102. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C0YYAyCa.js +1 -0
  103. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BlGE9v6E.js → cose-bilkent-JH36ORCC-C4zQLe-M.js} +1 -1
  104. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-DGl5ewph.js → cynefin-VYW2F7L2-wh5N2ReN.js} +1 -1
  105. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-DsJ_SVH_.js → cynefinDiagram-MW4NZA55-Dlu5wIqs.js} +1 -1
  106. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BIhjFdzv.js → dagre-VZM6K2ZE-CrDgthc6.js} +1 -1
  107. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Dmy1ctTb.js → diagram-7IWD3JNH-zN1clg27.js} +1 -1
  108. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-DYCtWUF3.js → diagram-B4RE2ZJO-BN-o7J7-.js} +1 -1
  109. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-BMZFHVd2.js → diagram-LBJQPF4R-C8SuPt0d.js} +1 -1
  110. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-b_U8gfaO.js → diagram-Q27KOJAE-BoizuHbZ.js} +1 -1
  111. package/dist/worker/console/static/assets/{diagram-UB23O5K3-Dzy_bn-I.js → diagram-UB23O5K3-BzUzXixG.js} +1 -1
  112. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-DJCiMNEI.js → ebnfDiagram-BXEA7PRR-Ev3lA4Cf.js} +1 -1
  113. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-28lD016B.js → erDiagram-JOGREHBK-GALUzRbs.js} +1 -1
  114. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-DGyHJmIw.js → flowDiagram-UKHOOZJN-CZJN65Sb.js} +1 -1
  115. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CzdwUjrO.js → ganttDiagram-PKOTCBZU-JLB9OESl.js} +1 -1
  116. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-D9bGwuo8.js → gitGraphDiagram-DS77QQ5N-CcvCr4GD.js} +1 -1
  117. package/dist/worker/console/static/assets/index-CW88Fjbw.js +406 -0
  118. package/dist/worker/console/static/assets/index-DP0ZJl39.css +1 -0
  119. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-LJgJUQFC.js → infoDiagram-6WML65LV-HG5z7aVg.js} +1 -1
  120. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-98LaB8zb.js → ishikawaDiagram-WSZJBQD7-BHEu9chR.js} +1 -1
  121. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-D-ISzwlQ.js → journeyDiagram-NVQOT4AX-Cp_Up8-a.js} +1 -1
  122. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-gfYOI-EL.js → kanban-definition-27J2QSJJ-DvVeY-em.js} +1 -1
  123. package/dist/worker/console/static/assets/{linear-CyntZgKV.js → linear-aGmcjgTn.js} +1 -1
  124. package/dist/worker/console/static/assets/{mermaid.core-BBOR7dg0.js → mermaid.core-Biyo-tqA.js} +5 -5
  125. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-CQAgwPMk.js → mindmap-definition-FAOFIHXS-DyxY2pIb.js} +1 -1
  126. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DkMiZFuV.js → pegDiagram-VL7TDLO6-CkdGuc50.js} +1 -1
  127. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DLNXa_0J.js → pieDiagram-7S7Q4E2Y-NNQI9xVR.js} +1 -1
  128. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-Dc7bdzjm.js → quadrantDiagram-CIZ2JOQS-PAJkuz_G.js} +1 -1
  129. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BxMB0QqW.js → railroadDiagram-AXF67PYL-ZwrQod0O.js} +1 -1
  130. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-FjumoKn4.js → requirementDiagram-LRYGKXZP-D3ZfrzM2.js} +1 -1
  131. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-CkhKc_E3.js → sankeyDiagram-W5VNT64P-DDGFmWk5.js} +1 -1
  132. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DoBWqTTH.js → sequenceDiagram-SI44F4Z6-DO6mmXTA.js} +1 -1
  133. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-CQeh_YeE.js → sizeCapture-X5ZJPWSS-DMbCQiJt.js} +1 -1
  134. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-LuHmFW-I.js → stateDiagram-OKZ733FA-Di_YUEfC.js} +1 -1
  135. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DrdDvela.js +1 -0
  136. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-CHwbDctY.js → swimlanes-SLNWSIFB-Bn2PphNJ.js} +2 -2
  137. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DPkEAULl.js +8 -0
  138. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CoMngk0X.js → timeline-definition-Z64GVDOM-DPh8pRsp.js} +1 -1
  139. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-CURYbtbO.js → vennDiagram-T6HMQDX7-GMfl26pW.js} +1 -1
  140. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-BdqSECjU.js → wardleyDiagram-T6FBY63Y-DTlmVrmF.js} +1 -1
  141. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-DbMUYuAg.js → xychartDiagram-ELKLHX3M-YGBjKAS5.js} +1 -1
  142. package/dist/worker/console/static/index.html +2 -2
  143. package/dist/worker/console/static-src/app/console-types.js +40 -1
  144. package/dist/worker/console/static-src/app/useOperatorActions.js +6 -1
  145. package/dist/worker/console/static-src/app/useRecoveryActions.js +2 -2
  146. package/dist/worker/console/static-src/app/useRecoveryConsole.js +38 -2
  147. package/dist/worker/console/static-src/app/useRunProgress.js +46 -0
  148. package/dist/worker/console/static-src/app/useTaskWizard.js +73 -1
  149. package/dist/worker/console/static-src/operator-chat/cards/failure-category-advice.js +25 -0
  150. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +74 -26
  151. package/dist/worker/console/static-src/operator-chat/input-history.js +76 -0
  152. package/dist/worker/console/static-src/operator-chat/mutation-gate.js +1 -1
  153. package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +328 -0
  154. package/dist/worker/console/static-src/operator-chat/resource-diagnostics.js +204 -0
  155. package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +8 -1
  156. package/dist/worker/console/static-src/operator-chat/turn-submission.js +54 -0
  157. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +3 -0
  158. package/dist/worker/console/static-src/operator-chat/useChatStream.js +237 -11
  159. package/dist/worker/console/static-src/operator-chat/useChatThread.js +50 -22
  160. package/dist/worker/console/static-src/operator-chat/useComposer.js +32 -0
  161. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +26 -1
  162. package/dist/worker/console/static-src/pages/tasks/run-id-resolution.js +79 -0
  163. package/dist/worker/console/static-src/pages/tasks/run-ownership-verify.js +23 -0
  164. package/dist/worker/console/static-src/pages/tasks/run-panel-progress.js +110 -0
  165. package/dist/worker/loop-agent/loop-agent-client.js +7 -3
  166. package/dist/worker/materialize/harness-task-lineage.js +5 -2
  167. package/dist/worker/observability/init-runtime-activity.js +26 -0
  168. package/dist/worker/observability/read-model.js +22 -0
  169. package/dist/worker/observe/node-input.js +160 -3
  170. package/dist/worker/observe/node-process.js +377 -0
  171. package/dist/worker/observe/routes.js +224 -3
  172. package/dist/worker/observe/shell-handler-keys.js +34 -0
  173. package/dist/worker/observe/static/api.js +67 -1
  174. package/dist/worker/observe/static/constants.js +8 -1
  175. package/dist/worker/observe/static/dag-history-labels.js +4 -0
  176. package/dist/worker/observe/static/format.js +60 -0
  177. package/dist/worker/observe/static/index.html +5 -0
  178. package/dist/worker/observe/static/markdown-render.js +20 -1
  179. package/dist/worker/observe/static/operator-chrome.js +4 -0
  180. package/dist/worker/observe/static/prompt-restart-candidates.d.ts +18 -0
  181. package/dist/worker/observe/static/prompt-restart-candidates.js +36 -0
  182. package/dist/worker/observe/static/state.js +13 -2
  183. package/dist/worker/observe/static/styles.css +85 -0
  184. package/dist/worker/observe/static/views/dag-inspector.js +912 -8
  185. package/dist/worker/observe/static/views/dag.js +8 -1
  186. package/dist/worker/observe/static/views/failures.js +5 -2
  187. package/dist/worker/observe/static/views/session-timeline.js +78 -9
  188. package/dist/worker/observe/static/views/task.js +55 -0
  189. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +856 -54
  190. package/dist/workflows/dag/backend-test-case-manifest.js +4 -0
  191. package/dist/workflows/dag/backend-test-execution-contract.js +18 -9
  192. package/dist/workflows/dag/backend-test-gap-fill.js +205 -0
  193. package/dist/workflows/dag/backend-test-intake-context.js +32 -0
  194. package/dist/workflows/dag/backend-test-layout.js +133 -0
  195. package/dist/workflows/dag/backend-test-markdown-workflow.js +343 -33
  196. package/dist/workflows/dag/backend-test-module-stem.js +5 -0
  197. package/dist/workflows/dag/backend-test-pytest-collection.js +454 -36
  198. package/dist/workflows/dag/backend-test-result-contract.js +1 -1
  199. package/dist/workflows/dag/backend-test-scenario-param.js +884 -140
  200. package/dist/workflows/dag/backend-test-scenario-partitions.js +321 -0
  201. package/dist/workflows/dag/backend-test-writer-completeness.js +110 -61
  202. package/dist/workflows/dag/contract-output-registry.js +15 -0
  203. package/dist/workflows/dag/contract-validator-registrations.js +3 -1
  204. package/dist/workflows/dag/convergence/controller.js +141 -0
  205. package/dist/workflows/dag/dynamic-runtime/loop-until.js +1 -1
  206. package/dist/workflows/dag/dynamic-runtime/map.js +25 -9
  207. package/dist/workflows/dag/dynamic-runtime/shared.js +1 -1
  208. package/dist/workflows/dag/failure-category.js +7 -116
  209. package/dist/workflows/dag/frontend-implementation-contract.js +465 -6
  210. package/dist/workflows/dag/frontend-plan-render.js +24 -0
  211. package/dist/workflows/dag/frontend-prewrite-gate.js +293 -84
  212. package/dist/workflows/dag/frontend-repair.js +14 -0
  213. package/dist/workflows/dag/frontend-review-context.js +92 -10
  214. package/dist/workflows/dag/frontend-risk.js +15 -2
  215. package/dist/workflows/dag/frontend-test-case-checklist.js +30 -4
  216. package/dist/workflows/dag/frontend-test-case-manifest.js +11 -4
  217. package/dist/workflows/dag/frontend-test-case-quality.js +11 -16
  218. package/dist/workflows/dag/frontend-test-environment-probe.js +230 -0
  219. package/dist/workflows/dag/frontend-test-html-report.js +3 -1
  220. package/dist/workflows/dag/frontend-test-l5-report.js +3 -1
  221. package/dist/workflows/dag/frontend-test-layout.js +159 -0
  222. package/dist/workflows/dag/frontend-test-markdown.js +61 -0
  223. package/dist/workflows/dag/frontend-test-result-contract.js +188 -42
  224. package/dist/workflows/dag/frontend-test-standard-scenarios.js +70 -0
  225. package/dist/workflows/dag/frontend-verification-trace.js +29 -4
  226. package/dist/workflows/dag/init-hybrid.js +1019 -209
  227. package/dist/workflows/dag/node-execution.js +228 -15
  228. package/dist/workflows/dag/prompt.js +60 -1
  229. package/dist/workflows/dag/rerun-feedback.js +212 -0
  230. package/dist/workflows/dag/rerun-plan.js +224 -27
  231. package/dist/workflows/dag/rerun-run.js +61 -6
  232. package/dist/workflows/dag/rerun-task.js +61 -2
  233. package/dist/workflows/dag/retry-policy.js +109 -0
  234. package/dist/workflows/dag/structured-output-repair.js +712 -0
  235. package/dist/workflows/dag/types.js +185 -12
  236. package/dist/workflows/dag/validate.js +64 -20
  237. package/docs/README.md +1 -0
  238. package/docs/architecture/evolution.md +45 -0
  239. package/docs/init-surface.manifest.json +6 -0
  240. package/docs/operations/README.md +1 -1
  241. package/docs/operations/local-development-environment.md +1 -5
  242. package/docs/skills/vetted-skill-registry.md +14 -0
  243. package/docs/templates/README.md +2 -2
  244. package/docs/templates/agent-dag.schema.json +44 -0
  245. package/docs/templates/backend-test-dag.json +60 -35
  246. package/docs/templates/frontend-implementation-contract.schema.json +681 -24
  247. package/docs/templates/frontend-test-case-checklist.md +2 -2
  248. package/docs/templates/frontend-test-dag.json +8 -10
  249. package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +1 -1
  250. package/docs/templates/init-managed-agents.md +13 -3
  251. package/harness.json +6 -4
  252. package/package.json +12 -4
  253. package/scripts/next-info.mjs +356 -0
  254. package/scripts/next-publish-gate.mjs +238 -0
  255. package/scripts/release-source-binding.mjs +251 -0
  256. package/skills/analyze-product-requirements/SKILL.md +8 -5
  257. package/skills/analyze-product-requirements/references/acceptance-criteria.md +1 -1
  258. package/skills/analyze-product-requirements/references/clarification-and-knowledge.md +10 -6
  259. package/skills/analyze-product-requirements/references/example.md +50 -0
  260. package/skills/analyze-product-requirements/references/forward-test-cases.md +11 -7
  261. package/skills/analyze-product-requirements/references/kb-integration.md +5 -5
  262. package/skills/analyze-product-requirements/references/product-analysis-schema.md +8 -4
  263. package/skills/analyze-product-requirements/references/product-requirement-schema.md +2 -2
  264. package/skills/analyze-product-requirements/references/requirement-clarification-schema.md +1 -1
  265. package/skills/analyze-product-requirements/scripts/test-validators.mjs +11 -1
  266. package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +20 -1
  267. package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +9 -0
  268. package/skills/codebase-scout/SKILL.md +1 -1
  269. package/skills/fe-test-ui-scout/SKILL.md +65 -0
  270. package/skills/fe-test-ui-scout/references/ledger-schema.md +62 -0
  271. package/skills/fe-test-ui-scout/references/recon-protocol.md +54 -0
  272. package/skills/frontend-implementation/references/node-contracts.md +2 -2
  273. package/skills/improve-codebase-architecture/SKILL.md +81 -0
  274. package/skills/improve-codebase-architecture/deepening.md +37 -0
  275. package/skills/improve-codebase-architecture/html-report.md +123 -0
  276. package/skills/improve-codebase-architecture/interface-design.md +44 -0
  277. package/skills/improve-codebase-architecture/language.md +53 -0
  278. package/skills/loop-agent/references/command-reference.md +11 -4
  279. package/skills/loop-agent/references/hybrid-dag.md +7 -0
  280. package/skills/playwright-cli/SKILL.md +23 -1
  281. package/skills/playwright-cli-case-generator/SKILL.md +29 -8
  282. package/dist/worker/console/static/assets/channel-BBOTJHvD.js +0 -1
  283. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Cm8Yf8tD.js +0 -1
  284. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Cm8Yf8tD.js +0 -1
  285. package/dist/worker/console/static/assets/index-B-qWGThd.js +0 -325
  286. package/dist/worker/console/static/assets/index-C-xRgY_0.css +0 -1
  287. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DZihfdKc.js +0 -1
  288. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CKnXCjHX.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,8 +57,8 @@ 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;
61
- const { setInput, setPendingImages } = composer;
60
+ const { applySseBlock, setMessages, setStreaming, setStreamingAssistantId, streaming, setTurnPhase, setDeferredTurn, } = thread;
61
+ const { setInput, setPendingImages, recordHistory } = composer;
62
62
  const showNotice = useCallback((message) => {
63
63
  if (!setNotice)
64
64
  return;
@@ -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,212 @@ 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_ACTIVE") {
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
+ /** DELETE the queued turn; "restore" moves its text back into the composer,
499
+ * "discard" drops it entirely (2026-08-23 queue chip actions). */
500
+ const requestDeferredTurnDelete = useCallback(async (mode) => {
501
+ const activeSession = refs.sessionRef.current;
502
+ const deferred = thread.deferredTurn;
503
+ if (!activeSession || !deferred)
504
+ return;
505
+ try {
506
+ const response = await fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(activeSession.sessionId)}/deferred-turn`, {
507
+ method: "DELETE",
508
+ headers: {
509
+ "Content-Type": "application/json",
510
+ "x-loop-console-confirmation": confirmationToken(),
511
+ },
512
+ credentials: "include",
513
+ body: JSON.stringify({
514
+ clientRequestId: deferred.clientRequestId,
515
+ expectedRevision: deferred.revision,
516
+ }),
517
+ });
518
+ const body = (await response.json().catch(() => ({})));
519
+ if (!response.ok || !body.cancelled) {
520
+ setError(body.error?.message ?? "待发送消息撤回失败");
521
+ return;
522
+ }
523
+ setDeferredTurn(null);
524
+ if (mode === "restore") {
525
+ setInput(body.deferredTurn?.text ?? "");
526
+ setPendingImages(body.deferredTurn?.images ?? []);
527
+ showNotice("待发送消息已移回输入框");
528
+ }
529
+ else {
530
+ showNotice("已删除排队消息");
531
+ }
532
+ setError(null);
533
+ }
534
+ catch (error) {
535
+ setError(error instanceof Error ? error.message : String(error));
536
+ }
537
+ }, [
538
+ origin,
539
+ refs,
540
+ thread.deferredTurn,
541
+ setDeferredTurn,
542
+ setInput,
543
+ setPendingImages,
544
+ setError,
545
+ showNotice,
546
+ ]);
547
+ const cancelDeferredTurn = useCallback(() => requestDeferredTurnDelete("restore"), [requestDeferredTurnDelete]);
548
+ const discardDeferredTurn = useCallback(() => requestDeferredTurnDelete("discard"), [requestDeferredTurnDelete]);
366
549
  const sendPrompt = useCallback(async (text, images) => {
550
+ const initialSession = refs.sessionRef.current;
551
+ if (!initialSession || (!text.trim() && images.length === 0))
552
+ return;
553
+ const ownership = ownershipBagFromRefs(refs);
554
+ const route = decideSubmissionRoute({
555
+ turnPhase: turnPhaseFromOwnership(ownership),
556
+ blockedByTurnId: ownership.stoppingTurnId ?? ownership.activeTurnId,
557
+ });
558
+ const clientRequestId = requestId("chat-submit");
559
+ if (route.action === "hold") {
560
+ const phase = turnPhaseFromOwnership(ownership);
561
+ if (phase === "stopping")
562
+ showNotice("正在确认上一轮停止状态,当前消息已保留");
563
+ else if (phase === "running")
564
+ showNotice("正在确认当前回合状态,当前消息已保留");
565
+ return;
566
+ }
567
+ recordHistory?.(text);
568
+ if (route.action === "queue-deferred") {
569
+ const deferred = await queueDeferredTurn({
570
+ text,
571
+ images,
572
+ clientRequestId,
573
+ blockedByTurnId: route.blockedByTurnId,
574
+ });
575
+ if (deferred.action !== "send-now")
576
+ return;
577
+ }
367
578
  const activeSession = refs.sessionRef.current;
368
579
  const generation = refs.sessionGenerationRef.current;
369
580
  if (!activeSession ||
581
+ activeSession.sessionId !== initialSession.sessionId ||
370
582
  (!text.trim() && images.length === 0) ||
371
583
  refs.streamingRef.current ||
372
584
  refs.submitInFlightRef.current ||
373
585
  // AC-R5-002: ownership lease blocks send until terminal release.
374
- Boolean(refs.pendingSubmissionRef.current))
586
+ refs.pendingSubmissionRef.current)
375
587
  return;
376
588
  // AC-001: acquire synchronous submit ownership before the first await.
377
589
  refs.submitInFlightRef.current = true;
378
590
  const submitGeneration = ++refs.submitGenerationRef.current;
379
- const clientRequestId = requestId("chat-submit");
380
591
  const userMessageId = requestId("u");
381
592
  const assistantId = requestId("a");
382
593
  const draftText = text.trim();
@@ -424,6 +635,7 @@ export function useChatStream(params) {
424
635
  setAutoScroll(true);
425
636
  setError(null);
426
637
  refs.streamingRef.current = true;
638
+ setTurnPhase("running");
427
639
  refs.assistantIdRef.current = assistantId;
428
640
  setStreaming(true);
429
641
  setStreamingAssistantId(assistantId);
@@ -519,7 +731,10 @@ export function useChatStream(params) {
519
731
  setAutoScroll,
520
732
  setError,
521
733
  setStreaming,
734
+ setTurnPhase,
522
735
  setStreamingAssistantId,
736
+ queueDeferredTurn,
737
+ recordHistory,
523
738
  ]);
524
739
  const handleStop = useCallback(async () => {
525
740
  // AC-R5-001: durable stopRequested is set synchronously before any await so
@@ -535,6 +750,7 @@ export function useChatStream(params) {
535
750
  notice: noticeSink(),
536
751
  });
537
752
  applyOwnershipBagToRefs(refs, bag);
753
+ setTurnPhase("stopping");
538
754
  // Immediate abort already scheduled for a known turnId — coalesce via
539
755
  // abortPostedTurnIds; stopInFlight only gates concurrent in-flight POSTs.
540
756
  if (stopResult.abortedTurnId) {
@@ -643,6 +859,9 @@ export function useChatStream(params) {
643
859
  },
644
860
  });
645
861
  applyOwnershipBagToRefs(refs, bag);
862
+ setTurnPhase(turnPhaseFromOwnership(bag));
863
+ if (state && "deferredTurn" in state)
864
+ setDeferredTurn(state.deferredTurn ?? null);
646
865
  if (result === "bound") {
647
866
  // US-BUSY-ACTIVE-BOUND: keep streaming/busy with same id.
648
867
  setStreaming(true);
@@ -742,6 +961,7 @@ export function useChatStream(params) {
742
961
  refs,
743
962
  session,
744
963
  streaming,
964
+ setTurnPhase,
745
965
  markIdleIfCurrent,
746
966
  releaseSubmitOwnership,
747
967
  postAbortForTurn,
@@ -763,5 +983,11 @@ export function useChatStream(params) {
763
983
  }
764
984
  };
765
985
  }, [session, recoveryVersion, refs, startEventsStream]);
766
- return { sendPrompt, handleStop, openEventsStream };
986
+ return {
987
+ sendPrompt,
988
+ handleStop,
989
+ cancelDeferredTurn,
990
+ discardDeferredTurn,
991
+ openEventsStream,
992
+ };
767
993
  }
@@ -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,
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, } f
2
2
  import { filterSlashCommands, mergeSlashCommands, } from "../../chat/shortcuts.js";
3
3
  import { confirmationToken } from "./format.js";
4
4
  import { clampShortcutIndex } from "./slash-palette-nav.js";
5
+ import { createInputHistoryState, navigateInputHistory, recordInputHistoryEntry, resetInputHistoryBrowsing, } from "./input-history.js";
5
6
  import { attachRuntimeSnapshotController } from "./useRuntimeSnapshot.js";
6
7
  /** Composer state: input text, @file palette, image attachments, /shortcuts,
7
8
  * IME composition guards, auto-grow and server-side draft sync. */
@@ -31,6 +32,8 @@ export function useComposer(params) {
31
32
  const [dynamicCommandsError, setDynamicCommandsError] = useState(null);
32
33
  const [commandsEpoch, setCommandsEpoch] = useState(0);
33
34
  const textareaRef = useRef(null);
35
+ /** Arrow-key input history; unsent drafts survive history browsing. */
36
+ const historyRef = useRef(createInputHistoryState());
34
37
  /** Active session generation for /commands; bumped immediately on switch (AC-2). */
35
38
  const commandsSessionRef = useRef(sessionId);
36
39
  const commandsGenerationRef = useRef(0);
@@ -38,6 +41,33 @@ export function useComposer(params) {
38
41
  setInput(value);
39
42
  setShortcutOpen(value.startsWith("/"));
40
43
  setShortcutIndex(0);
44
+ // Manual typing edits a recalled entry in place → back to present draft.
45
+ historyRef.current = resetInputHistoryBrowsing(historyRef.current);
46
+ }, []);
47
+ /** Recall an earlier input via ↑/↓; returns true when the key was consumed. */
48
+ const navigateHistory = useCallback((direction) => {
49
+ const textarea = textareaRef.current;
50
+ const decision = navigateInputHistory(historyRef.current, direction, {
51
+ text: input,
52
+ caretStart: textarea?.selectionStart ?? input.length,
53
+ caretEnd: textarea?.selectionEnd ?? input.length,
54
+ });
55
+ if (!decision.handled)
56
+ return false;
57
+ historyRef.current = decision.state;
58
+ setInput(decision.text);
59
+ const caret = decision.text.length;
60
+ queueMicrotask(() => {
61
+ const el = textareaRef.current;
62
+ if (el && el.value === decision.text) {
63
+ el.setSelectionRange(caret, caret);
64
+ }
65
+ });
66
+ return true;
67
+ }, [input, setInput]);
68
+ /** Record a submitted text into history (consecutive duplicates collapse). */
69
+ const recordHistory = useCallback((text) => {
70
+ historyRef.current = recordInputHistoryEntry(historyRef.current, text);
41
71
  }, []);
42
72
  // Immediate invalidation on A→B even when B palette never opens (AC-2).
43
73
  useLayoutEffect(() => {
@@ -234,6 +264,8 @@ export function useComposer(params) {
234
264
  input,
235
265
  setInput,
236
266
  handleInputChange,
267
+ navigateHistory,
268
+ recordHistory,
237
269
  fileOptions,
238
270
  pendingImages,
239
271
  setPendingImages,
@@ -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,