@tea-agent/loop-agent 0.42.0-next.9 → 0.42.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 (236) hide show
  1. package/CHANGELOG.md +111 -45
  2. package/dist/application/dag/run-dag.js +8 -2
  3. package/dist/application/evaluation/budget.js +19 -1
  4. package/dist/application/task-lifecycle/advance.js +26 -8
  5. package/dist/application/task-lifecycle/observe.js +43 -29
  6. package/dist/application/task-lifecycle/plan-transitions.js +5 -4
  7. package/dist/build-stamp.json +3 -3
  8. package/dist/cli/program.js +1 -1
  9. package/dist/commands/dag-rerun-task.js +2 -0
  10. package/dist/commands/task-source-prepare.js +3 -1
  11. package/dist/executors/dag-pi-executor.js +2584 -600
  12. package/dist/executors/pi-executor.js +22 -1
  13. package/dist/executors/pi-extension-resolver.js +14 -2
  14. package/dist/executors/pi-sdk-executor.js +140 -39
  15. package/dist/executors/shell-executor.js +135 -55
  16. package/dist/shared/dag-failure-category.js +6 -0
  17. package/dist/shared/frontend-execution-policy.js +23 -0
  18. package/dist/task/config-types.js +4 -0
  19. package/dist/task/contract/apply.js +36 -2
  20. package/dist/task/source-prepare/ledger-reconciliation.js +2 -2
  21. package/dist/task/source-prepare/ledger-review.js +6 -9
  22. package/dist/task/source-prepare/parse-intent.js +7 -0
  23. package/dist/task/source-prepare/semantic-intake.js +16 -26
  24. package/dist/task/source-prepare/source-fidelity-pi.js +28 -9
  25. package/dist/task/source-references.js +48 -23
  26. package/dist/worker/console/chat/assistant-content.js +23 -2
  27. package/dist/worker/console/chat/browser-policy.js +143 -0
  28. package/dist/worker/console/chat/browser-routes.js +148 -0
  29. package/dist/worker/console/chat/chat-event-store.js +4 -2
  30. package/dist/worker/console/chat/explore-tools.js +13 -0
  31. package/dist/worker/console/chat/pi-runtime.js +173 -94
  32. package/dist/worker/console/chat/resource-loader.js +4 -1
  33. package/dist/worker/console/chat/routes.js +208 -66
  34. package/dist/worker/console/chat/scm-routes.js +217 -0
  35. package/dist/worker/console/chat/scm-service.js +283 -0
  36. package/dist/worker/console/chat/scm-tools.js +111 -0
  37. package/dist/worker/console/chat/sdd-data-alignment.js +78 -11
  38. package/dist/worker/console/chat/session-catalog.js +32 -0
  39. package/dist/worker/console/chat/session-mode-view.js +48 -0
  40. package/dist/worker/console/chat/session-mode.js +218 -0
  41. package/dist/worker/console/chat/session-store.js +27 -7
  42. package/dist/worker/console/chat/shortcuts.js +6 -0
  43. package/dist/worker/console/chat/subagents/agent-tool.js +62 -0
  44. package/dist/worker/console/chat/subagents/explore-agent.js +169 -0
  45. package/dist/worker/console/chat/subagents/index.js +5 -0
  46. package/dist/worker/console/chat/subagents/orchestrator.js +273 -0
  47. package/dist/worker/console/chat/subagents/tool-policy.js +53 -0
  48. package/dist/worker/console/chat/subagents/types.js +13 -0
  49. package/dist/worker/console/chat/terminal-routes.js +216 -0
  50. package/dist/worker/console/chat/terminal-sessions.js +284 -0
  51. package/dist/worker/console/chat/terminal-tools.js +199 -0
  52. package/dist/worker/console/chat/tool-preview.js +21 -0
  53. package/dist/worker/console/chat/tools.js +13 -1
  54. package/dist/worker/console/chat/turn-process.js +1 -0
  55. package/dist/worker/console/interview/tools.js +1 -0
  56. package/dist/worker/console/server.js +2 -28
  57. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C6n9_m0P.js → abnfDiagram-N423BO3Z-C9eI7nEo.js} +1 -1
  58. package/dist/worker/console/static/assets/{arc-DQh-IfZ1.js → arc-VJWsxWhB.js} +1 -1
  59. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-54NnrwUC.js → architectureDiagram-T3A2C74G-BGcyiMSo.js} +1 -1
  60. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-pivRALGK.js → blockDiagram-VBNYF7ZC-BScnFwyi.js} +1 -1
  61. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BR7OV2NJ.js → c4Diagram-5PPSVZJV-Bl9_BsLi.js} +1 -1
  62. package/dist/worker/console/static/assets/channel-B6sQYuOE.js +1 -0
  63. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-B1Aq6BcK.js → chunk-2GRJ4B5K-F1uKxiUt.js} +1 -1
  64. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-CbdU0rjo.js → chunk-2Q5K7J3B-rSRMynVu.js} +1 -1
  65. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-Dqi2GJeD.js → chunk-5RXB4S5H-DAsJm1WD.js} +1 -1
  66. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BYn1Hu4R.js → chunk-5VM5RSS4-DQIdVI0a.js} +1 -1
  67. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DRLjDL0k.js → chunk-6Q2QTUOP-4A_8rJ-Q.js} +1 -1
  68. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CY9Xx2jV.js → chunk-GF5L2VYU-BHnT-vjh.js} +1 -1
  69. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BNCZs7_z.js → chunk-JWPE2WC7-Rq7QKBkn.js} +1 -1
  70. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DZ8AStLL.js → chunk-KBJHAD2P-BXh-AI2u.js} +1 -1
  71. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DsxjYrzz.js → chunk-RYQCIY6F-BblcLy8m.js} +1 -1
  72. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-Ddx1KC1l.js → chunk-XXDRQBXY-zHZmVnB6.js} +1 -1
  73. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Z6s5QoeI.js +1 -0
  74. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Z6s5QoeI.js +1 -0
  75. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-W9TveCnK.js → cose-bilkent-JH36ORCC-Cmwz0Wi4.js} +1 -1
  76. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-l9j_ztZH.js → cynefin-VYW2F7L2-Dq76MHQU.js} +1 -1
  77. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-BMJMKi4G.js → cynefinDiagram-MW4NZA55-zPahV1fM.js} +1 -1
  78. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-Bb4mG9pH.js → dagre-VZM6K2ZE-DQJ3xPc_.js} +1 -1
  79. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-BMgL_Qv0.js → diagram-7IWD3JNH-BwX7u6fv.js} +1 -1
  80. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-Bvo2T4OQ.js → diagram-B4RE2ZJO-CodnDCco.js} +1 -1
  81. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-_5kWRN9c.js → diagram-LBJQPF4R-gAjtWkxG.js} +1 -1
  82. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-DqdMrltM.js → diagram-Q27KOJAE-CGCqz0cS.js} +1 -1
  83. package/dist/worker/console/static/assets/{diagram-UB23O5K3-CDDYsNkp.js → diagram-UB23O5K3-RHwYEywd.js} +1 -1
  84. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-c4nfnZUV.js → ebnfDiagram-BXEA7PRR-B6seb_p_.js} +1 -1
  85. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DnkUcNMs.js → erDiagram-JOGREHBK-CmwCKYe8.js} +1 -1
  86. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-Dz0KGLZE.js → flowDiagram-UKHOOZJN-BfNsaZ0V.js} +1 -1
  87. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-Cj1t1uka.js → ganttDiagram-PKOTCBZU-zmYgi_Z3.js} +1 -1
  88. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-tp2FrBHd.js → gitGraphDiagram-DS77QQ5N-BDEzzSKF.js} +1 -1
  89. package/dist/worker/console/static/assets/index-DgenUAfc.js +468 -0
  90. package/dist/worker/console/static/assets/index-aF4u-Y__.css +1 -0
  91. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DQwJS8DH.js → infoDiagram-6WML65LV-DeSz8l42.js} +1 -1
  92. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-eimCQWD4.js → ishikawaDiagram-WSZJBQD7-B1LV29aH.js} +1 -1
  93. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-Dd9Gbwgv.js → journeyDiagram-NVQOT4AX-CCw4W2oU.js} +1 -1
  94. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-Qve3cyft.js → kanban-definition-27J2QSJJ-BozaMoRJ.js} +1 -1
  95. package/dist/worker/console/static/assets/{linear-UXKSe36Z.js → linear-G1JR40SX.js} +1 -1
  96. package/dist/worker/console/static/assets/{mermaid.core-CWhj4JXN.js → mermaid.core-CCgPF0oA.js} +5 -5
  97. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-yTcwf4SJ.js → mindmap-definition-FAOFIHXS-CEqnCHnD.js} +1 -1
  98. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-_7qToaZP.js → pegDiagram-VL7TDLO6-DpmQPKuI.js} +1 -1
  99. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-CXmrKmDm.js → pieDiagram-7S7Q4E2Y-BP501AxR.js} +1 -1
  100. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-hMmrJyaS.js → quadrantDiagram-CIZ2JOQS-D1dBhl_A.js} +1 -1
  101. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BXw8tCBe.js → railroadDiagram-AXF67PYL-DgZDS5pY.js} +1 -1
  102. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-BmjgnLZl.js → requirementDiagram-LRYGKXZP-BzB3pyIq.js} +1 -1
  103. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-Bj77SPpN.js → sankeyDiagram-W5VNT64P-B9ZprFFP.js} +1 -1
  104. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-B2FDVysL.js → sequenceDiagram-SI44F4Z6-CC2-_XrA.js} +1 -1
  105. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-UChNY9ZZ.js → sizeCapture-X5ZJPWSS-UVtAUqgB.js} +1 -1
  106. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-wbAEkbd7.js → stateDiagram-OKZ733FA-CWcS5JYG.js} +1 -1
  107. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BooX8u1Q.js +1 -0
  108. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DCEkU8HR.js → swimlanes-SLNWSIFB-C4p-dn06.js} +2 -2
  109. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-D64_eqqE.js +8 -0
  110. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-BGp7H06U.js → timeline-definition-Z64GVDOM-Dnz52OHX.js} +1 -1
  111. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DsCIQuWR.js → vennDiagram-T6HMQDX7-CzTE0Nfu.js} +1 -1
  112. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-cRZLP4Xe.js → wardleyDiagram-T6FBY63Y-NAR9cjQQ.js} +1 -1
  113. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CP0xLR9Y.js → xychartDiagram-ELKLHX3M-DVMk8413.js} +1 -1
  114. package/dist/worker/console/static/index.html +2 -2
  115. package/dist/worker/console/static-src/chat-view-types.js +1 -1
  116. package/dist/worker/console/static-src/operator-chat/chat-link.js +94 -0
  117. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +142 -7
  118. package/dist/worker/console/static-src/operator-chat/pending-user-message.js +44 -0
  119. package/dist/worker/console/static-src/operator-chat/process-label.js +41 -0
  120. package/dist/worker/console/static-src/operator-chat/tools-catalog.js +21 -2
  121. package/dist/worker/console/static-src/operator-chat/turn-group-equality.js +13 -0
  122. package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +2 -0
  123. package/dist/worker/console/static-src/operator-chat/use-searchable-hidden.js +51 -0
  124. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +33 -18
  125. package/dist/worker/console/static-src/operator-chat/useChatStream.js +48 -16
  126. package/dist/worker/console/static-src/operator-chat/useChatThread.js +170 -7
  127. package/dist/worker/console/static-src/operator-chat/useComposer.js +13 -7
  128. package/dist/worker/console/static-src/operator-chat/user-turn-anchor-equality.js +21 -0
  129. package/dist/worker/console/static-src/shell/workspace-route.js +4 -0
  130. package/dist/worker/console/workspace-context.js +15 -1
  131. package/dist/worker/observe/node-transparency.js +81 -72
  132. package/dist/worker/observe/routes.js +20 -1
  133. package/dist/worker/observe/static/constants.js +22 -22
  134. package/dist/worker/observe/static/dag-context-reason-labels.js +19 -0
  135. package/dist/worker/observe/static/dag-history-labels.js +1 -0
  136. package/dist/worker/observe/static/dag-inspector-humanize.d.ts +16 -0
  137. package/dist/worker/observe/static/dag-inspector-humanize.js +342 -0
  138. package/dist/worker/observe/static/dag-node-purpose.js +10 -10
  139. package/dist/worker/observe/static/dom.js +20 -1
  140. package/dist/worker/observe/static/format-pool.d.ts +2 -0
  141. package/dist/worker/observe/static/format-pool.js +6 -0
  142. package/dist/worker/observe/static/format.js +7 -0
  143. package/dist/worker/observe/static/index.html +4 -4
  144. package/dist/worker/observe/static/inspect-workspace.js +34 -7
  145. package/dist/worker/observe/static/inspector-submission.js +32 -0
  146. package/dist/worker/observe/static/kpi.js +1 -0
  147. package/dist/worker/observe/static/prompt-restart-candidates.js +4 -2
  148. package/dist/worker/observe/static/relations.js +7 -5
  149. package/dist/worker/observe/static/router.js +13 -0
  150. package/dist/worker/observe/static/run-processing.js +2 -0
  151. package/dist/worker/observe/static/shell-chrome.js +36 -3
  152. package/dist/worker/observe/static/state.js +35 -2
  153. package/dist/worker/observe/static/styles.css +431 -39
  154. package/dist/worker/observe/static/task-failure-labels.d.ts +4 -0
  155. package/dist/worker/observe/static/task-failure-labels.js +67 -0
  156. package/dist/worker/observe/static/task-history.js +12 -0
  157. package/dist/worker/observe/static/views/batch.js +6 -13
  158. package/dist/worker/observe/static/views/dag-graph.js +51 -4
  159. package/dist/worker/observe/static/views/dag-inspector.js +1069 -426
  160. package/dist/worker/observe/static/views/dag-trajectory.js +3 -0
  161. package/dist/worker/observe/static/views/dag.d.ts +6 -0
  162. package/dist/worker/observe/static/views/dag.js +64 -22
  163. package/dist/worker/observe/static/views/dags.js +2 -0
  164. package/dist/worker/observe/static/views/dashboard.js +21 -12
  165. package/dist/worker/observe/static/views/failures.js +21 -11
  166. package/dist/worker/observe/static/views/feature.js +11 -29
  167. package/dist/worker/observe/static/views/pool.js +37 -28
  168. package/dist/worker/observe/static/views/run.js +48 -5
  169. package/dist/worker/observe/static/views/session-timeline.js +194 -243
  170. package/dist/worker/observe/static/views/task.js +81 -62
  171. package/dist/workflows/dag/backend-test-plan-protocol.js +104 -0
  172. package/dist/workflows/dag/budget-enforcement.js +53 -3
  173. package/dist/workflows/dag/dag-retry-schema.js +3 -0
  174. package/dist/workflows/dag/frontend-capacity.js +9 -0
  175. package/dist/workflows/dag/frontend-contract-facts.js +130 -0
  176. package/dist/workflows/dag/frontend-design-policy.js +103 -17
  177. package/dist/workflows/dag/frontend-durable-tools.js +193 -0
  178. package/dist/workflows/dag/frontend-execution-groups.js +24 -0
  179. package/dist/workflows/dag/frontend-implementation-contract.js +423 -41
  180. package/dist/workflows/dag/frontend-input-projection.js +76 -0
  181. package/dist/workflows/dag/frontend-plan-completeness.js +186 -0
  182. package/dist/workflows/dag/frontend-plan-recovery-policy.js +18 -0
  183. package/dist/workflows/dag/frontend-plan-render.js +21 -3
  184. package/dist/workflows/dag/frontend-prewrite-gate.js +1 -1
  185. package/dist/workflows/dag/frontend-recovery-controller.js +32 -8
  186. package/dist/workflows/dag/frontend-recovery-lineage.js +13 -0
  187. package/dist/workflows/dag/frontend-recovery-plan.js +4 -1
  188. package/dist/workflows/dag/frontend-recovery-run.js +146 -16
  189. package/dist/workflows/dag/frontend-review-scopes.js +139 -0
  190. package/dist/workflows/dag/frontend-risk.js +2 -0
  191. package/dist/workflows/dag/frontend-session-budget.js +249 -0
  192. package/dist/workflows/dag/frontend-shadow-dual-write.js +37 -3
  193. package/dist/workflows/dag/frontend-shape-facts.js +12 -2
  194. package/dist/workflows/dag/frontend-shape.js +46 -7
  195. package/dist/workflows/dag/frontend-test-execution-evidence.js +49 -0
  196. package/dist/workflows/dag/frontend-typed-event-store.js +14 -0
  197. package/dist/workflows/dag/frontend-verification-trace.js +38 -4
  198. package/dist/workflows/dag/frontend-writer-admission.js +2 -2
  199. package/dist/workflows/dag/init-hybrid.js +70 -35
  200. package/dist/workflows/dag/node-execution.js +225 -226
  201. package/dist/workflows/dag/prompt.js +4 -0
  202. package/dist/workflows/dag/recovery-lease.js +106 -16
  203. package/dist/workflows/dag/rerun-feedback.js +1 -0
  204. package/dist/workflows/dag/rerun-plan.js +96 -4
  205. package/dist/workflows/dag/rerun-run.js +28 -6
  206. package/dist/workflows/dag/rerun-task.js +221 -18
  207. package/dist/workflows/dag/retry-policy.js +27 -10
  208. package/dist/workflows/dag/runner-exit-diagnostics.js +125 -0
  209. package/dist/workflows/dag/runner.js +246 -19
  210. package/dist/workflows/dag/structured-output-repair.js +4 -1
  211. package/dist/workflows/dag/types.js +10 -3
  212. package/dist/workflows/dag/validate.js +10 -8
  213. package/dist/workflows/dag/workspace-checkpoint.js +66 -0
  214. package/docs/operations/README.md +2 -0
  215. package/docs/templates/agent-dag.schema.json +2 -2
  216. package/docs/templates/backend-test-dag.json +6 -4
  217. package/docs/templates/frontend-design-contract.md +4 -4
  218. package/docs/templates/frontend-implementation-contract.schema.json +68 -4
  219. package/docs/templates/frontend-implementation-dag.json +5 -5
  220. package/harness.json +1 -1
  221. package/package.json +8 -6
  222. package/skills/frontend-contract/SKILL.md +2 -1
  223. package/skills/frontend-contract/references/contract-protocol.md +19 -3
  224. package/skills/frontend-design-review/SKILL.md +12 -11
  225. package/skills/frontend-plan/SKILL.md +22 -2
  226. package/skills/frontend-plan/references/decision-contract.md +114 -5
  227. package/skills/frontend-plan/references/design-decisions.md +32 -0
  228. package/skills/frontend-review/SKILL.md +10 -11
  229. package/skills/frontend-scout/references/scout-evidence.md +4 -0
  230. package/dist/worker/console/static/assets/channel-DsZxrgqe.js +0 -1
  231. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-CuToPeGV.js +0 -1
  232. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-CuToPeGV.js +0 -1
  233. package/dist/worker/console/static/assets/index-D9Sc0f0y.js +0 -449
  234. package/dist/worker/console/static/assets/index-DDQc5a50.css +0 -1
  235. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-yPoT19ft.js +0 -1
  236. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-VWdarchG.js +0 -8
@@ -0,0 +1,44 @@
1
+ /** Insert the optimistic user bubble and assistant slot exactly once.
2
+ *
3
+ * The optimistic user row is the ONLY row carrying `pending: true`. Every
4
+ * rollback path deletes pending rows by clientRequestId; a durable row that
5
+ * already replaced it must survive (2026-09-08). */
6
+ export function appendPendingSubmission(messages, pending) {
7
+ if (messages.some((message) => message.clientRequestId === pending.clientRequestId))
8
+ return [...messages];
9
+ return [...messages, { ...pending.user, pending: true }, pending.assistant];
10
+ }
11
+ /** Merge a durable user message into its optimistic or snapshot row in place.
12
+ *
13
+ * The replacement drops the `pending` marker: the row is server truth from now
14
+ * on, so a later adopt/rejected/connect rollback for the same clientRequestId
15
+ * must not delete it. Matching the durable id also dedupes retained SSE replay
16
+ * against a session snapshot whose stored row predates clientRequestId. */
17
+ export function replacePendingUserMessage(messages, clientRequestId, durable) {
18
+ let replaced = false;
19
+ const next = [];
20
+ for (const message of messages) {
21
+ const matches = message.clientRequestId === clientRequestId || message.id === durable.id;
22
+ if (!matches) {
23
+ next.push(message);
24
+ continue;
25
+ }
26
+ if (replaced)
27
+ continue;
28
+ replaced = true;
29
+ const { pending: _pending, ...rest } = message;
30
+ next.push({ ...rest, ...durable, clientRequestId });
31
+ }
32
+ return replaced ? next : [...next, durable];
33
+ }
34
+ /** Keep an unconfirmed optimistic bubble when a recovery snapshot lacks it. */
35
+ export function mergePendingSubmissionOnHydration(canonical, pending) {
36
+ if (!pending || canonical.some((message) => message.clientRequestId === pending.clientRequestId))
37
+ return [...canonical];
38
+ return appendPendingSubmission(canonical, pending);
39
+ }
40
+ /** Drop only the unconfirmed optimistic rows for a submission. A durable row
41
+ * that already replaced the optimistic one is left untouched. */
42
+ export function removePendingSubmission(messages, clientRequestId) {
43
+ return messages.filter((message) => message.clientRequestId !== clientRequestId || message.pending !== true);
44
+ }
@@ -0,0 +1,41 @@
1
+ import { isSubagentTool } from "../../chat/tool-preview.js";
2
+ /**
3
+ * Count the collapsed-turn disclosure buckets exactly like DSH
4
+ * `TurnProcessNodeView`: tool calls, process messages and subagents, in that
5
+ * order. Built-in subagent delegations (`Agent` tool) count as subagents, not
6
+ * as ordinary tool calls (2026-09-08 alignment).
7
+ *
8
+ * @param items - ordered process nodes of one turn.
9
+ * @returns the three bucket counts.
10
+ */
11
+ export function countTurnProcessItems(items) {
12
+ let toolCalls = 0;
13
+ let messages = 0;
14
+ let subagents = 0;
15
+ for (const item of items) {
16
+ if (item.kind === "tool") {
17
+ if (isSubagentTool(item.tool.toolName))
18
+ subagents += 1;
19
+ else
20
+ toolCalls += 1;
21
+ continue;
22
+ }
23
+ messages += 1;
24
+ }
25
+ return { toolCalls, messages, subagents };
26
+ }
27
+ /**
28
+ * Summary label for a completed turn's process disclosure.
29
+ *
30
+ * @param counts - bucket counts from {@link countTurnProcessItems}.
31
+ * @returns `N 次工具调用 · M 条消息 · K 个 subagent`, or「已思考」when empty.
32
+ */
33
+ export function buildTurnProcessLabel(counts) {
34
+ return ([
35
+ counts.toolCalls > 0 ? `${counts.toolCalls} 次工具调用` : "",
36
+ counts.messages > 0 ? `${counts.messages} 条消息` : "",
37
+ counts.subagents > 0 ? `${counts.subagents} 个 subagent` : "",
38
+ ]
39
+ .filter(Boolean)
40
+ .join(" · ") || "已思考");
41
+ }
@@ -1,7 +1,10 @@
1
1
  /**
2
2
  * ADR 0011: full Pi tools + optional safe-* explore surface.
3
+ * The `Agent` (Explore) entry is injected from capabilities.builtinSubagents
4
+ * (ADR 0022 server truth); the static entry below is only a fallback for
5
+ * older backends that do not send the field yet.
3
6
  */
4
- const EXPLORE_TOOLS = [
7
+ const STATIC_EXPLORE_TOOLS = [
5
8
  {
6
9
  toolId: "read",
7
10
  kind: "explore",
@@ -58,6 +61,12 @@ const EXPLORE_TOOLS = [
58
61
  description: "固定参数查看 git diff;敏感路径不可查看且结果自动脱敏。",
59
62
  },
60
63
  ];
64
+ /** Fallback when the backend does not advertise builtinSubagents yet. */
65
+ const FALLBACK_AGENT_ENTRY = {
66
+ toolId: "Agent",
67
+ kind: "explore",
68
+ description: "启动内置只读 Explore 子 Agent,执行代码库搜索、符号查找和调用链追踪。",
69
+ };
61
70
  export function buildToolCatalog(capabilities) {
62
71
  if (!capabilities)
63
72
  return null;
@@ -68,7 +77,17 @@ export function buildToolCatalog(capabilities) {
68
77
  kind: t.kind ?? "read",
69
78
  cli: t.cli,
70
79
  }));
71
- const explore = [...EXPLORE_TOOLS];
80
+ // ADR 0022: subagents come from the server's builtinSubagents field;
81
+ // static entries never duplicate a server-advertised toolId.
82
+ const subagentTools = (capabilities.builtinSubagents ?? []).map((s) => ({
83
+ toolId: s.toolId,
84
+ description: s.description,
85
+ kind: "explore",
86
+ }));
87
+ const staticTools = subagentTools.length > 0
88
+ ? STATIC_EXPLORE_TOOLS
89
+ : [...STATIC_EXPLORE_TOOLS, FALLBACK_AGENT_ENTRY];
90
+ const explore = [...staticTools, ...subagentTools];
72
91
  return {
73
92
  explore,
74
93
  operator: operatorTools,
@@ -0,0 +1,13 @@
1
+ function sameReferences(previous, next) {
2
+ return (previous.length === next.length &&
3
+ previous.every((value, index) => value === next[index]));
4
+ }
5
+ /** A completed group's rendered projection is reusable only while every source
6
+ * object that feeds grouping and process-item derivation remains unchanged. */
7
+ export function sameTurnGroupInputs(previous, next) {
8
+ return (previous.key === next.key &&
9
+ previous.user === next.user &&
10
+ previous.live === next.live &&
11
+ sameReferences(previous.assistants, next.assistants) &&
12
+ sameReferences(previous.tools, next.tools));
13
+ }
@@ -224,6 +224,7 @@ export function applyAdoptActiveOwnership(bag, input) {
224
224
  releaseSubmitOwnership(bag, input.pending.clientRequestId);
225
225
  if (!input.current)
226
226
  return;
227
+ input.ui?.removePendingMessage?.(input.pending.clientRequestId);
227
228
  input.ui?.clearEmptyAssistant(input.assistantId);
228
229
  input.ui?.setInput(input.text);
229
230
  input.ui?.setPendingImages(input.images);
@@ -253,6 +254,7 @@ export function applyRejectedOwnership(bag, input) {
253
254
  input.ui?.setStreaming(false);
254
255
  input.ui?.setStreamingAssistantId(null);
255
256
  input.ui?.clearEmptyAssistant(input.assistantId);
257
+ input.ui?.removePendingMessage?.(input.pending.clientRequestId);
256
258
  input.ui?.setInput(input.text);
257
259
  input.ui?.setPendingImages(input.images);
258
260
  }
@@ -0,0 +1,51 @@
1
+ import { useEffect, useLayoutEffect, useRef } from "react";
2
+ /** Whether this browser can reveal `hidden="until-found"` content through
3
+ * `beforematch` (Chrome 102+, Firefox 148+, Safari: not supported at all).
4
+ * `content-visibility` support does NOT imply this — Safari 18+ and
5
+ * Firefox 125–147 have the former without the latter. */
6
+ function supportsUntilFound() {
7
+ return typeof document !== "undefined" && "onbeforematch" in document.body;
8
+ }
9
+ /**
10
+ * Collapse content without removing it from the document: the element gets
11
+ * `hidden="until-found"` so browser find-in-page can still reach the text, and
12
+ * a match fires `beforematch`, which reveals (expands) the disclosure again.
13
+ *
14
+ * Mirrors DSH `useSearchableHidden` (2026-09-08 alignment). Browsers without
15
+ * `until-found` support get a plain `hidden=""` element, which keeps the
16
+ * pre-alignment behavior (collapsed and not searchable).
17
+ *
18
+ * @param hidden - whether the content is collapsed.
19
+ * @param reveal - expand callback invoked on `beforematch` and when the
20
+ * collapsed subtree already owns focus.
21
+ * @returns ref for the collapsible container.
22
+ */
23
+ export function useSearchableHidden(hidden, reveal) {
24
+ const ref = useRef(null);
25
+ useLayoutEffect(() => {
26
+ const element = ref.current;
27
+ if (element === null)
28
+ return;
29
+ // Focus would otherwise be lost inside a hidden subtree.
30
+ if (hidden && element.contains(element.ownerDocument.activeElement)) {
31
+ reveal();
32
+ return;
33
+ }
34
+ if (hidden) {
35
+ element.setAttribute("hidden", supportsUntilFound() ? "until-found" : "");
36
+ }
37
+ else {
38
+ element.removeAttribute("hidden");
39
+ }
40
+ }, [hidden, reveal]);
41
+ useEffect(() => {
42
+ const element = ref.current;
43
+ if (element === null)
44
+ return;
45
+ element.addEventListener("beforematch", reveal);
46
+ return () => {
47
+ element.removeEventListener("beforematch", reveal);
48
+ };
49
+ }, [reveal]);
50
+ return ref;
51
+ }
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef, useState } from "react";
2
2
  import { runtimeSelectionFromRecord } from "../../chat/runtime-selection.js";
3
3
  import { hydrateSessionThread } from "../../chat/turn-process.js";
4
4
  import { forgetActiveChatSession, readActiveChatSessionId, rememberActiveChatSession, } from "../../chat/workspace-landing.js";
5
+ import { DEFAULT_CONSOLE_CHAT_SESSION_MODE } from "../../chat/session-mode-view.js";
5
6
  import { confirmationToken } from "./format.js";
6
7
  import { isLostChatSessionError } from "./runtime-snapshot-store.js";
7
8
  import { upsertCompactionMessage } from "./compaction-message.js";
@@ -67,6 +68,8 @@ function sameSessionSummary(a, b) {
67
68
  a.model?.modelId === b.model?.modelId &&
68
69
  a.thinkingLevel === b.thinkingLevel &&
69
70
  a.isBlank === b.isBlank &&
71
+ (a.sessionMode ?? DEFAULT_CONSOLE_CHAT_SESSION_MODE) ===
72
+ (b.sessionMode ?? DEFAULT_CONSOLE_CHAT_SESSION_MODE) &&
70
73
  a.optimisticState === b.optimisticState &&
71
74
  sameQueueSummary(a, b));
72
75
  }
@@ -83,8 +86,18 @@ export function stabilizeSessionList(previous, next) {
83
86
  });
84
87
  return changed ? stabilized : previous;
85
88
  }
89
+ /** Project a session detail/list record into the active ChatSessionInfo. */
90
+ export function projectActiveChatSession(session, fallbackWorkspaceId) {
91
+ const workspaceId = session.workspaceId?.trim() || fallbackWorkspaceId?.trim() || "";
92
+ return {
93
+ sessionId: session.sessionId,
94
+ ...(workspaceId ? { workspaceId } : {}),
95
+ sessionMode: session.sessionMode ?? DEFAULT_CONSOLE_CHAT_SESSION_MODE,
96
+ ...runtimeSelectionFromRecord(session),
97
+ };
98
+ }
86
99
  /** Select a server-confirmed blank session owned by the requested workspace. */
87
- export function findReusableBlankSession(list, workspaceId) {
100
+ export function findReusableBlankSession(list, workspaceId, sessionMode = DEFAULT_CONSOLE_CHAT_SESSION_MODE) {
88
101
  const scope = workspaceId.trim();
89
102
  if (!scope)
90
103
  return undefined;
@@ -93,7 +106,8 @@ export function findReusableBlankSession(list, workspaceId) {
93
106
  item.isBlank === true &&
94
107
  item.turnPhase === "idle" &&
95
108
  !item.activeTurnId &&
96
- !item.optimisticState);
109
+ !item.optimisticState &&
110
+ (item.sessionMode ?? DEFAULT_CONSOLE_CHAT_SESSION_MODE) === sessionMode);
97
111
  }
98
112
  export async function createSessionAndPrimeTitleWatcher(input) {
99
113
  if (input.creatingRef?.current) {
@@ -127,7 +141,13 @@ export async function createSessionAndPrimeTitleWatcher(input) {
127
141
  const body = (await res.json());
128
142
  if (!body.session)
129
143
  throw new Error("创建会话响应缺少 durable session");
130
- const durable = { ...body.session, isBlank: true };
144
+ const durable = {
145
+ ...body.session,
146
+ isBlank: true,
147
+ sessionMode: body.session.sessionMode ??
148
+ input.options?.sessionMode ??
149
+ DEFAULT_CONSOLE_CHAT_SESSION_MODE,
150
+ };
131
151
  const created = {
132
152
  sessionId: durable.sessionId,
133
153
  ...(durable.workspaceId || input.options?.workspaceId
@@ -135,6 +155,7 @@ export async function createSessionAndPrimeTitleWatcher(input) {
135
155
  workspaceId: durable.workspaceId ?? input.options?.workspaceId,
136
156
  }
137
157
  : {}),
158
+ sessionMode: durable.sessionMode,
138
159
  ...runtimeSelectionFromRecord(durable),
139
160
  };
140
161
  const nextList = replaceOptimisticSession(input.sessionListRef.current, input.options?.optimisticSessionId, durable);
@@ -402,11 +423,7 @@ export function useChatSessions(params) {
402
423
  }
403
424
  if (selectionVersion !== sessionSelectionVersionRef.current)
404
425
  return false;
405
- const next = {
406
- sessionId: body.session.sessionId,
407
- ...(body.session.workspaceId ? { workspaceId: body.session.workspaceId } : {}),
408
- ...runtimeSelectionFromRecord(body.session),
409
- };
426
+ const next = projectActiveChatSession(body.session);
410
427
  // AC-002: session switch (same or cross workspace) never aborts other
411
428
  // sessions' background request controllers; the single /events owner is
412
429
  // re-bound by useChatStream's session effect.
@@ -449,6 +466,7 @@ export function useChatSessions(params) {
449
466
  pendingQuestion: body.session.pendingQuestion ?? null,
450
467
  todos: body.session.todos ?? [],
451
468
  goal: body.session.goal ?? null,
469
+ history: body.history,
452
470
  });
453
471
  rememberActiveChatSession(window.localStorage, body.session.workspaceId ?? ownerWorkspaceId ?? workspaceId, sessionId);
454
472
  setActivePreviewPath(null);
@@ -537,15 +555,7 @@ export function useChatSessions(params) {
537
555
  selectionVersion !== sessionSelectionVersionRef.current ||
538
556
  !body.session)
539
557
  return;
540
- const restored = {
541
- sessionId: body.session.sessionId,
542
- ...(body.session.workspaceId
543
- ? { workspaceId: body.session.workspaceId }
544
- : workspaceId
545
- ? { workspaceId }
546
- : {}),
547
- ...runtimeSelectionFromRecord(body.session),
548
- };
558
+ const restored = projectActiveChatSession(body.session, workspaceId);
549
559
  refs.sessionGenerationRef.current += 1;
550
560
  refs.sessionRef.current = restored;
551
561
  // Do not seed Last-Event-ID here: session messages are restored
@@ -585,6 +595,7 @@ export function useChatSessions(params) {
585
595
  pendingQuestion: body.session.pendingQuestion ?? null,
586
596
  todos: body.session.todos ?? [],
587
597
  goal: body.session.goal ?? null,
598
+ history: body.history,
588
599
  });
589
600
  }
590
601
  catch {
@@ -617,6 +628,7 @@ export function useChatSessions(params) {
617
628
  setError("创建会话需要选择一个有效的工作区(不会回退默认工作区)");
618
629
  return Promise.reject(new Error("WORKSPACE_REQUIRED"));
619
630
  }
631
+ const sessionMode = options?.sessionMode ?? DEFAULT_CONSOLE_CHAT_SESSION_MODE;
620
632
  const optimisticSessionId = `optimistic-chat-${Date.now()}-${Math.random().toString(16).slice(2)}`;
621
633
  const now = new Date().toISOString();
622
634
  const optimisticRow = {
@@ -625,6 +637,7 @@ export function useChatSessions(params) {
625
637
  state: "active",
626
638
  updatedAt: now,
627
639
  turnPhase: "idle",
640
+ sessionMode,
628
641
  optimisticState: "creating",
629
642
  };
630
643
  pendingCreatedRowsRef.current.set(optimisticSessionId, optimisticRow);
@@ -667,9 +680,10 @@ export function useChatSessions(params) {
667
680
  }
668
681
  // A caller-provided snapshot is an explicit new-session boundary; reusing
669
682
  // an existing shell would silently drop that context suffix.
683
+ const sessionMode = options?.sessionMode ?? DEFAULT_CONSOLE_CHAT_SESSION_MODE;
670
684
  const reusable = options?.snapshot
671
685
  ? undefined
672
- : findReusableBlankSession(sessionListRef.current, scope);
686
+ : findReusableBlankSession(sessionListRef.current, scope, sessionMode);
673
687
  if (reusable) {
674
688
  if (previousSession?.sessionId === reusable.sessionId)
675
689
  return previousSession;
@@ -679,6 +693,7 @@ export function useChatSessions(params) {
679
693
  return {
680
694
  sessionId: reusable.sessionId,
681
695
  ...(reusable.workspaceId ? { workspaceId: reusable.workspaceId } : {}),
696
+ sessionMode: reusable.sessionMode ?? sessionMode,
682
697
  ...runtimeSelectionFromRecord(reusable),
683
698
  };
684
699
  }
@@ -3,6 +3,7 @@ import { formatOperatorUserError } from "../../operator-user-error.js";
3
3
  import { confirmationToken, requestId, sleep } from "./format.js";
4
4
  import { classifyCreateTurnNetworkFailure, classifyCreateTurnResponse, createPendingSubmission, decideSubmissionRoute, isRuntimeBusyUnion, isSubmissionCurrent, isTerminalTurnState, keepAmbiguousAfterRetry, reconcileDeferredSubmissionFromState, reconcilePendingSubmissionFromState, resolveAmbiguousFromState, } from "./turn-submission.js";
5
5
  import { applyAbortDecision, applyPendingReconcileDecision, classifyAbortResponse, markStopRequested, operatorChatAbortUrl, operatorChatEventsUrl, operatorChatStateUrl, operatorChatTurnsUrl, releaseOwnershipOnTerminalEvidence, releaseSubmitOwnership as releaseOwnershipBag, requestStopOnOwnership, settleCreateTurnOutcome, turnPhaseFromOwnership, visualTurnPhaseFromOwnership, } from "./turn-stream-controller.js";
6
+ import { appendPendingSubmission, removePendingSubmission } from "./pending-user-message.js";
6
7
  /** Project ChatRefs into the executable ownership bag used by the controller. */
7
8
  function ownershipBagFromRefs(refs) {
8
9
  return {
@@ -52,7 +53,11 @@ function clearEmptyAssistant(setMessages, assistantId) {
52
53
  return;
53
54
  setMessages((messages) => messages.filter((message) => !(message.id === assistantId &&
54
55
  message.role === "assistant" &&
55
- !message.text.trim())));
56
+ !message.text.trim() &&
57
+ // A row carrying only thinking/tool blocks is real content. Deleting
58
+ // it here dropped the turn's process nodes while the settled path in
59
+ // chat-sse-events deliberately keeps them (2026-09-08).
60
+ !(message.content && message.content.length > 0))));
56
61
  }
57
62
  /** One GET /events stream owns all Chat events. Sending a prompt creates a
58
63
  * detached Turn resource only; it never opens a second inline SSE consumer. */
@@ -139,7 +144,10 @@ export function useChatStream(params) {
139
144
  current.generation === generation)
140
145
  current.ready = true;
141
146
  replay = false;
142
- continue;
147
+ // Fall through: the applier owns the buffered-replay flush and
148
+ // returns immediately for this event. `continue` here silently
149
+ // dropped that flush, so a reconnect that landed after the
150
+ // turn settled never restored the answer (2026-09-08).
143
151
  }
144
152
  const assistantId = refs.assistantIdRef.current ?? fallbackAssistantId;
145
153
  const eventId = applySseBlock(block, assistantId, {
@@ -364,6 +372,7 @@ export function useChatStream(params) {
364
372
  setPendingImages: setPendingImagesUi,
365
373
  setStreaming,
366
374
  setStreamingAssistantId,
375
+ removePendingMessage: (requestId) => setMessages((messages) => removePendingSubmission(messages, requestId)),
367
376
  },
368
377
  rejectedUi: {
369
378
  setError,
@@ -373,6 +382,7 @@ export function useChatStream(params) {
373
382
  setInput,
374
383
  setPendingImages: setPendingImagesUi,
375
384
  formatError: formatOperatorUserError,
385
+ removePendingMessage: (requestId) => setMessages((messages) => removePendingSubmission(messages, requestId)),
376
386
  },
377
387
  // AC-R5-001: durable stopRequested + accepted/adopt yields one abort.
378
388
  onAutoAbort: (turnId) => {
@@ -669,6 +679,29 @@ export function useChatStream(params) {
669
679
  userMessageId,
670
680
  });
671
681
  refs.pendingSubmissionRef.current = pending;
682
+ const userMsg = {
683
+ id: userMessageId,
684
+ role: "user",
685
+ text: draftText,
686
+ createdAt: Date.now(),
687
+ clientRequestId,
688
+ };
689
+ const assistantMsg = {
690
+ id: assistantId,
691
+ role: "assistant",
692
+ text: "",
693
+ createdAt: Date.now(),
694
+ };
695
+ // React must receive the optimistic projection before the first await so
696
+ // SSE readiness latency cannot hide the user's submitted message.
697
+ setMessages((messages) => appendPendingSubmission(messages, {
698
+ clientRequestId,
699
+ user: userMsg,
700
+ assistant: assistantMsg,
701
+ }));
702
+ setInput("");
703
+ setAutoScroll(true);
704
+ setError(null);
672
705
  try {
673
706
  await ensureEventsConnected(activeSession.sessionId, generation);
674
707
  }
@@ -678,28 +711,27 @@ export function useChatStream(params) {
678
711
  setError(formatOperatorUserError(error instanceof Error ? error.message : String(error)));
679
712
  }
680
713
  releaseSubmitOwnership(clientRequestId);
714
+ setMessages((messages) => removePendingSubmission(messages, clientRequestId));
715
+ clearEmptyAssistant(setMessages, assistantId);
716
+ setInput(draftText);
717
+ setPendingImages(draftImages);
681
718
  return;
682
719
  }
683
720
  if (refs.sessionRef.current?.sessionId !== activeSession.sessionId ||
684
721
  refs.sessionGenerationRef.current !== generation) {
685
722
  releaseSubmitOwnership(clientRequestId);
723
+ if (refs.sessionRef.current?.sessionId === activeSession.sessionId) {
724
+ // Same session, newer generation: the activation snapshot replaced
725
+ // the thread, so the optimistic row is gone. Restore the draft
726
+ // instead of losing the user's text silently (2026-09-08).
727
+ setMessages((messages) => removePendingSubmission(messages, clientRequestId));
728
+ clearEmptyAssistant(setMessages, assistantId);
729
+ setInput(draftText);
730
+ setPendingImages(draftImages);
731
+ }
686
732
  return;
687
733
  }
688
- const userMsg = {
689
- id: userMessageId,
690
- role: "user",
691
- text: draftText,
692
- createdAt: Date.now(),
693
- };
694
- setMessages((messages) => [
695
- ...messages,
696
- userMsg,
697
- { id: assistantId, role: "assistant", text: "", createdAt: Date.now() },
698
- ]);
699
- setInput("");
700
734
  // Attachments stay until accepted (AC/UIS-accepted).
701
- setAutoScroll(true);
702
- setError(null);
703
735
  refs.streamingRef.current = true;
704
736
  setTurnPhase("running");
705
737
  refs.assistantIdRef.current = assistantId;