@tea-agent/loop-agent 0.40.0-next.1 → 0.40.0-next.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (208) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +106 -109
  3. package/README.md +2 -2
  4. package/dist/application/task-lifecycle/advance.js +12 -0
  5. package/dist/build-stamp.json +3 -3
  6. package/dist/cli/update/runtime-activity.js +1 -29
  7. package/dist/commands/dag-rerun.js +1 -1
  8. package/dist/executors/dag-pi-executor.js +69 -7
  9. package/dist/executors/pi-executor.js +4 -2
  10. package/dist/executors/pi-sdk-executor.js +46 -1
  11. package/dist/executors/pi-writer-tool-policy.js +57 -1
  12. package/dist/executors/shell-executor.js +43 -3
  13. package/dist/executors/workspace-write-snapshot.js +68 -0
  14. package/dist/infrastructure/harness/atomic-write.js +23 -0
  15. package/dist/shared/dag-prompt-override.js +4 -5
  16. package/dist/shared/update/npm-client.js +33 -11
  17. package/dist/task/config-types.js +10 -0
  18. package/dist/task/contract/apply.js +11 -1
  19. package/dist/task/contract/constants.js +2 -0
  20. package/dist/task/contract/hash.js +3 -0
  21. package/dist/task/contract/observe.js +25 -4
  22. package/dist/task/contract/paths.js +2 -1
  23. package/dist/task/contract/project.js +15 -1
  24. package/dist/task/contract/schema.js +2 -0
  25. package/dist/task/contract/transaction.js +12 -0
  26. package/dist/task/source-prepare/completeness.js +188 -0
  27. package/dist/task/source-prepare/fragment-inventory.js +474 -0
  28. package/dist/task/source-prepare/index.js +3 -0
  29. package/dist/task/source-prepare/ledger-reconciliation.js +127 -0
  30. package/dist/task/source-prepare/ledger-review.js +214 -0
  31. package/dist/task/source-prepare/ledger.js +545 -0
  32. package/dist/task/source-prepare/prepare.js +262 -1
  33. package/dist/task/source-prepare/semantic-intake.js +19 -3
  34. package/dist/task/source-prepare/source-fidelity-pi.js +384 -0
  35. package/dist/task/source-prepare/types.js +22 -0
  36. package/dist/worker/cli.js +12 -26
  37. package/dist/worker/console/chat/chat-event-store.js +75 -5
  38. package/dist/worker/console/chat/context-panel.js +7 -0
  39. package/dist/worker/console/chat/deferred-turn.js +12 -0
  40. package/dist/worker/console/chat/pi-runtime.js +331 -18
  41. package/dist/worker/console/chat/provider-error.js +98 -0
  42. package/dist/worker/console/chat/routes.js +389 -56
  43. package/dist/worker/console/chat/session-stats.js +179 -0
  44. package/dist/worker/console/chat/session-store.js +93 -63
  45. package/dist/worker/console/chat/todos.js +115 -0
  46. package/dist/worker/console/chat/tool-preview.js +42 -0
  47. package/dist/worker/console/chat/turn-process.js +20 -55
  48. package/dist/worker/console/chat/user-questions.js +266 -0
  49. package/dist/worker/console/chat/workspace-landing.js +79 -0
  50. package/dist/worker/console/console-handoff.js +82 -0
  51. package/dist/worker/console/console-update-and-init.js +115 -0
  52. package/dist/worker/console/console-update-runtime.js +84 -0
  53. package/dist/worker/console/draft-store.js +49 -0
  54. package/dist/worker/console/native-directory-picker.js +13 -0
  55. package/dist/worker/console/operation-store.js +1 -1
  56. package/dist/worker/console/operator-actions.js +129 -4
  57. package/dist/worker/console/operator-surface-health.js +1 -0
  58. package/dist/worker/console/prd-intake-bridge.js +13 -0
  59. package/dist/worker/console/routes.js +312 -1
  60. package/dist/worker/console/security.js +4 -4
  61. package/dist/worker/console/server.js +583 -172
  62. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-Br92EGb7.js → abnfDiagram-N423BO3Z-Bs-CDAXM.js} +1 -1
  63. package/dist/worker/console/static/assets/{arc-oulImtQq.js → arc-CAkA3We3.js} +1 -1
  64. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-tyHc6aqO.js → architectureDiagram-T3A2C74G-DEmI_zqr.js} +1 -1
  65. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-CY86BSpT.js → blockDiagram-VBNYF7ZC-uDHANebE.js} +1 -1
  66. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BKKsDmNs.js → c4Diagram-5PPSVZJV-Bv97b4_B.js} +1 -1
  67. package/dist/worker/console/static/assets/channel-amxUpk7o.js +1 -0
  68. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-CXZdjl3B.js → chunk-2GRJ4B5K-shmQKsFz.js} +1 -1
  69. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-B-aIFDQ5.js → chunk-2Q5K7J3B-BAraNvcN.js} +1 -1
  70. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BDgAW_B2.js → chunk-5RXB4S5H-BirhxUop.js} +1 -1
  71. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-DABa4UPR.js → chunk-5VM5RSS4-C9LgUJpR.js} +1 -1
  72. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-aGbysVOx.js → chunk-6Q2QTUOP-DrU-s-t7.js} +1 -1
  73. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-B8MSZDVs.js → chunk-GF5L2VYU-Tx-H_FA2.js} +1 -1
  74. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-CpelV73g.js → chunk-JWPE2WC7-BjLtNFrR.js} +1 -1
  75. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BE04ty3z.js → chunk-KBJHAD2P-UNLVwSsy.js} +1 -1
  76. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-BUxTkaaR.js → chunk-RYQCIY6F-CBEUX8XD.js} +1 -1
  77. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-CYYBVKfX.js → chunk-XXDRQBXY-WTL7IZOq.js} +1 -1
  78. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-bejwBVIz.js +1 -0
  79. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-bejwBVIz.js +1 -0
  80. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CEQF_xmQ.js → cose-bilkent-JH36ORCC-CkHG6W3s.js} +1 -1
  81. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-CqpxN5IT.js → cynefin-VYW2F7L2-BRzFqTaL.js} +1 -1
  82. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-B3PzanVi.js → cynefinDiagram-MW4NZA55-2_CFp_i7.js} +1 -1
  83. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BSUGCNx1.js → dagre-VZM6K2ZE-BJtsmdkE.js} +1 -1
  84. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-CdLevU5T.js → diagram-7IWD3JNH-DNEFFo6w.js} +1 -1
  85. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BjpF_Kok.js → diagram-B4RE2ZJO-B_IWV7eo.js} +1 -1
  86. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-C4I6NJny.js → diagram-LBJQPF4R-DvvRx2dz.js} +1 -1
  87. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-C8hecCr5.js → diagram-Q27KOJAE-CsibsWLg.js} +1 -1
  88. package/dist/worker/console/static/assets/{diagram-UB23O5K3-Daod6tOS.js → diagram-UB23O5K3-les_djBQ.js} +1 -1
  89. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BvvmazGv.js → ebnfDiagram-BXEA7PRR-BEm2n7qJ.js} +1 -1
  90. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DeN_vfV4.js → erDiagram-JOGREHBK-BI7vHHVm.js} +1 -1
  91. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-D6dq1t9z.js → flowDiagram-UKHOOZJN-CUFCERrB.js} +1 -1
  92. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DziIdNY4.js → ganttDiagram-PKOTCBZU-wxyLiGWt.js} +1 -1
  93. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Dy4s5CXr.js → gitGraphDiagram-DS77QQ5N-Bt897K8Z.js} +1 -1
  94. package/dist/worker/console/static/assets/index-BHhUOTri.css +1 -0
  95. package/dist/worker/console/static/assets/index-DqJbO3-p.js +407 -0
  96. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-Czv5eYx2.js → infoDiagram-6WML65LV-DYNx_IPv.js} +1 -1
  97. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-Bsk3nFRT.js → ishikawaDiagram-WSZJBQD7-CzpLNEu6.js} +1 -1
  98. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DxGmNj5K.js → journeyDiagram-NVQOT4AX-97f9owNU.js} +1 -1
  99. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CYCFqkO_.js → kanban-definition-27J2QSJJ-BxM9hyBn.js} +1 -1
  100. package/dist/worker/console/static/assets/{linear-B0YzRwrd.js → linear-ByuxcHvp.js} +1 -1
  101. package/dist/worker/console/static/assets/{mermaid.core-t3EZtIGh.js → mermaid.core-DwPGvpCv.js} +5 -5
  102. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-BTXpTv0T.js → mindmap-definition-FAOFIHXS-DCoqG9RT.js} +1 -1
  103. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DU8UkKqA.js → pegDiagram-VL7TDLO6-BLSBmBMx.js} +1 -1
  104. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DP4y8ohp.js → pieDiagram-7S7Q4E2Y-B0t_0oJW.js} +1 -1
  105. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-B07vNaYR.js → quadrantDiagram-CIZ2JOQS-sTzDk7Nt.js} +1 -1
  106. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-xDWktb2i.js → railroadDiagram-AXF67PYL-B3jRdATC.js} +1 -1
  107. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-BLNMKqeS.js → requirementDiagram-LRYGKXZP-D-cTtXEN.js} +1 -1
  108. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-n0adalxH.js → sankeyDiagram-W5VNT64P-B25igJRE.js} +1 -1
  109. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BbnS-2DL.js → sequenceDiagram-SI44F4Z6-jW_lM1_D.js} +1 -1
  110. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-B4uPpCLu.js → sizeCapture-X5ZJPWSS-CrzTgsHE.js} +1 -1
  111. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA--MMbB0fX.js → stateDiagram-OKZ733FA-B01MvlqE.js} +1 -1
  112. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BvFKEtm2.js +1 -0
  113. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-BXUgp2QY.js → swimlanes-SLNWSIFB-YcF12FiU.js} +2 -2
  114. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DIB2eVg-.js +8 -0
  115. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CsaMxki7.js → timeline-definition-Z64GVDOM-ClnRcg2s.js} +1 -1
  116. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-C2IuG9iH.js → vennDiagram-T6HMQDX7-B6gr_z-q.js} +1 -1
  117. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CmEWOXCW.js → wardleyDiagram-T6FBY63Y-CvSgguGH.js} +1 -1
  118. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-D_9LE0MG.js → xychartDiagram-ELKLHX3M-23OtSZCb.js} +1 -1
  119. package/dist/worker/console/static/index.html +2 -2
  120. package/dist/worker/console/static-src/app/console-types.js +68 -4
  121. package/dist/worker/console/static-src/app/useConsoleShell.js +30 -12
  122. package/dist/worker/console/static-src/app/useOperatorActions.js +41 -6
  123. package/dist/worker/console/static-src/app/useRecoveryConsole.js +10 -11
  124. package/dist/worker/console/static-src/app/useRunProgress.js +2 -1
  125. package/dist/worker/console/static-src/app/useTaskWizard.js +74 -2
  126. package/dist/worker/console/static-src/night/useNightBoard.js +8 -5
  127. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +141 -8
  128. package/dist/worker/console/static-src/operator-chat/compaction-message.js +79 -0
  129. package/dist/worker/console/static-src/operator-chat/details-dag-actions.js +122 -0
  130. package/dist/worker/console/static-src/operator-chat/refs.js +3 -0
  131. package/dist/worker/console/static-src/operator-chat/runtime-selection-labels.js +27 -0
  132. package/dist/worker/console/static-src/operator-chat/spatial-overlay.js +1 -2
  133. package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +25 -0
  134. package/dist/worker/console/static-src/operator-chat/turn-submission.js +3 -2
  135. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +181 -87
  136. package/dist/worker/console/static-src/operator-chat/useChatStream.js +159 -44
  137. package/dist/worker/console/static-src/operator-chat/useChatThread.js +177 -8
  138. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +63 -9
  139. package/dist/worker/console/static-src/operator-chat/useWorkspaceBrowserGate.js +4 -0
  140. package/dist/worker/console/static-src/operator-chat/workspace-layout-mode.js +3 -3
  141. package/dist/worker/console/static-src/shell/useWorkspaces.js +299 -0
  142. package/dist/worker/console/static-src/shell/workspace-route.js +339 -0
  143. package/dist/worker/console/workspace-context.js +234 -0
  144. package/dist/worker/console/workspace-registry.js +214 -0
  145. package/dist/worker/observability/init-runtime-activity.js +26 -0
  146. package/dist/worker/observe/node-input.js +88 -2
  147. package/dist/worker/observe/routes.js +188 -0
  148. package/dist/worker/observe/server.js +4 -0
  149. package/dist/worker/observe/static/api.js +29 -9
  150. package/dist/worker/observe/static/app.js +18 -70
  151. package/dist/worker/observe/static/dag-node-purpose.d.ts +6 -0
  152. package/dist/worker/observe/static/dag-node-purpose.js +203 -0
  153. package/dist/worker/observe/static/format.js +37 -0
  154. package/dist/worker/observe/static/index.html +6 -1
  155. package/dist/worker/observe/static/inspect-workspace.js +307 -0
  156. package/dist/worker/observe/static/operator-chrome.css +145 -20
  157. package/dist/worker/observe/static/operator-chrome.d.ts +20 -3
  158. package/dist/worker/observe/static/operator-chrome.js +330 -100
  159. package/dist/worker/observe/static/prompt-restart-candidates.d.ts +18 -0
  160. package/dist/worker/observe/static/prompt-restart-candidates.js +36 -0
  161. package/dist/worker/observe/static/router.d.ts +46 -0
  162. package/dist/worker/observe/static/router.js +25 -1
  163. package/dist/worker/observe/static/state.d.ts +50 -0
  164. package/dist/worker/observe/static/state.js +174 -2
  165. package/dist/worker/observe/static/styles.css +357 -35
  166. package/dist/worker/observe/static/views/dag-graph.js +4 -2
  167. package/dist/worker/observe/static/views/dag-inspector.js +533 -79
  168. package/dist/worker/observe/static/views/dag.d.ts +13 -0
  169. package/dist/worker/observe/static/views/dag.js +6 -4
  170. package/dist/worker/observe/static/views/dashboard.js +6 -4
  171. package/dist/worker/observe/static/views/night.js +2 -2
  172. package/dist/worker/observe/static/views/pool.js +1 -0
  173. package/dist/worker/observe/static/views/run.js +3 -1
  174. package/dist/worker/observe/static/views/task.js +56 -1
  175. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +29 -1
  176. package/dist/workflows/dag/backend-test-markdown-workflow.js +176 -1
  177. package/dist/workflows/dag/backend-test-pytest-collection.js +98 -14
  178. package/dist/workflows/dag/backend-test-writer-completeness.js +17 -17
  179. package/dist/workflows/dag/dynamic-runtime/map.js +1 -0
  180. package/dist/workflows/dag/frontend-implementation-contract.js +88 -2
  181. package/dist/workflows/dag/frontend-plan-render.js +24 -0
  182. package/dist/workflows/dag/frontend-prewrite-gate.js +197 -0
  183. package/dist/workflows/dag/frontend-review-context.js +92 -10
  184. package/dist/workflows/dag/init-hybrid.js +238 -51
  185. package/dist/workflows/dag/interrupt-request.js +3 -3
  186. package/dist/workflows/dag/lifecycle.js +3 -3
  187. package/dist/workflows/dag/node-execution.js +45 -0
  188. package/dist/workflows/dag/types.js +62 -12
  189. package/docs/README.md +2 -0
  190. package/docs/architecture/evolution.md +45 -0
  191. package/docs/init-surface.manifest.json +1 -0
  192. package/docs/operations/local-development-environment.md +21 -0
  193. package/docs/templates/README.md +1 -1
  194. package/docs/templates/agent-dag.schema.json +18 -0
  195. package/docs/templates/backend-test-dag.json +265 -237
  196. package/docs/templates/frontend-implementation-contract.schema.json +681 -24
  197. package/harness.json +3 -3
  198. package/package.json +3 -1
  199. package/skills/loop-agent/references/hybrid-dag.md +7 -0
  200. package/dist/worker/console/static/assets/channel-TY36zt-i.js +0 -1
  201. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Dk7QCChM.js +0 -1
  202. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Dk7QCChM.js +0 -1
  203. package/dist/worker/console/static/assets/index-C3GgHg-g.css +0 -1
  204. package/dist/worker/console/static/assets/index-Ci8ksvBm.js +0 -406
  205. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BqOigMQ4.js +0 -1
  206. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Caxz9UA2.js +0 -8
  207. package/dist/worker/console/static-src/operator-chat/activity-rail-presentation.js +0 -73
  208. package/dist/worker/console/static-src/operator-chat/useActivityRailTransition.js +0 -59
@@ -1,7 +1,9 @@
1
1
  import { useCallback, useEffect, useMemo, useState } from "react";
2
2
  import { buildTurnProcessGroups, hydrateSessionThread, selectRecentTurnGroups, } from "../../chat/turn-process.js";
3
+ import { mergeOperationCardState, } from "../../chat/operation-card.js";
3
4
  import { createChatSseEventApplier, } from "./chat-sse-events.js";
4
5
  import { taskContextUnchanged } from "./format.js";
6
+ import { upsertCompactionMessage } from "./compaction-message.js";
5
7
  const EMPTY_USAGE = {
6
8
  inputTokens: 0,
7
9
  outputTokens: 0,
@@ -9,13 +11,105 @@ const EMPTY_USAGE = {
9
11
  cacheWriteTokens: 0,
10
12
  totalTokens: 0,
11
13
  };
14
+ const EMPTY_SESSION_STATS = {
15
+ turns: 0,
16
+ steps: 0,
17
+ llmMs: 0,
18
+ toolMs: 0,
19
+ ttftMs: 0,
20
+ ttftSteps: 0,
21
+ decodeMs: 0,
22
+ decodeTokens: 0,
23
+ };
24
+ /** R8 AC-R8-003: project a raw GET /operations/:id fact (the same shape the
25
+ * server persists) into a browser OperationCardState. Field-validated; any
26
+ * malformed fact returns undefined (no fabricated cards). */
27
+ function operationCardFromOperationFact(op) {
28
+ const str = (key) => {
29
+ const value = op[key];
30
+ return typeof value === "string" && value ? value : undefined;
31
+ };
32
+ const operationId = str("operationId");
33
+ const action = str("action");
34
+ const state = str("state");
35
+ const createdAt = str("createdAt");
36
+ if (!operationId || !action || !state || !createdAt)
37
+ return undefined;
38
+ const updatedAt = str("updatedAt") ?? str("finishedAt") ?? str("startedAt") ?? createdAt;
39
+ const dagRunId = str("dagRunId");
40
+ return {
41
+ schemaVersion: 1,
42
+ operationId,
43
+ action,
44
+ state,
45
+ createdAt,
46
+ updatedAt,
47
+ inspectHref: `/inspect/?operationId=${encodeURIComponent(operationId)}#/`,
48
+ ...(str("taskId") ? { taskId: str("taskId") } : {}),
49
+ ...(dagRunId ? { dagRunId } : {}),
50
+ ...(str("errorCode") ? { errorCode: str("errorCode") } : {}),
51
+ activityRef: `operation:${operationId}`,
52
+ previewHash: `${operationId}:${state}:${updatedAt}`,
53
+ };
54
+ }
55
+ /**
56
+ * Apply one settled operation-link mutation to the visible card projection.
57
+ *
58
+ * The mutation identity is captured before the request starts. Both the
59
+ * session and workspace are fenced before and after the follow-up read so a
60
+ * late response can never leak into a newly selected workspace/session.
61
+ */
62
+ export async function applyOperationLinkMutationProjection(input) {
63
+ const { identity } = input;
64
+ const stillOwnsProjection = () => {
65
+ const current = input.currentOwner();
66
+ return (current.sessionId === identity.sessionId &&
67
+ current.workspaceId === identity.workspaceId);
68
+ };
69
+ if (!identity.workspaceId || !stillOwnsProjection())
70
+ return false;
71
+ if (identity.action === "remove") {
72
+ input.setOperationCards((current) => current.filter((card) => card.operationId !== identity.operationId));
73
+ return true;
74
+ }
75
+ try {
76
+ const workspaceQuery = new URLSearchParams({
77
+ workspaceId: identity.workspaceId,
78
+ });
79
+ const fetchImpl = input.fetchFn ?? fetch;
80
+ const res = await fetchImpl(`${input.origin}/api/operator/v1/operations/${encodeURIComponent(identity.operationId)}?${workspaceQuery.toString()}`, { credentials: "include" });
81
+ if (!res.ok || !stillOwnsProjection())
82
+ return false;
83
+ const body = (await res.json());
84
+ const operation = body.operation;
85
+ if (body.workspaceId !== identity.workspaceId ||
86
+ !operation ||
87
+ operation.workspaceId !== identity.workspaceId ||
88
+ operation.operationId !== identity.operationId ||
89
+ !stillOwnsProjection())
90
+ return false;
91
+ const card = operationCardFromOperationFact(operation);
92
+ if (!card)
93
+ return false;
94
+ input.setOperationCards((current) => stillOwnsProjection()
95
+ ? mergeOperationCardState(current, card)
96
+ : current);
97
+ return stillOwnsProjection();
98
+ }
99
+ catch {
100
+ return false;
101
+ }
102
+ }
103
+ function mergeCompactionMessages(messages, compactions) {
104
+ return (compactions ?? []).reduce((result, item) => upsertCompactionMessage(result, item), messages);
105
+ }
12
106
  /**
13
107
  * Thread state + SSE block parsing. The event-application logic itself lives
14
108
  * in chat-sse-events.ts (createChatSseEventApplier); this hook owns the state
15
109
  * slices it patches and the derived timeline projections.
16
110
  */
17
111
  export function useChatThread(params) {
18
- const { origin, refs, sessionId, setError, onReconcile } = params;
112
+ const { origin, refs, sessionId, workspaceId, setError, onReconcile } = params;
19
113
  // setProcessOpen remains on the hook API for session lifecycle close/reset
20
114
  // (UI-08); the SSE applier must not open the process panel.
21
115
  void params.setProcessOpen;
@@ -30,12 +124,36 @@ export function useChatThread(params) {
30
124
  const [taskContext, setTaskContext] = useState(null);
31
125
  const [contextLoading, setContextLoading] = useState(false);
32
126
  const [usage, setUsage] = useState(EMPTY_USAGE);
127
+ const [sessionStats, setSessionStats] = useState(EMPTY_SESSION_STATS);
33
128
  const [streaming, setStreaming] = useState(false);
34
129
  const [turnPhase, setTurnPhase] = useState("idle");
35
130
  const [deferredTurn, setDeferredTurn] = useState(null);
131
+ const [deferredTurns, setDeferredTurns] = useState([]);
132
+ const [pendingQuestion, setPendingQuestion] = useState(null);
133
+ const [todos, setTodos] = useState([]);
36
134
  /** Assistant bubble id for the in-flight turn; only this row shows the streaming cursor. */
37
135
  const [streamingAssistantId, setStreamingAssistantId] = useState(null);
38
136
  const [showSystemInMessages] = useState(false);
137
+ const refreshSessionTelemetry = useCallback(async () => {
138
+ const targetSessionId = refs.sessionRef.current?.sessionId;
139
+ if (!targetSessionId)
140
+ return;
141
+ try {
142
+ const res = await fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(targetSessionId)}`, { credentials: "include" });
143
+ if (!res.ok)
144
+ return;
145
+ const body = (await res.json());
146
+ if (refs.sessionRef.current?.sessionId !== targetSessionId)
147
+ return;
148
+ if (body.sessionStats)
149
+ setSessionStats(body.sessionStats);
150
+ if (body.usage)
151
+ setUsage(body.usage);
152
+ }
153
+ catch {
154
+ // Telemetry is supplementary; the normal event stream remains authoritative.
155
+ }
156
+ }, [origin, refs]);
39
157
  // All inputs are stable (state setters + the memoized refs bag), so the
40
158
  // applier identity is stable across renders.
41
159
  const applyChatSseEvent = useMemo(() => createChatSseEventApplier({
@@ -48,13 +166,17 @@ export function useChatThread(params) {
48
166
  setStreaming,
49
167
  setTurnPhase,
50
168
  setDeferredTurn,
169
+ setDeferredTurns,
170
+ setPendingQuestion,
171
+ setTodos,
51
172
  setStreamingAssistantId,
52
173
  setUnread,
53
174
  setOperationCards,
54
175
  setUsage,
176
+ onTelemetryRefresh: () => void refreshSessionTelemetry(),
55
177
  setMessages,
56
178
  setToolCalls,
57
- }), [refs, onReconcile, setError]);
179
+ }), [refs, onReconcile, setError, refreshSessionTelemetry]);
58
180
  /** Parse + apply one raw SSE block (frames separated by \n\n). Returns the
59
181
  * eventId if the block carried an `id:` line (for Last-Event-ID tracking). */
60
182
  const applySseBlock = useCallback((block, assistantId, options) => {
@@ -124,6 +246,21 @@ export function useChatThread(params) {
124
246
  }, 350);
125
247
  return () => window.clearTimeout(timer);
126
248
  }, [sessionId, operationCards, humanGateCards, interview, loadTaskContext]);
249
+ /** R8 AC-R8-003: deterministic mutation-driven operationCards updates.
250
+ * Binds to the identity captured when the real POST/DELETE was issued:
251
+ * - add → consume server truth for that operation (GET operation) and
252
+ * upsert the card when the captured session still matches.
253
+ * - remove → drop the captured operation's card immediately (the server
254
+ * tombstone guarantees it never resurrects on replay). */
255
+ const applyOperationLinkMutation = useCallback((identity) => applyOperationLinkMutationProjection({
256
+ origin,
257
+ identity,
258
+ currentOwner: () => ({
259
+ sessionId: refs.sessionRef.current?.sessionId,
260
+ workspaceId,
261
+ }),
262
+ setOperationCards,
263
+ }), [origin, refs, workspaceId]);
127
264
  const timelineMessages = useMemo(() => messages.filter((m) => m.role !== "system" || showSystemInMessages), [messages, showSystemInMessages]);
128
265
  useEffect(() => {
129
266
  const onVisibility = () => {
@@ -193,9 +330,13 @@ export function useChatThread(params) {
193
330
  setInterview(null);
194
331
  setTaskContext(null);
195
332
  setUsage(EMPTY_USAGE);
333
+ setSessionStats(EMPTY_SESSION_STATS);
196
334
  setStreaming(false);
197
335
  setTurnPhase("idle");
198
336
  setDeferredTurn(null);
337
+ setDeferredTurns([]);
338
+ setPendingQuestion(null);
339
+ setTodos([]);
199
340
  setStreamingAssistantId(null);
200
341
  setUnread(0);
201
342
  setHistoryLimit(50);
@@ -203,6 +344,7 @@ export function useChatThread(params) {
203
344
  }, [refs]);
204
345
  /** Adopt a freshly activated session's hydrated thread + streaming flags. */
205
346
  const applyActivatedSession = useCallback((next) => {
347
+ const visuallyStopped = next.turnPhase === "stopping";
206
348
  setMessages(next.messages);
207
349
  setToolCalls(next.toolCalls);
208
350
  setOperationCards([]);
@@ -210,11 +352,17 @@ export function useChatThread(params) {
210
352
  setArtifactCards([]);
211
353
  setTaskContext(null);
212
354
  setInterview(null);
213
- setUsage(EMPTY_USAGE);
355
+ setUsage(next.usage ?? EMPTY_USAGE);
356
+ setSessionStats(next.sessionStats ?? EMPTY_SESSION_STATS);
214
357
  refs.usageResponseKeysRef.current = new Set();
215
- setStreaming(next.streaming);
216
- setTurnPhase(next.turnPhase ?? (next.streaming ? "running" : "idle"));
358
+ setStreaming(visuallyStopped ? false : next.streaming);
359
+ setTurnPhase(visuallyStopped
360
+ ? "idle"
361
+ : (next.turnPhase ?? (next.streaming ? "running" : "idle")));
217
362
  setDeferredTurn(next.deferredTurn ?? null);
363
+ setDeferredTurns(next.deferredTurns ?? (next.deferredTurn ? [next.deferredTurn] : []));
364
+ setPendingQuestion(next.pendingQuestion ?? null);
365
+ setTodos(next.todos ?? []);
218
366
  setStreamingAssistantId(null);
219
367
  setUnread(0);
220
368
  setHistoryLimit(50);
@@ -234,10 +382,21 @@ export function useChatThread(params) {
234
382
  refs.sessionRef.current?.sessionId !== targetSessionId)
235
383
  return false;
236
384
  const activeTurnId = body.session.activeTurnId ?? null;
385
+ const visuallyStopped = body.session.turnPhase === "stopping";
237
386
  refs.activeTurnIdRef.current = activeTurnId;
238
- refs.streamingRef.current = Boolean(activeTurnId);
239
- setTurnPhase(body.session.turnPhase ?? (activeTurnId ? "running" : "idle"));
387
+ refs.streamingRef.current = visuallyStopped ? false : Boolean(activeTurnId);
388
+ setStreaming(visuallyStopped ? false : Boolean(activeTurnId));
389
+ setTurnPhase(visuallyStopped
390
+ ? "idle"
391
+ : (body.session.turnPhase ?? (activeTurnId ? "running" : "idle")));
240
392
  setDeferredTurn(body.session.deferredTurn ?? null);
393
+ setDeferredTurns(body.session.deferredTurns ?? (body.session.deferredTurn ? [body.session.deferredTurn] : []));
394
+ setPendingQuestion(body.session.pendingQuestion ?? null);
395
+ setTodos(body.session.todos ?? []);
396
+ if (body.sessionStats)
397
+ setSessionStats(body.sessionStats);
398
+ if (body.usage)
399
+ setUsage(body.usage);
241
400
  // Resume the events stream from the snapshot tail so the next
242
401
  // reconnect never immediately re-expires (AC-003). The refs below
243
402
  // are reset so any follow-up tail replay re-applies cleanly.
@@ -246,7 +405,7 @@ export function useChatThread(params) {
246
405
  refs.turnAssistantIdsRef.current = new Map();
247
406
  refs.sealedAssistantIdsRef.current = new Set();
248
407
  const hydrated = hydrateSessionThread(body.session.messages ?? []);
249
- setMessages(hydrated.messages);
408
+ setMessages(mergeCompactionMessages(hydrated.messages, body.compactions));
250
409
  setToolCalls(hydrated.toolCalls);
251
410
  setStreamingAssistantId(null);
252
411
  return true;
@@ -262,8 +421,11 @@ export function useChatThread(params) {
262
421
  setMessages,
263
422
  setToolCalls,
264
423
  setStreamingAssistantId,
424
+ setStreaming,
265
425
  setTurnPhase,
266
426
  setDeferredTurn,
427
+ setPendingQuestion,
428
+ setTodos,
267
429
  ]);
268
430
  return {
269
431
  messages,
@@ -281,12 +443,18 @@ export function useChatThread(params) {
281
443
  taskContext,
282
444
  contextLoading,
283
445
  usage,
446
+ sessionStats,
284
447
  streaming,
285
448
  setStreaming,
286
449
  turnPhase,
287
450
  setTurnPhase,
288
451
  deferredTurn,
289
452
  setDeferredTurn,
453
+ deferredTurns,
454
+ setDeferredTurns,
455
+ pendingQuestion,
456
+ setPendingQuestion,
457
+ todos,
290
458
  streamingAssistantId,
291
459
  setStreamingAssistantId,
292
460
  visibleMessages,
@@ -295,6 +463,7 @@ export function useChatThread(params) {
295
463
  applyChatSseEvent,
296
464
  applySseBlock,
297
465
  loadTaskContext,
466
+ applyOperationLinkMutation,
298
467
  resetForNewSession,
299
468
  applyActivatedSession,
300
469
  recoverSession,
@@ -1,21 +1,32 @@
1
1
  import { useCallback, useEffect, useRef, useState } from "react";
2
2
  import { ACTIVE_CHAT_SESSION_STORAGE_KEY } from "../../chat/workspace-landing.js";
3
- /** Left-rail repo tree + file preview tabs state. */
3
+ /** Details-rail repo tree + file preview tabs state. */
4
4
  export function useRepoBrowser(params) {
5
- const { origin, refs, sessionId, setError } = params;
5
+ const { origin, refs, sessionId, workspaceId, blocked, setError } = params;
6
6
  const [tree, setTree] = useState({});
7
7
  const [expandedDirs, setExpandedDirs] = useState(new Set());
8
8
  const [treeError, setTreeError] = useState(null);
9
9
  const [previews, setPreviews] = useState([]);
10
10
  const [activePreviewPath, setActivePreviewPath] = useState(null);
11
11
  const [previewLoading, setPreviewLoading] = useState(false);
12
+ const workspaceScopeRef = useRef(workspaceId ?? null);
13
+ workspaceScopeRef.current = workspaceId ?? null;
14
+ const autoPreviewedWorkspaceRef = useRef(new Set());
12
15
  const expandedDirsRef = useRef(new Set());
13
16
  expandedDirsRef.current = expandedDirs;
17
+ const workspaceQuery = workspaceId
18
+ ? `&workspaceId=${encodeURIComponent(workspaceId)}`
19
+ : "";
14
20
  const loadTree = useCallback(async (directory = "") => {
21
+ if (blocked)
22
+ return; // AC-R5-002: fail-closed, zero repo requests
23
+ const requestWorkspaceId = workspaceId ?? null;
15
24
  try {
16
25
  setTreeError(null);
17
- const res = await fetch(`${origin}/api/operator/v1/chat/repo/tree?path=${encodeURIComponent(directory)}`, { credentials: "include" });
26
+ const res = await fetch(`${origin}/api/operator/v1/chat/repo/tree?path=${encodeURIComponent(directory)}${workspaceQuery}`, { credentials: "include" });
18
27
  const body = (await res.json());
28
+ if (workspaceScopeRef.current !== requestWorkspaceId)
29
+ return;
19
30
  if (!res.ok)
20
31
  throw new Error(body.error?.message ?? "无法读取仓库目录");
21
32
  setTree((current) => ({
@@ -24,9 +35,11 @@ export function useRepoBrowser(params) {
24
35
  }));
25
36
  }
26
37
  catch (error) {
38
+ if (workspaceScopeRef.current !== requestWorkspaceId)
39
+ return;
27
40
  setTreeError(error instanceof Error ? error.message : String(error));
28
41
  }
29
- }, [origin]);
42
+ }, [origin, workspaceQuery, workspaceId, blocked]);
30
43
  useEffect(() => {
31
44
  void loadTree();
32
45
  }, [loadTree]);
@@ -53,10 +66,15 @@ export function useRepoBrowser(params) {
53
66
  return () => document.removeEventListener("visibilitychange", onVisibility);
54
67
  }, [refreshTree]);
55
68
  const openPreview = useCallback(async (file) => {
69
+ if (blocked)
70
+ return; // AC-R5-002: fail-closed
71
+ const requestWorkspaceId = workspaceId ?? null;
56
72
  setPreviewLoading(true);
57
73
  try {
58
- const res = await fetch(`${origin}/api/operator/v1/chat/repo/file?path=${encodeURIComponent(file)}`, { credentials: "include" });
74
+ const res = await fetch(`${origin}/api/operator/v1/chat/repo/file?path=${encodeURIComponent(file)}${workspaceQuery}`, { credentials: "include" });
59
75
  const body = (await res.json());
76
+ if (workspaceScopeRef.current !== requestWorkspaceId)
77
+ return;
60
78
  if (!res.ok || !body.data)
61
79
  throw new Error(body.error?.message ?? "无法预览文件");
62
80
  setPreviews((current) => {
@@ -72,12 +90,48 @@ export function useRepoBrowser(params) {
72
90
  setActivePreviewPath(body.data.path);
73
91
  }
74
92
  catch (error) {
93
+ if (workspaceScopeRef.current !== requestWorkspaceId)
94
+ return;
75
95
  setError(error instanceof Error ? error.message : String(error));
76
96
  }
77
97
  finally {
78
- setPreviewLoading(false);
98
+ if (workspaceScopeRef.current === requestWorkspaceId)
99
+ setPreviewLoading(false);
79
100
  }
80
- }, [origin, refs, setError]);
101
+ }, [origin, workspaceQuery, workspaceId, refs, setError, blocked]);
102
+ // A workspace switch must immediately remove the previous workspace's tree
103
+ // and file contents while the newly scoped requests are in flight.
104
+ useEffect(() => {
105
+ setTree({});
106
+ setExpandedDirs(new Set());
107
+ setTreeError(null);
108
+ setPreviews([]);
109
+ setActivePreviewPath(null);
110
+ setPreviewLoading(false);
111
+ }, [workspaceId]);
112
+ // DSH parity: Details opens as a real tree + preview split. Select a real
113
+ // root file once per workspace (README first), never placeholder content.
114
+ useEffect(() => {
115
+ if (blocked || activePreviewPath || previews.length > 0)
116
+ return;
117
+ const workspaceKey = workspaceId ?? "__default__";
118
+ if (autoPreviewedWorkspaceRef.current.has(workspaceKey))
119
+ return;
120
+ const rootFiles = (tree[""] ?? []).filter((entry) => entry.kind === "file");
121
+ if (rootFiles.length === 0)
122
+ return;
123
+ const initial = rootFiles.find((entry) => /^readme(?:\.[^.]+)?$/i.test(entry.name)) ??
124
+ rootFiles[0];
125
+ autoPreviewedWorkspaceRef.current.add(workspaceKey);
126
+ void openPreview(initial.path);
127
+ }, [
128
+ activePreviewPath,
129
+ blocked,
130
+ openPreview,
131
+ previews.length,
132
+ tree,
133
+ workspaceId,
134
+ ]);
81
135
  useEffect(() => {
82
136
  if (!sessionId)
83
137
  return;
@@ -95,7 +149,7 @@ export function useRepoBrowser(params) {
95
149
  if (!paths.length)
96
150
  return;
97
151
  void Promise.all(paths.map(async (file) => {
98
- const res = await fetch(`${origin}/api/operator/v1/chat/repo/file?path=${encodeURIComponent(file)}`, { credentials: "include" });
152
+ const res = await fetch(`${origin}/api/operator/v1/chat/repo/file?path=${encodeURIComponent(file)}${workspaceQuery}`, { credentials: "include" });
99
153
  if (!res.ok)
100
154
  return undefined;
101
155
  return (await res.json()).data;
@@ -106,7 +160,7 @@ export function useRepoBrowser(params) {
106
160
  setPreviews(available);
107
161
  setActivePreviewPath(available.at(-1)?.path ?? null);
108
162
  });
109
- }, [origin, sessionId, refs]);
163
+ }, [origin, workspaceQuery, sessionId, refs]);
110
164
  const toggleDir = useCallback((dir) => {
111
165
  setExpandedDirs((current) => {
112
166
  const next = new Set(current);
@@ -0,0 +1,4 @@
1
+ export const WORKSPACE_BROWSER_OK = { status: "ok" };
2
+ export function workspaceBrowserGateActive(gate) {
3
+ return gate.status !== "ok";
4
+ }
@@ -1,12 +1,12 @@
1
1
  /** Workspace layout modes resolved from measured Chat container width. */
2
2
  /** Width thresholds in CSS px against `.oc-workspace` (not window/UA). */
3
3
  export const WORKSPACE_LAYOUT_THRESHOLDS = {
4
- comfortableMin: 1450,
5
- compactMin: 1100,
4
+ comfortableMin: 1200,
5
+ compactMin: 720,
6
6
  };
7
7
  /**
8
8
  * Resolve the three-band workspace mode from a measured container width.
9
- * Boundaries: comfortable >=1450, compact 1100-1449, narrow <1100.
9
+ * Boundaries: comfortable >=1200, compact 720-1199, narrow <720.
10
10
  */
11
11
  export function resolveWorkspaceLayoutMode(widthPx) {
12
12
  if (!Number.isFinite(widthPx) || widthPx < 0)