@tea-agent/loop-agent 0.43.0-next.1 → 0.43.0-next.11

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 (207) hide show
  1. package/CHANGELOG.md +144 -9
  2. package/dist/application/dag/run-dag.js +5 -0
  3. package/dist/application/evaluation/budget.js +19 -1
  4. package/dist/application/task-lifecycle/advance.js +17 -4
  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/executors/dag-pi-executor.js +1237 -586
  9. package/dist/executors/pi-executor.js +22 -1
  10. package/dist/executors/pi-sdk-executor.js +140 -39
  11. package/dist/executors/shell-executor.js +8 -4
  12. package/dist/shared/frontend-execution-policy.js +23 -0
  13. package/dist/task/config-types.js +4 -0
  14. package/dist/task/source-prepare/ledger-reconciliation.js +2 -2
  15. package/dist/task/source-prepare/ledger-review.js +6 -9
  16. package/dist/task/source-prepare/semantic-intake.js +16 -26
  17. package/dist/task/source-prepare/source-fidelity-pi.js +28 -9
  18. package/dist/task/source-references.js +48 -23
  19. package/dist/worker/console/chat/assistant-content.js +23 -2
  20. package/dist/worker/console/chat/browser-policy.js +113 -0
  21. package/dist/worker/console/chat/browser-routes.js +140 -0
  22. package/dist/worker/console/chat/explore-tools.js +13 -0
  23. package/dist/worker/console/chat/pi-runtime/custom-tools/ask-user-question.js +44 -0
  24. package/dist/worker/console/chat/pi-runtime/custom-tools/explore-tools.js +202 -0
  25. package/dist/worker/console/chat/pi-runtime/custom-tools/goal-tools.js +79 -0
  26. package/dist/worker/console/chat/pi-runtime/custom-tools/operator-tools.js +86 -0
  27. package/dist/worker/console/chat/pi-runtime/custom-tools/todo-write.js +38 -0
  28. package/dist/worker/console/chat/pi-runtime/inspection-types.js +28 -0
  29. package/dist/worker/console/chat/pi-runtime/inspection.js +439 -0
  30. package/dist/worker/console/chat/pi-runtime/sdk-bindings.js +153 -0
  31. package/dist/worker/console/chat/pi-runtime/sdk-session.js +5 -0
  32. package/dist/worker/console/chat/pi-runtime.js +207 -1178
  33. package/dist/worker/console/chat/routes.js +208 -71
  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 +65 -9
  38. package/dist/worker/console/chat/session-catalog.js +32 -0
  39. package/dist/worker/console/chat/session-mode-view.js +49 -0
  40. package/dist/worker/console/chat/session-mode.js +219 -0
  41. package/dist/worker/console/chat/session-store.js +22 -6
  42. package/dist/worker/console/chat/shortcuts.js +6 -0
  43. package/dist/worker/console/chat/terminal-routes.js +216 -0
  44. package/dist/worker/console/chat/terminal-sessions.js +348 -0
  45. package/dist/worker/console/chat/terminal-tools.js +211 -0
  46. package/dist/worker/console/chat/tool-preview.js +11 -0
  47. package/dist/worker/console/chat/tools.js +2 -0
  48. package/dist/worker/console/server.js +2 -28
  49. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-B0Q0nClf.js → abnfDiagram-N423BO3Z-CTSK0xbU.js} +1 -1
  50. package/dist/worker/console/static/assets/{arc-DCPjC19G.js → arc-_2MfctFP.js} +1 -1
  51. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CP5n9jmG.js → architectureDiagram-T3A2C74G-DsDe2wTg.js} +1 -1
  52. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-CmaWi_Wg.js → blockDiagram-VBNYF7ZC-Dtz_779U.js} +1 -1
  53. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-Ct5tcMfZ.js → c4Diagram-5PPSVZJV-LaULi-gw.js} +1 -1
  54. package/dist/worker/console/static/assets/channel-CqS4yBWT.js +1 -0
  55. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-C0osm1Zf.js → chunk-2GRJ4B5K-DuHBqqX8.js} +1 -1
  56. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-CDWviED7.js → chunk-2Q5K7J3B-BZgZs1dn.js} +1 -1
  57. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-mtSk1-j7.js → chunk-5RXB4S5H-tzwmlIO_.js} +1 -1
  58. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-D5J9PHC7.js → chunk-5VM5RSS4-CeYrLkSn.js} +1 -1
  59. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-Begg4WAa.js → chunk-6Q2QTUOP-C92cgilm.js} +1 -1
  60. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-sF1AEy7T.js → chunk-GF5L2VYU-XOXpGC0F.js} +1 -1
  61. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BqbsWXZb.js → chunk-JWPE2WC7-CGHbJyCH.js} +1 -1
  62. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BKJrPcJ6.js → chunk-KBJHAD2P-Bs2-LTzf.js} +1 -1
  63. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-CcwNMRho.js → chunk-RYQCIY6F-rN7up7E4.js} +1 -1
  64. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-BEJi3iIs.js → chunk-XXDRQBXY-B_fWjKpR.js} +1 -1
  65. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DVOxNOq0.js +1 -0
  66. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DVOxNOq0.js +1 -0
  67. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-DRLgTvVu.js → cose-bilkent-JH36ORCC-DE3SdJvB.js} +1 -1
  68. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BoAVG3cs.js → cynefin-VYW2F7L2-B-4yuRAX.js} +1 -1
  69. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Bzol06hR.js → cynefinDiagram-MW4NZA55-TiCPFuO8.js} +1 -1
  70. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-UmliJM77.js → dagre-VZM6K2ZE-7crzLMzA.js} +1 -1
  71. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-BWLcJkFo.js → diagram-7IWD3JNH-Cvr-x6Fd.js} +1 -1
  72. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-CgApOm-O.js → diagram-B4RE2ZJO-BzcRHUjH.js} +1 -1
  73. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-Bh3RgTLs.js → diagram-LBJQPF4R-BjkAXilh.js} +1 -1
  74. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-Dsh-D5nE.js → diagram-Q27KOJAE-BkzyATK7.js} +1 -1
  75. package/dist/worker/console/static/assets/{diagram-UB23O5K3-Dkbbpcpb.js → diagram-UB23O5K3-rZuTarYP.js} +1 -1
  76. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-3EBaA3t3.js → ebnfDiagram-BXEA7PRR-CAYENaN1.js} +1 -1
  77. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-Bq2Dc5ok.js → erDiagram-JOGREHBK-DCEam0Uq.js} +1 -1
  78. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-De7y55ha.js → flowDiagram-UKHOOZJN-e_dH4wVO.js} +1 -1
  79. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-fLiDbQRh.js → ganttDiagram-PKOTCBZU-CvZa2zOK.js} +1 -1
  80. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Du617mp1.js → gitGraphDiagram-DS77QQ5N-CbT5TuKp.js} +1 -1
  81. package/dist/worker/console/static/assets/index-CUPeoE8W.js +468 -0
  82. package/dist/worker/console/static/assets/index-SAANIK8g.css +1 -0
  83. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-BwmlF-XP.js → infoDiagram-6WML65LV-BWzHQa_Z.js} +1 -1
  84. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-zsGtS59u.js → ishikawaDiagram-WSZJBQD7-BTjlovtU.js} +1 -1
  85. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DTTPTe3f.js → journeyDiagram-NVQOT4AX-DkvQoCc1.js} +1 -1
  86. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BkXg9aP-.js → kanban-definition-27J2QSJJ-CHSzGi3W.js} +1 -1
  87. package/dist/worker/console/static/assets/{linear-7U2ue5IE.js → linear-DZMkTCWH.js} +1 -1
  88. package/dist/worker/console/static/assets/{mermaid.core-BUuGHmWO.js → mermaid.core-BIMhty7A.js} +5 -5
  89. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-7q6fX0Sv.js → mindmap-definition-FAOFIHXS-Dx-tyMl3.js} +1 -1
  90. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-CkQPJN55.js → pegDiagram-VL7TDLO6-C6G34rbY.js} +1 -1
  91. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-CCal_tfX.js → pieDiagram-7S7Q4E2Y-CHK9XOfm.js} +1 -1
  92. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-B7bpxyAZ.js → quadrantDiagram-CIZ2JOQS-CDARkI0w.js} +1 -1
  93. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-CJmww7D-.js → railroadDiagram-AXF67PYL-DiuUe0ZQ.js} +1 -1
  94. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-D6ldWJEv.js → requirementDiagram-LRYGKXZP-RE61UR56.js} +1 -1
  95. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-VGh33I9e.js → sankeyDiagram-W5VNT64P-KDNLHMEG.js} +1 -1
  96. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-CCkDrIjU.js → sequenceDiagram-SI44F4Z6-CeVgXnD9.js} +1 -1
  97. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-NC8f5Otb.js → sizeCapture-X5ZJPWSS-CXwGyjjG.js} +1 -1
  98. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-_92ezZdF.js → stateDiagram-OKZ733FA-BjTfozab.js} +1 -1
  99. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BXoYsyr2.js +1 -0
  100. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-BdUCGtbP.js → swimlanes-SLNWSIFB-BaXOB8GT.js} +2 -2
  101. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DfKliS4e.js +8 -0
  102. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CdD1H8ia.js → timeline-definition-Z64GVDOM-C3r2hqUU.js} +1 -1
  103. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-Dkdk3oFo.js → vennDiagram-T6HMQDX7-MWKgl1S9.js} +1 -1
  104. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CKu2uPPY.js → wardleyDiagram-T6FBY63Y-C9iFcZPk.js} +1 -1
  105. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-ClqAnuG8.js → xychartDiagram-ELKLHX3M-DqnM24Jp.js} +1 -1
  106. package/dist/worker/console/static/index.html +2 -2
  107. package/dist/worker/console/static-src/operator-chat/active-turn-tail.js +10 -0
  108. package/dist/worker/console/static-src/operator-chat/chat-link.js +94 -0
  109. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +122 -10
  110. package/dist/worker/console/static-src/operator-chat/compaction-message.js +7 -0
  111. package/dist/worker/console/static-src/operator-chat/pending-user-message.js +27 -15
  112. package/dist/worker/console/static-src/operator-chat/process-label.js +41 -0
  113. package/dist/worker/console/static-src/operator-chat/turn-group-equality.js +13 -0
  114. package/dist/worker/console/static-src/operator-chat/turn-process-disclosure.js +10 -0
  115. package/dist/worker/console/static-src/operator-chat/use-searchable-hidden.js +51 -0
  116. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +44 -24
  117. package/dist/worker/console/static-src/operator-chat/useChatStream.js +18 -2
  118. package/dist/worker/console/static-src/operator-chat/useChatThread.js +41 -8
  119. package/dist/worker/console/static-src/operator-chat/useComposer.js +26 -11
  120. package/dist/worker/console/static-src/operator-chat/user-turn-anchor-equality.js +21 -0
  121. package/dist/worker/console/workspace-context.js +15 -1
  122. package/dist/worker/observe/node-transparency.js +81 -72
  123. package/dist/worker/observe/routes.js +20 -1
  124. package/dist/worker/observe/static/dag-inspector-humanize.js +3 -0
  125. package/dist/worker/observe/static/dom.js +20 -1
  126. package/dist/worker/observe/static/format-pool.d.ts +2 -0
  127. package/dist/worker/observe/static/format-pool.js +6 -0
  128. package/dist/worker/observe/static/format.js +7 -0
  129. package/dist/worker/observe/static/inspect-workspace.js +34 -7
  130. package/dist/worker/observe/static/inspector-submission.js +32 -0
  131. package/dist/worker/observe/static/kpi.js +1 -0
  132. package/dist/worker/observe/static/relations.js +2 -0
  133. package/dist/worker/observe/static/router.js +13 -0
  134. package/dist/worker/observe/static/run-processing.js +2 -0
  135. package/dist/worker/observe/static/shell-chrome.js +36 -3
  136. package/dist/worker/observe/static/state.js +35 -2
  137. package/dist/worker/observe/static/styles.css +263 -39
  138. package/dist/worker/observe/static/task-failure-labels.d.ts +4 -0
  139. package/dist/worker/observe/static/task-failure-labels.js +67 -0
  140. package/dist/worker/observe/static/task-history.js +12 -0
  141. package/dist/worker/observe/static/views/batch.js +6 -13
  142. package/dist/worker/observe/static/views/dag-graph.js +50 -3
  143. package/dist/worker/observe/static/views/dag-inspector.js +742 -268
  144. package/dist/worker/observe/static/views/dag-trajectory.js +3 -0
  145. package/dist/worker/observe/static/views/dag.d.ts +6 -0
  146. package/dist/worker/observe/static/views/dag.js +49 -11
  147. package/dist/worker/observe/static/views/dags.js +2 -0
  148. package/dist/worker/observe/static/views/dashboard.js +21 -12
  149. package/dist/worker/observe/static/views/failures.js +21 -11
  150. package/dist/worker/observe/static/views/feature.js +11 -29
  151. package/dist/worker/observe/static/views/pool.js +37 -28
  152. package/dist/worker/observe/static/views/run.js +48 -5
  153. package/dist/worker/observe/static/views/session-timeline.js +189 -240
  154. package/dist/worker/observe/static/views/task.js +81 -62
  155. package/dist/workflows/dag/budget-enforcement.js +53 -3
  156. package/dist/workflows/dag/frontend-capacity.js +9 -0
  157. package/dist/workflows/dag/frontend-closeout.js +1 -1
  158. package/dist/workflows/dag/frontend-design-policy.js +2 -1
  159. package/dist/workflows/dag/frontend-durable-tools.js +193 -0
  160. package/dist/workflows/dag/frontend-execution-groups.js +24 -0
  161. package/dist/workflows/dag/frontend-implementation-contract.js +13 -0
  162. package/dist/workflows/dag/frontend-input-projection.js +76 -0
  163. package/dist/workflows/dag/frontend-plan-completeness.js +186 -0
  164. package/dist/workflows/dag/frontend-plan-recovery-policy.js +18 -0
  165. package/dist/workflows/dag/frontend-plan-render.js +10 -3
  166. package/dist/workflows/dag/frontend-prewrite-gate.js +3 -3
  167. package/dist/workflows/dag/frontend-recovery-controller.js +7 -7
  168. package/dist/workflows/dag/frontend-recovery-lineage.js +13 -0
  169. package/dist/workflows/dag/frontend-recovery-run.js +4 -0
  170. package/dist/workflows/dag/frontend-review-context.js +2 -1
  171. package/dist/workflows/dag/frontend-review-scopes.js +139 -0
  172. package/dist/workflows/dag/frontend-session-budget.js +249 -0
  173. package/dist/workflows/dag/frontend-shadow-dual-write.js +20 -2
  174. package/dist/workflows/dag/frontend-shape-facts.js +12 -2
  175. package/dist/workflows/dag/frontend-shape.js +30 -1
  176. package/dist/workflows/dag/frontend-test-execution-evidence.js +3 -2
  177. package/dist/workflows/dag/frontend-typed-event-store.js +11 -0
  178. package/dist/workflows/dag/frontend-verification-trace.js +1 -1
  179. package/dist/workflows/dag/init-hybrid.js +65 -167
  180. package/dist/workflows/dag/node-execution.js +38 -155
  181. package/dist/workflows/dag/prompt.js +4 -0
  182. package/dist/workflows/dag/rerun-plan.js +7 -1
  183. package/dist/workflows/dag/runner.js +26 -1
  184. package/dist/workflows/dag/types.js +6 -0
  185. package/docs/operations/README.md +1 -0
  186. package/docs/templates/frontend-design-contract.md +4 -4
  187. package/docs/templates/frontend-implementation-contract.schema.json +34 -2
  188. package/docs/templates/frontend-implementation-dag.json +14 -13
  189. package/package.json +6 -3
  190. package/skills/frontend-bounded-implement/SKILL.md +3 -3
  191. package/skills/frontend-bounded-implement/references/code-standards.md +3 -3
  192. package/skills/frontend-contract/SKILL.md +2 -1
  193. package/skills/frontend-contract/references/contract-protocol.md +22 -4
  194. package/skills/frontend-design-review/SKILL.md +22 -16
  195. package/skills/frontend-design-review/references/review-checklist.md +7 -7
  196. package/skills/frontend-plan/SKILL.md +21 -2
  197. package/skills/frontend-plan/references/decision-contract.md +42 -23
  198. package/skills/frontend-review/SKILL.md +22 -29
  199. package/skills/frontend-review/references/review-findings.md +24 -43
  200. package/skills/frontend-scout/references/scout-evidence.md +4 -0
  201. package/dist/worker/console/static/assets/channel-DAS07MdS.js +0 -1
  202. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Cwk-5jiW.js +0 -1
  203. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Cwk-5jiW.js +0 -1
  204. package/dist/worker/console/static/assets/index-C0O48S_P.js +0 -449
  205. package/dist/worker/console/static/assets/index-CzKf4U8P.css +0 -1
  206. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DyJg5gzW.js +0 -1
  207. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-B1cw_0Uy.js +0 -8
@@ -1,25 +1,35 @@
1
- /** Insert the optimistic user bubble and assistant slot exactly once. */
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). */
2
6
  export function appendPendingSubmission(messages, pending) {
3
7
  if (messages.some((message) => message.clientRequestId === pending.clientRequestId))
4
8
  return [...messages];
5
- return [...messages, pending.user, pending.assistant];
9
+ return [...messages, { ...pending.user, pending: true }, pending.assistant];
6
10
  }
7
- /** Replace an optimistic user bubble with its durable server message in place. */
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. */
8
17
  export function replacePendingUserMessage(messages, clientRequestId, durable) {
9
18
  let replaced = false;
10
- const next = messages.filter((message) => {
11
- if (message.clientRequestId !== clientRequestId)
12
- return true;
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
+ }
13
26
  if (replaced)
14
- return false;
27
+ continue;
15
28
  replaced = true;
16
- return true;
17
- });
18
- return replaced
19
- ? next.map((message) => message.clientRequestId === clientRequestId
20
- ? { ...message, ...durable, clientRequestId }
21
- : message)
22
- : [...next, durable];
29
+ const { pending: _pending, ...rest } = message;
30
+ next.push({ ...rest, ...durable, clientRequestId });
31
+ }
32
+ return replaced ? next : [...next, durable];
23
33
  }
24
34
  /** Keep an unconfirmed optimistic bubble when a recovery snapshot lacks it. */
25
35
  export function mergePendingSubmissionOnHydration(canonical, pending) {
@@ -27,6 +37,8 @@ export function mergePendingSubmissionOnHydration(canonical, pending) {
27
37
  return [...canonical];
28
38
  return appendPendingSubmission(canonical, pending);
29
39
  }
40
+ /** Drop only the unconfirmed optimistic rows for a submission. A durable row
41
+ * that already replaced the optimistic one is left untouched. */
30
42
  export function removePendingSubmission(messages, clientRequestId) {
31
- return messages.filter((message) => message.clientRequestId !== clientRequestId);
43
+ return messages.filter((message) => message.clientRequestId !== clientRequestId || message.pending !== true);
32
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
+ }
@@ -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
+ }
@@ -0,0 +1,10 @@
1
+ export function turnProcessDisclosureKey(groupKey, answerId) {
2
+ return `${groupKey}:generation-${answerId ?? "none"}`;
3
+ }
4
+ export function isTurnProcessCompact(input) {
5
+ return (!input.historyHasMore &&
6
+ !input.live &&
7
+ !input.hasError &&
8
+ input.processItemCount > 0 &&
9
+ !input.expanded);
10
+ }
@@ -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,9 +2,10 @@ 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
- import { upsertCompactionMessage } from "./compaction-message.js";
8
+ import { upsertCompactionMessage, } from "./compaction-message.js";
8
9
  import { describeMutationGateFailure, isMutationGateFailureCode, isStaleTokenFailure, } from "./mutation-gate.js";
9
10
  import { SessionTitleWatcher } from "./session-title-watcher.js";
10
11
  /** Pure list mutations keep optimistic UI deterministic and easy to verify. */
@@ -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);
@@ -158,7 +179,7 @@ function mergeCompactionMessages(messages, compactions) {
158
179
  * compact, plus browser-refresh recovery from the durable session store. */
159
180
  export function useChatSessions(params) {
160
181
  const { origin, refs, setSession, thread, composer, browser, setError, setNotice, onCompactionChange, setProcessOpen, setAutoScroll, } = params;
161
- const { applyActivatedSession, resetForNewSession } = thread;
182
+ const { applyActivatedSession, resetForNewSession, setMessages } = thread;
162
183
  const { setInput, setPendingImages } = composer;
163
184
  const { setPreviews, setActivePreviewPath } = browser;
164
185
  const { workspaceId, blocked } = params;
@@ -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.
@@ -538,15 +555,7 @@ export function useChatSessions(params) {
538
555
  selectionVersion !== sessionSelectionVersionRef.current ||
539
556
  !body.session)
540
557
  return;
541
- const restored = {
542
- sessionId: body.session.sessionId,
543
- ...(body.session.workspaceId
544
- ? { workspaceId: body.session.workspaceId }
545
- : workspaceId
546
- ? { workspaceId }
547
- : {}),
548
- ...runtimeSelectionFromRecord(body.session),
549
- };
558
+ const restored = projectActiveChatSession(body.session, workspaceId);
550
559
  refs.sessionGenerationRef.current += 1;
551
560
  refs.sessionRef.current = restored;
552
561
  // Do not seed Last-Event-ID here: session messages are restored
@@ -619,6 +628,7 @@ export function useChatSessions(params) {
619
628
  setError("创建会话需要选择一个有效的工作区(不会回退默认工作区)");
620
629
  return Promise.reject(new Error("WORKSPACE_REQUIRED"));
621
630
  }
631
+ const sessionMode = options?.sessionMode ?? DEFAULT_CONSOLE_CHAT_SESSION_MODE;
622
632
  const optimisticSessionId = `optimistic-chat-${Date.now()}-${Math.random().toString(16).slice(2)}`;
623
633
  const now = new Date().toISOString();
624
634
  const optimisticRow = {
@@ -627,6 +637,7 @@ export function useChatSessions(params) {
627
637
  state: "active",
628
638
  updatedAt: now,
629
639
  turnPhase: "idle",
640
+ sessionMode,
630
641
  optimisticState: "creating",
631
642
  };
632
643
  pendingCreatedRowsRef.current.set(optimisticSessionId, optimisticRow);
@@ -669,9 +680,10 @@ export function useChatSessions(params) {
669
680
  }
670
681
  // A caller-provided snapshot is an explicit new-session boundary; reusing
671
682
  // an existing shell would silently drop that context suffix.
683
+ const sessionMode = options?.sessionMode ?? DEFAULT_CONSOLE_CHAT_SESSION_MODE;
672
684
  const reusable = options?.snapshot
673
685
  ? undefined
674
- : findReusableBlankSession(sessionListRef.current, scope);
686
+ : findReusableBlankSession(sessionListRef.current, scope, sessionMode);
675
687
  if (reusable) {
676
688
  if (previousSession?.sessionId === reusable.sessionId)
677
689
  return previousSession;
@@ -681,6 +693,7 @@ export function useChatSessions(params) {
681
693
  return {
682
694
  sessionId: reusable.sessionId,
683
695
  ...(reusable.workspaceId ? { workspaceId: reusable.workspaceId } : {}),
696
+ sessionMode: reusable.sessionMode ?? sessionMode,
684
697
  ...runtimeSelectionFromRecord(reusable),
685
698
  };
686
699
  }
@@ -763,11 +776,18 @@ export function useChatSessions(params) {
763
776
  return;
764
777
  }
765
778
  setError(null);
766
- // pi-web parity: reload the compacted session, then leave a distinct
767
- // compaction summary card in the timeline plus a transient toast.
779
+ // Keep the current projection and converge the lifecycle row in place.
780
+ // Re-activating here would clear the timeline and replay the full event ring.
768
781
  const summary = body.snapshot?.summary?.trim();
769
782
  const summaryText = summary || "历史上下文已精简,关键决策与当前任务已保留。";
770
- await activateSession(current.sessionId);
783
+ const compaction = body.compaction;
784
+ if (compaction &&
785
+ refs.sessionRef.current?.sessionId === current.sessionId) {
786
+ if (compaction.eventId) {
787
+ refs.seenEventIdsRef.current.add(compaction.eventId);
788
+ }
789
+ setMessages((messages) => upsertCompactionMessage(messages, compaction));
790
+ }
771
791
  setNotice({
772
792
  id: Date.now(),
773
793
  message: `已压缩上下文:${summaryText.slice(0, 72)}${summaryText.length > 72 ? "…" : ""}`,
@@ -785,7 +805,7 @@ export function useChatSessions(params) {
785
805
  setError,
786
806
  setNotice,
787
807
  onCompactionChange,
788
- activateSession,
808
+ setMessages,
789
809
  sessionOwnerQuery,
790
810
  ]);
791
811
  const updateSession = async (sessionId, patch) => {
@@ -53,7 +53,11 @@ function clearEmptyAssistant(setMessages, assistantId) {
53
53
  return;
54
54
  setMessages((messages) => messages.filter((message) => !(message.id === assistantId &&
55
55
  message.role === "assistant" &&
56
- !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))));
57
61
  }
58
62
  /** One GET /events stream owns all Chat events. Sending a prompt creates a
59
63
  * detached Turn resource only; it never opens a second inline SSE consumer. */
@@ -140,7 +144,10 @@ export function useChatStream(params) {
140
144
  current.generation === generation)
141
145
  current.ready = true;
142
146
  replay = false;
143
- 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).
144
151
  }
145
152
  const assistantId = refs.assistantIdRef.current ?? fallbackAssistantId;
146
153
  const eventId = applySseBlock(block, assistantId, {
@@ -713,6 +720,15 @@ export function useChatStream(params) {
713
720
  if (refs.sessionRef.current?.sessionId !== activeSession.sessionId ||
714
721
  refs.sessionGenerationRef.current !== generation) {
715
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
+ }
716
732
  return;
717
733
  }
718
734
  // Attachments stay until accepted (AC/UIS-accepted).
@@ -1,10 +1,13 @@
1
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
1
+ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, } from "react";
2
2
  import { buildTurnProcessGroups, hydrateSessionThread, selectRecentTurnGroups, } from "../../chat/turn-process.js";
3
+ import { isToolFailure } from "../../chat/tool-preview.js";
3
4
  import { mergeOperationCardState, } from "../../chat/operation-card.js";
4
5
  import { createChatSseEventApplier, } from "./chat-sse-events.js";
5
6
  import { mergePendingSubmissionOnHydration } from "./pending-user-message.js";
6
7
  import { taskContextUnchanged } from "./format.js";
7
8
  import { upsertCompactionMessage } from "./compaction-message.js";
9
+ import { sameTurnGroupInputs } from "./turn-group-equality.js";
10
+ import { selectActiveTurnTail } from "./active-turn-tail.js";
8
11
  const EMPTY_USAGE = {
9
12
  inputTokens: 0,
10
13
  outputTokens: 0,
@@ -115,6 +118,12 @@ export function useChatThread(params) {
115
118
  // (UI-08); the SSE applier must not open the process panel.
116
119
  void params.setProcessOpen;
117
120
  const [messages, setMessages] = useState([]);
121
+ /** Rendered-messages mirror for the replay flush: it must resolve its target
122
+ * row synchronously, before React commits the queued updater (2026-09-08). */
123
+ const messagesRef = useRef([]);
124
+ useLayoutEffect(() => {
125
+ messagesRef.current = messages;
126
+ }, [messages]);
118
127
  const [toolCalls, setToolCalls] = useState([]);
119
128
  const [operationCards, setOperationCards] = useState([]);
120
129
  const [artifactCards, setArtifactCards] = useState([]);
@@ -187,6 +196,7 @@ export function useChatThread(params) {
187
196
  onTelemetryRefresh: () => void refreshSessionTelemetry(),
188
197
  setMessages,
189
198
  setToolCalls,
199
+ getMessages: () => messagesRef.current,
190
200
  }), [refs, onReconcile, setError, refreshSessionTelemetry]);
191
201
  /** Parse + apply one raw SSE block (frames separated by \n\n). Returns the
192
202
  * eventId if the block carried an `id:` line (for Last-Event-ID tracking). */
@@ -285,7 +295,7 @@ export function useChatThread(params) {
285
295
  if (toolCalls.length === 0)
286
296
  return null;
287
297
  const running = toolCalls.filter((t) => t.status === "running");
288
- const errored = toolCalls.filter((t) => t.status === "error");
298
+ const errored = toolCalls.filter((t) => isToolFailure(t));
289
299
  const current = running[running.length - 1];
290
300
  let statusLabel = "完成";
291
301
  let statusKind = "ok";
@@ -317,10 +327,7 @@ export function useChatThread(params) {
317
327
  previousTimeline?.activeAssistantId === streamingAssistantId &&
318
328
  (previousTimeline?.messages.every((message, index) => message.id === streamingAssistantId || timelineMessages[index]?.id === message.id) ?? false);
319
329
  if (canBuildActiveTail && previousTimeline) {
320
- let tailStart = activeIndex;
321
- while (tailStart > 0 && timelineMessages[tailStart - 1]?.role !== "user")
322
- tailStart -= 1;
323
- const tailMessages = timelineMessages.slice(tailStart);
330
+ const tailMessages = selectActiveTurnTail(timelineMessages, activeIndex);
324
331
  const tailIds = new Set(tailMessages.map((message) => message.id));
325
332
  const tailTurnIds = new Set(tailMessages.map((message) => message.turnId).filter(Boolean));
326
333
  const tailTools = toolCalls.filter((tool) => tailIds.has(tool.assistantMessageId ?? "") ||
@@ -348,7 +355,9 @@ export function useChatThread(params) {
348
355
  const previous = new Map(completedGroupsRef.current.map((group) => [group.key, group]));
349
356
  const all = built.map((group) => {
350
357
  const cached = previous.get(group.key);
351
- return cached && !group.live ? cached : group;
358
+ return cached && !group.live && sameTurnGroupInputs(cached, group)
359
+ ? cached
360
+ : group;
352
361
  });
353
362
  completedGroupsRef.current = all.filter((group) => !group.live);
354
363
  previousTimelineRef.current = {
@@ -450,7 +459,31 @@ export function useChatThread(params) {
450
459
  /** Adopt a freshly activated session's hydrated thread + streaming flags. */
451
460
  const applyActivatedSession = useCallback((next) => {
452
461
  const visuallyStopped = next.turnPhase === "stopping";
453
- setMessages(next.messages);
462
+ // A submission still owned for THIS session keeps its optimistic bubble
463
+ // across an activation snapshot (refresh / reopen); a lease owned by
464
+ // another session must never leak into this thread (2026-09-08).
465
+ const ownedPending = refs.pendingSubmissionRef.current?.sessionId ===
466
+ refs.sessionRef.current?.sessionId
467
+ ? refs.pendingSubmissionRef.current
468
+ : undefined;
469
+ setMessages(mergePendingSubmissionOnHydration(next.messages, ownedPending
470
+ ? {
471
+ clientRequestId: ownedPending.clientRequestId,
472
+ user: {
473
+ id: ownedPending.userMessageId,
474
+ role: "user",
475
+ text: ownedPending.text,
476
+ createdAt: Date.now(),
477
+ clientRequestId: ownedPending.clientRequestId,
478
+ },
479
+ assistant: {
480
+ id: ownedPending.assistantId,
481
+ role: "assistant",
482
+ text: "",
483
+ createdAt: Date.now(),
484
+ },
485
+ }
486
+ : undefined));
454
487
  setToolCalls(next.toolCalls);
455
488
  setOperationCards([]);
456
489
  setHumanGateCards([]);
@@ -7,7 +7,7 @@ import { attachRuntimeSnapshotController } from "./useRuntimeSnapshot.js";
7
7
  /** Composer state: input text, @file palette, image attachments, /shortcuts,
8
8
  * IME composition guards, auto-grow and server-side draft sync. */
9
9
  export function useComposer(params) {
10
- const { origin, sessionId, refs, setError, onUnrecoverableSession } = params;
10
+ const { origin, sessionId, sessionMode, refs, setError, onUnrecoverableSession, } = params;
11
11
  const [input, setInputState] = useState("");
12
12
  const [fileOptions, setFileOptions] = useState([]);
13
13
  const [pendingImages, setPendingImages] = useState([]);
@@ -28,7 +28,10 @@ export function useComposer(params) {
28
28
  }, []);
29
29
  const [shortcutIndex, setShortcutIndex] = useState(0);
30
30
  const [dynamicCommands, setDynamicCommands] = useState([]);
31
- const [commandCapabilities, setCommandCapabilities] = useState({ sddBingo: false });
31
+ const [commandCapabilities, setCommandCapabilities] = useState({
32
+ sddBingo: false,
33
+ sessionMode: undefined,
34
+ });
32
35
  const [dynamicCommandsLoading, setDynamicCommandsLoading] = useState(false);
33
36
  const [dynamicCommandsError, setDynamicCommandsError] = useState(null);
34
37
  const [commandsEpoch, setCommandsEpoch] = useState(0);
@@ -75,10 +78,10 @@ export function useComposer(params) {
75
78
  commandsSessionRef.current = sessionId;
76
79
  commandsGenerationRef.current += 1;
77
80
  setDynamicCommands([]);
78
- setCommandCapabilities({ sddBingo: false });
81
+ setCommandCapabilities({ sddBingo: false, sessionMode });
79
82
  setDynamicCommandsError(null);
80
83
  setDynamicCommandsLoading(false);
81
- }, [sessionId]);
84
+ }, [sessionId, sessionMode]);
82
85
  const loadDynamicCommands = useCallback(async () => {
83
86
  if (!sessionId) {
84
87
  setDynamicCommands([]);
@@ -124,7 +127,10 @@ export function useComposer(params) {
124
127
  if (!isCurrent())
125
128
  return;
126
129
  setDynamicCommands(items);
127
- setCommandCapabilities({ sddBingo: body?.capabilities?.sddBingo === true });
130
+ setCommandCapabilities({
131
+ sddBingo: body?.capabilities?.sddBingo === true,
132
+ sessionMode: body?.capabilities?.sessionMode ?? sessionMode,
133
+ });
128
134
  // Compact server fail-soft / partial-source warning; local Operator/Web still merge.
129
135
  const warning = typeof body?.warning === "string" ? body.warning.trim() : "";
130
136
  setDynamicCommandsError(warning ? warning.slice(0, 160) : null);
@@ -134,14 +140,14 @@ export function useComposer(params) {
134
140
  return;
135
141
  setDynamicCommandsError("动态命令加载失败");
136
142
  setDynamicCommands([]);
137
- setCommandCapabilities({ sddBingo: false });
143
+ setCommandCapabilities({ sddBingo: false, sessionMode });
138
144
  }
139
145
  finally {
140
146
  if (isCurrent()) {
141
147
  setDynamicCommandsLoading(false);
142
148
  }
143
149
  }
144
- }, [origin, sessionId]);
150
+ }, [origin, sessionId, sessionMode]);
145
151
  // Lazy-load dynamic commands when slash palette opens or session changes.
146
152
  useEffect(() => {
147
153
  if (!sessionId)
@@ -206,13 +212,22 @@ export function useComposer(params) {
206
212
  const removeImage = useCallback((image) => {
207
213
  setPendingImages((items) => items.filter((item) => item !== image));
208
214
  }, []);
209
- // Auto-grow textarea height.
210
- useLayoutEffect(() => {
215
+ // Chromium handles growth in CSS without forcing a layout read on every key.
216
+ // Keep a deferred fallback for browsers that do not support field-sizing.
217
+ useEffect(() => {
211
218
  const ta = textareaRef.current;
212
219
  if (!ta)
213
220
  return;
214
- ta.style.height = "auto";
215
- ta.style.height = `${Math.min(ta.scrollHeight, 280)}px`;
221
+ if (typeof CSS !== "undefined" &&
222
+ CSS.supports("field-sizing", "content")) {
223
+ ta.style.removeProperty("height");
224
+ return;
225
+ }
226
+ const frame = window.requestAnimationFrame(() => {
227
+ ta.style.height = "auto";
228
+ ta.style.height = `${Math.min(ta.scrollHeight, 280)}px`;
229
+ });
230
+ return () => window.cancelAnimationFrame(frame);
216
231
  }, [input]);
217
232
  // Debounced server-side composer draft sync.
218
233
  useEffect(() => {
@@ -0,0 +1,21 @@
1
+ /** User anchors only depend on the ordered user-message objects, not on
2
+ * assistant/process updates that rebuild their containing turn groups. */
3
+ export function sameUserMessages(previous, next) {
4
+ let previousIndex = 0;
5
+ let nextIndex = 0;
6
+ for (;;) {
7
+ while (previousIndex < previous.length && !previous[previousIndex].user) {
8
+ previousIndex += 1;
9
+ }
10
+ while (nextIndex < next.length && !next[nextIndex].user) {
11
+ nextIndex += 1;
12
+ }
13
+ if (previousIndex === previous.length || nextIndex === next.length) {
14
+ return previousIndex === previous.length && nextIndex === next.length;
15
+ }
16
+ if (previous[previousIndex].user !== next[nextIndex].user)
17
+ return false;
18
+ previousIndex += 1;
19
+ nextIndex += 1;
20
+ }
21
+ }
@@ -37,6 +37,8 @@ import { UserQuestionRegistry } from "./chat/user-questions.js";
37
37
  import { TodoListRegistry } from "./chat/todos.js";
38
38
  import { GoalRegistry } from "./chat/goals.js";
39
39
  import { GoalRoundDriver } from "./chat/goal-round-driver.js";
40
+ import { TerminalSessionRegistry } from "./chat/terminal-sessions.js";
41
+ import { ScmService } from "./chat/scm-service.js";
40
42
  import { driveGoalRound, recoverDeferredTurns } from "./chat/routes.js";
41
43
  import { createOperationEventStore, } from "./operation-sse.js";
42
44
  import { OperationStore } from "../../infrastructure/console/operation-store.js";
@@ -254,7 +256,9 @@ export async function createWorkspaceContext(record, deps) {
254
256
  }
255
257
  },
256
258
  });
257
- const sddDataAlignment = new SddDataAlignmentService(chatStore, new AimaxAuthReader(), deps.fetchImpl);
259
+ const aimaxAuth = new AimaxAuthReader();
260
+ await aimaxAuth.getToken();
261
+ const sddDataAlignment = new SddDataAlignmentService(chatStore, aimaxAuth, deps.fetchImpl);
258
262
  const chatEventEpoch = randomBytes(12).toString("hex");
259
263
  const chatEvents = createChatEventStore({
260
264
  appData,
@@ -285,6 +289,12 @@ export async function createWorkspaceContext(record, deps) {
285
289
  operations,
286
290
  operationEvents: events,
287
291
  });
292
+ // Persistent PTY terminal sessions shared by the model tool (terminal) and
293
+ // the Console UI terminal panel (HTTP API), keyed per chat session owner.
294
+ const terminalRegistry = new TerminalSessionRegistry();
295
+ // Workspace-scoped git source control shared by the model tool (scm) and
296
+ // the Console UI SCM panel (HTTP API).
297
+ const scmService = new ScmService(repoRoot);
288
298
  const chatRuntime = new ConsolePiRuntime({
289
299
  cwd: repoRoot,
290
300
  sessionDir: appData.chatSessions,
@@ -294,6 +304,8 @@ export async function createWorkspaceContext(record, deps) {
294
304
  todoRegistry,
295
305
  goalRegistry,
296
306
  sddDataAlignment,
307
+ terminalRegistry,
308
+ scmService,
297
309
  });
298
310
  let chatRouteDeps;
299
311
  const goalRoundDriver = new GoalRoundDriver((sessionId) => driveGoalRound(chatRouteDeps, sessionId));
@@ -308,6 +320,8 @@ export async function createWorkspaceContext(record, deps) {
308
320
  sddDataAlignment,
309
321
  goalRoundDriver,
310
322
  operationLinker: chatOperationLinker,
323
+ terminals: terminalRegistry,
324
+ scm: scmService,
311
325
  appData,
312
326
  bootToken: deps.bootToken,
313
327
  getConsoleOrigin: deps.getConsoleOrigin,