@tea-agent/loop-agent 0.39.0-next.8 → 0.39.0

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 (254) hide show
  1. package/AGENTS.md +4 -1
  2. package/CHANGELOG.md +264 -67
  3. package/README.md +11 -5
  4. package/bin/loop-agent.js +7 -3
  5. package/dist/application/dag/generate-task-dag.js +9 -0
  6. package/dist/application/task-lifecycle/advance.js +4 -0
  7. package/dist/build-stamp.json +3 -3
  8. package/dist/cli/command-definitions.js +16 -5
  9. package/dist/cli/program.js +16 -2
  10. package/dist/cli/update/notifier.js +2 -2
  11. package/dist/cli.js +2 -1
  12. package/dist/commands/dag-rerun.js +55 -1
  13. package/dist/commands/init-model-catalog.js +464 -0
  14. package/dist/commands/init-upgrade.js +265 -97
  15. package/dist/commands/init.js +428 -27
  16. package/dist/commands/inspect-next.js +8 -0
  17. package/dist/commands/task-advance.js +31 -0
  18. package/dist/executors/dag-pi-executor.js +124 -18
  19. package/dist/executors/pi-event-serializer.js +42 -1
  20. package/dist/executors/pi-executor.js +51 -0
  21. package/dist/executors/pi-extension-resolver.js +233 -0
  22. package/dist/executors/pi-playwright-cli-tool.js +74 -28
  23. package/dist/executors/pi-sdk-executor.js +187 -59
  24. package/dist/executors/shell-executor.js +554 -33
  25. package/dist/executors/shell-write-guard.js +7 -0
  26. package/dist/shared/dag-failure-category.js +138 -0
  27. package/dist/shared/dag-prompt-override.js +28 -0
  28. package/dist/shared/operator/capabilities.js +181 -27
  29. package/dist/shared/playwright-cli-command-policy.js +15 -0
  30. package/dist/shared/update/console-notifier.js +100 -0
  31. package/dist/{cli → shared}/update/npm-client.js +41 -8
  32. package/dist/{cli → shared}/update/state.js +41 -13
  33. package/dist/task/config-types.js +28 -0
  34. package/dist/task/contract/constants.js +1 -0
  35. package/dist/task/contract/diff.js +26 -0
  36. package/dist/task/contract/project.js +5 -0
  37. package/dist/task/contract/schema.js +6 -1
  38. package/dist/task/source-prepare/build-draft.js +15 -0
  39. package/dist/task/source-prepare/parse-intent.js +24 -4
  40. package/dist/worker/cli.js +31 -1
  41. package/dist/worker/console/chat/deferred-turn.js +91 -0
  42. package/dist/worker/console/chat/mcp-inventory.js +221 -0
  43. package/dist/worker/console/chat/operation-card.js +8 -1
  44. package/dist/worker/console/chat/pi-console-config.js +81 -10
  45. package/dist/worker/console/chat/pi-runtime.js +165 -83
  46. package/dist/worker/console/chat/repo-walk.js +10 -1
  47. package/dist/worker/console/chat/routes.js +480 -55
  48. package/dist/worker/console/chat/semantic-activity.js +20 -8
  49. package/dist/worker/console/chat/session-store.js +146 -2
  50. package/dist/worker/console/chat/turn-process.js +10 -10
  51. package/dist/worker/console/dag-execution-receipt.js +33 -0
  52. package/dist/worker/console/inspect-split.js +18 -0
  53. package/dist/worker/console/operation-run-facts.js +19 -3
  54. package/dist/worker/console/operator-actions.js +195 -4
  55. package/dist/worker/console/operator-user-error.js +2 -2
  56. package/dist/worker/console/pi-readiness.js +5 -4
  57. package/dist/worker/console/recovery-cta.js +4 -4
  58. package/dist/worker/console/recovery-error-copy.js +4 -4
  59. package/dist/worker/console/routes.js +43 -2
  60. package/dist/worker/console/security.js +63 -9
  61. package/dist/worker/console/server.js +42 -6
  62. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-BLXFe4qI.js → abnfDiagram-N423BO3Z-Br92EGb7.js} +1 -1
  63. package/dist/worker/console/static/assets/{arc-7iOJeqho.js → arc-oulImtQq.js} +1 -1
  64. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CYoyj5K7.js → architectureDiagram-T3A2C74G-tyHc6aqO.js} +1 -1
  65. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-fK01JNlR.js → blockDiagram-VBNYF7ZC-CY86BSpT.js} +1 -1
  66. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-bzbw5D_Z.js → c4Diagram-5PPSVZJV-BKKsDmNs.js} +1 -1
  67. package/dist/worker/console/static/assets/channel-TY36zt-i.js +1 -0
  68. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-Cf6hJFFG.js → chunk-2GRJ4B5K-CXZdjl3B.js} +1 -1
  69. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-DSFXR-et.js → chunk-2Q5K7J3B-B-aIFDQ5.js} +1 -1
  70. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BagJechl.js → chunk-5RXB4S5H-BDgAW_B2.js} +1 -1
  71. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-E7QNMFaW.js → chunk-5VM5RSS4-DABa4UPR.js} +1 -1
  72. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BG_eOBrC.js → chunk-6Q2QTUOP-aGbysVOx.js} +1 -1
  73. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-Bo42dqol.js → chunk-GF5L2VYU-B8MSZDVs.js} +1 -1
  74. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-dibM-87v.js → chunk-JWPE2WC7-CpelV73g.js} +1 -1
  75. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-Bytrp3Oe.js → chunk-KBJHAD2P-BE04ty3z.js} +1 -1
  76. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DBABILrr.js → chunk-RYQCIY6F-BUxTkaaR.js} +1 -1
  77. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-DCUk5Pf9.js → chunk-XXDRQBXY-CYYBVKfX.js} +1 -1
  78. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Dk7QCChM.js +1 -0
  79. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Dk7QCChM.js +1 -0
  80. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BlGE9v6E.js → cose-bilkent-JH36ORCC-CEQF_xmQ.js} +1 -1
  81. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-DGl5ewph.js → cynefin-VYW2F7L2-CqpxN5IT.js} +1 -1
  82. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-DsJ_SVH_.js → cynefinDiagram-MW4NZA55-B3PzanVi.js} +1 -1
  83. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BIhjFdzv.js → dagre-VZM6K2ZE-BSUGCNx1.js} +1 -1
  84. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Dmy1ctTb.js → diagram-7IWD3JNH-CdLevU5T.js} +1 -1
  85. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-DYCtWUF3.js → diagram-B4RE2ZJO-BjpF_Kok.js} +1 -1
  86. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-BMZFHVd2.js → diagram-LBJQPF4R-C4I6NJny.js} +1 -1
  87. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-b_U8gfaO.js → diagram-Q27KOJAE-C8hecCr5.js} +1 -1
  88. package/dist/worker/console/static/assets/{diagram-UB23O5K3-Dzy_bn-I.js → diagram-UB23O5K3-Daod6tOS.js} +1 -1
  89. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-DJCiMNEI.js → ebnfDiagram-BXEA7PRR-BvvmazGv.js} +1 -1
  90. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-28lD016B.js → erDiagram-JOGREHBK-DeN_vfV4.js} +1 -1
  91. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-DGyHJmIw.js → flowDiagram-UKHOOZJN-D6dq1t9z.js} +1 -1
  92. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CzdwUjrO.js → ganttDiagram-PKOTCBZU-DziIdNY4.js} +1 -1
  93. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-D9bGwuo8.js → gitGraphDiagram-DS77QQ5N-Dy4s5CXr.js} +1 -1
  94. package/dist/worker/console/static/assets/index-C3GgHg-g.css +1 -0
  95. package/dist/worker/console/static/assets/index-Ci8ksvBm.js +406 -0
  96. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-LJgJUQFC.js → infoDiagram-6WML65LV-Czv5eYx2.js} +1 -1
  97. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-98LaB8zb.js → ishikawaDiagram-WSZJBQD7-Bsk3nFRT.js} +1 -1
  98. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-D-ISzwlQ.js → journeyDiagram-NVQOT4AX-DxGmNj5K.js} +1 -1
  99. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-gfYOI-EL.js → kanban-definition-27J2QSJJ-CYCFqkO_.js} +1 -1
  100. package/dist/worker/console/static/assets/{linear-CyntZgKV.js → linear-B0YzRwrd.js} +1 -1
  101. package/dist/worker/console/static/assets/{mermaid.core-BBOR7dg0.js → mermaid.core-t3EZtIGh.js} +5 -5
  102. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-CQAgwPMk.js → mindmap-definition-FAOFIHXS-BTXpTv0T.js} +1 -1
  103. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DkMiZFuV.js → pegDiagram-VL7TDLO6-DU8UkKqA.js} +1 -1
  104. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DLNXa_0J.js → pieDiagram-7S7Q4E2Y-DP4y8ohp.js} +1 -1
  105. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-Dc7bdzjm.js → quadrantDiagram-CIZ2JOQS-B07vNaYR.js} +1 -1
  106. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BxMB0QqW.js → railroadDiagram-AXF67PYL-xDWktb2i.js} +1 -1
  107. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-FjumoKn4.js → requirementDiagram-LRYGKXZP-BLNMKqeS.js} +1 -1
  108. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-CkhKc_E3.js → sankeyDiagram-W5VNT64P-n0adalxH.js} +1 -1
  109. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DoBWqTTH.js → sequenceDiagram-SI44F4Z6-BbnS-2DL.js} +1 -1
  110. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-CQeh_YeE.js → sizeCapture-X5ZJPWSS-B4uPpCLu.js} +1 -1
  111. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-LuHmFW-I.js → stateDiagram-OKZ733FA--MMbB0fX.js} +1 -1
  112. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BqOigMQ4.js +1 -0
  113. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-CHwbDctY.js → swimlanes-SLNWSIFB-BXUgp2QY.js} +2 -2
  114. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Caxz9UA2.js +8 -0
  115. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CoMngk0X.js → timeline-definition-Z64GVDOM-CsaMxki7.js} +1 -1
  116. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-CURYbtbO.js → vennDiagram-T6HMQDX7-C2IuG9iH.js} +1 -1
  117. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-BdqSECjU.js → wardleyDiagram-T6FBY63Y-CmEWOXCW.js} +1 -1
  118. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-DbMUYuAg.js → xychartDiagram-ELKLHX3M-D_9LE0MG.js} +1 -1
  119. package/dist/worker/console/static/index.html +2 -2
  120. package/dist/worker/console/static-src/app/useOperatorActions.js +1 -1
  121. package/dist/worker/console/static-src/app/useRecoveryActions.js +2 -2
  122. package/dist/worker/console/static-src/app/useRecoveryConsole.js +38 -2
  123. package/dist/worker/console/static-src/app/useRunProgress.js +46 -0
  124. package/dist/worker/console/static-src/operator-chat/cards/failure-category-advice.js +25 -0
  125. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +74 -26
  126. package/dist/worker/console/static-src/operator-chat/input-history.js +76 -0
  127. package/dist/worker/console/static-src/operator-chat/mutation-gate.js +1 -1
  128. package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +328 -0
  129. package/dist/worker/console/static-src/operator-chat/resource-diagnostics.js +204 -0
  130. package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +8 -1
  131. package/dist/worker/console/static-src/operator-chat/turn-submission.js +54 -0
  132. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +3 -0
  133. package/dist/worker/console/static-src/operator-chat/useChatStream.js +237 -11
  134. package/dist/worker/console/static-src/operator-chat/useChatThread.js +50 -22
  135. package/dist/worker/console/static-src/operator-chat/useComposer.js +32 -0
  136. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +26 -1
  137. package/dist/worker/console/static-src/pages/tasks/run-id-resolution.js +79 -0
  138. package/dist/worker/console/static-src/pages/tasks/run-ownership-verify.js +23 -0
  139. package/dist/worker/console/static-src/pages/tasks/run-panel-progress.js +110 -0
  140. package/dist/worker/loop-agent/loop-agent-client.js +7 -3
  141. package/dist/worker/materialize/harness-task-lineage.js +5 -2
  142. package/dist/worker/observability/read-model.js +22 -0
  143. package/dist/worker/observe/node-input.js +74 -3
  144. package/dist/worker/observe/node-process.js +377 -0
  145. package/dist/worker/observe/routes.js +36 -3
  146. package/dist/worker/observe/shell-handler-keys.js +34 -0
  147. package/dist/worker/observe/static/api.js +66 -0
  148. package/dist/worker/observe/static/constants.js +8 -1
  149. package/dist/worker/observe/static/dag-history-labels.js +4 -0
  150. package/dist/worker/observe/static/format.js +23 -0
  151. package/dist/worker/observe/static/markdown-render.js +20 -1
  152. package/dist/worker/observe/static/operator-chrome.js +4 -0
  153. package/dist/worker/observe/static/state.js +13 -2
  154. package/dist/worker/observe/static/styles.css +73 -0
  155. package/dist/worker/observe/static/views/dag-inspector.js +612 -8
  156. package/dist/worker/observe/static/views/dag.js +8 -1
  157. package/dist/worker/observe/static/views/failures.js +5 -2
  158. package/dist/worker/observe/static/views/session-timeline.js +78 -9
  159. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +827 -53
  160. package/dist/workflows/dag/backend-test-case-manifest.js +4 -0
  161. package/dist/workflows/dag/backend-test-execution-contract.js +18 -9
  162. package/dist/workflows/dag/backend-test-gap-fill.js +205 -0
  163. package/dist/workflows/dag/backend-test-intake-context.js +32 -0
  164. package/dist/workflows/dag/backend-test-layout.js +133 -0
  165. package/dist/workflows/dag/backend-test-markdown-workflow.js +167 -32
  166. package/dist/workflows/dag/backend-test-module-stem.js +5 -0
  167. package/dist/workflows/dag/backend-test-pytest-collection.js +368 -33
  168. package/dist/workflows/dag/backend-test-result-contract.js +1 -1
  169. package/dist/workflows/dag/backend-test-scenario-param.js +884 -140
  170. package/dist/workflows/dag/backend-test-scenario-partitions.js +321 -0
  171. package/dist/workflows/dag/backend-test-writer-completeness.js +100 -56
  172. package/dist/workflows/dag/contract-output-registry.js +15 -0
  173. package/dist/workflows/dag/contract-validator-registrations.js +3 -1
  174. package/dist/workflows/dag/convergence/controller.js +141 -0
  175. package/dist/workflows/dag/dynamic-runtime/loop-until.js +1 -1
  176. package/dist/workflows/dag/dynamic-runtime/map.js +25 -9
  177. package/dist/workflows/dag/dynamic-runtime/shared.js +1 -1
  178. package/dist/workflows/dag/failure-category.js +7 -116
  179. package/dist/workflows/dag/frontend-implementation-contract.js +377 -4
  180. package/dist/workflows/dag/frontend-plan-render.js +1 -1
  181. package/dist/workflows/dag/frontend-prewrite-gate.js +96 -84
  182. package/dist/workflows/dag/frontend-repair.js +14 -0
  183. package/dist/workflows/dag/frontend-risk.js +15 -2
  184. package/dist/workflows/dag/frontend-test-case-checklist.js +30 -4
  185. package/dist/workflows/dag/frontend-test-case-manifest.js +11 -4
  186. package/dist/workflows/dag/frontend-test-case-quality.js +11 -16
  187. package/dist/workflows/dag/frontend-test-environment-probe.js +230 -0
  188. package/dist/workflows/dag/frontend-test-html-report.js +3 -1
  189. package/dist/workflows/dag/frontend-test-l5-report.js +3 -1
  190. package/dist/workflows/dag/frontend-test-layout.js +159 -0
  191. package/dist/workflows/dag/frontend-test-markdown.js +61 -0
  192. package/dist/workflows/dag/frontend-test-result-contract.js +188 -42
  193. package/dist/workflows/dag/frontend-test-standard-scenarios.js +70 -0
  194. package/dist/workflows/dag/frontend-verification-trace.js +29 -4
  195. package/dist/workflows/dag/init-hybrid.js +894 -205
  196. package/dist/workflows/dag/node-execution.js +183 -15
  197. package/dist/workflows/dag/prompt.js +60 -1
  198. package/dist/workflows/dag/rerun-feedback.js +212 -0
  199. package/dist/workflows/dag/rerun-plan.js +224 -27
  200. package/dist/workflows/dag/rerun-run.js +61 -6
  201. package/dist/workflows/dag/rerun-task.js +61 -2
  202. package/dist/workflows/dag/retry-policy.js +109 -0
  203. package/dist/workflows/dag/structured-output-repair.js +712 -0
  204. package/dist/workflows/dag/types.js +143 -1
  205. package/dist/workflows/dag/validate.js +64 -20
  206. package/docs/init-surface.manifest.json +5 -0
  207. package/docs/operations/README.md +1 -1
  208. package/docs/operations/local-development-environment.md +1 -5
  209. package/docs/skills/vetted-skill-registry.md +14 -0
  210. package/docs/templates/README.md +1 -1
  211. package/docs/templates/agent-dag.schema.json +44 -0
  212. package/docs/templates/backend-test-dag.json +60 -35
  213. package/docs/templates/frontend-implementation-contract.schema.json +1 -1
  214. package/docs/templates/frontend-test-case-checklist.md +2 -2
  215. package/docs/templates/frontend-test-dag.json +8 -10
  216. package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +1 -1
  217. package/docs/templates/init-managed-agents.md +13 -3
  218. package/harness.json +6 -4
  219. package/package.json +12 -4
  220. package/scripts/next-info.mjs +356 -0
  221. package/scripts/next-publish-gate.mjs +238 -0
  222. package/scripts/release-source-binding.mjs +251 -0
  223. package/skills/analyze-product-requirements/SKILL.md +8 -5
  224. package/skills/analyze-product-requirements/references/acceptance-criteria.md +1 -1
  225. package/skills/analyze-product-requirements/references/clarification-and-knowledge.md +10 -6
  226. package/skills/analyze-product-requirements/references/example.md +50 -0
  227. package/skills/analyze-product-requirements/references/forward-test-cases.md +11 -7
  228. package/skills/analyze-product-requirements/references/kb-integration.md +5 -5
  229. package/skills/analyze-product-requirements/references/product-analysis-schema.md +8 -4
  230. package/skills/analyze-product-requirements/references/product-requirement-schema.md +2 -2
  231. package/skills/analyze-product-requirements/references/requirement-clarification-schema.md +1 -1
  232. package/skills/analyze-product-requirements/scripts/test-validators.mjs +11 -1
  233. package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +20 -1
  234. package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +9 -0
  235. package/skills/codebase-scout/SKILL.md +1 -1
  236. package/skills/fe-test-ui-scout/SKILL.md +65 -0
  237. package/skills/fe-test-ui-scout/references/ledger-schema.md +62 -0
  238. package/skills/fe-test-ui-scout/references/recon-protocol.md +54 -0
  239. package/skills/frontend-implementation/references/node-contracts.md +2 -2
  240. package/skills/improve-codebase-architecture/SKILL.md +81 -0
  241. package/skills/improve-codebase-architecture/deepening.md +37 -0
  242. package/skills/improve-codebase-architecture/html-report.md +123 -0
  243. package/skills/improve-codebase-architecture/interface-design.md +44 -0
  244. package/skills/improve-codebase-architecture/language.md +53 -0
  245. package/skills/loop-agent/references/command-reference.md +11 -4
  246. package/skills/playwright-cli/SKILL.md +23 -1
  247. package/skills/playwright-cli-case-generator/SKILL.md +29 -8
  248. package/dist/worker/console/static/assets/channel-BBOTJHvD.js +0 -1
  249. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Cm8Yf8tD.js +0 -1
  250. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Cm8Yf8tD.js +0 -1
  251. package/dist/worker/console/static/assets/index-B-qWGThd.js +0 -325
  252. package/dist/worker/console/static/assets/index-C-xRgY_0.css +0 -1
  253. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DZihfdKc.js +0 -1
  254. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CKnXCjHX.js +0 -8
@@ -10,7 +10,7 @@ import { parseAssistantContent } from "../../chat/assistant-content.js";
10
10
  * original request would have. `assistantId` is the in-flight assistant
11
11
  * message id to patch for streaming text; reconnect creates one if none. */
12
12
  export function createChatSseEventApplier(deps) {
13
- const { refs, onReconcile, setError, setInterview, setHumanGateCards, setArtifactCards, setStreaming, setStreamingAssistantId, setUnread, setOperationCards, setUsage, setMessages, setToolCalls, } = deps;
13
+ const { refs, onReconcile, setError, setInterview, setHumanGateCards, setArtifactCards, setStreaming, setTurnPhase = () => undefined, setDeferredTurn = () => undefined, setStreamingAssistantId, setUnread, setOperationCards, setUsage, setMessages, setToolCalls, } = deps;
14
14
  return (eventName, data, assistantId, options) => {
15
15
  if (options?.generation !== undefined &&
16
16
  refs.sessionGenerationRef.current !== options.generation)
@@ -49,6 +49,41 @@ export function createChatSseEventApplier(deps) {
49
49
  const eventAt = typeof data.at === "string" && Number.isFinite(Date.parse(data.at))
50
50
  ? Date.parse(data.at)
51
51
  : Date.now();
52
+ if (eventName === "queue_update") {
53
+ const revision = typeof payload.revision === "number" ? payload.revision : -1;
54
+ setDeferredTurn((current) => {
55
+ if (current && revision < current.revision)
56
+ return current;
57
+ return payload.deferredTurn && typeof payload.deferredTurn === "object"
58
+ ? payload.deferredTurn
59
+ : null;
60
+ });
61
+ return;
62
+ }
63
+ if (eventName === "user_message") {
64
+ const message = payload.message && typeof payload.message === "object"
65
+ ? payload.message
66
+ : null;
67
+ if (message &&
68
+ typeof message.id === "string" &&
69
+ typeof message.text === "string") {
70
+ const createdAt = typeof message.at === "string" && Number.isFinite(Date.parse(message.at))
71
+ ? Date.parse(message.at)
72
+ : Date.now();
73
+ setMessages((messages) => messages.some((item) => item.id === message.id)
74
+ ? messages
75
+ : [
76
+ ...messages,
77
+ {
78
+ id: message.id,
79
+ role: "user",
80
+ text: message.text,
81
+ createdAt,
82
+ },
83
+ ]);
84
+ }
85
+ return;
86
+ }
52
87
  if (turnId &&
53
88
  (eventName === "agent_start" || eventName === "message_update") &&
54
89
  refs.settledTurnIdsRef.current.has(turnId)) {
@@ -153,6 +188,7 @@ export function createChatSseEventApplier(deps) {
153
188
  }
154
189
  refs.streamingRef.current = true;
155
190
  setStreaming(true);
191
+ setTurnPhase("running");
156
192
  setStreamingAssistantId(resolvedAssistantId);
157
193
  return;
158
194
  }
@@ -168,6 +204,7 @@ export function createChatSseEventApplier(deps) {
168
204
  refs.activeTurnIdRef.current = null;
169
205
  refs.streamingRef.current = false;
170
206
  setStreaming(false);
207
+ setTurnPhase("idle");
171
208
  setStreamingAssistantId(null);
172
209
  if (eventName === "agent_settled" &&
173
210
  !replay &&
@@ -175,19 +212,30 @@ export function createChatSseEventApplier(deps) {
175
212
  document.hidden) {
176
213
  setUnread((current) => current + 1);
177
214
  }
178
- setMessages((m) => m.filter((msg) => !(msg.id === resolvedAssistantId &&
179
- msg.role === "assistant" &&
180
- !msg.text.trim())));
181
- // Close out tool calls that never got a tool_result (abort/fail):
182
- // without this the process rail keeps showing "运行中" forever even
183
- // though the turn is over. Match by turnId when the frame carries one
184
- // (covers tools attributed to remapped/stale assistant ids); fall back
185
- // to the resolved assistant id otherwise.
186
- setToolCalls((prev) => prev.map((t) => t.status === "running" &&
187
- ((turnId !== undefined && t.turnId === turnId) ||
188
- t.assistantMessageId === resolvedAssistantId)
189
- ? { ...t, status: "error", finishedAt: Date.now() }
190
- : t));
215
+ // Close dangling running tools first so the empty-bubble decision can
216
+ // see whether this assistant still owns any cards.
217
+ let assistantOwnsTools = false;
218
+ setToolCalls((prev) => {
219
+ assistantOwnsTools = prev.some((t) => t.assistantMessageId === resolvedAssistantId);
220
+ return prev.map((t) => t.status === "running" &&
221
+ ((turnId !== undefined && t.turnId === turnId) ||
222
+ t.assistantMessageId === resolvedAssistantId)
223
+ ? { ...t, status: "error", finishedAt: Date.now() }
224
+ : t);
225
+ });
226
+ // Drop only a true placeholder. An empty bubble that still owns tools
227
+ // (or has content blocks) is the grouping anchor; deleting it orphans
228
+ // the previous turn's bash cards onto the next prompt.
229
+ setMessages((m) => m.filter((msg) => {
230
+ if (msg.id !== resolvedAssistantId || msg.role !== "assistant") {
231
+ return true;
232
+ }
233
+ if (msg.text.trim())
234
+ return true;
235
+ if (msg.content && msg.content.length > 0)
236
+ return true;
237
+ return assistantOwnsTools;
238
+ }));
191
239
  return;
192
240
  }
193
241
  if (eventName === "operation-ref" || eventName === "operation-status") {
@@ -227,6 +275,15 @@ export function createChatSseEventApplier(deps) {
227
275
  const cacheWrite = sample.cacheWriteTokens ?? 0;
228
276
  const total = sample.totalTokens ?? input + output + cacheRead + cacheWrite;
229
277
  const cu = sample.contextUsage;
278
+ const nextContext = cu && typeof cu === "object"
279
+ ? {
280
+ tokens: typeof cu.tokens === "number" ? cu.tokens : null,
281
+ contextWindow: typeof cu.contextWindow === "number"
282
+ ? cu.contextWindow
283
+ : 0,
284
+ percent: typeof cu.percent === "number" ? cu.percent : null,
285
+ }
286
+ : current.contextUsage;
230
287
  return {
231
288
  inputTokens: current.inputTokens + input,
232
289
  outputTokens: current.outputTokens + output,
@@ -236,18 +293,9 @@ export function createChatSseEventApplier(deps) {
236
293
  cost: sample.cost === undefined
237
294
  ? current.cost
238
295
  : (current.cost ?? 0) + sample.cost,
239
- // Context usage is a live snapshot, not an accumulator: replace it.
240
- ...(cu && typeof cu === "object"
241
- ? {
242
- contextUsage: {
243
- tokens: typeof cu.tokens === "number" ? cu.tokens : null,
244
- contextWindow: typeof cu.contextWindow === "number"
245
- ? cu.contextWindow
246
- : 0,
247
- percent: typeof cu.percent === "number" ? cu.percent : null,
248
- },
249
- }
250
- : {}),
296
+ // Context fill is independent of per-message token deltas. Keep the
297
+ // last known snapshot unless this event carries a fresh one.
298
+ ...(nextContext ? { contextUsage: nextContext } : {}),
251
299
  };
252
300
  });
253
301
  return;
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Pure composer input-history state machine for Operator Chat.
3
+ *
4
+ * Extracted so ArrowUp/ArrowUp recall, unsent-draft retention and history
5
+ * recording can be unit-tested without mounting the React composer.
6
+ */
7
+ export const INPUT_HISTORY_LIMIT = 100;
8
+ export function createInputHistoryState() {
9
+ return { entries: [], index: -1, draft: "" };
10
+ }
11
+ /** Record one sent text and reset browsing back to the present draft. */
12
+ export function recordInputHistoryEntry(state, text) {
13
+ if (!text.trim())
14
+ return state;
15
+ if (state.entries[state.entries.length - 1] === text) {
16
+ return state.index === -1 ? state : { ...state, index: -1, draft: "" };
17
+ }
18
+ const entries = [...state.entries, text].slice(-INPUT_HISTORY_LIMIT);
19
+ return { entries, index: -1, draft: "" };
20
+ }
21
+ /** Leave browse mode (manual typing edits the recalled text in place). */
22
+ export function resetInputHistoryBrowsing(state) {
23
+ return state.index === -1 ? state : { ...state, index: -1 };
24
+ }
25
+ /**
26
+ * Recall an earlier input with the arrow keys (terminal-style).
27
+ *
28
+ * Up only engages while the caret sits on the first line and Down while it
29
+ * sits on the last line, so plain caret movement inside multi-line drafts is
30
+ * never swallowed. The unsent draft is stashed when browsing leaves the
31
+ * present position and restored when Down walks past the newest entry.
32
+ */
33
+ export function navigateInputHistory(state, direction, context) {
34
+ const { text, caretStart, caretEnd } = context;
35
+ if (state.entries.length === 0)
36
+ return { handled: false };
37
+ if (direction === "up") {
38
+ if (text.slice(0, caretStart).includes("\n"))
39
+ return { handled: false };
40
+ if (state.index === -1) {
41
+ const index = state.entries.length - 1;
42
+ return {
43
+ handled: true,
44
+ state: { ...state, index, draft: text },
45
+ text: state.entries[index],
46
+ };
47
+ }
48
+ if (state.index === 0) {
49
+ // Already at the oldest entry: stay there (terminal behaviour).
50
+ return { handled: true, state, text: state.entries[0] };
51
+ }
52
+ const index = state.index - 1;
53
+ return {
54
+ handled: true,
55
+ state: { ...state, index },
56
+ text: state.entries[index],
57
+ };
58
+ }
59
+ if (state.index === -1)
60
+ return { handled: false };
61
+ if (text.slice(caretEnd).includes("\n"))
62
+ return { handled: false };
63
+ if (state.index >= state.entries.length - 1) {
64
+ return {
65
+ handled: true,
66
+ state: { ...state, index: -1 },
67
+ text: state.draft,
68
+ };
69
+ }
70
+ const index = state.index + 1;
71
+ return {
72
+ handled: true,
73
+ state: { ...state, index },
74
+ text: state.entries[index],
75
+ };
76
+ }
@@ -30,7 +30,7 @@ export function isStaleTokenFailure(code) {
30
30
  /** Chinese, actionable copy for mutation-gate rejections. */
31
31
  export function describeMutationGateFailure(code) {
32
32
  if (isStaleTokenFailure(code)) {
33
- return "页面已过期(控制台已重启),请刷新页面(F5)后重试。";
33
+ return "页面凭证已失效(控制台可能已重启),请刷新页面(F5)后重试。";
34
34
  }
35
35
  if (code === "host-not-loopback") {
36
36
  return "请求主机校验失败:非回环绑定时要求 Host 头,请刷新页面或检查访问地址。";
@@ -0,0 +1,328 @@
1
+ /** Shown near the preview header when window.open is blocked or Blob/URL creation fails. */
2
+ export const PREVIEW_OPEN_POPUP_BLOCKED = "无法在浏览器中打开:弹窗被拦截";
3
+ export const PREVIEW_TRUNCATION_NOTICE = "内容已截断,以下为已加载片段。";
4
+ /** Window features that force a real OS window instead of a browser tab.
5
+ * Do not include `noopener` here: browsers then return null even on success. */
6
+ export function previewOpenWindowFeatures(input) {
7
+ const availWidth = input?.availWidth ?? 1440;
8
+ const availHeight = input?.availHeight ?? 900;
9
+ const width = Math.min(1280, Math.max(880, Math.floor(availWidth * 0.82)));
10
+ const height = Math.min(960, Math.max(640, Math.floor(availHeight * 0.88)));
11
+ return [
12
+ "popup=yes",
13
+ `width=${width}`,
14
+ `height=${height}`,
15
+ "left=72",
16
+ "top=36",
17
+ "menubar=yes",
18
+ "toolbar=yes",
19
+ "location=yes",
20
+ "status=yes",
21
+ "resizable=yes",
22
+ "scrollbars=yes",
23
+ ].join(",");
24
+ }
25
+ export function escapeHtml(text) {
26
+ return text
27
+ .replaceAll("&", "&amp;")
28
+ .replaceAll("<", "&lt;")
29
+ .replaceAll(">", "&gt;")
30
+ .replaceAll('"', "&quot;")
31
+ .replaceAll("'", "&#39;");
32
+ }
33
+ function normalizedLanguage(language) {
34
+ return language.trim().toLowerCase();
35
+ }
36
+ export function isHtmlPreviewLanguage(language) {
37
+ const lang = normalizedLanguage(language);
38
+ return lang === "html" || lang === "htm";
39
+ }
40
+ export function isMarkdownPreviewLanguage(language) {
41
+ const lang = normalizedLanguage(language);
42
+ return lang === "md" || lang === "markdown";
43
+ }
44
+ function formatTextBody(preview) {
45
+ if (normalizedLanguage(preview.language) !== "json")
46
+ return preview.content;
47
+ try {
48
+ return JSON.stringify(JSON.parse(preview.content), null, 2);
49
+ }
50
+ catch {
51
+ return preview.content;
52
+ }
53
+ }
54
+ function isSafeHttpHref(href) {
55
+ return /^https?:\/\//i.test(href.trim());
56
+ }
57
+ function renderMarkdownInline(text) {
58
+ const withCode = [];
59
+ const coded = text.replace(/`([^`]+)`/g, (_all, code) => {
60
+ withCode.push(`<code>${code}</code>`);
61
+ return `\u0000${withCode.length - 1}\u0000`;
62
+ });
63
+ const linked = coded.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (_all, label, href) => {
64
+ if (!isSafeHttpHref(href))
65
+ return label;
66
+ return `<a href="${href}" rel="noopener noreferrer" target="_blank">${label}</a>`;
67
+ });
68
+ const emphasized = linked
69
+ .replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
70
+ .replace(/__([^_]+)__/g, "<strong>$1</strong>")
71
+ .replace(/\*([^*]+)\*/g, "<em>$1</em>");
72
+ return emphasized.replace(/\u0000(\d+)\u0000/g, (_all, idx) => {
73
+ return withCode[Number(idx)] ?? "";
74
+ });
75
+ }
76
+ function flushParagraph(lines) {
77
+ if (!lines.length)
78
+ return "";
79
+ return `<p>${renderMarkdownInline(lines.join(" "))}</p>`;
80
+ }
81
+ function renderMarkdownBlocks(escaped) {
82
+ const lines = escaped.replaceAll("\r\n", "\n").split("\n");
83
+ const out = [];
84
+ let paragraph = [];
85
+ let listKind = null;
86
+ const closeList = () => {
87
+ if (listKind) {
88
+ out.push(listKind === "ul" ? "</ul>" : "</ol>");
89
+ listKind = null;
90
+ }
91
+ };
92
+ const closeParagraph = () => {
93
+ if (paragraph.length) {
94
+ out.push(flushParagraph(paragraph));
95
+ paragraph = [];
96
+ }
97
+ };
98
+ for (const line of lines) {
99
+ const heading = /^(#{1,6})\s+(.+)$/.exec(line);
100
+ if (heading) {
101
+ closeParagraph();
102
+ closeList();
103
+ const level = heading[1].length;
104
+ out.push(`<h${level}>${renderMarkdownInline(heading[2])}</h${level}>`);
105
+ continue;
106
+ }
107
+ if (/^---+$/.test(line.trim()) || /^\*\*\*+$/.test(line.trim())) {
108
+ closeParagraph();
109
+ closeList();
110
+ out.push("<hr>");
111
+ continue;
112
+ }
113
+ const quote = /^&gt;\s?(.*)$/.exec(line);
114
+ if (quote) {
115
+ closeParagraph();
116
+ closeList();
117
+ out.push(`<blockquote><p>${renderMarkdownInline(quote[1] ?? "")}</p></blockquote>`);
118
+ continue;
119
+ }
120
+ const ul = /^[-*]\s+(.+)$/.exec(line);
121
+ if (ul) {
122
+ closeParagraph();
123
+ if (listKind !== "ul") {
124
+ closeList();
125
+ out.push("<ul>");
126
+ listKind = "ul";
127
+ }
128
+ out.push(`<li>${renderMarkdownInline(ul[1])}</li>`);
129
+ continue;
130
+ }
131
+ const ol = /^\d+\.\s+(.+)$/.exec(line);
132
+ if (ol) {
133
+ closeParagraph();
134
+ if (listKind !== "ol") {
135
+ closeList();
136
+ out.push("<ol>");
137
+ listKind = "ol";
138
+ }
139
+ out.push(`<li>${renderMarkdownInline(ol[1])}</li>`);
140
+ continue;
141
+ }
142
+ if (line.trim() === "") {
143
+ closeParagraph();
144
+ closeList();
145
+ continue;
146
+ }
147
+ closeList();
148
+ paragraph.push(line);
149
+ }
150
+ closeParagraph();
151
+ closeList();
152
+ return out.join("\n");
153
+ }
154
+ /** Conservative Markdown → HTML for the detached reading tab. Input is escaped first. */
155
+ export function renderMarkdownLite(source) {
156
+ const escaped = escapeHtml(source);
157
+ const chunks = [];
158
+ const fence = /```([^\n`]*)\n([\s\S]*?)```/g;
159
+ let last = 0;
160
+ let match;
161
+ while ((match = fence.exec(escaped))) {
162
+ if (match.index > last) {
163
+ chunks.push(renderMarkdownBlocks(escaped.slice(last, match.index)));
164
+ }
165
+ chunks.push(`<pre class="md-code"><code>${match[2] ?? ""}</code></pre>`);
166
+ last = match.index + match[0].length;
167
+ }
168
+ if (last < escaped.length) {
169
+ chunks.push(renderMarkdownBlocks(escaped.slice(last)));
170
+ }
171
+ return chunks.filter(Boolean).join("\n");
172
+ }
173
+ function readingPageCss() {
174
+ return `
175
+ :root { color-scheme: light dark; }
176
+ html, body { margin: 0; min-height: 100%; background: #f7f5f0; color: #1f1b16; }
177
+ body { font: 16px/1.65 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; }
178
+ .bar { position: sticky; top: 0; z-index: 1; display: flex; gap: 12px; align-items: center; padding: 10px 16px 10px 20px; border-bottom: 1px solid #e4ddd2; background: #fffdf8; }
179
+ .bar .meta { min-width: 0; flex: 1; display: flex; flex-direction: column; gap: 2px; }
180
+ .bar strong { font-size: 13px; font-weight: 650; word-break: break-all; }
181
+ .bar .kind { color: #6b6258; font-size: 12px; }
182
+ .modes { display: inline-flex; gap: 2px; padding: 2px; border: 1px solid #e4ddd2; border-radius: 8px; background: #f3eee6; }
183
+ .modes button { appearance: none; border: 0; background: transparent; color: #6b6258; font: 12px/1.2 inherit; padding: 5px 10px; border-radius: 6px; cursor: pointer; }
184
+ .modes button:hover { color: #1f1b16; }
185
+ .modes button.is-active { background: #fff; color: #1f1b16; font-weight: 600; box-shadow: 0 1px 2px rgb(0 0 0 / 8%); }
186
+ .notice { margin: 0; padding: 8px 20px; background: #fff6d8; color: #5c4b00; font-size: 13px; }
187
+ .pane { display: none; }
188
+ .pane.is-active { display: block; }
189
+ .doc { max-width: 52rem; margin: 0 auto; padding: 32px 24px 96px; }
190
+ .doc.md h1, .doc.md h2, .doc.md h3, .doc.md h4 { line-height: 1.25; margin: 1.4em 0 0.5em; }
191
+ .doc.md h1 { font-size: 2rem; }
192
+ .doc.md h2 { font-size: 1.45rem; }
193
+ .doc.md p { margin: 0.8em 0; }
194
+ .doc.md ul, .doc.md ol { margin: 0.6em 0; padding-left: 1.4em; }
195
+ .doc.md blockquote { margin: 0.8em 0; padding: 0.2em 0.9em; border-left: 3px solid #d9cfc0; color: #4a433c; }
196
+ .doc.md hr { border: 0; border-top: 1px solid #e4ddd2; margin: 1.6em 0; }
197
+ .doc.md a { color: #0b57d0; }
198
+ .doc.md code, .md-code, .source { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 0.92em; }
199
+ .doc.md :not(pre) > code { padding: 0.1em 0.35em; border-radius: 4px; background: #efe8dc; }
200
+ .md-code, .source { margin: 1em 0; padding: 14px 16px; overflow: auto; white-space: pre-wrap; background: #fff; border: 1px solid #e4ddd2; border-radius: 8px; }
201
+ .frame { margin: 0; padding: 0; }
202
+ iframe.page { display: block; width: 100%; height: calc(100vh - 53px); border: 0; background: #fff; }
203
+ `.trim();
204
+ }
205
+ function formattedSource(preview) {
206
+ return escapeHtml(formatTextBody(preview));
207
+ }
208
+ function previewPane(preview) {
209
+ if (isHtmlPreviewLanguage(preview.language)) {
210
+ return `<div class="frame"><iframe class="page" sandbox="" srcdoc="${escapeHtml(preview.content)}"></iframe></div>`;
211
+ }
212
+ if (isMarkdownPreviewLanguage(preview.language)) {
213
+ return `<article class="doc md">${renderMarkdownLite(preview.content)}</article>`;
214
+ }
215
+ return `<article class="doc"><pre class="source">${formattedSource(preview)}</pre></article>`;
216
+ }
217
+ function sourcePane(preview) {
218
+ return `<article class="doc"><pre class="source">${formattedSource(preview)}</pre></article>`;
219
+ }
220
+ function readingPageScript() {
221
+ return `<script>
222
+ (function () {
223
+ var previewBtn = document.getElementById("view-preview");
224
+ var sourceBtn = document.getElementById("view-source");
225
+ var previewPane = document.getElementById("pane-preview");
226
+ var sourcePane = document.getElementById("pane-source");
227
+ if (!previewBtn || !sourceBtn || !previewPane || !sourcePane) return;
228
+ function show(mode) {
229
+ var source = mode === "source";
230
+ previewBtn.classList.toggle("is-active", !source);
231
+ sourceBtn.classList.toggle("is-active", source);
232
+ previewBtn.setAttribute("aria-pressed", String(!source));
233
+ sourceBtn.setAttribute("aria-pressed", String(source));
234
+ previewPane.classList.toggle("is-active", !source);
235
+ sourcePane.classList.toggle("is-active", source);
236
+ }
237
+ previewBtn.addEventListener("click", function () { show("preview"); });
238
+ sourceBtn.addEventListener("click", function () { show("source"); });
239
+ })();
240
+ </script>`;
241
+ }
242
+ /** Build the Blob document HTML from the already-loaded preview snapshot. */
243
+ export function buildPreviewBrowserDocument(preview) {
244
+ const title = escapeHtml(preview.path);
245
+ const notice = preview.truncated
246
+ ? `<p class="notice">${escapeHtml(PREVIEW_TRUNCATION_NOTICE)}</p>`
247
+ : "";
248
+ const lang = normalizedLanguage(preview.language);
249
+ return `<!doctype html>
250
+ <html lang="zh-CN">
251
+ <head>
252
+ <meta charset="utf-8">
253
+ <meta name="viewport" content="width=device-width, initial-scale=1">
254
+ <title>${title}</title>
255
+ <style>
256
+ ${readingPageCss()}
257
+ </style>
258
+ </head>
259
+ <body>
260
+ <header class="bar">
261
+ <div class="meta">
262
+ <strong>${title}</strong>
263
+ <span class="kind">${escapeHtml(lang || "text")}${preview.truncated ? " · 已截断" : ""}</span>
264
+ </div>
265
+ <div class="modes" role="group" aria-label="查看模式">
266
+ <button type="button" id="view-preview" class="is-active" aria-pressed="true">预览</button>
267
+ <button type="button" id="view-source" aria-pressed="false">查看原文</button>
268
+ </div>
269
+ </header>
270
+ ${notice}
271
+ <div id="pane-preview" class="pane is-active">${previewPane(preview)}</div>
272
+ <div id="pane-source" class="pane">${sourcePane(preview)}</div>
273
+ ${readingPageScript()}
274
+ </body>
275
+ </html>`;
276
+ }
277
+ function screenAvail() {
278
+ const screen = globalThis.screen;
279
+ return {
280
+ availWidth: screen?.availWidth,
281
+ availHeight: screen?.availHeight,
282
+ };
283
+ }
284
+ function defaultHost() {
285
+ const g = globalThis;
286
+ if (typeof g.open !== "function") {
287
+ throw new Error("window.open is unavailable");
288
+ }
289
+ return {
290
+ open: g.open.bind(g),
291
+ createObjectURL: g.URL.createObjectURL.bind(g.URL),
292
+ revokeObjectURL: g.URL.revokeObjectURL.bind(g.URL),
293
+ };
294
+ }
295
+ /**
296
+ * Open the current preview in a new OS window from already-loaded
297
+ * path/content/language/truncated. Does not fetch the repo file API again.
298
+ *
299
+ * Width/height window features ask the browser for a real window, not a tab.
300
+ * Do not pass `noopener` as a feature: browsers then return null even on
301
+ * success. Detach via `opener = null` after a real handle is obtained. Keep
302
+ * the Blob URL alive — revoking immediately blanks the page.
303
+ */
304
+ export function openPreviewInBrowser(preview, host = defaultHost(), features = previewOpenWindowFeatures(screenAvail())) {
305
+ let objectUrl;
306
+ try {
307
+ const documentHtml = buildPreviewBrowserDocument(preview);
308
+ const blob = new Blob([documentHtml], { type: "text/html;charset=utf-8" });
309
+ objectUrl = host.createObjectURL(blob);
310
+ const opened = host.open(objectUrl, "_blank", features);
311
+ if (!opened) {
312
+ host.revokeObjectURL(objectUrl);
313
+ return { ok: false, error: PREVIEW_OPEN_POPUP_BLOCKED };
314
+ }
315
+ try {
316
+ opened.opener = null;
317
+ }
318
+ catch {
319
+ // Some hosts expose a frozen handle; the window is still open.
320
+ }
321
+ return { ok: true };
322
+ }
323
+ catch {
324
+ if (objectUrl)
325
+ host.revokeObjectURL(objectUrl);
326
+ return { ok: false, error: PREVIEW_OPEN_POPUP_BLOCKED };
327
+ }
328
+ }