@tea-agent/loop-agent 0.44.0-next.1 → 0.44.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 (226) hide show
  1. package/CHANGELOG.md +379 -416
  2. package/dist/application/evaluation/budget.js +21 -0
  3. package/dist/application/evaluation/corpus-hash.js +10 -15
  4. package/dist/application/evaluation/corpus.js +2 -1
  5. package/dist/application/evaluation/frontend-browser-acceptance.js +77 -0
  6. package/dist/application/evaluation/frontend-corpus-browser-acceptance.js +175 -0
  7. package/dist/application/evaluation/frontend-gateway-evidence.js +226 -0
  8. package/dist/application/evaluation/frontend-pair-registration.js +143 -0
  9. package/dist/application/evaluation/frontend-paired-summary.js +150 -0
  10. package/dist/application/evaluation/frontend-run-observation.js +144 -0
  11. package/dist/application/evaluation/frontend-shared-evidence.js +105 -0
  12. package/dist/application/evaluation/types.js +45 -12
  13. package/dist/application/task-lifecycle/observe.js +26 -1
  14. package/dist/build-stamp.json +3 -3
  15. package/dist/cli/command-definitions.js +11 -0
  16. package/dist/cli/program.js +4 -0
  17. package/dist/commands/task-advance.js +3 -5
  18. package/dist/commands/task-source-prepare.js +13 -1
  19. package/dist/executors/dag-pi/guards.js +4 -0
  20. package/dist/executors/dag-pi/model-config.js +30 -0
  21. package/dist/executors/dag-pi/plan/fact-adoption.js +55 -0
  22. package/dist/{workflows/dag/frontend-plan-completeness.js → executors/dag-pi/plan/facts.js} +22 -28
  23. package/dist/executors/dag-pi/plan/finalize-tools.js +259 -0
  24. package/dist/executors/dag-pi/plan/ledger-contract.js +55 -0
  25. package/dist/executors/dag-pi/plan/record-tools.js +861 -0
  26. package/dist/executors/dag-pi/plan/schema.js +129 -0
  27. package/dist/executors/dag-pi/plan/session.js +1 -0
  28. package/dist/executors/dag-pi/plan/tool-helpers.js +16 -0
  29. package/dist/executors/dag-pi/tools/contract-tools.js +515 -0
  30. package/dist/executors/dag-pi/tools/design-terminal-tools.js +159 -0
  31. package/dist/executors/dag-pi/tools/plan-decision-tools.js +529 -0
  32. package/dist/executors/dag-pi/tools/review-terminal-tools.js +159 -0
  33. package/dist/executors/dag-pi/tools/scout-evidence-tools.js +270 -0
  34. package/dist/executors/dag-pi-executor.js +398 -3384
  35. package/dist/executors/pi-executor.js +6 -1
  36. package/dist/executors/pi-sdk-executor.js +112 -19
  37. package/dist/executors/shell-executor.js +88 -12
  38. package/dist/executors/shell-presets.js +1 -1
  39. package/dist/executors/shell-write-guard.js +26 -3
  40. package/dist/infrastructure/console/operation-store.js +48 -14
  41. package/dist/infrastructure/evaluation/corpus-store.js +37 -12
  42. package/dist/shared/file-identity.js +23 -0
  43. package/dist/shared/git-workspace-status.js +482 -0
  44. package/dist/shared/operator/capabilities.js +10 -5
  45. package/dist/shared/path-safety.js +2 -2
  46. package/dist/shared/wizard-local-telemetry.js +51 -0
  47. package/dist/task/config-types.js +22 -8
  48. package/dist/task/contract/project.js +13 -0
  49. package/dist/task/contract/schema.js +2 -7
  50. package/dist/task/frontend-preflight.js +1 -1
  51. package/dist/task/source-prepare/parse-intent.js +124 -5
  52. package/dist/task/source-prepare/prepare.js +6 -1
  53. package/dist/task/source-prepare/semantic-intake.js +18 -3
  54. package/dist/task/source-prepare/source-execution.js +65 -0
  55. package/dist/task/source-prepare/source-fidelity-pi.js +22 -9
  56. package/dist/task/source-prepare/source-provider-budget.js +290 -0
  57. package/dist/worker/console/chat/assistant-content.js +5 -34
  58. package/dist/worker/console/chat/chat-event-store.js +62 -10
  59. package/dist/worker/console/chat/context-insights.js +723 -0
  60. package/dist/worker/console/chat/conversation-nodes.js +383 -0
  61. package/dist/worker/console/chat/db-connections.js +854 -0
  62. package/dist/worker/console/chat/pi-runtime/custom-tools/operator-tools.js +1 -1
  63. package/dist/worker/console/chat/pi-runtime/custom-tools/sql-tools.js +655 -0
  64. package/dist/worker/console/chat/pi-runtime.js +445 -36
  65. package/dist/worker/console/chat/provider-error.js +0 -12
  66. package/dist/worker/console/chat/repo-browser.js +52 -4
  67. package/dist/worker/console/chat/routes.js +577 -72
  68. package/dist/worker/console/chat/sdd-data-alignment.js +8 -31
  69. package/dist/worker/console/chat/session-store.js +147 -19
  70. package/dist/worker/console/chat/shortcuts.js +7 -0
  71. package/dist/worker/console/chat/timeline-message-order.js +22 -0
  72. package/dist/worker/console/chat/tools.js +34 -0
  73. package/dist/worker/console/chat/turn-process.js +237 -66
  74. package/dist/worker/console/chat/user-turn-outline.js +19 -0
  75. package/dist/worker/console/console-update-runtime.js +2 -2
  76. package/dist/worker/console/dag-confirmation.js +3 -0
  77. package/dist/worker/console/dag-execution-receipt.js +3 -0
  78. package/dist/worker/console/operator-actions.js +32 -3
  79. package/dist/worker/console/operator-user-error.js +28 -1
  80. package/dist/worker/console/pi-readiness.js +4 -4
  81. package/dist/worker/console/prd-intake-bridge.js +5 -12
  82. package/dist/worker/console/recovery-cta.js +14 -14
  83. package/dist/worker/console/server.js +5 -1
  84. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-8-j6y-sd.js → abnfDiagram-N423BO3Z-r1NAPU_5.js} +1 -1
  85. package/dist/worker/console/static/assets/{arc-eoQiMvuk.js → arc-dT5T4sVL.js} +1 -1
  86. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-C4A3uMcI.js → architectureDiagram-T3A2C74G-CP_cOq46.js} +1 -1
  87. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-D4zD2F-Q.js → blockDiagram-VBNYF7ZC-sToEZxEf.js} +1 -1
  88. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-j1RkJziL.js → c4Diagram-5PPSVZJV-DSxwwi2m.js} +1 -1
  89. package/dist/worker/console/static/assets/channel-CyBY_yGk.js +1 -0
  90. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-YBHmhik1.js → chunk-2GRJ4B5K-B9fh0IkF.js} +1 -1
  91. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-COfWyo9P.js → chunk-2Q5K7J3B-CAUNl-A6.js} +1 -1
  92. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-I99OUkHY.js → chunk-5RXB4S5H-BomeqbNX.js} +1 -1
  93. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-XKxoJNJ4.js → chunk-5VM5RSS4-BNAuTOLE.js} +1 -1
  94. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DLT_cYx1.js → chunk-6Q2QTUOP-DfzjRD5y.js} +1 -1
  95. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CxcKZ9mV.js → chunk-GF5L2VYU-C3uE-yc2.js} +1 -1
  96. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-V1EqXdjY.js → chunk-JWPE2WC7-C3sQMMYr.js} +1 -1
  97. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DebTtdFp.js → chunk-KBJHAD2P-D_AtEYQL.js} +1 -1
  98. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-B-ivNRDf.js → chunk-RYQCIY6F-DW4TwhKv.js} +1 -1
  99. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-DC_Ds11b.js → chunk-XXDRQBXY-CTQDmJcp.js} +1 -1
  100. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-D5lN4E_E.js +1 -0
  101. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-D5lN4E_E.js +1 -0
  102. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-sWEqKwIb.js → cose-bilkent-JH36ORCC-iMqPuBsL.js} +1 -1
  103. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BXa_dcu4.js → cynefin-VYW2F7L2-DktIuDWs.js} +1 -1
  104. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-C-Mle84F.js → cynefinDiagram-MW4NZA55-BgVnjyEs.js} +1 -1
  105. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CXPDBITe.js → dagre-VZM6K2ZE-CR25E4bu.js} +1 -1
  106. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-CC-WJQfa.js → diagram-7IWD3JNH-CPM2h5HY.js} +1 -1
  107. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-CDAV5Vs4.js → diagram-B4RE2ZJO-DqF0x2g1.js} +1 -1
  108. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-CmFiAcNz.js → diagram-LBJQPF4R-G8BYp2eb.js} +1 -1
  109. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-DPBZHuyn.js → diagram-Q27KOJAE-BbC4kT_D.js} +1 -1
  110. package/dist/worker/console/static/assets/{diagram-UB23O5K3-jUlm_Ds3.js → diagram-UB23O5K3-CKZwZ-G5.js} +1 -1
  111. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-DWhQ3mfY.js → ebnfDiagram-BXEA7PRR-kJDmheia.js} +1 -1
  112. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-Tr2gMqet.js → erDiagram-JOGREHBK-DFTVHyxB.js} +1 -1
  113. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-DJjVQHPA.js → flowDiagram-UKHOOZJN-BqdN5_os.js} +1 -1
  114. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-D74dQ4u0.js → ganttDiagram-PKOTCBZU-SYDjVakh.js} +1 -1
  115. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-DwW0tZ0X.js → gitGraphDiagram-DS77QQ5N-Bf8bNpGA.js} +1 -1
  116. package/dist/worker/console/static/assets/index-B28Onmfy.css +1 -0
  117. package/dist/worker/console/static/assets/index-CTMZsDmh.js +486 -0
  118. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-ILCbxJyb.js → infoDiagram-6WML65LV-DIeAmJ45.js} +1 -1
  119. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-DeuWBQ89.js → ishikawaDiagram-WSZJBQD7-oikqUAlo.js} +1 -1
  120. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-CF5ih8Fk.js → journeyDiagram-NVQOT4AX-mKEF_xYT.js} +1 -1
  121. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-C7yOSuRO.js → kanban-definition-27J2QSJJ-BdWJHPnN.js} +1 -1
  122. package/dist/worker/console/static/assets/{linear-BDZ9riWi.js → linear-DGRlieMH.js} +1 -1
  123. package/dist/worker/console/static/assets/{mermaid.core-7pKqYtpZ.js → mermaid.core-4P9yPC9c.js} +5 -5
  124. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-LeJDybSU.js → mindmap-definition-FAOFIHXS-5PTSkos6.js} +1 -1
  125. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BJvT3pMD.js → pegDiagram-VL7TDLO6-DONIFpG5.js} +1 -1
  126. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-_rGqpMan.js → pieDiagram-7S7Q4E2Y-D_Z4S70M.js} +1 -1
  127. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-DPcSv5aA.js → quadrantDiagram-CIZ2JOQS-CAsjh8iY.js} +1 -1
  128. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Drxx4hkJ.js → railroadDiagram-AXF67PYL-Cx4Dd6ac.js} +1 -1
  129. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-BCTUdU4z.js → requirementDiagram-LRYGKXZP-cQb7mwyH.js} +1 -1
  130. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-B6wzbZmB.js → sankeyDiagram-W5VNT64P-Cd-DhUdf.js} +1 -1
  131. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BGt8d3QQ.js → sequenceDiagram-SI44F4Z6-AlKYRgIk.js} +1 -1
  132. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-7nlhJKo7.js → sizeCapture-X5ZJPWSS-CUy_ET4e.js} +1 -1
  133. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Cv2stqMA.js → stateDiagram-OKZ733FA-CTuVe6Sa.js} +1 -1
  134. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BsWhs8O7.js +1 -0
  135. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-qDAo4Yc1.js → swimlanes-SLNWSIFB-ChFLJb-v.js} +2 -2
  136. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CJcioYGH.js +8 -0
  137. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CKbDNKTr.js → timeline-definition-Z64GVDOM-DZZeiAPr.js} +1 -1
  138. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DI-9EHic.js → vennDiagram-T6HMQDX7-t4O91cZU.js} +1 -1
  139. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-BrzzRDpX.js → wardleyDiagram-T6FBY63Y-CV5qGHZu.js} +1 -1
  140. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-BclH5hGh.js → xychartDiagram-ELKLHX3M-DyXbytUI.js} +1 -1
  141. package/dist/worker/console/static/index.html +2 -2
  142. package/dist/worker/console/static-src/app/console-types.js +9 -1
  143. package/dist/worker/console/static-src/operator-chat/animation-frame-batcher.js +38 -0
  144. package/dist/worker/console/static-src/operator-chat/chat-link.js +20 -0
  145. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +186 -5
  146. package/dist/worker/console/static-src/operator-chat/chat-stream-continuity.js +101 -0
  147. package/dist/worker/console/static-src/operator-chat/compaction-message.js +18 -3
  148. package/dist/worker/console/static-src/operator-chat/context-insights-store.js +64 -0
  149. package/dist/worker/console/static-src/operator-chat/details-rail-surfaces.js +98 -0
  150. package/dist/worker/console/static-src/operator-chat/file-palette-nav.js +81 -0
  151. package/dist/worker/console/static-src/operator-chat/pending-user-message.js +17 -3
  152. package/dist/worker/console/static-src/operator-chat/process-label.js +27 -5
  153. package/dist/worker/console/static-src/operator-chat/tools-catalog.js +16 -7
  154. package/dist/worker/console/static-src/operator-chat/transcript-mode.js +21 -0
  155. package/dist/worker/console/static-src/operator-chat/turn-group-equality.js +4 -1
  156. package/dist/worker/console/static-src/operator-chat/turn-process-disclosure.js +13 -5
  157. package/dist/worker/console/static-src/operator-chat/turn-state-projection.js +166 -0
  158. package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +56 -2
  159. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +211 -40
  160. package/dist/worker/console/static-src/operator-chat/useChatStream.js +124 -27
  161. package/dist/worker/console/static-src/operator-chat/useChatThread.js +88 -20
  162. package/dist/worker/console/static-src/operator-chat/useComposer.js +80 -6
  163. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +13 -3
  164. package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +36 -17
  165. package/dist/worker/console/static-src/operator-chat/useRuntimeSnapshot.js +18 -0
  166. package/dist/worker/console/static-src/operator-chat/useWorkspaceBrowserGate.js +4 -0
  167. package/dist/worker/console/static-src/shell/console-update-reload.js +1 -1
  168. package/dist/worker/console/workspace-context.js +2 -0
  169. package/dist/worker/console/workspace-initialization.js +3 -3
  170. package/dist/worker/observability/read-model.js +23 -2
  171. package/dist/worker/observe/node-context-insights.js +479 -0
  172. package/dist/worker/observe/static/console-theme.css +174 -0
  173. package/dist/worker/observe/static/console-theme.js +36 -0
  174. package/dist/worker/observe/static/operator-chrome.css +85 -0
  175. package/dist/worker/observe/static/operator-chrome.js +51 -0
  176. package/dist/worker/scheduler/scheduled-goal-dispatch.js +3 -2
  177. package/dist/worker/scheduler/scheduled-goal-evidence.js +66 -3
  178. package/dist/worker/scheduler/scheduled-goal-store.js +10 -0
  179. package/dist/workflows/dag/budget-enforcement.js +31 -2
  180. package/dist/workflows/dag/frontend-closeout.js +3 -1
  181. package/dist/workflows/dag/frontend-contract-facts.js +11 -8
  182. package/dist/workflows/dag/frontend-design-policy.js +6 -6
  183. package/dist/workflows/dag/frontend-durable-tools.js +90 -70
  184. package/dist/workflows/dag/frontend-implementation-contract.js +146 -57
  185. package/dist/workflows/dag/frontend-plan-canary.js +66 -16
  186. package/dist/workflows/dag/frontend-plan-decision-contract.js +13 -21
  187. package/dist/workflows/dag/frontend-plan-progress.js +92 -0
  188. package/dist/workflows/dag/frontend-recovery-plan.js +10 -5
  189. package/dist/workflows/dag/frontend-recovery-run.js +51 -46
  190. package/dist/workflows/dag/frontend-repair-assertions.js +36 -0
  191. package/dist/workflows/dag/frontend-review-context.js +29 -1
  192. package/dist/workflows/dag/frontend-review-scopes.js +92 -2
  193. package/dist/workflows/dag/frontend-risk.js +8 -3
  194. package/dist/workflows/dag/frontend-root-observation.js +261 -0
  195. package/dist/workflows/dag/frontend-session-budget.js +52 -39
  196. package/dist/workflows/dag/frontend-session-context.js +10 -0
  197. package/dist/workflows/dag/frontend-test-execution-evidence.js +206 -48
  198. package/dist/workflows/dag/frontend-typed-event-store.js +11 -1
  199. package/dist/workflows/dag/frontend-verification-trace.js +6 -0
  200. package/dist/workflows/dag/frontend-writer-admission.js +3 -1
  201. package/dist/workflows/dag/init-hybrid.js +108 -37
  202. package/dist/workflows/dag/node-execution.js +11 -11
  203. package/dist/workflows/dag/output-protocol.js +1 -0
  204. package/dist/workflows/dag/rerun-feedback.js +35 -4
  205. package/dist/workflows/dag/rerun-task.js +62 -66
  206. package/dist/workflows/dag/runner.js +58 -12
  207. package/dist/workflows/dag/types.js +15 -8
  208. package/dist/workflows/loop/benchmark.js +11 -8
  209. package/docs/README.md +1 -1
  210. package/docs/architecture/runtime-boundaries.md +1 -1
  211. package/docs/governance/harness-methodology-verification.md +11 -0
  212. package/docs/templates/backend-test-dag.json +5 -4
  213. package/docs/templates/frontend-implementation-contract.schema.json +31 -11
  214. package/package.json +6 -4
  215. package/skills/frontend-design-review/SKILL.md +8 -11
  216. package/skills/frontend-design-review/references/review-checklist.md +3 -4
  217. package/skills/frontend-review/SKILL.md +5 -1
  218. package/skills/loop-agent/references/command-reference.md +8 -0
  219. package/skills/loop-agent/references/hybrid-dag.md +1 -1
  220. package/dist/worker/console/static/assets/channel-ChE7y-cx.js +0 -1
  221. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C01TCf2X.js +0 -1
  222. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C01TCf2X.js +0 -1
  223. package/dist/worker/console/static/assets/index-BWkIfcrK.css +0 -1
  224. package/dist/worker/console/static/assets/index-Cdkvw_H6.js +0 -469
  225. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DC7V5vcq.js +0 -1
  226. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Bwy4QUTO.js +0 -8
@@ -5,6 +5,8 @@ import { requestId } from "./format.js";
5
5
  import { parseAssistantContent, } from "../../chat/assistant-content.js";
6
6
  import { sortChatTimelineMessages, upsertCompactionMessage, } from "./compaction-message.js";
7
7
  import { replacePendingUserMessage } from "./pending-user-message.js";
8
+ import { conversationNodeFromEvent, upsertConversationNode, } from "../../chat/conversation-nodes.js";
9
+ import { createAnimationFrameBatcher } from "./animation-frame-batcher.js";
8
10
  /** Shared handler for a single parsed Chat SSE event (M1 reconnect).
9
11
  *
10
12
  * Used by BOTH the inline prompt POST SSE and the background reconnect
@@ -12,7 +14,7 @@ import { replacePendingUserMessage } from "./pending-user-message.js";
12
14
  * original request would have. `assistantId` is the in-flight assistant
13
15
  * message id to patch for streaming text; reconnect creates one if none. */
14
16
  export function createChatSseEventApplier(deps) {
15
- const { refs, onReconcile, setError, setInterview, setHumanGateCards, setArtifactCards, setStreaming, setTurnPhase = () => undefined, setDeferredTurn = () => undefined, setDeferredTurns = () => undefined, setStreamingAssistantId, setUnread, setOperationCards, setUsage, onTelemetryRefresh, setMessages, setToolCalls, setPendingQuestion = () => undefined, setTodos = () => undefined, setGoal = () => undefined, getMessages, } = deps;
17
+ const { refs, onReconcile, setError, setInterview, setHumanGateCards, setArtifactCards, setStreaming, setTurnPhase = () => undefined, setDeferredTurn = () => undefined, setDeferredTurns = () => undefined, setStreamingAssistantId, setUnread, setOperationCards, setUsage, onTelemetryRefresh, setMessages, setToolCalls, setPendingQuestion = () => undefined, setTodos = () => undefined, setGoal = () => undefined, setOperatorActions = () => undefined, setSddSessionIdentity = () => undefined, setConversationNodes = () => undefined, setTurnState = () => undefined, getMessages, } = deps;
16
18
  let queueOwner;
17
19
  let queueRevision = -1;
18
20
  /** Retained-replay text per turn. Pi `message_update.text` is the cumulative
@@ -20,6 +22,36 @@ export function createChatSseEventApplier(deps) {
20
22
  * turn matters and buffering cannot lose content. Flushed on `stream_ready`
21
23
  * onto the turn's newest assistant row (2026-09-08). */
22
24
  const replayTextByTurn = new Map();
25
+ const messageUpdateBatcher = createAnimationFrameBatcher({
26
+ schedule: (callback) => {
27
+ if (typeof window !== "undefined" && window.requestAnimationFrame) {
28
+ return window.requestAnimationFrame(callback);
29
+ }
30
+ return globalThis.setTimeout(callback, 16);
31
+ },
32
+ cancel: (handle) => {
33
+ if (typeof window !== "undefined" && window.cancelAnimationFrame) {
34
+ window.cancelAnimationFrame(handle);
35
+ }
36
+ else {
37
+ globalThis.clearTimeout(handle);
38
+ }
39
+ },
40
+ publish: (values) => {
41
+ const sessionId = refs.sessionRef.current?.sessionId;
42
+ const generation = refs.sessionGenerationRef.current;
43
+ setMessages((messages) => {
44
+ let next = messages;
45
+ for (const item of values.values()) {
46
+ if (item.sessionId !== sessionId || item.generation !== generation)
47
+ continue;
48
+ next = item.apply(next);
49
+ }
50
+ return next;
51
+ });
52
+ },
53
+ });
54
+ const useFrameBatching = typeof window !== "undefined" && typeof window.requestAnimationFrame === "function";
23
55
  const flushReplayText = () => {
24
56
  if (replayTextByTurn.size === 0)
25
57
  return;
@@ -94,6 +126,9 @@ export function createChatSseEventApplier(deps) {
94
126
  if (options?.generation !== undefined &&
95
127
  refs.sessionGenerationRef.current !== options.generation)
96
128
  return;
129
+ if (typeof data.sessionId === "string" &&
130
+ data.sessionId !== refs.sessionRef.current?.sessionId)
131
+ return;
97
132
  const replay = options?.replay === true;
98
133
  if (eventName === "stream_ready") {
99
134
  // The retained replay is fully buffered: publish it now so a reconnect
@@ -101,6 +136,10 @@ export function createChatSseEventApplier(deps) {
101
136
  flushReplayText();
102
137
  return;
103
138
  }
139
+ // Structural events define visible boundaries in the DSH timeline. Publish
140
+ // any coalesced text before applying them so rows never appear out of order.
141
+ if (eventName !== "message_update" || replay)
142
+ messageUpdateBatcher.flush();
104
143
  if (eventName === "reconcile") {
105
144
  // Cursor aged out of the server ring (or the turn changed under us):
106
145
  // retained history may be incomplete. Surface recovery via the callback
@@ -173,6 +212,64 @@ export function createChatSseEventApplier(deps) {
173
212
  const eventAt = typeof data.at === "string" && Number.isFinite(Date.parse(data.at))
174
213
  ? Date.parse(data.at)
175
214
  : Date.now();
215
+ const nodeKinds = new Set([
216
+ "step_start",
217
+ "step_end",
218
+ "model_retry",
219
+ "turn_error",
220
+ "turn_max_tokens",
221
+ "request_context",
222
+ "command",
223
+ "unknown",
224
+ ]);
225
+ if (nodeKinds.has(eventName) &&
226
+ turnId &&
227
+ eventSeq !== undefined &&
228
+ typeof data.eventId === "string") {
229
+ const node = conversationNodeFromEvent({
230
+ eventId: data.eventId,
231
+ kind: eventName,
232
+ seq: eventSeq,
233
+ turnId,
234
+ at: new Date(eventAt).toISOString(),
235
+ data: payload,
236
+ });
237
+ if (node)
238
+ setConversationNodes((current) => upsertConversationNode(current, node));
239
+ return;
240
+ }
241
+ if (eventName === "operator-action") {
242
+ const eventId = typeof data.eventId === "string" ? data.eventId : undefined;
243
+ if (!eventId ||
244
+ eventSeq === undefined ||
245
+ data.sessionId !== refs.sessionRef.current?.sessionId ||
246
+ payload.action !== "sdd-bingo" ||
247
+ typeof payload.skillName !== "string" ||
248
+ !payload.skillName.trim() ||
249
+ typeof payload.telemetrySessionId !== "string" ||
250
+ !payload.telemetrySessionId.trim())
251
+ return;
252
+ const action = {
253
+ eventId,
254
+ seq: eventSeq,
255
+ at: eventAt,
256
+ ...(turnId ? { turnId } : {}),
257
+ action: "sdd-bingo",
258
+ skillName: payload.skillName.trim(),
259
+ telemetrySessionId: payload.telemetrySessionId.trim(),
260
+ };
261
+ setOperatorActions((current) => {
262
+ const existing = current.findIndex((item) => item.eventId === eventId);
263
+ const next = existing < 0
264
+ ? [...current, action]
265
+ : current.map((item, index) => (index === existing ? action : item));
266
+ return next.sort((left, right) => left.seq - right.seq || left.at - right.at);
267
+ });
268
+ setSddSessionIdentity({
269
+ telemetrySessionId: action.telemetrySessionId,
270
+ });
271
+ return;
272
+ }
176
273
  if (eventName === "queue_update") {
177
274
  const revision = typeof payload.revision === "number" ? payload.revision : -1;
178
275
  const owner = refs.sessionRef.current?.sessionId;
@@ -228,6 +325,9 @@ export function createChatSseEventApplier(deps) {
228
325
  : {}),
229
326
  ...(eventSeq === undefined ? {} : { eventSeq }),
230
327
  ...(turnId ? { turnId } : {}),
328
+ ...(Array.isArray(message.attachments)
329
+ ? { attachments: message.attachments }
330
+ : {}),
231
331
  };
232
332
  // Any frame carrying the clientRequestId replaces the optimistic
233
333
  // row in place — even after a /state reconcile already released
@@ -407,6 +507,8 @@ export function createChatSseEventApplier(deps) {
407
507
  if (turnId) {
408
508
  refs.settledTurnIdsRef.current.delete(turnId);
409
509
  refs.activeTurnIdRef.current = turnId;
510
+ if (!replay)
511
+ setTurnState(turnId, "running");
410
512
  }
411
513
  if (refs.stopRequestedRef.current &&
412
514
  (!turnId ||
@@ -425,13 +527,17 @@ export function createChatSseEventApplier(deps) {
425
527
  return;
426
528
  }
427
529
  if (eventName === "agent_settled" || eventName === "agent_end") {
428
- if (turnId)
429
- refs.settledTurnIdsRef.current.add(turnId);
430
530
  // `agent_end` means the SDK run ended, but the Console keeps the only
431
531
  // GET stream through an idle grace for agent_settled/state reconciliation
432
532
  // (M4.4). It must not prematurely close the visible turn.
433
533
  if (eventName === "agent_end")
434
534
  return;
535
+ if (turnId) {
536
+ refs.settledTurnIdsRef.current.add(turnId);
537
+ // Settlement proves closure but not its outcome. The stream owner starts
538
+ // an authoritative /state reconcile; do not guess settled here because
539
+ // abort/error closeout persists the exact state just after this event.
540
+ }
435
541
  const pending = refs.pendingSubmissionRef.current;
436
542
  if (pending && turnId && pending.acceptedTurnId === turnId) {
437
543
  refs.releasedClientRequestIdsRef.current.add(pending.clientRequestId);
@@ -643,7 +749,7 @@ export function createChatSseEventApplier(deps) {
643
749
  refs.sealedAssistantIdsRef.current.add(durableMessageId);
644
750
  }
645
751
  const assistantIdForEvent = resolvedAssistantId;
646
- setMessages((m) => {
752
+ const applyMessageUpdate = (m) => {
647
753
  // Rename a locally-generated row to the durable id in place so we
648
754
  // never render a twin bubble for the same persistence.
649
755
  const rows = durableMessageId
@@ -686,7 +792,20 @@ export function createChatSseEventApplier(deps) {
686
792
  ...(content ? { content } : {}),
687
793
  },
688
794
  ]);
689
- });
795
+ };
796
+ if (eventName === "message_update" && useFrameBatching) {
797
+ const sessionId = refs.sessionRef.current?.sessionId;
798
+ if (sessionId) {
799
+ messageUpdateBatcher.enqueue(`${sessionId}:${assistantIdForEvent}`, {
800
+ sessionId,
801
+ generation: refs.sessionGenerationRef.current,
802
+ apply: applyMessageUpdate,
803
+ });
804
+ }
805
+ }
806
+ else {
807
+ setMessages(applyMessageUpdate);
808
+ }
690
809
  if (eventName === "message_end")
691
810
  sealedIds.add(assistantIdForEvent);
692
811
  }
@@ -696,6 +815,10 @@ export function createChatSseEventApplier(deps) {
696
815
  : requestId("t");
697
816
  const toolName = typeof payload.toolName === "string" ? payload.toolName : "tool";
698
817
  const argsPreview = typeof payload.argsPreview === "string" ? payload.argsPreview : "{}";
818
+ const toolStep = typeof payload.step === "number" ? payload.step : undefined;
819
+ const parentToolCallId = typeof payload.parentToolCallId === "string" && payload.parentToolCallId
820
+ ? payload.parentToolCallId
821
+ : undefined;
699
822
  setToolCalls((prev) => {
700
823
  if (prev.some((t) => t.id === id)) {
701
824
  return prev.map((t) => {
@@ -707,6 +830,8 @@ export function createChatSseEventApplier(deps) {
707
830
  status: "running",
708
831
  turnId: turnId ?? t.turnId,
709
832
  assistantMessageId: resolvedAssistantId,
833
+ ...(toolStep === undefined ? {} : { step: toolStep }),
834
+ ...(parentToolCallId ? { parentToolCallId } : {}),
710
835
  ...(eventSeq === undefined ? {} : { eventSeq }),
711
836
  };
712
837
  });
@@ -722,6 +847,54 @@ export function createChatSseEventApplier(deps) {
722
847
  ...(eventSeq === undefined ? {} : { eventSeq }),
723
848
  turnId,
724
849
  assistantMessageId: resolvedAssistantId,
850
+ ...(toolStep === undefined ? {} : { step: toolStep }),
851
+ ...(parentToolCallId ? { parentToolCallId } : {}),
852
+ },
853
+ ];
854
+ });
855
+ }
856
+ else if (eventName === "tool_update") {
857
+ const id = typeof payload.toolCallId === "string"
858
+ ? payload.toolCallId
859
+ : requestId("u");
860
+ const toolName = typeof payload.toolName === "string" ? payload.toolName : "tool";
861
+ const resultText = typeof payload.resultPreview === "string"
862
+ ? payload.resultPreview
863
+ : "";
864
+ const toolStep = typeof payload.step === "number" ? payload.step : undefined;
865
+ const parentToolCallId = typeof payload.parentToolCallId === "string" && payload.parentToolCallId
866
+ ? payload.parentToolCallId
867
+ : undefined;
868
+ setToolCalls((prev) => {
869
+ const exists = prev.some((tool) => tool.id === id);
870
+ if (exists) {
871
+ return prev.map((tool) => tool.id !== id
872
+ ? tool
873
+ : {
874
+ ...tool,
875
+ result: resultText,
876
+ resultText,
877
+ status: "running",
878
+ turnId: turnId ?? tool.turnId,
879
+ assistantMessageId: tool.assistantMessageId ?? resolvedAssistantId,
880
+ ...(toolStep === undefined ? {} : { step: toolStep }),
881
+ ...(parentToolCallId ? { parentToolCallId } : {}),
882
+ });
883
+ }
884
+ return [
885
+ ...prev,
886
+ {
887
+ id,
888
+ toolName,
889
+ result: resultText,
890
+ resultText,
891
+ status: "running",
892
+ createdAt: eventAt,
893
+ ...(eventSeq === undefined ? {} : { eventSeq }),
894
+ turnId,
895
+ assistantMessageId: resolvedAssistantId,
896
+ ...(toolStep === undefined ? {} : { step: toolStep }),
897
+ ...(parentToolCallId ? { parentToolCallId } : {}),
725
898
  },
726
899
  ];
727
900
  });
@@ -735,6 +908,10 @@ export function createChatSseEventApplier(deps) {
735
908
  const resultText = typeof payload.resultPreview === "string"
736
909
  ? payload.resultPreview
737
910
  : "{}";
911
+ const toolStep = typeof payload.step === "number" ? payload.step : undefined;
912
+ const parentToolCallId = typeof payload.parentToolCallId === "string" && payload.parentToolCallId
913
+ ? payload.parentToolCallId
914
+ : undefined;
738
915
  // Plan A (2026-08-16): the server carries the accepted-operation link on
739
916
  // the frame; persist it on the tool entry so the semantic event resolves
740
917
  // an operation:<id> ref (mirrors the hydrated path backfill).
@@ -756,6 +933,8 @@ export function createChatSseEventApplier(deps) {
756
933
  ...(eventSeq === undefined ? {} : { finishedSeq: eventSeq }),
757
934
  turnId: turnId ?? t.turnId,
758
935
  assistantMessageId: t.assistantMessageId ?? resolvedAssistantId,
936
+ ...(toolStep === undefined ? {} : { step: toolStep }),
937
+ ...(parentToolCallId ? { parentToolCallId } : {}),
759
938
  ...(resultOperationId
760
939
  ? { operationId: resultOperationId }
761
940
  : t.operationId
@@ -779,6 +958,8 @@ export function createChatSseEventApplier(deps) {
779
958
  : { eventSeq, finishedSeq: eventSeq }),
780
959
  turnId,
781
960
  assistantMessageId: resolvedAssistantId,
961
+ ...(toolStep === undefined ? {} : { step: toolStep }),
962
+ ...(parentToolCallId ? { parentToolCallId } : {}),
782
963
  ...(resultOperationId ? { operationId: resultOperationId } : {}),
783
964
  },
784
965
  ];
@@ -0,0 +1,101 @@
1
+ /** Parse the persisted Chat SSE cursor shape `<epoch>:<seq>`. */
2
+ export function parseChatEventCursor(eventId) {
3
+ const raw = eventId?.trim();
4
+ if (!raw)
5
+ return null;
6
+ const separator = raw.lastIndexOf(":");
7
+ if (separator <= 0)
8
+ return null;
9
+ const epoch = raw.slice(0, separator);
10
+ const seqPart = raw.slice(separator + 1);
11
+ if (!/^\d+$/.test(seqPart))
12
+ return null;
13
+ const seq = Number(seqPart);
14
+ if (!Number.isSafeInteger(seq) || seq < 0)
15
+ return null;
16
+ return { epoch, seq };
17
+ }
18
+ /** Keep a cursor monotonic within one epoch; a valid new epoch replaces it. */
19
+ export function selectLaterChatEventId(currentEventId, candidateEventId) {
20
+ const candidate = parseChatEventCursor(candidateEventId);
21
+ if (!candidate)
22
+ return currentEventId ?? null;
23
+ const current = parseChatEventCursor(currentEventId);
24
+ if (!current ||
25
+ current.epoch !== candidate.epoch ||
26
+ candidate.seq > current.seq) {
27
+ return candidateEventId.trim();
28
+ }
29
+ return currentEventId.trim();
30
+ }
31
+ function sseBlockEventId(block) {
32
+ for (const rawLine of block.split("\n")) {
33
+ const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
34
+ if (!line.startsWith("id:"))
35
+ continue;
36
+ return line.slice(3).trimStart().trimEnd() || null;
37
+ }
38
+ return null;
39
+ }
40
+ /**
41
+ * A repair buffer belongs to the event-log generation that produced it. If the
42
+ * reconnect crossed a Console restart, replay from the replacement epoch is
43
+ * authoritative and old buffered frames must not be applied or restore the old
44
+ * Last-Event-ID. Control/comment frames are not persisted repair facts.
45
+ */
46
+ export function selectChatSseRepairBlocks(blocks, readyCursor) {
47
+ const ready = parseChatEventCursor(readyCursor);
48
+ if (!ready)
49
+ return [];
50
+ return blocks.filter((block) => {
51
+ const buffered = parseChatEventCursor(sseBlockEventId(block));
52
+ return buffered?.epoch === ready.epoch;
53
+ });
54
+ }
55
+ /** Read the dense event-store watermark that starts the live follow phase. */
56
+ export function readChatStreamReadyCursor(block) {
57
+ let eventName = "message";
58
+ let dataText = "";
59
+ for (const rawLine of block.split("\n")) {
60
+ const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
61
+ if (line.startsWith("event:"))
62
+ eventName = line.slice(6).trim();
63
+ else if (line.startsWith("data:"))
64
+ dataText += line.slice(5).trimStart();
65
+ }
66
+ if (eventName !== "stream_ready" || !dataText)
67
+ return null;
68
+ try {
69
+ const data = JSON.parse(dataText);
70
+ if (typeof data.epoch !== "string" ||
71
+ !data.epoch ||
72
+ typeof data.lastSeq !== "number" ||
73
+ !Number.isSafeInteger(data.lastSeq) ||
74
+ data.lastSeq < 0)
75
+ return null;
76
+ return `${data.epoch}:${data.lastSeq}`;
77
+ }
78
+ catch {
79
+ return null;
80
+ }
81
+ }
82
+ /**
83
+ * Detect a dropped live persisted frame before it can advance the replay cursor.
84
+ * Epoch changes remain server-owned because an epoch_mismatch reconcile frame
85
+ * is followed by snapshot events from the replacement epoch.
86
+ */
87
+ export function inspectChatSseContinuity(block, lastEventId) {
88
+ const current = parseChatEventCursor(lastEventId);
89
+ const incoming = parseChatEventCursor(sseBlockEventId(block));
90
+ if (!current || !incoming || current.epoch !== incoming.epoch) {
91
+ return { action: "apply" };
92
+ }
93
+ if (incoming.seq > current.seq + 1) {
94
+ return {
95
+ action: "reconnect",
96
+ expectedSeq: current.seq + 1,
97
+ receivedSeq: incoming.seq,
98
+ };
99
+ }
100
+ return { action: "apply" };
101
+ }
@@ -1,4 +1,4 @@
1
- import { compareChatOrder } from "../../chat/turn-order.js";
1
+ import { ensureLinkedSubmissionOrder } from "../../chat/timeline-message-order.js";
2
2
  function finiteToken(value) {
3
3
  return typeof value === "number" && Number.isFinite(value)
4
4
  ? Math.max(0, Math.floor(value))
@@ -59,9 +59,24 @@ export function compactionMessageFromEvent(item) {
59
59
  },
60
60
  };
61
61
  }
62
- /** Stable chronological order: server sequence first, legacy time fallback. */
62
+ /** Stable chronological order: server sequence first, legacy time/id fallback. */
63
63
  export function sortChatTimelineMessages(messages) {
64
- return [...messages].sort(compareChatOrder);
64
+ return ensureLinkedSubmissionOrder([...messages].sort((a, b) => {
65
+ if (a.eventSeq !== undefined &&
66
+ b.eventSeq !== undefined &&
67
+ a.eventSeq !== b.eventSeq) {
68
+ return a.eventSeq - b.eventSeq;
69
+ }
70
+ if (a.createdAt !== b.createdAt)
71
+ return a.createdAt - b.createdAt;
72
+ if (a.eventSeq !== undefined && b.eventSeq === undefined)
73
+ return 1;
74
+ if (a.eventSeq === undefined && b.eventSeq !== undefined)
75
+ return -1;
76
+ // Modern JS sort is stable; keep insertion order for indistinguishable
77
+ // legacy rows instead of inventing chronology from random ids.
78
+ return 0;
79
+ }));
65
80
  }
66
81
  export function upsertCompactionMessage(messages, item) {
67
82
  const next = compactionMessageFromEvent(item);
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Operator Chat — context insights store (dsh-context parity).
3
+ *
4
+ * Module-level external store so every surface that needs the dashboard
5
+ * (Details-rail context tab, /context overlay) shares one fetch lifecycle
6
+ * keyed by sessionId. Read-only GET; no mutation gate needed.
7
+ */
8
+ import { useSyncExternalStore } from "react";
9
+ let state = { sessionId: null, loading: false };
10
+ const listeners = new Set();
11
+ function emit() {
12
+ for (const listener of listeners)
13
+ listener();
14
+ }
15
+ function setState(next) {
16
+ state = next;
17
+ emit();
18
+ }
19
+ /** Attach the store to a session; null detaches and clears. */
20
+ export function attachContextInsights(sessionId) {
21
+ if (state.sessionId === sessionId)
22
+ return;
23
+ setState({ sessionId, loading: Boolean(sessionId) });
24
+ if (!sessionId)
25
+ return;
26
+ void refreshContextInsights();
27
+ }
28
+ /** Force a refetch (spatial overlay open, manual refresh button). */
29
+ export async function refreshContextInsights() {
30
+ const sessionId = state.sessionId;
31
+ if (!sessionId)
32
+ return;
33
+ setState({ ...state, loading: true });
34
+ try {
35
+ const res = await fetch(`/api/operator/v1/chat/sessions/${encodeURIComponent(sessionId)}/context-insights`, { credentials: "include" });
36
+ const body = (await res.json().catch(() => null));
37
+ if (state.sessionId !== sessionId)
38
+ return; // stale reply
39
+ if (!res.ok || !body?.insights) {
40
+ setState({
41
+ sessionId,
42
+ loading: false,
43
+ error: body?.error?.message ?? `加载上下文洞察失败(${res.status})`,
44
+ });
45
+ return;
46
+ }
47
+ setState({ sessionId, loading: false, insights: body.insights });
48
+ }
49
+ catch (error) {
50
+ if (state.sessionId !== sessionId)
51
+ return;
52
+ setState({
53
+ sessionId,
54
+ loading: false,
55
+ error: error instanceof Error ? error.message : String(error),
56
+ });
57
+ }
58
+ }
59
+ export function useContextInsights() {
60
+ return useSyncExternalStore((onStoreChange) => {
61
+ listeners.add(onStoreChange);
62
+ return () => listeners.delete(onStoreChange);
63
+ }, () => state, () => state);
64
+ }
@@ -0,0 +1,98 @@
1
+ export const DETAILS_SURFACE_ORDER = [
2
+ "repo",
3
+ "session",
4
+ "bench",
5
+ ];
6
+ export const DETAILS_SURFACES = {
7
+ repo: {
8
+ id: "repo",
9
+ label: "仓库",
10
+ description: "当前仓库的文件与 Git",
11
+ pages: ["files", "scm"],
12
+ },
13
+ session: {
14
+ id: "session",
15
+ label: "会话",
16
+ description: "这次对话的注入与检查",
17
+ pages: [
18
+ "context",
19
+ "insights",
20
+ "calls",
21
+ "activity",
22
+ "trajectory",
23
+ "goal",
24
+ "dag",
25
+ ],
26
+ },
27
+ bench: {
28
+ id: "bench",
29
+ label: "工作台",
30
+ description: "终端、预览、数据库",
31
+ pages: ["terminals", "browser", "databases"],
32
+ },
33
+ };
34
+ const TAB_TO_SURFACE = {
35
+ files: "repo",
36
+ scm: "repo",
37
+ context: "session",
38
+ insights: "session",
39
+ calls: "session",
40
+ activity: "session",
41
+ trajectory: "session",
42
+ goal: "session",
43
+ dag: "session",
44
+ terminals: "bench",
45
+ browser: "bench",
46
+ databases: "bench",
47
+ };
48
+ export const DEFAULT_PAGE_BY_SURFACE = {
49
+ repo: "files",
50
+ session: "context",
51
+ bench: "terminals",
52
+ };
53
+ export function surfaceForTab(tab) {
54
+ return TAB_TO_SURFACE[tab];
55
+ }
56
+ export function pagesOnSurface(surface, available) {
57
+ const allowed = new Set(DETAILS_SURFACES[surface].pages);
58
+ return available.filter((tab) => allowed.has(tab));
59
+ }
60
+ export function surfacesWithPages(available) {
61
+ return DETAILS_SURFACE_ORDER.filter((surface) => pagesOnSurface(surface, available).length > 0);
62
+ }
63
+ export function restorePageOnSurface(surface, available, last) {
64
+ const pages = pagesOnSurface(surface, available);
65
+ if (pages.length === 0)
66
+ return null;
67
+ if (last && pages.includes(last))
68
+ return last;
69
+ return pages[0] ?? null;
70
+ }
71
+ export function adjacentSurface(current, availableSurfaces, delta) {
72
+ if (availableSurfaces.length < 2)
73
+ return null;
74
+ const index = availableSurfaces.indexOf(current);
75
+ if (index < 0)
76
+ return availableSurfaces[0] ?? null;
77
+ return availableSurfaces[(index + delta + availableSurfaces.length) % availableSurfaces.length] ?? null;
78
+ }
79
+ export function adjacentPage(current, pages, delta) {
80
+ if (pages.length < 2)
81
+ return null;
82
+ const index = pages.indexOf(current);
83
+ if (index < 0)
84
+ return pages[0] ?? null;
85
+ return pages[(index + delta + pages.length) % pages.length] ?? null;
86
+ }
87
+ export function temporaryDetailsTab(id) {
88
+ switch (id) {
89
+ case "context":
90
+ case "insights":
91
+ case "calls":
92
+ case "activity":
93
+ case "goal":
94
+ return id;
95
+ default:
96
+ return null;
97
+ }
98
+ }