mixdog 0.9.151 → 0.9.153

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 (307) hide show
  1. package/LICENSES/browser-import-NOTICE.txt +11 -0
  2. package/NOTICE.md +11 -0
  3. package/README.md +108 -68
  4. package/package.json +17 -4
  5. package/scripts/computer-task-cost-report.mjs +144 -0
  6. package/scripts/run-office-live-tests.mjs +32 -0
  7. package/scripts/tool-stress.mjs +0 -2
  8. package/src/agents/front-worker/AGENT.md +13 -0
  9. package/src/agents/front-worker/agent.json +6 -0
  10. package/src/agents/heavy-worker/AGENT.md +10 -11
  11. package/src/agents/heavy-worker/agent.json +1 -1
  12. package/src/agents/maintainer/AGENT.md +0 -2
  13. package/src/agents/reviewer/AGENT.md +9 -13
  14. package/src/agents/reviewer/agent.json +1 -1
  15. package/src/agents/security/AGENT.md +14 -0
  16. package/src/agents/security/agent.json +6 -0
  17. package/src/agents/worker/AGENT.md +8 -8
  18. package/src/agents/worker/agent.json +1 -1
  19. package/src/agents/writer/AGENT.md +14 -0
  20. package/src/agents/writer/agent.json +6 -0
  21. package/src/defaults/skills/setup/SKILL.md +18 -27
  22. package/src/headless-exec.mjs +3 -10
  23. package/src/headless-exec.test.mjs +2 -7
  24. package/src/rules/agent/00-common.md +4 -7
  25. package/src/rules/lead/01-general.md +0 -3
  26. package/src/rules/shared/10-tool-workflow.md +10 -2
  27. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +183 -26
  28. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.test.mjs +378 -0
  29. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +4 -0
  30. package/src/runtime/agent/orchestrator/cache-break-trace.mjs +109 -0
  31. package/src/runtime/agent/orchestrator/cache-break-trace.test.mjs +115 -0
  32. package/src/runtime/agent/orchestrator/config.mjs +13 -68
  33. package/src/runtime/agent/orchestrator/context/collect-skills.test.mjs +153 -0
  34. package/src/runtime/agent/orchestrator/context/collect.mjs +49 -45
  35. package/src/runtime/agent/orchestrator/mcp/client.mjs +242 -15
  36. package/src/runtime/agent/orchestrator/mcp/features.test.mjs +66 -0
  37. package/src/runtime/agent/orchestrator/mcp/security.test.mjs +36 -20
  38. package/src/runtime/agent/orchestrator/providers/admission-scheduler-metrics.test.mjs +47 -0
  39. package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +16 -3
  40. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +54 -33
  41. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +1 -0
  42. package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +7 -0
  43. package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +7 -0
  44. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +3 -8
  45. package/src/runtime/agent/orchestrator/providers/gemini.mjs +7 -0
  46. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +19 -3
  47. package/src/runtime/agent/orchestrator/providers/lib/provider-replay.mjs +50 -0
  48. package/src/runtime/agent/orchestrator/providers/lib/sse-framing.mjs +93 -0
  49. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +30 -9
  50. package/src/runtime/agent/orchestrator/providers/media-parity.test.mjs +59 -0
  51. package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +3 -3
  52. package/src/runtime/agent/orchestrator/providers/openai-codex-identity.test.mjs +71 -0
  53. package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +18 -26
  54. package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +8 -0
  55. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +7 -0
  56. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +25 -2
  57. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +35 -4
  58. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +47 -2
  59. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +103 -12
  60. package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +13 -0
  61. package/src/runtime/agent/orchestrator/providers/openai-turn-state.mjs +68 -0
  62. package/src/runtime/agent/orchestrator/providers/openai-ws-delta-parity.test.mjs +122 -6
  63. package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +36 -11
  64. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +144 -93
  65. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +18 -13
  66. package/src/runtime/agent/orchestrator/providers/provider-replay.test.mjs +288 -0
  67. package/src/runtime/agent/orchestrator/providers/sse-framing.test.mjs +719 -0
  68. package/src/runtime/agent/orchestrator/providers/stream-json-pool.mjs +399 -39
  69. package/src/runtime/agent/orchestrator/providers/stream-json-worker.mjs +26 -0
  70. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +86 -7
  71. package/src/runtime/agent/orchestrator/session/agent-loop.test.mjs +222 -0
  72. package/src/runtime/agent/orchestrator/session/browser-snapshot-supersession.mjs +61 -0
  73. package/src/runtime/agent/orchestrator/session/browser-snapshot-supersession.test.mjs +83 -0
  74. package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +13 -0
  75. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +26 -2
  76. package/src/runtime/agent/orchestrator/session/compact-policy.test.mjs +112 -0
  77. package/src/runtime/agent/orchestrator/session/compaction-read-reset.test.mjs +192 -0
  78. package/src/runtime/agent/orchestrator/session/context-usage-restart.test.mjs +72 -0
  79. package/src/runtime/agent/orchestrator/session/context-utils.mjs +100 -15
  80. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +17 -1
  81. package/src/runtime/agent/orchestrator/session/eager-dispatch.test.mjs +74 -0
  82. package/src/runtime/agent/orchestrator/session/image-strip-recovery.mjs +12 -8
  83. package/src/runtime/agent/orchestrator/session/image-strip-recovery.test.mjs +20 -1
  84. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +110 -2
  85. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +3 -16
  86. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.test.mjs +28 -0
  87. package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +2 -2
  88. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +6 -0
  89. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +15 -1
  90. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +6 -0
  91. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +111 -39
  92. package/src/runtime/agent/orchestrator/session/manager/ask-session.test.mjs +46 -0
  93. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +30 -6
  94. package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +3 -0
  95. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +27 -3
  96. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +26 -54
  97. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.test.mjs +63 -1
  98. package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +3 -0
  99. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +4 -110
  100. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.mjs +580 -0
  101. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.test.mjs +593 -0
  102. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +119 -190
  103. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +115 -0
  104. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +43 -8
  105. package/src/runtime/agent/orchestrator/session/provider-prefix-guard.mjs +129 -12
  106. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +9 -2
  107. package/src/runtime/agent/orchestrator/session/store/listing.mjs +8 -1
  108. package/src/runtime/agent/orchestrator/session/store/save-worker.mjs +15 -1
  109. package/src/runtime/agent/orchestrator/session/store-live-publication.test.mjs +83 -0
  110. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +36 -2
  111. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +157 -30
  112. package/src/runtime/agent/orchestrator/session/store-summary-reader.test.mjs +64 -0
  113. package/src/runtime/agent/orchestrator/session/store-summary-visibility.mjs +57 -0
  114. package/src/runtime/agent/orchestrator/session/store-summary-visibility.test.mjs +219 -0
  115. package/src/runtime/agent/orchestrator/session/store.mjs +26 -2
  116. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +22 -0
  117. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool-cwd-env.test.mjs +14 -0
  118. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +19 -10
  119. package/src/runtime/agent/orchestrator/tools/builtin/find-search-budget.test.mjs +30 -14
  120. package/src/runtime/agent/orchestrator/tools/builtin/git-command-policy.mjs +15 -4
  121. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +73 -15
  122. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.test.mjs +76 -2
  123. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +24 -683
  124. package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +64 -72
  125. package/src/runtime/agent/orchestrator/tools/builtin/native-search-runner.mjs +2 -5
  126. package/src/runtime/agent/orchestrator/tools/builtin/native-search-transport.mjs +75 -0
  127. package/src/runtime/agent/orchestrator/tools/builtin/native-search-transport.test.mjs +24 -0
  128. package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +6 -6
  129. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +12 -0
  130. package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +4 -1
  131. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +24 -5
  132. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.test.mjs +44 -0
  133. package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +12 -2
  134. package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +35 -21
  135. package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +28 -13
  136. package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.test.mjs +54 -0
  137. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +11 -11
  138. package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
  139. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +12 -0
  140. package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +11 -11
  141. package/src/runtime/attachments/pdf-extract.mjs +3 -0
  142. package/src/runtime/attachments/pdfjs-runtime.mjs +13 -0
  143. package/src/runtime/bridge-clients.test.mjs +827 -0
  144. package/src/runtime/browser-bridge/action-schema.mjs +251 -0
  145. package/src/runtime/browser-bridge/client.mjs +152 -0
  146. package/src/runtime/browser-bridge/tool-defs.mjs +159 -0
  147. package/src/runtime/computer-bridge/action-schema.mjs +653 -0
  148. package/src/runtime/computer-bridge/client.mjs +231 -0
  149. package/src/runtime/computer-bridge/tool-defs.mjs +33 -0
  150. package/src/runtime/media/store.mjs +18 -8
  151. package/src/runtime/media/store.test.mjs +14 -0
  152. package/src/runtime/memory/index.mjs +1 -1
  153. package/src/runtime/memory/lib/transcript-ingest.mjs +8 -6
  154. package/src/runtime/memory/lib/transcript-ingest.test.mjs +52 -0
  155. package/src/runtime/office/assurance-benchmark.mjs +358 -0
  156. package/src/runtime/office/assurance.mjs +833 -0
  157. package/src/runtime/office/benchmark.mjs +496 -0
  158. package/src/runtime/office/capabilities.mjs +551 -0
  159. package/src/runtime/office/com-adapter.mjs +283 -0
  160. package/src/runtime/office/content-model.mjs +183 -0
  161. package/src/runtime/office/contract-benchmark.mjs +105 -0
  162. package/src/runtime/office/design-library.mjs +1562 -0
  163. package/src/runtime/office/design-system.mjs +1966 -0
  164. package/src/runtime/office/index.mjs +2413 -0
  165. package/src/runtime/office/journal.mjs +82 -0
  166. package/src/runtime/office/office-assurance.test.mjs +371 -0
  167. package/src/runtime/office/office-com-host.ps1 +3781 -0
  168. package/src/runtime/office/office-com-session-host.ps1 +855 -0
  169. package/src/runtime/office/office-live-runtime.test.mjs +819 -0
  170. package/src/runtime/office/office-runtime.test.mjs +2070 -0
  171. package/src/runtime/office/ooxml-validator.mjs +271 -0
  172. package/src/runtime/office/pagination.mjs +132 -0
  173. package/src/runtime/office/pdf-adapter.mjs +591 -0
  174. package/src/runtime/office/pdf-analysis.mjs +372 -0
  175. package/src/runtime/office/pdf-render.mjs +167 -0
  176. package/src/runtime/office/pdf-security.mjs +63 -0
  177. package/src/runtime/office/polish-benchmark.mjs +190 -0
  178. package/src/runtime/office/portable-ooxml.mjs +1867 -0
  179. package/src/runtime/office/quality-live-benchmark.mjs +486 -0
  180. package/src/runtime/office/quality-pipeline.mjs +136 -0
  181. package/src/runtime/office/tabular.mjs +302 -0
  182. package/src/runtime/office/templates/mixdog-executive.pptx +0 -0
  183. package/src/runtime/office/templates/mixdog-executive.pptx.mixdog.json +190 -0
  184. package/src/runtime/office/tool-defs.mjs +113 -0
  185. package/src/runtime/office/visual-diff.mjs +89 -0
  186. package/src/runtime/office/xlsx-assertions.mjs +172 -0
  187. package/src/runtime/office/xlsx-contract.mjs +147 -0
  188. package/src/runtime/shared/agent-route-config.mjs +9 -0
  189. package/src/runtime/shared/child-spawn-gate.mjs +17 -0
  190. package/src/runtime/shared/child-spawn-remote.mjs +69 -7
  191. package/src/runtime/shared/child-spawn-remote.test.mjs +124 -0
  192. package/src/runtime/shared/pristine-execution-contract.json +2 -1
  193. package/src/runtime/shared/provider-api-key.mjs +1 -0
  194. package/src/runtime/shared/session-runtime-health.mjs +91 -0
  195. package/src/runtime/shared/session-runtime-health.test.mjs +53 -0
  196. package/src/runtime/shared/skill-document.mjs +86 -0
  197. package/src/runtime/shared/skill-document.test.mjs +81 -0
  198. package/src/runtime/shared/tool-execution-contract.mjs +16 -0
  199. package/src/runtime/shared/tool-surface.mjs +27 -0
  200. package/src/runtime/shared/turn-snapshot-store.mjs +1 -0
  201. package/src/runtime/shared/turn-snapshot.mjs +32 -9
  202. package/src/runtime/shared/user-cwd.mjs +33 -14
  203. package/src/runtime/shared/user-cwd.test.mjs +43 -0
  204. package/src/session-runtime/agent-disable.test.mjs +20 -1
  205. package/src/session-runtime/context-status.mjs +9 -3
  206. package/src/session-runtime/cwd-plugins.mjs +26 -26
  207. package/src/session-runtime/cwd-plugins.test.mjs +75 -0
  208. package/src/session-runtime/cwd-tool-routing.test.mjs +5 -0
  209. package/src/session-runtime/deferred-tool-delta.mjs +104 -0
  210. package/src/session-runtime/deferred-tool-delta.test.mjs +40 -0
  211. package/src/session-runtime/global-extensions.mjs +26 -0
  212. package/src/session-runtime/global-extensions.test.mjs +28 -0
  213. package/src/session-runtime/global-settings-session-identity.test.mjs +76 -0
  214. package/src/session-runtime/goal-runtime.mjs +1141 -0
  215. package/src/session-runtime/goal-runtime.test.mjs +645 -0
  216. package/src/session-runtime/lifecycle-api.mjs +58 -11
  217. package/src/session-runtime/lifecycle-api.test.mjs +183 -0
  218. package/src/session-runtime/mcp-glue.mjs +49 -47
  219. package/src/session-runtime/plugin-mcp-standard.test.mjs +177 -0
  220. package/src/session-runtime/plugin-mcp.mjs +96 -22
  221. package/src/session-runtime/prewarm.mjs +1 -10
  222. package/src/session-runtime/provider-request-snapshot.mjs +33 -9
  223. package/src/session-runtime/provider-request-snapshot.test.mjs +27 -5
  224. package/src/session-runtime/resource-api-global.test.mjs +109 -0
  225. package/src/session-runtime/resource-api.mjs +168 -116
  226. package/src/session-runtime/runtime-core.mjs +208 -29
  227. package/src/session-runtime/runtime-tunables.mjs +6 -8
  228. package/src/session-runtime/session-lifecycle.mjs +34 -12
  229. package/src/session-runtime/session-title.mjs +2 -2
  230. package/src/session-runtime/session-turn-api.mjs +37 -25
  231. package/src/session-runtime/skills-api.mjs +79 -25
  232. package/src/session-runtime/skills-api.test.mjs +99 -0
  233. package/src/session-runtime/tool-catalog-data.mjs +1 -1
  234. package/src/session-runtime/tool-catalog-schema.mjs +0 -5
  235. package/src/session-runtime/tool-catalog.mjs +42 -112
  236. package/src/session-runtime/tool-defs.mjs +6 -1
  237. package/src/session-runtime/tool-policy-surface.test.mjs +93 -17
  238. package/src/session-runtime/tool-surface.mjs +30 -8
  239. package/src/session-runtime/workflow-agents-api.mjs +8 -25
  240. package/src/session-runtime/workflow.mjs +8 -6
  241. package/src/standalone/agent-dispatch-broker.mjs +5 -9
  242. package/src/standalone/agent-tool/helpers.mjs +2 -2
  243. package/src/standalone/agent-tool/job-views.mjs +3 -1
  244. package/src/standalone/agent-tool/notify.mjs +11 -9
  245. package/src/standalone/agent-tool/notify.test.mjs +37 -0
  246. package/src/standalone/agent-tool/render.mjs +3 -0
  247. package/src/standalone/agent-tool/spawn-flow.mjs +61 -16
  248. package/src/standalone/agent-tool/spawn-flow.test.mjs +41 -0
  249. package/src/standalone/agent-tool/tool-def.mjs +1 -1
  250. package/src/standalone/agent-tool/worker-index.mjs +123 -0
  251. package/src/standalone/agent-tool/worker-index.test.mjs +132 -0
  252. package/src/standalone/agent-tool/worker-rows.mjs +2 -1
  253. package/src/standalone/agent-tool.mjs +57 -10
  254. package/src/standalone/daemon.mjs +113 -15
  255. package/src/standalone/hook-bus/config.mjs +1 -0
  256. package/src/standalone/plugin-admin.mjs +17 -0
  257. package/src/standalone/plugin-admin.test.mjs +7 -0
  258. package/src/standalone/provider-admin.mjs +1 -0
  259. package/src/standalone/session-protocol.mjs +5 -0
  260. package/src/standalone/session-runtime-agent-control-client.mjs +133 -0
  261. package/src/standalone/session-runtime-agent-control-client.test.mjs +71 -0
  262. package/src/standalone/session-runtime-dispatch-cancel.test.mjs +139 -0
  263. package/src/standalone/session-runtime-host-factory.mjs +17 -0
  264. package/src/standalone/session-runtime-host-factory.test.mjs +31 -0
  265. package/src/standalone/session-runtime-host-health.test.mjs +5 -0
  266. package/src/standalone/session-runtime-host.mjs +709 -58
  267. package/src/standalone/session-runtime-inline-host.mjs +258 -0
  268. package/src/standalone/session-runtime-inline-host.test.mjs +385 -0
  269. package/src/standalone/session-runtime-provider-cooldown.mjs +78 -0
  270. package/src/standalone/session-runtime-provider-cooldown.test.mjs +80 -0
  271. package/src/standalone/session-runtime-shard-host.test.mjs +642 -0
  272. package/src/standalone/session-runtime-shard-router.mjs +149 -0
  273. package/src/standalone/session-runtime-shard-router.test.mjs +101 -0
  274. package/src/standalone/session-runtime-worker.mjs +302 -26
  275. package/src/standalone/session-service-agent-tree.test.mjs +984 -0
  276. package/src/standalone/session-service-goal-recovery.test.mjs +177 -0
  277. package/src/standalone/session-service.mjs +711 -11
  278. package/src/tui/app/app-view.jsx +1 -0
  279. package/src/tui/app/core-memory-picker.mjs +2 -0
  280. package/src/tui/app/extension-pickers.mjs +1 -1
  281. package/src/tui/app/maintenance-pickers.mjs +3 -3
  282. package/src/tui/app/model-options.mjs +2 -0
  283. package/src/tui/app/model-picker.mjs +14 -4
  284. package/src/tui/app/panel-handoff.test.mjs +311 -0
  285. package/src/tui/app/project-picker.mjs +1 -0
  286. package/src/tui/app/route-pickers.mjs +21 -6
  287. package/src/tui/app/settings-picker.mjs +26 -3
  288. package/src/tui/app/slash-commands.mjs +1 -0
  289. package/src/tui/app/slash-dispatch.mjs +73 -22
  290. package/src/tui/app/theme-effort-pickers.mjs +7 -2
  291. package/src/tui/components/Picker.jsx +10 -1
  292. package/src/tui/dist/index.mjs +187 -45
  293. package/src/tui/lib/voice-setup.mjs +4 -1
  294. package/src/tui/session/agent-message-source.test.mjs +37 -0
  295. package/src/tui/session/completion-card-restore.test.mjs +50 -0
  296. package/src/tui/session/context-state.mjs +3 -13
  297. package/src/tui/session/goal-continuation.mjs +141 -0
  298. package/src/tui/session/goal-continuation.test.mjs +203 -0
  299. package/src/tui/session/queue-helpers.mjs +5 -2
  300. package/src/tui/session/session-action-surface.test.mjs +190 -0
  301. package/src/tui/session/session-api-ext.mjs +48 -8
  302. package/src/tui/session/session-api.mjs +73 -1
  303. package/src/tui/session/session-flow.mjs +31 -1
  304. package/src/tui/session/turn.mjs +88 -15
  305. package/src/tui/session-local.mjs +39 -3
  306. package/src/vendor/statusline/src/gateway/route-meta.mjs +31 -0
  307. package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +0 -301
@@ -0,0 +1,855 @@
1
+ $ErrorActionPreference = 'Stop'
2
+ [Console]::InputEncoding = [System.Text.Encoding]::UTF8
3
+ [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
4
+
5
+ $env:MIXDOG_OFFICE_HOST_LIBRARY = '1'
6
+ . (Join-Path $PSScriptRoot 'office-com-host.ps1')
7
+ Remove-Item Env:MIXDOG_OFFICE_HOST_LIBRARY -ErrorAction SilentlyContinue
8
+
9
+ if (-not ('MixdogOfficeInterop' -as [type])) {
10
+ Add-Type -TypeDefinition @'
11
+ using System;
12
+ using System.IO;
13
+ using System.Runtime.InteropServices;
14
+ using System.Runtime.InteropServices.ComTypes;
15
+ using System.Text;
16
+
17
+ public static class MixdogOfficeInterop
18
+ {
19
+ private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
20
+
21
+ [DllImport("ole32.dll")]
22
+ private static extern int GetRunningObjectTable(int reserved, out IRunningObjectTable runningObjectTable);
23
+
24
+ [DllImport("ole32.dll")]
25
+ private static extern int CreateBindCtx(int reserved, out IBindCtx bindContext);
26
+
27
+ [DllImport("user32.dll")]
28
+ private static extern bool EnumWindows(EnumWindowsProc callback, IntPtr lParam);
29
+
30
+ [DllImport("user32.dll", CharSet = CharSet.Unicode)]
31
+ private static extern int GetClassName(IntPtr hWnd, StringBuilder className, int maxCount);
32
+
33
+ [DllImport("user32.dll", CharSet = CharSet.Unicode)]
34
+ private static extern int GetWindowText(IntPtr hWnd, StringBuilder text, int maxCount);
35
+
36
+ [DllImport("user32.dll")]
37
+ private static extern bool IsWindowVisible(IntPtr hWnd);
38
+
39
+ [DllImport("user32.dll", CharSet = CharSet.Auto)]
40
+ private static extern IntPtr SendMessage(IntPtr hWnd, uint message, IntPtr wParam, IntPtr lParam);
41
+
42
+ [DllImport("user32.dll")]
43
+ private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint processId);
44
+
45
+ [DllImport("user32.dll")]
46
+ private static extern IntPtr GetForegroundWindow();
47
+
48
+ [DllImport("kernel32.dll")]
49
+ private static extern uint GetCurrentThreadId();
50
+
51
+ [DllImport("user32.dll")]
52
+ private static extern bool AttachThreadInput(uint attachThreadId, uint attachToThreadId, bool attach);
53
+
54
+ [DllImport("user32.dll")]
55
+ private static extern bool IsIconic(IntPtr hWnd);
56
+
57
+ [DllImport("user32.dll")]
58
+ private static extern bool ShowWindowAsync(IntPtr hWnd, int command);
59
+
60
+ [DllImport("user32.dll")]
61
+ private static extern bool BringWindowToTop(IntPtr hWnd);
62
+
63
+ [DllImport("user32.dll")]
64
+ private static extern bool SetForegroundWindow(IntPtr hWnd);
65
+
66
+ private static string NormalizePath(string value)
67
+ {
68
+ if (String.IsNullOrWhiteSpace(value)) return String.Empty;
69
+ value = value.Trim();
70
+ while (value.StartsWith("!", StringComparison.Ordinal)) value = value.Substring(1);
71
+ Uri uri;
72
+ if (Uri.TryCreate(value, UriKind.Absolute, out uri) && uri.IsFile) value = uri.LocalPath;
73
+ try { return Path.GetFullPath(value).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); }
74
+ catch { return String.Empty; }
75
+ }
76
+
77
+ public static object TryGetRunningFile(string path)
78
+ {
79
+ IRunningObjectTable table = null;
80
+ IBindCtx context = null;
81
+ IEnumMoniker enumerator = null;
82
+ try
83
+ {
84
+ if (GetRunningObjectTable(0, out table) != 0 || table == null) return null;
85
+ if (CreateBindCtx(0, out context) != 0 || context == null) return null;
86
+ table.EnumRunning(out enumerator);
87
+ if (enumerator == null) return null;
88
+ enumerator.Reset();
89
+ string expected = NormalizePath(path);
90
+ IMoniker[] monikers = new IMoniker[1];
91
+ while (enumerator.Next(1, monikers, IntPtr.Zero) == 0)
92
+ {
93
+ IMoniker moniker = monikers[0];
94
+ try
95
+ {
96
+ string displayName;
97
+ moniker.GetDisplayName(context, null, out displayName);
98
+ if (!String.Equals(NormalizePath(displayName), expected, StringComparison.OrdinalIgnoreCase)) continue;
99
+ object value;
100
+ table.GetObject(moniker, out value);
101
+ return value;
102
+ }
103
+ catch { }
104
+ finally
105
+ {
106
+ if (moniker != null && Marshal.IsComObject(moniker)) Marshal.ReleaseComObject(moniker);
107
+ }
108
+ }
109
+ return null;
110
+ }
111
+ finally
112
+ {
113
+ if (enumerator != null && Marshal.IsComObject(enumerator)) Marshal.ReleaseComObject(enumerator);
114
+ if (context != null && Marshal.IsComObject(context)) Marshal.ReleaseComObject(context);
115
+ if (table != null && Marshal.IsComObject(table)) Marshal.ReleaseComObject(table);
116
+ }
117
+ }
118
+
119
+ public static void RegisterExcelInstances()
120
+ {
121
+ EnumWindows(delegate(IntPtr hWnd, IntPtr lParam)
122
+ {
123
+ StringBuilder className = new StringBuilder(64);
124
+ GetClassName(hWnd, className, className.Capacity);
125
+ if (String.Equals(className.ToString(), "XLMAIN", StringComparison.OrdinalIgnoreCase))
126
+ {
127
+ SendMessage(hWnd, 0x0400u + 18u, IntPtr.Zero, IntPtr.Zero);
128
+ }
129
+ return true;
130
+ }, IntPtr.Zero);
131
+ }
132
+
133
+ public static int ProcessIdForWindow(long hWnd)
134
+ {
135
+ uint processId;
136
+ GetWindowThreadProcessId(new IntPtr(hWnd), out processId);
137
+ return unchecked((int)processId);
138
+ }
139
+
140
+ public static long FindWindowByClassAndTitle(string expectedClass, string titlePart)
141
+ {
142
+ IntPtr found = IntPtr.Zero;
143
+ EnumWindows(delegate(IntPtr hWnd, IntPtr lParam)
144
+ {
145
+ if (!IsWindowVisible(hWnd)) return true;
146
+ StringBuilder className = new StringBuilder(64);
147
+ GetClassName(hWnd, className, className.Capacity);
148
+ if (!String.Equals(className.ToString(), expectedClass, StringComparison.OrdinalIgnoreCase)) return true;
149
+ StringBuilder title = new StringBuilder(1024);
150
+ GetWindowText(hWnd, title, title.Capacity);
151
+ if (title.ToString().IndexOf(titlePart, StringComparison.OrdinalIgnoreCase) < 0) return true;
152
+ found = hWnd;
153
+ return false;
154
+ }, IntPtr.Zero);
155
+ return found.ToInt64();
156
+ }
157
+
158
+ public static bool ActivateWindow(long hWnd)
159
+ {
160
+ IntPtr target = new IntPtr(hWnd);
161
+ if (target == IntPtr.Zero) return false;
162
+ uint ignored;
163
+ uint currentThread = GetCurrentThreadId();
164
+ uint foregroundThread = GetWindowThreadProcessId(GetForegroundWindow(), out ignored);
165
+ uint targetThread = GetWindowThreadProcessId(target, out ignored);
166
+ bool attachedForeground = false;
167
+ bool attachedTarget = false;
168
+ try
169
+ {
170
+ if (foregroundThread != 0 && foregroundThread != currentThread)
171
+ attachedForeground = AttachThreadInput(currentThread, foregroundThread, true);
172
+ if (targetThread != 0 && targetThread != currentThread)
173
+ attachedTarget = AttachThreadInput(currentThread, targetThread, true);
174
+ ShowWindowAsync(target, IsIconic(target) ? 9 : 5);
175
+ BringWindowToTop(target);
176
+ SetForegroundWindow(target);
177
+ return GetForegroundWindow() == target;
178
+ }
179
+ finally
180
+ {
181
+ if (attachedTarget) AttachThreadInput(currentThread, targetThread, false);
182
+ if (attachedForeground) AttachThreadInput(currentThread, foregroundThread, false);
183
+ }
184
+ }
185
+ }
186
+ '@
187
+ }
188
+
189
+ function Session-Response($state, $value) {
190
+ $result = [ordered]@{
191
+ ok = $true
192
+ session = [string]$state.Id
193
+ mode = [string]$state.Mode
194
+ backend = 'microsoft-office-com'
195
+ }
196
+ foreach ($entry in $value.GetEnumerator()) { $result[$entry.Key] = $entry.Value }
197
+ return $result
198
+ }
199
+
200
+ function Same-DocumentPath($document, [string]$path) {
201
+ try {
202
+ return [string]::Equals(
203
+ [System.IO.Path]::GetFullPath([string]$document.FullName),
204
+ [System.IO.Path]::GetFullPath($path),
205
+ [System.StringComparison]::OrdinalIgnoreCase
206
+ )
207
+ } catch {
208
+ return $false
209
+ }
210
+ }
211
+
212
+ function Find-RunningDocumentExact([string]$format, [string]$path) {
213
+ if ($format -eq 'xlsx') { [MixdogOfficeInterop]::RegisterExcelInstances() }
214
+ for ($attempt = 0; $attempt -lt 10; $attempt++) {
215
+ $candidate = [MixdogOfficeInterop]::TryGetRunningFile($path)
216
+ if ($null -ne $candidate) {
217
+ if (Same-DocumentPath $candidate $path) { return $candidate }
218
+ try { [void][System.Runtime.InteropServices.Marshal]::ReleaseComObject($candidate) } catch {}
219
+ }
220
+ Start-Sleep -Milliseconds 100
221
+ }
222
+ $app = Active-Application (ProgId-ForFormat $format)
223
+ if ($null -eq $app) { return $null }
224
+ try {
225
+ return Find-OpenDocument $app $format $path
226
+ } finally {
227
+ try { [void][System.Runtime.InteropServices.Marshal]::ReleaseComObject($app) } catch {}
228
+ }
229
+ }
230
+
231
+ function File-AppearsOpen([string]$path) {
232
+ $stream = $null
233
+ try {
234
+ $stream = [System.IO.File]::Open(
235
+ $path,
236
+ [System.IO.FileMode]::Open,
237
+ [System.IO.FileAccess]::ReadWrite,
238
+ [System.IO.FileShare]::None
239
+ )
240
+ return $false
241
+ } catch {
242
+ return $true
243
+ } finally {
244
+ if ($null -ne $stream) { $stream.Dispose() }
245
+ }
246
+ }
247
+
248
+ function New-OfficeApplication([string]$format, [bool]$visible) {
249
+ $app = New-Object -ComObject (ProgId-ForFormat $format)
250
+ try { $app.DisplayAlerts = 0 } catch {}
251
+ try { $app.AutomationSecurity = 3 } catch {}
252
+ if ($format -ne 'pptx') {
253
+ try { $app.Visible = $visible } catch {}
254
+ }
255
+ return $app
256
+ }
257
+
258
+ function Set-OfficeVisible($app, $document, [string]$format) {
259
+ try { $app.Visible = $true } catch {}
260
+ switch ($format) {
261
+ 'docx' { try { $document.ActiveWindow.Visible = $true } catch {} }
262
+ 'xlsx' { try { $document.Windows.Item(1).Visible = $true } catch {} }
263
+ }
264
+ }
265
+
266
+ function Open-OwnedDocument($app, [string]$format, [string]$path, [bool]$visible) {
267
+ switch ($format) {
268
+ 'docx' { $document = $app.Documents.Open($path) }
269
+ 'xlsx' {
270
+ $document = $app.Workbooks.Open($path)
271
+ try { $app.CalculateFullRebuild() } catch {}
272
+ }
273
+ 'pptx' { $document = $app.Presentations.Open($path, $false, $false, $visible) }
274
+ }
275
+ if ($visible) { Set-OfficeVisible $app $document $format }
276
+ return $document
277
+ }
278
+
279
+ function Create-OwnedDocument($app, [string]$format, [string]$path, [bool]$visible) {
280
+ $saveFormat = Office-SaveFormatForPath $format $path
281
+ switch ($format) {
282
+ 'docx' {
283
+ $document = $app.Documents.Add()
284
+ try { $document.SaveAs2($path, $saveFormat) } catch { $document.SaveAs($path, $saveFormat) }
285
+ }
286
+ 'xlsx' {
287
+ $document = $app.Workbooks.Add()
288
+ $document.SaveAs($path, $saveFormat)
289
+ }
290
+ 'pptx' {
291
+ $document = $app.Presentations.Add()
292
+ $document.SaveAs($path, $saveFormat)
293
+ }
294
+ }
295
+ if ($visible) { Set-OfficeVisible $app $document $format }
296
+ return $document
297
+ }
298
+
299
+ function Application-Hwnd($app, $document, [string]$format) {
300
+ if ($format -eq 'docx' -and $null -ne $document) {
301
+ try {
302
+ $hWnd = [long]$document.ActiveWindow.Hwnd
303
+ if ($hWnd) { return $hWnd }
304
+ } catch {}
305
+ }
306
+ if ($format -eq 'pptx' -and $null -ne $document) {
307
+ try {
308
+ $hWnd = [long]$document.Windows.Item(1).HWND
309
+ if ($hWnd) { return $hWnd }
310
+ } catch {}
311
+ }
312
+ try {
313
+ $hWnd = [long]$app.Hwnd
314
+ if ($hWnd) { return $hWnd }
315
+ } catch {}
316
+ try {
317
+ $hWnd = [long]$app.HWND
318
+ if ($hWnd) { return $hWnd }
319
+ } catch {}
320
+ if ($format -eq 'pptx' -and $null -ne $document) {
321
+ try {
322
+ $title = [System.IO.Path]::GetFileName([string]$document.FullName)
323
+ $hWnd = [long][MixdogOfficeInterop]::FindWindowByClassAndTitle('PPTFrameClass', $title)
324
+ if ($hWnd) { return $hWnd }
325
+ } catch {}
326
+ }
327
+ return 0
328
+ }
329
+
330
+ function Set-OfficeForeground([long]$hWnd) {
331
+ if (-not $hWnd) { return $false }
332
+ for ($attempt = 0; $attempt -lt 10; $attempt++) {
333
+ try {
334
+ if ([MixdogOfficeInterop]::ActivateWindow($hWnd)) { return $true }
335
+ } catch {
336
+ return $false
337
+ }
338
+ Start-Sleep -Milliseconds 50
339
+ }
340
+ return $false
341
+ }
342
+
343
+ function Open-SessionState($payload) {
344
+ $id = [string]$payload.session
345
+ if ([string]::IsNullOrWhiteSpace($id)) { throw 'open_session requires session' }
346
+ $format = ([string]$payload.format).ToLowerInvariant()
347
+ if (@('docx', 'xlsx', 'pptx') -notcontains $format) { throw "Unsupported Office session format: $format" }
348
+ $path = [System.IO.Path]::GetFullPath([string]$payload.path)
349
+ $mode = ([string]$payload.mode).ToLowerInvariant()
350
+ if ($mode -eq 'live') { $mode = 'attach' }
351
+ if (@('attach', 'visible', 'background') -notcontains $mode) { throw "Unsupported Office session mode: $mode" }
352
+ $create = [bool]$payload.create
353
+ if ($create -and $mode -eq 'attach') { throw 'create does not support attach mode' }
354
+
355
+ $app = $null
356
+ $document = $null
357
+ $ownership = 'owned'
358
+ try {
359
+ if (-not $create -and @('attach', 'visible') -contains $mode) {
360
+ $document = Find-RunningDocumentExact $format $path
361
+ if ($null -ne $document) {
362
+ $app = $document.Application
363
+ $ownership = 'attached'
364
+ } elseif ($mode -eq 'attach') {
365
+ throw "The exact document is not registered as open in Microsoft Office: $path"
366
+ } elseif (File-AppearsOpen $path) {
367
+ throw "The document appears open but its exact COM object is unavailable; refusing to open a duplicate: $path"
368
+ }
369
+ }
370
+
371
+ if ($null -eq $document) {
372
+ if ($create) {
373
+ if ((Test-Path -LiteralPath $path) -and -not [bool]$payload.overwrite) {
374
+ throw "Office create target already exists: $path"
375
+ }
376
+ $directory = [System.IO.Path]::GetDirectoryName($path)
377
+ if ($directory) { [System.IO.Directory]::CreateDirectory($directory) | Out-Null }
378
+ } elseif (-not (Test-Path -LiteralPath $path)) {
379
+ throw "Office document not found: $path"
380
+ }
381
+ $visible = $mode -eq 'visible'
382
+ $app = New-OfficeApplication $format $visible
383
+ $document = if ($create) {
384
+ Create-OwnedDocument $app $format $path $visible
385
+ } else {
386
+ Open-OwnedDocument $app $format $path $visible
387
+ }
388
+ } elseif ($mode -eq 'visible') {
389
+ Set-OfficeVisible $app $document $format
390
+ }
391
+
392
+ $hWnd = Application-Hwnd $app $document $format
393
+ if ($mode -eq 'visible' -and $format -eq 'pptx') {
394
+ for ($attempt = 0; -not $hWnd -and $attempt -lt 20; $attempt++) {
395
+ Start-Sleep -Milliseconds 100
396
+ $hWnd = Application-Hwnd $app $document $format
397
+ }
398
+ }
399
+ $foregroundActivated = $mode -eq 'visible' -and $format -eq 'pptx' -and (Set-OfficeForeground $hWnd)
400
+ $processId = if ($hWnd) { [MixdogOfficeInterop]::ProcessIdForWindow($hWnd) } else { 0 }
401
+ return [pscustomobject]@{
402
+ Id = $id
403
+ Format = $format
404
+ Path = $path
405
+ Mode = $mode
406
+ Ownership = $ownership
407
+ App = $app
408
+ Document = $document
409
+ Visible = $mode -ne 'background'
410
+ AppPid = $processId
411
+ WindowHwnd = $hWnd
412
+ ForegroundActivated = [bool]$foregroundActivated
413
+ ExcelDpiRepairs = 0
414
+ DocumentId = "${format}:$($path.ToLowerInvariant())"
415
+ }
416
+ } catch {
417
+ if ($null -ne $document) {
418
+ if ($ownership -eq 'owned') { try { $document.Close($false) } catch {} }
419
+ try { [void][System.Runtime.InteropServices.Marshal]::ReleaseComObject($document) } catch {}
420
+ }
421
+ if ($null -ne $app) {
422
+ if ($ownership -eq 'owned') { try { $app.Quit() } catch {} }
423
+ try { [void][System.Runtime.InteropServices.Marshal]::ReleaseComObject($app) } catch {}
424
+ }
425
+ throw
426
+ }
427
+ }
428
+
429
+ function Close-SessionState($state, [bool]$save) {
430
+ if ($save) { Save-Document $state.Document $state.Format }
431
+ $process = $null
432
+ if ($state.Ownership -eq 'owned' -and [int]$state.AppPid -gt 0) {
433
+ try { $process = [System.Diagnostics.Process]::GetProcessById([int]$state.AppPid) } catch {}
434
+ }
435
+ if ($state.Ownership -eq 'owned') {
436
+ try { $state.Document.Close($false) } catch {}
437
+ }
438
+ try { [void][System.Runtime.InteropServices.Marshal]::FinalReleaseComObject($state.Document) } catch {}
439
+ $state.Document = $null
440
+ if ($state.Ownership -eq 'owned') {
441
+ try { $state.App.Quit() } catch {}
442
+ }
443
+ try { [void][System.Runtime.InteropServices.Marshal]::FinalReleaseComObject($state.App) } catch {}
444
+ $state.App = $null
445
+ $exited = $null -eq $process
446
+ if (-not $exited) {
447
+ try { $exited = [bool]$process.WaitForExit(250) } catch { $exited = $true }
448
+ }
449
+ if (-not $exited) {
450
+ try {
451
+ $process.Kill()
452
+ $exited = [bool]$process.WaitForExit(1000)
453
+ } catch {}
454
+ }
455
+ if ($null -ne $process) { try { $process.Dispose() } catch {} }
456
+ }
457
+
458
+ function Repair-ExcelPageSetupDpi([string]$path) {
459
+ Add-Type -AssemblyName System.IO.Compression -ErrorAction SilentlyContinue
460
+ Add-Type -AssemblyName System.IO.Compression.FileSystem -ErrorAction SilentlyContinue
461
+ $stream = $null
462
+ $archive = $null
463
+ $changed = 0
464
+ try {
465
+ $stream = [System.IO.File]::Open(
466
+ [System.IO.Path]::GetFullPath($path),
467
+ [System.IO.FileMode]::Open,
468
+ [System.IO.FileAccess]::ReadWrite,
469
+ [System.IO.FileShare]::None
470
+ )
471
+ $archive = [System.IO.Compression.ZipArchive]::new(
472
+ $stream,
473
+ [System.IO.Compression.ZipArchiveMode]::Update,
474
+ $false
475
+ )
476
+ $entries = @($archive.Entries | Where-Object { $_.FullName -match '^xl/worksheets/sheet\d+\.xml$' })
477
+ foreach ($entry in $entries) {
478
+ $part = $entry.Open()
479
+ $reader = [System.IO.StreamReader]::new($part, [System.Text.Encoding]::UTF8, $true, 4096, $false)
480
+ try { $xml = $reader.ReadToEnd() } finally { $reader.Dispose() }
481
+ $updated = [regex]::Replace($xml, '\b(horizontalDpi|verticalDpi)="0"', '$1="96"')
482
+ if ($updated -eq $xml) { continue }
483
+ $part = $entry.Open()
484
+ $part.SetLength(0)
485
+ $writer = [System.IO.StreamWriter]::new($part, [System.Text.UTF8Encoding]::new($false), 4096, $false)
486
+ try { $writer.Write($updated) } finally { $writer.Dispose() }
487
+ $changed++
488
+ }
489
+ } finally {
490
+ if ($null -ne $archive) { $archive.Dispose() }
491
+ if ($null -ne $stream) { $stream.Dispose() }
492
+ }
493
+ return $changed
494
+ }
495
+
496
+ function Reopen-BackgroundExcelSession($state, [string]$restorePath = '') {
497
+ if ($state.Format -ne 'xlsx' -or $state.Mode -ne 'background' -or $state.Ownership -ne 'owned') {
498
+ throw 'Full-file Excel restore is available only for owned background sessions'
499
+ }
500
+ $current = $state.Document
501
+ try { $current.Close($false) } finally {
502
+ try { [void][System.Runtime.InteropServices.Marshal]::ReleaseComObject($current) } catch {}
503
+ $state.Document = $null
504
+ }
505
+ if (-not [string]::IsNullOrWhiteSpace($restorePath)) {
506
+ [System.IO.File]::Copy([System.IO.Path]::GetFullPath($restorePath), [System.IO.Path]::GetFullPath($state.Path), $true)
507
+ }
508
+ $state.ExcelDpiRepairs = Repair-ExcelPageSetupDpi $state.Path
509
+ $state.Document = $state.App.Workbooks.Open($state.Path)
510
+ try { $state.App.CalculateFullRebuild() } catch {}
511
+ }
512
+
513
+ function Reopen-BackgroundWordSession($state) {
514
+ if ($state.Format -ne 'docx' -or $state.Mode -ne 'background' -or $state.Ownership -ne 'owned') {
515
+ throw 'Word reopen validation is available only for owned background sessions'
516
+ }
517
+ $current = $state.Document
518
+ try { $current.Close($false) } finally {
519
+ try { [void][System.Runtime.InteropServices.Marshal]::ReleaseComObject($current) } catch {}
520
+ $state.Document = $null
521
+ }
522
+ $state.Document = $state.App.Documents.Open($state.Path)
523
+ }
524
+
525
+ function Reopen-BackgroundPowerPointSession($state, [string]$restorePath = '') {
526
+ if ($state.Format -ne 'pptx' -or $state.Mode -ne 'background' -or $state.Ownership -ne 'owned') {
527
+ throw 'PowerPoint process reopen is available only for owned background sessions'
528
+ }
529
+ if ($null -ne $state.Document) {
530
+ try { $state.Document.Close() } finally {
531
+ try { [void][System.Runtime.InteropServices.Marshal]::ReleaseComObject($state.Document) } catch {}
532
+ $state.Document = $null
533
+ }
534
+ }
535
+ if ($null -ne $state.App) {
536
+ try { $state.App.Quit() } catch {}
537
+ try { [void][System.Runtime.InteropServices.Marshal]::FinalReleaseComObject($state.App) } catch {}
538
+ $state.App = $null
539
+ }
540
+ if (-not [string]::IsNullOrWhiteSpace($restorePath)) {
541
+ [System.IO.File]::Copy(
542
+ [System.IO.Path]::GetFullPath($restorePath),
543
+ [System.IO.Path]::GetFullPath($state.Path),
544
+ $true
545
+ )
546
+ }
547
+ $state.App = New-OfficeApplication 'pptx' $false
548
+ $state.Document = Open-OwnedDocument $state.App 'pptx' $state.Path $false
549
+ $state.WindowHwnd = Application-Hwnd $state.App $state.Document 'pptx'
550
+ $state.AppPid = if ($state.WindowHwnd) {
551
+ [MixdogOfficeInterop]::ProcessIdForWindow([long]$state.WindowHwnd)
552
+ } else {
553
+ 0
554
+ }
555
+ return $state.Document
556
+ }
557
+
558
+ function Invoke-SessionAction($state, $payload) {
559
+ $document = $state.Document
560
+ $format = $state.Format
561
+ switch ([string]$payload.action) {
562
+ 'snapshot' {
563
+ $value = Snapshot-Document $document $format $payload
564
+ return Session-Response $state ([ordered]@{ value = $value })
565
+ }
566
+ 'issues' {
567
+ $wasSaved = [bool]$document.Saved
568
+ $value = Issues-Document $document $format $payload
569
+ if ($wasSaved -and -not [bool]$document.Saved) { $document.Saved = $true }
570
+ return Session-Response $state ([ordered]@{ value = $value })
571
+ }
572
+ 'validate' {
573
+ # The persistent session already proves that the exact document is open.
574
+ # Starting a second Word/Excel/PowerPoint COM application can bind to the
575
+ # same process; quitting that validator then destroys this live session.
576
+ $wasSaved = [bool]$document.Saved
577
+ $snapshot = Snapshot-Document $document $format ([ordered]@{})
578
+ $inspection = if ($null -ne $payload.inspectIssues -and -not [bool]$payload.inspectIssues) {
579
+ [ordered]@{ ok = $true; issueCount = 0; issues = @() }
580
+ } else {
581
+ Issues-Document $document $format ([ordered]@{})
582
+ }
583
+ if ($wasSaved -and -not [bool]$document.Saved) { $document.Saved = $true }
584
+ $value = [ordered]@{
585
+ ok = [bool]$inspection.ok
586
+ opened = $true
587
+ issueCount = [int]$inspection.issueCount
588
+ issues = @($inspection.issues)
589
+ snapshotFingerprint = Snapshot-Fingerprint $snapshot
590
+ documentSaved = [bool]$document.Saved
591
+ }
592
+ return Session-Response $state ([ordered]@{ value = $value })
593
+ }
594
+ 'checkpoint' {
595
+ $output = [System.IO.Path]::GetFullPath([string]$payload.output)
596
+ $value = Snapshot-Document $document $format $payload
597
+ if ($format -eq 'docx') {
598
+ return Session-Response $state ([ordered]@{
599
+ fingerprint = Snapshot-Fingerprint $value
600
+ saved = [bool]$document.Saved
601
+ value = $value
602
+ })
603
+ }
604
+ Save-DocumentCopy $document $format $output
605
+ return Session-Response $state ([ordered]@{
606
+ output = $output
607
+ saved = [bool]$document.Saved
608
+ value = $value
609
+ })
610
+ }
611
+ 'save_copy' {
612
+ $output = [System.IO.Path]::GetFullPath([string]$payload.output)
613
+ Save-DocumentCopy $document $format $output
614
+ return Session-Response $state ([ordered]@{
615
+ output = $output
616
+ saved = [bool]$document.Saved
617
+ })
618
+ }
619
+ 'replace_presentation_from_source' {
620
+ if ($format -ne 'pptx' -or $state.Mode -ne 'background' -or $state.Ownership -ne 'owned') {
621
+ throw 'PowerPoint source replacement is available only for owned background sessions'
622
+ }
623
+ $sourcePath = [System.IO.Path]::GetFullPath([string]$payload.source)
624
+ $checkpoint = [System.IO.Path]::GetFullPath([string]$payload.checkpoint)
625
+ $current = $state.Document
626
+ try {
627
+ $current.Close()
628
+ try { [void][System.Runtime.InteropServices.Marshal]::ReleaseComObject($current) } catch {}
629
+ $state.Document = $null
630
+ $document = $state.App.Presentations.Open($sourcePath, $false, $false, $false)
631
+ $state.Document = $document
632
+ $document.SaveAs($state.Path, (Office-SaveFormatForPath 'pptx' $state.Path))
633
+ $sourceSlideCount = [int]$document.Slides.Count
634
+ $selected = if ($payload.slides) {
635
+ @($payload.slides | ForEach-Object { [int]$_ })
636
+ } else {
637
+ if ($sourceSlideCount -le 0) { @() } else { @(1..$sourceSlideCount) }
638
+ }
639
+ if (@($selected | Where-Object { $_ -lt 1 -or $_ -gt $sourceSlideCount }).Count -gt 0) {
640
+ throw "import_slides selection is outside source deck range 1-$sourceSlideCount"
641
+ }
642
+ $identitySelection = $selected.Count -eq $sourceSlideCount
643
+ if ($identitySelection) {
644
+ for ($identityIndex = 1; $identityIndex -le $sourceSlideCount; $identityIndex++) {
645
+ if ([int]$selected[$identityIndex - 1] -ne $identityIndex) {
646
+ $identitySelection = $false
647
+ break
648
+ }
649
+ }
650
+ }
651
+ if (-not $identitySelection) {
652
+ $sourceIds = @{}
653
+ $keep = @{}
654
+ foreach ($slideNumber in $selected) {
655
+ if (-not $keep.ContainsKey([int]$slideNumber)) {
656
+ $keep[[int]$slideNumber] = $true
657
+ $sourceIds[[int]$slideNumber] = [int]$document.Slides.Item([int]$slideNumber).SlideID
658
+ }
659
+ }
660
+ for ($slideIndex = $sourceSlideCount; $slideIndex -ge 1; $slideIndex--) {
661
+ if (-not $keep.ContainsKey($slideIndex)) { $document.Slides.Item($slideIndex).Delete() }
662
+ }
663
+ $placed = @{}
664
+ for ($targetIndex = 1; $targetIndex -le $selected.Count; $targetIndex++) {
665
+ $sourceSlide = [int]$selected[$targetIndex - 1]
666
+ $sourceId = [int]$sourceIds[$sourceSlide]
667
+ if (-not $placed.ContainsKey($sourceSlide)) {
668
+ $document.Slides.FindBySlideID($sourceId).MoveTo($targetIndex)
669
+ $placed[$sourceSlide] = $true
670
+ } else {
671
+ $duplicates = $document.Slides.FindBySlideID($sourceId).Duplicate()
672
+ $duplicates.Item(1).MoveTo($targetIndex)
673
+ }
674
+ }
675
+ }
676
+ Save-Document $document $format
677
+ $null = Reopen-BackgroundPowerPointSession $state
678
+ $document = $state.Document
679
+ $value = Snapshot-Document $document $format ([ordered]@{})
680
+ return Session-Response $state ([ordered]@{
681
+ saved = $true
682
+ results = @([ordered]@{
683
+ op = 'import_slides'
684
+ changed = $true
685
+ count = [int]$selected.Count
686
+ source = $sourcePath
687
+ replacedEmptyDeck = $true
688
+ })
689
+ value = $value
690
+ })
691
+ } catch {
692
+ $line = [int]$_.InvocationInfo.ScriptLineNumber
693
+ $message = $_.Exception.Message
694
+ if ($null -ne $state.Document) {
695
+ try { $state.Document.Close() } catch {}
696
+ try { [void][System.Runtime.InteropServices.Marshal]::ReleaseComObject($state.Document) } catch {}
697
+ $state.Document = $null
698
+ }
699
+ $restoreWarning = ''
700
+ try {
701
+ $restorePath = if (Test-Path -LiteralPath $checkpoint) { $checkpoint } else { '' }
702
+ $null = Reopen-BackgroundPowerPointSession $state $restorePath
703
+ } catch {
704
+ $restoreWarning = " Restore reopen failed: $($_.Exception.Message)"
705
+ }
706
+ throw "replace_presentation_from_source failed at office-com-session-host.ps1:$line`: $message$restoreWarning"
707
+ }
708
+ }
709
+ 'batch' {
710
+ $excelCheckpoint = ''
711
+ if ($format -eq 'xlsx') {
712
+ $extension = [System.IO.Path]::GetExtension($state.Path)
713
+ $excelCheckpoint = Join-Path ([System.IO.Path]::GetTempPath()) "mixdog-excel-batch-$([guid]::NewGuid().ToString('N'))$extension"
714
+ Save-DocumentCopy $document $format $excelCheckpoint
715
+ }
716
+ try {
717
+ $applied = Apply-Operations $document $format $payload.operations $true ([bool]$payload.requireChanges)
718
+ $shouldSave = $state.Mode -eq 'background' -or [bool]$payload.save
719
+ if ($shouldSave) { Save-Document $document $format }
720
+ return Session-Response $state ([ordered]@{
721
+ saved = $shouldSave
722
+ results = $applied.results
723
+ undoUnits = $applied.undoUnits
724
+ })
725
+ } catch {
726
+ if ($excelCheckpoint) {
727
+ try {
728
+ if ($state.Mode -eq 'background' -and $state.Ownership -eq 'owned') {
729
+ Reopen-BackgroundExcelSession $state
730
+ } else {
731
+ Restore-ExcelCheckpoint $document $excelCheckpoint
732
+ }
733
+ } catch {}
734
+ }
735
+ throw
736
+ } finally {
737
+ if ($excelCheckpoint) { Remove-Item $excelCheckpoint -Force -ErrorAction SilentlyContinue }
738
+ }
739
+ }
740
+ 'rollback' {
741
+ if ($format -eq 'xlsx' -and $state.Mode -eq 'background' -and $state.Ownership -eq 'owned') {
742
+ Reopen-BackgroundExcelSession $state ([string]$payload.checkpoint)
743
+ $document = $state.Document
744
+ } else {
745
+ Rollback-LiveDocument $document $format ([string]$payload.checkpoint) ([int]$payload.undoUnits)
746
+ }
747
+ $value = Snapshot-Document $document $format $payload
748
+ if ($state.Mode -eq 'background') { Save-Document $document $format }
749
+ return Session-Response $state ([ordered]@{ rolledBack = $true; value = $value })
750
+ }
751
+ 'save' {
752
+ Save-Document $document $format
753
+ return Session-Response $state ([ordered]@{ saved = $true; path = $state.Path })
754
+ }
755
+ 'post_save_validate' {
756
+ Save-Document $document $format
757
+ $reopened = $false
758
+ if ($state.Mode -eq 'background' -and $state.Ownership -eq 'owned') {
759
+ switch ($format) {
760
+ 'docx' { $null = Reopen-BackgroundWordSession $state }
761
+ 'xlsx' { $null = Reopen-BackgroundExcelSession $state }
762
+ 'pptx' { $null = Reopen-BackgroundPowerPointSession $state }
763
+ }
764
+ $document = $state.Document
765
+ $reopened = $true
766
+ }
767
+ $snapshot = Snapshot-Document $document $format ([ordered]@{})
768
+ $inspection = Issues-Document $document $format ([ordered]@{})
769
+ return Session-Response $state ([ordered]@{
770
+ value = [ordered]@{
771
+ ok = [bool]$inspection.ok
772
+ opened = $true
773
+ reopened = $reopened
774
+ persisted = $reopened
775
+ excelDpiRepairs = $(if ($format -eq 'xlsx') { [int]$state.ExcelDpiRepairs } else { 0 })
776
+ issueCount = [int]$inspection.issueCount
777
+ issues = @($inspection.issues)
778
+ snapshot = $snapshot
779
+ snapshotFingerprint = Snapshot-Fingerprint $snapshot
780
+ documentSaved = [bool]$document.Saved
781
+ }
782
+ })
783
+ }
784
+ 'render' {
785
+ $output = [System.IO.Path]::GetFullPath([string]$payload.output)
786
+ $wasSaved = [bool]$document.Saved
787
+ Render-Document $document $format $output
788
+ if ($wasSaved -and -not [bool]$document.Saved) { $document.Saved = $true }
789
+ return Session-Response $state ([ordered]@{ output = $output })
790
+ }
791
+ default { throw "Unsupported Office session action: $($payload.action)" }
792
+ }
793
+ }
794
+
795
+ $state = $null
796
+ try {
797
+ while ($null -ne ($raw = [Console]::In.ReadLine())) {
798
+ if ([string]::IsNullOrWhiteSpace($raw)) { continue }
799
+ $requestId = ''
800
+ $closeAfterResponse = $false
801
+ $stateToClose = $null
802
+ try {
803
+ $payload = $raw | ConvertFrom-Json
804
+ $requestId = [string]$payload.requestId
805
+ if ($payload.action -eq 'open_session') {
806
+ if ($null -ne $state) { throw 'Office session is already open in this host' }
807
+ $state = Open-SessionState $payload
808
+ $result = Session-Response $state ([ordered]@{
809
+ opened = $true
810
+ ownership = $state.Ownership
811
+ visible = $state.Visible
812
+ appPid = $state.AppPid
813
+ windowHwnd = $state.WindowHwnd
814
+ foregroundActivated = $state.ForegroundActivated
815
+ documentId = $state.DocumentId
816
+ path = $state.Path
817
+ })
818
+ } else {
819
+ if ($null -eq $state) { throw 'Office session host has no open document' }
820
+ if ([string]$payload.session -ne [string]$state.Id) { throw 'Office session id does not match this host' }
821
+ if ($payload.action -eq 'close_session') {
822
+ if ([bool]$payload.save) { Save-Document $state.Document $state.Format }
823
+ $result = Session-Response $state ([ordered]@{
824
+ closed = $true
825
+ saved = [bool]$payload.save
826
+ ownership = $state.Ownership
827
+ appPid = $state.AppPid
828
+ path = $state.Path
829
+ })
830
+ $stateToClose = $state
831
+ $state = $null
832
+ $closeAfterResponse = $true
833
+ } else {
834
+ $result = Invoke-SessionAction $state $payload
835
+ }
836
+ }
837
+ } catch {
838
+ $result = [ordered]@{
839
+ ok = $false
840
+ backend = 'microsoft-office-com'
841
+ error = [string]$_.Exception.Message
842
+ }
843
+ }
844
+ if ($requestId) { $result['requestId'] = $requestId }
845
+ Emit-Json $result
846
+ if ($closeAfterResponse) {
847
+ try { Close-SessionState $stateToClose $false } catch {}
848
+ break
849
+ }
850
+ }
851
+ } finally {
852
+ if ($null -ne $state) {
853
+ try { Close-SessionState $state $false } catch {}
854
+ }
855
+ }