@timurproko/a1 0.1.8-dev.472 → 0.1.8-dev.508

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 (370) hide show
  1. package/bin/cli.js +4 -5
  2. package/bin/guardian.js +5 -1
  3. package/bin/module-identity.js +53 -98
  4. package/bin/module-resolver.js +138 -0
  5. package/bin/pinned-pi-public.js +25 -6
  6. package/bin/ui.js +7 -2
  7. package/bin/update-recovery.js +76 -7
  8. package/bin/warmup.js +3 -0
  9. package/dist/{integrations/pi/session-ui → app/session-shell}/clipboard-diagnostics.d.ts +1 -1
  10. package/dist/{integrations/pi/session-ui → app/session-shell}/clipboard-image.js +1 -1
  11. package/dist/app/session-shell/helper-pool.d.ts +33 -0
  12. package/dist/app/session-shell/helper-pool.js +98 -0
  13. package/dist/{integrations/pi/session-ui → app/session-shell}/image-preparation-client.d.ts +1 -1
  14. package/dist/{integrations/pi/session-ui → app/session-shell}/image-preparation-client.js +1 -1
  15. package/dist/{integrations/pi/session-ui → app/session-shell}/image-preparation.js +1 -1
  16. package/dist/{integrations/pi/session-ui → app/session-shell}/image-source.js +1 -1
  17. package/dist/{integrations/pi/session-ui → app/session-shell}/image-worker.js +1 -1
  18. package/dist/app/session-shell/index.d.ts +7 -0
  19. package/dist/app/session-shell/index.js +4 -0
  20. package/dist/app/session-shell/paste-executor.d.ts +18 -0
  21. package/dist/{integrations/pi/session-ui → app/session-shell}/paste-executor.js +50 -29
  22. package/dist/{integrations/pi/session-ui → app/session-shell}/paste-helper.js +1 -1
  23. package/dist/app/session-shell/paste-preparation-client.d.ts +31 -0
  24. package/dist/{integrations/pi/session-ui → app/session-shell}/paste-preparation-client.js +19 -8
  25. package/dist/{integrations/pi/session-ui → app/session-shell}/paste-protocol.d.ts +1 -1
  26. package/dist/{integrations/pi/session-ui → app/session-shell}/paste-protocol.js +1 -1
  27. package/dist/app/session-shell/paste-text-preparation.d.ts +6 -0
  28. package/dist/{integrations/pi/session-ui/paste-text-preparation.d.ts → app/session-shell/paste-types.d.ts} +1 -4
  29. package/dist/{integrations/pi/session-ui → app/session-shell}/path-chip-presentation.d.ts +1 -1
  30. package/dist/{integrations/pi/session-ui → app/session-shell}/prompt-chips.d.ts +5 -1
  31. package/dist/{integrations/pi/session-ui → app/session-shell}/prompt-chips.js +6 -2
  32. package/dist/{integrations/pi/session-ui → app/session-shell}/prompt-history-controller.d.ts +2 -2
  33. package/dist/{integrations/pi/session-ui → app/session-shell}/prompt-history-controller.js +2 -2
  34. package/dist/{integrations/pi/session-ui → app/session-shell}/prompt-suggestion-controller.d.ts +1 -1
  35. package/dist/{integrations/pi/session-ui → app/session-shell}/prompt-suggestion-controller.js +2 -2
  36. package/dist/{integrations/pi/session-ui → app/session-shell}/quit-outro.js +1 -1
  37. package/dist/{integrations/pi/session-ui → app/session-shell}/response-copy-coordinator.d.ts +1 -1
  38. package/dist/{integrations/pi/session-ui → app/session-shell}/response-copy-coordinator.js +3 -5
  39. package/dist/{integrations/pi/session-ui → app/session-shell}/response-copy-helper.js +1 -1
  40. package/dist/{integrations/pi/session-ui → app/session-shell}/response-copy-protocol.d.ts +1 -1
  41. package/dist/{integrations/pi/session-ui → app/session-shell}/response-copy-transport.d.ts +10 -2
  42. package/dist/{integrations/pi/session-ui → app/session-shell}/response-copy-transport.js +45 -19
  43. package/dist/{integrations/pi/session-ui → app/session-shell}/session-shell-root.d.ts +75 -53
  44. package/dist/{integrations/pi/session-ui → app/session-shell}/session-shell-root.js +30 -20
  45. package/dist/{integrations/pi/session-ui → app/session-shell}/session-shell.d.ts +9 -8
  46. package/dist/{integrations/pi/session-ui → app/session-shell}/session-shell.js +108 -67
  47. package/dist/{integrations/pi/session-ui → app/session-shell}/session-viewport-controller.d.ts +5 -5
  48. package/dist/{integrations/pi/session-ui → app/session-shell}/session-viewport-controller.js +5 -5
  49. package/dist/{integrations/pi/session-ui → app/session-shell}/system-clipboard.d.ts +1 -1
  50. package/dist/{integrations/pi/session-ui → app/session-shell}/system-clipboard.js +1 -1
  51. package/dist/cli/index.d.ts +8 -5
  52. package/dist/cli/index.js +4 -5
  53. package/dist/composition/index.d.ts +0 -1
  54. package/dist/composition/index.js +0 -1
  55. package/dist/composition/owned-ui.d.ts +2 -2
  56. package/dist/composition/owned-ui.js +35 -36
  57. package/dist/composition/settings-route-host.d.ts +2 -2
  58. package/dist/contracts/agent-engine/domain.d.ts +6 -1
  59. package/dist/contracts/agent-engine/index.d.ts +11 -8
  60. package/dist/contracts/agent-engine/index.js +6 -8
  61. package/dist/contracts/owned-ui/extension-ui.d.ts +8 -0
  62. package/dist/contracts/owned-ui/index.d.ts +12 -8
  63. package/dist/contracts/owned-ui/index.js +8 -8
  64. package/dist/features/launch/index.d.ts +12 -6
  65. package/dist/features/launch/index.js +6 -6
  66. package/dist/features/launch/intent.d.ts +5 -5
  67. package/dist/features/launch/intent.js +2 -2
  68. package/dist/features/owned-ui/index.d.ts +6 -6
  69. package/dist/features/owned-ui/index.js +4 -6
  70. package/dist/features/owned-ui/run.d.ts +2 -2
  71. package/dist/features/owned-ui/settings-app.d.ts +2 -2
  72. package/dist/features/owned-ui/settings-app.js +18 -5
  73. package/dist/features/prompt-history/service.d.ts +1 -1
  74. package/dist/features/prompt-history/service.js +1 -1
  75. package/dist/features/prompt-history/store.js +3 -7
  76. package/dist/features/prompt-suggestions/diagnostics.js +1 -1
  77. package/dist/features/prompt-suggestions/index.d.ts +2 -1
  78. package/dist/features/prompt-suggestions/index.js +1 -1
  79. package/dist/foundation/launch-guardian/index.d.ts +2 -1
  80. package/dist/foundation/launch-guardian/index.js +1 -1
  81. package/dist/foundation/lifecycle/index.d.ts +11 -6
  82. package/dist/foundation/lifecycle/index.js +5 -6
  83. package/dist/foundation/process-containment/index.d.ts +8 -6
  84. package/dist/foundation/process-containment/index.js +6 -6
  85. package/dist/foundation/process-containment/native-guardian-containment.js +3 -3
  86. package/dist/foundation/protocol/index.d.ts +3 -2
  87. package/dist/foundation/protocol/index.js +2 -2
  88. package/dist/foundation/release/bootstrap.d.ts +0 -4
  89. package/dist/foundation/release/bootstrap.js +1 -82
  90. package/dist/foundation/release/dependency-layer.d.ts +2 -5
  91. package/dist/foundation/release/endpoints.d.ts +4 -0
  92. package/dist/foundation/release/endpoints.js +86 -2
  93. package/dist/foundation/release/index.d.ts +30 -16
  94. package/dist/foundation/release/index.js +16 -16
  95. package/dist/foundation/release/release.d.ts +1 -1
  96. package/dist/foundation/release/types.d.ts +11 -0
  97. package/dist/foundation/release/types.js +1 -0
  98. package/dist/foundation/release/update-transaction.d.ts +1 -1
  99. package/dist/foundation/release/update.d.ts +2 -1
  100. package/dist/foundation/release/update.js +3 -18
  101. package/dist/foundation/startup/index.d.ts +4 -2
  102. package/dist/foundation/startup/index.js +2 -2
  103. package/dist/foundation/startup/startup-descriptor.js +2 -2
  104. package/dist/foundation/storage/control-store.d.ts +1 -45
  105. package/dist/foundation/storage/control-store.js +54 -277
  106. package/dist/foundation/storage/index.d.ts +1 -1
  107. package/dist/foundation/storage/index.js +1 -1
  108. package/dist/foundation/supervision/index.d.ts +2 -2
  109. package/dist/foundation/supervision/index.js +2 -2
  110. package/dist/foundation/supervision/server.d.ts +5 -10
  111. package/dist/foundation/supervision/server.js +7 -7
  112. package/dist/foundation/terminal-cleanup/index.d.ts +3 -2
  113. package/dist/foundation/terminal-cleanup/index.js +2 -2
  114. package/dist/integrations/pi/components/editor-interaction.d.ts +1 -1
  115. package/dist/integrations/pi/components/history-editor-loader.d.ts +1 -1
  116. package/dist/integrations/pi/components/index.d.ts +20 -13
  117. package/dist/integrations/pi/components/index.js +12 -13
  118. package/dist/integrations/pi/components/owned-editor-ux.js +5 -8
  119. package/dist/integrations/pi/components/resources/builtin-themes.d.ts +7 -5
  120. package/dist/integrations/pi/components/resources/builtin-themes.js +7 -5
  121. package/dist/integrations/pi/components/shell-editor-autocomplete.js +1 -1
  122. package/dist/integrations/pi/components/shell-extension-ui.js +2 -1
  123. package/dist/integrations/pi/components/shell-footer-status.js +1 -1
  124. package/dist/integrations/pi/components/shell-presenters-info.d.ts +1 -1
  125. package/dist/integrations/pi/components/shell-presenters-info.js +1 -1
  126. package/dist/integrations/pi/components/shell-presenters-transcript.js +1 -1
  127. package/dist/integrations/pi/components/shell-selectors-dialogs.d.ts +9 -3
  128. package/dist/integrations/pi/components/shell-selectors-dialogs.js +9 -6
  129. package/dist/integrations/pi/components/shell-shared-facade.d.ts +3 -1
  130. package/dist/integrations/pi/components/shell-shared-facade.js +4 -2
  131. package/dist/integrations/pi/components/submitted-prompt-adapter.d.ts +1 -1
  132. package/dist/integrations/pi/components/submitted-prompt-adapter.js +1 -1
  133. package/dist/integrations/pi/components/tool-image-presentation.d.ts +1 -1
  134. package/dist/integrations/pi/components/tool-image-presentation.js +4 -7
  135. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.d.ts +36 -22
  136. package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.js +43 -7
  137. package/dist/integrations/pi/components/upstream/assets/earendil-image.json +2 -2
  138. package/dist/integrations/pi/components/upstream/components/countdown-timer.d.ts +6 -3
  139. package/dist/integrations/pi/components/upstream/components/daxnuts.d.ts +8 -1
  140. package/dist/integrations/pi/components/upstream/components/daxnuts.js +7 -3
  141. package/dist/integrations/pi/components/upstream/components/earendil-announcement.d.ts +1 -1
  142. package/dist/integrations/pi/components/upstream/components/earendil-announcement.js +10 -4
  143. package/dist/integrations/pi/components/upstream/components/extension-editor.d.ts +7 -4
  144. package/dist/integrations/pi/components/upstream/components/extension-editor.js +7 -4
  145. package/dist/integrations/pi/components/upstream/components/mermaid.js +8 -4
  146. package/dist/integrations/pi/components/upstream/components/owned-editor.d.ts +9 -5
  147. package/dist/integrations/pi/components/upstream/components/owned-editor.js +10 -6
  148. package/dist/integrations/pi/components/upstream/components/scoped-models-selector.d.ts +8 -4
  149. package/dist/integrations/pi/components/upstream/components/scoped-models-selector.js +27 -25
  150. package/dist/integrations/pi/components/upstream/components/session-footer.d.ts +1 -1
  151. package/dist/integrations/pi/components/upstream/components/session-footer.js +8 -10
  152. package/dist/integrations/pi/components/upstream/components/session-selector-search.js +6 -4
  153. package/dist/integrations/pi/components/upstream/components/session-selector.d.ts +1 -1
  154. package/dist/integrations/pi/components/upstream/components/session-selector.js +8 -4
  155. package/dist/integrations/pi/components/upstream/components/skill-invocation-message.d.ts +7 -5
  156. package/dist/integrations/pi/components/upstream/components/skill-invocation-message.js +8 -6
  157. package/dist/integrations/pi/components/upstream/components/status-indicator.d.ts +9 -4
  158. package/dist/integrations/pi/components/upstream/components/status-indicator.js +15 -5
  159. package/dist/integrations/pi/components/upstream/components/tool-execution.d.ts +8 -2
  160. package/dist/integrations/pi/components/upstream/components/tool-execution.js +58 -37
  161. package/dist/integrations/pi/components/upstream/components/tree-selector.d.ts +7 -4
  162. package/dist/integrations/pi/components/upstream/components/tree-selector.js +7 -4
  163. package/dist/integrations/pi/components/upstream/components/trust-selector.d.ts +1 -1
  164. package/dist/integrations/pi/components/upstream/components/trust-selector.js +10 -5
  165. package/dist/integrations/pi/components/upstream/external-editor.js +8 -4
  166. package/dist/integrations/pi/components/upstream/history/editor-core.d.ts +13 -4
  167. package/dist/integrations/pi/components/upstream/history/editor-core.js +112 -30
  168. package/dist/integrations/pi/components/upstream/history/kill-ring.d.ts +6 -3
  169. package/dist/integrations/pi/components/upstream/history/kill-ring.js +6 -3
  170. package/dist/integrations/pi/components/upstream/history/printable-key.js +7 -4
  171. package/dist/integrations/pi/components/upstream/history/text-helpers.js +6 -3
  172. package/dist/integrations/pi/components/upstream/history/undo-stack.d.ts +6 -3
  173. package/dist/integrations/pi/components/upstream/history/undo-stack.js +6 -3
  174. package/dist/integrations/pi/components/upstream/history/word-navigation.js +6 -3
  175. package/dist/integrations/pi/components/upstream/model-search.d.ts +5 -2
  176. package/dist/integrations/pi/components/upstream/theme/theme-controller.d.ts +8 -0
  177. package/dist/integrations/pi/components/upstream/theme/theme-controller.js +0 -3
  178. package/dist/integrations/pi/components/upstream/theme/theme.d.ts +2 -2
  179. package/dist/integrations/pi/components/upstream/theme/theme.js +17 -6
  180. package/dist/integrations/pi/engine/adapter.d.ts +39 -138
  181. package/dist/integrations/pi/engine/adapter.js +338 -3226
  182. package/dist/integrations/pi/engine/command-dispatch.d.ts +41 -0
  183. package/dist/integrations/pi/engine/command-dispatch.js +125 -0
  184. package/dist/integrations/pi/engine/event-delivery.d.ts +100 -0
  185. package/dist/integrations/pi/engine/event-delivery.js +184 -0
  186. package/dist/integrations/pi/engine/extension-ui-binding.d.ts +35 -0
  187. package/dist/integrations/pi/engine/extension-ui-binding.js +81 -0
  188. package/dist/integrations/pi/engine/index.d.ts +18 -14
  189. package/dist/integrations/pi/engine/index.js +10 -14
  190. package/dist/integrations/pi/engine/message-values.d.ts +29 -0
  191. package/dist/integrations/pi/engine/message-values.js +168 -0
  192. package/dist/integrations/pi/engine/pending-delivery.d.ts +2 -2
  193. package/dist/integrations/pi/engine/pending-delivery.js +2 -4
  194. package/dist/integrations/pi/engine/pi-settings-metadata.json +382 -371
  195. package/dist/integrations/pi/engine/prompt-suggestions.d.ts +30 -0
  196. package/dist/integrations/pi/engine/prompt-suggestions.js +102 -0
  197. package/dist/integrations/pi/engine/provider-authentication.d.ts +34 -0
  198. package/dist/integrations/pi/engine/provider-authentication.js +209 -0
  199. package/dist/integrations/pi/engine/resource-catalog.d.ts +49 -0
  200. package/dist/integrations/pi/engine/resource-catalog.js +289 -0
  201. package/dist/integrations/pi/engine/resources/changelog.json +4 -4
  202. package/dist/integrations/pi/engine/session-events.d.ts +48 -0
  203. package/dist/integrations/pi/engine/session-events.js +223 -0
  204. package/dist/integrations/pi/engine/session-integration.js +2 -4
  205. package/dist/integrations/pi/engine/session-runtime.d.ts +84 -0
  206. package/dist/integrations/pi/engine/session-runtime.js +249 -0
  207. package/dist/integrations/pi/engine/{settings-effects.d.ts → settings-bridge.d.ts} +57 -2
  208. package/dist/integrations/pi/engine/settings-bridge.js +514 -0
  209. package/dist/integrations/pi/engine/settings-metadata.d.ts +31 -0
  210. package/dist/integrations/pi/engine/settings-metadata.js +67 -0
  211. package/dist/integrations/pi/engine/settings-port.d.ts +42 -0
  212. package/dist/integrations/pi/engine/settings-port.js +353 -0
  213. package/dist/integrations/pi/engine/transcript-projection.d.ts +39 -0
  214. package/dist/integrations/pi/engine/transcript-projection.js +405 -0
  215. package/dist/integrations/pi/engine/usage-view.d.ts +4 -0
  216. package/dist/integrations/pi/engine/usage-view.js +56 -0
  217. package/dist/integrations/pi/engine/workflow-contexts.d.ts +59 -0
  218. package/dist/integrations/pi/engine/workflow-contexts.js +316 -0
  219. package/dist/integrations/pi/engine/workflow-runner.d.ts +60 -0
  220. package/dist/integrations/pi/engine/workflow-runner.js +509 -0
  221. package/dist/integrations/pi/engine/workflow-support.d.ts +33 -0
  222. package/dist/integrations/pi/engine/workflow-support.js +352 -0
  223. package/dist/integrations/pi/engine/workflows.d.ts +63 -2
  224. package/dist/integrations/pi/engine/workflows.js +5 -2
  225. package/dist/integrations/pi/startup-public.js +576 -554
  226. package/dist/integrations/pi/startup-public.manifest.json +1730 -984
  227. package/dist/integrations/pi/tui-runtime/adapter.js +10 -13
  228. package/dist/integrations/pi/tui-runtime/contracts.d.ts +11 -1
  229. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +1 -1
  230. package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +4 -7
  231. package/dist/integrations/pi/tui-runtime/index.d.ts +8 -6
  232. package/dist/integrations/pi/tui-runtime/index.js +4 -6
  233. package/dist/integrations/pi/tui-runtime/mouse-report-input.js +2 -4
  234. package/dist/integrations/pi/tui-runtime/overlay-geometry.d.ts +2 -2
  235. package/dist/integrations/pi/tui-runtime/overlay-geometry.js +4 -5
  236. package/dist/integrations/pi/tui-runtime/presentation-adapter.js +1 -1
  237. package/dist/native/darwin-arm64/manifest.json +1 -1
  238. package/dist/native/linux-x64/manifest.json +1 -1
  239. package/dist/native/win32-x64/manifest.json +2 -2
  240. package/dist/native/win32-x64/process-guardian.exe +0 -0
  241. package/dist/runtime-payload-inventory.json +51320 -46509
  242. package/dist/ui/apps/index.d.ts +5 -3
  243. package/dist/ui/apps/index.js +3 -3
  244. package/dist/ui/components/index.d.ts +47 -25
  245. package/dist/ui/components/index.js +25 -25
  246. package/dist/ui/components/prompt-input.js +1 -4
  247. package/dist/ui/settings/declarations.d.ts +119 -0
  248. package/dist/ui/settings/declarations.js +20 -11
  249. package/dist/ui/settings/index.d.ts +10 -6
  250. package/dist/ui/settings/index.js +5 -6
  251. package/dist/ui/settings/manager.d.ts +54 -0
  252. package/dist/ui/settings/{session.js → manager.js} +101 -9
  253. package/dist/ui/settings/sections.d.ts +2 -7
  254. package/docs/architecture/boundaries.md +4 -4
  255. package/docs/architecture/history-editor-provenance.md +1 -1
  256. package/docs/architecture/project-structure.md +6 -6
  257. package/docs/architecture/prompt-suggestions.md +1 -1
  258. package/docs/architecture/terminal-host-proof-gate.md +1 -1
  259. package/docs/architecture/terminal-host-spike-evidence.md +1 -1
  260. package/docs/architecture/toolchain.md +8 -0
  261. package/docs/architecture/ui-reference-provenance.md +4 -4
  262. package/docs/ci-release-runbook.md +7 -1
  263. package/docs/features/modal-content-interaction.md +2 -2
  264. package/docs/local-worktree-cleanup.md +4 -2
  265. package/docs/validation.md +6 -4
  266. package/package.json +7 -11
  267. package/bin/pi-tui.d.ts +0 -6
  268. package/bin/pi-tui.js +0 -28
  269. package/bin/sync-pi-tui-proxy.js +0 -97
  270. package/dist/cli/version.d.ts +0 -2
  271. package/dist/cli/version.js +0 -23
  272. package/dist/composition/agent-engine-bridge.d.ts +0 -3
  273. package/dist/composition/agent-engine-bridge.js +0 -108
  274. package/dist/composition/process.d.ts +0 -16
  275. package/dist/composition/process.js +0 -45
  276. package/dist/contracts/workspace/index.d.ts +0 -2
  277. package/dist/contracts/workspace/index.js +0 -2
  278. package/dist/contracts/workspace/model.d.ts +0 -248
  279. package/dist/contracts/workspace/model.js +0 -2
  280. package/dist/contracts/workspace/validation.d.ts +0 -13
  281. package/dist/contracts/workspace/validation.js +0 -335
  282. package/dist/features/owned-ui/customization.d.ts +0 -40
  283. package/dist/features/owned-ui/customization.js +0 -110
  284. package/dist/features/owned-ui/diagnostics.d.ts +0 -34
  285. package/dist/features/owned-ui/diagnostics.js +0 -95
  286. package/dist/features/workspace/capabilities.d.ts +0 -31
  287. package/dist/features/workspace/capabilities.js +0 -75
  288. package/dist/features/workspace/index.d.ts +0 -6
  289. package/dist/features/workspace/index.js +0 -6
  290. package/dist/features/workspace/presentation.d.ts +0 -30
  291. package/dist/features/workspace/presentation.js +0 -70
  292. package/dist/features/workspace/reconciliation.d.ts +0 -15
  293. package/dist/features/workspace/reconciliation.js +0 -82
  294. package/dist/features/workspace/reducer.d.ts +0 -43
  295. package/dist/features/workspace/reducer.js +0 -214
  296. package/dist/features/workspace/router.d.ts +0 -32
  297. package/dist/features/workspace/router.js +0 -125
  298. package/dist/features/workspace/store.d.ts +0 -18
  299. package/dist/features/workspace/store.js +0 -55
  300. package/dist/foundation/native-host-protocol/codec.d.ts +0 -13
  301. package/dist/foundation/native-host-protocol/codec.js +0 -58
  302. package/dist/foundation/native-host-protocol/evidence.d.ts +0 -67
  303. package/dist/foundation/native-host-protocol/evidence.js +0 -141
  304. package/dist/foundation/native-host-protocol/index.d.ts +0 -4
  305. package/dist/foundation/native-host-protocol/index.js +0 -4
  306. package/dist/foundation/native-host-protocol/messages.d.ts +0 -56
  307. package/dist/foundation/native-host-protocol/messages.js +0 -149
  308. package/dist/foundation/native-host-protocol/proof-gate.d.ts +0 -13
  309. package/dist/foundation/native-host-protocol/proof-gate.js +0 -33
  310. package/dist/foundation/structured-agent-runtime/backpressure.d.ts +0 -47
  311. package/dist/foundation/structured-agent-runtime/backpressure.js +0 -119
  312. package/dist/foundation/structured-agent-runtime/commands.d.ts +0 -53
  313. package/dist/foundation/structured-agent-runtime/commands.js +0 -165
  314. package/dist/foundation/structured-agent-runtime/handshake.d.ts +0 -41
  315. package/dist/foundation/structured-agent-runtime/handshake.js +0 -151
  316. package/dist/foundation/structured-agent-runtime/index.d.ts +0 -5
  317. package/dist/foundation/structured-agent-runtime/index.js +0 -5
  318. package/dist/foundation/structured-agent-runtime/reconnection.d.ts +0 -38
  319. package/dist/foundation/structured-agent-runtime/reconnection.js +0 -130
  320. package/dist/foundation/structured-agent-runtime/state.d.ts +0 -57
  321. package/dist/foundation/structured-agent-runtime/state.js +0 -119
  322. package/dist/integrations/pi/components/upstream/components/custom-entry.d.ts +0 -17
  323. package/dist/integrations/pi/components/upstream/components/custom-entry.js +0 -51
  324. package/dist/integrations/pi/components/upstream/components/first-time-setup.d.ts +0 -24
  325. package/dist/integrations/pi/components/upstream/components/first-time-setup.js +0 -105
  326. package/dist/integrations/pi/components/upstream/components/markdown-transform.d.ts +0 -2
  327. package/dist/integrations/pi/components/upstream/components/markdown-transform.js +0 -18
  328. package/dist/integrations/pi/engine/model-auth-integration.d.ts +0 -31
  329. package/dist/integrations/pi/engine/model-auth-integration.js +0 -62
  330. package/dist/integrations/pi/engine/resource-extension-integration.d.ts +0 -32
  331. package/dist/integrations/pi/engine/resource-extension-integration.js +0 -81
  332. package/dist/integrations/pi/engine/settings-effects.js +0 -249
  333. package/dist/integrations/pi/engine/settings-integration.d.ts +0 -43
  334. package/dist/integrations/pi/engine/settings-integration.js +0 -227
  335. package/dist/integrations/pi/engine/workflow-controllers.d.ts +0 -21
  336. package/dist/integrations/pi/engine/workflow-controllers.js +0 -49
  337. package/dist/integrations/pi/session-ui/index.d.ts +0 -5
  338. package/dist/integrations/pi/session-ui/index.js +0 -5
  339. package/dist/integrations/pi/session-ui/paste-executor.d.ts +0 -10
  340. package/dist/integrations/pi/session-ui/paste-preparation-client.d.ts +0 -16
  341. package/dist/integrations/pi/session-ui/route-host.d.ts +0 -5
  342. package/dist/integrations/pi/tui-runtime/conformance.d.ts +0 -18
  343. package/dist/integrations/pi/tui-runtime/conformance.js +0 -145
  344. package/dist/ui/components/prompt-row.d.ts +0 -14
  345. package/dist/ui/components/prompt-row.js +0 -20
  346. package/dist/ui/settings/session.d.ts +0 -36
  347. package/dist/ui/settings/store.d.ts +0 -29
  348. package/dist/ui/settings/store.js +0 -91
  349. package/dist/{integrations/pi/session-ui → app/session-shell}/clipboard-diagnostics.js +2 -2
  350. package/dist/{integrations/pi/session-ui → app/session-shell}/clipboard-image.d.ts +0 -0
  351. package/dist/{integrations/pi/session-ui → app/session-shell}/editor-hyperlink-budget.d.ts +0 -0
  352. package/dist/{integrations/pi/session-ui → app/session-shell}/editor-hyperlink-budget.js +0 -0
  353. package/dist/{integrations/pi/session-ui → app/session-shell}/image-preparation.d.ts +0 -0
  354. package/dist/{integrations/pi/session-ui → app/session-shell}/image-source.d.ts +0 -0
  355. package/dist/{integrations/pi/session-ui → app/session-shell}/image-worker.d.ts +0 -0
  356. package/dist/{integrations/pi/session-ui → app/session-shell}/paste-helper.d.ts +0 -0
  357. package/dist/{integrations/pi/session-ui → app/session-shell}/paste-text-preparation.js +0 -0
  358. package/dist/{integrations/pi/session-ui → app/session-shell}/paste-text-worker.d.ts +0 -0
  359. package/dist/{integrations/pi/session-ui → app/session-shell}/paste-text-worker.js +0 -0
  360. package/dist/{integrations/pi/session-ui/response-copy-helper.d.ts → app/session-shell/paste-types.js} +0 -0
  361. package/dist/{integrations/pi/session-ui → app/session-shell}/path-chip-presentation.js +0 -0
  362. package/dist/{integrations/pi/session-ui → app/session-shell}/quit-outro-effects.d.ts +0 -0
  363. package/dist/{integrations/pi/session-ui → app/session-shell}/quit-outro-effects.js +0 -0
  364. package/dist/{integrations/pi/session-ui → app/session-shell}/quit-outro.d.ts +0 -0
  365. package/dist/{integrations/pi/session-ui/route-host.js → app/session-shell/response-copy-helper.d.ts} +0 -0
  366. package/dist/{integrations/pi/session-ui → app/session-shell}/response-copy-protocol.js +0 -0
  367. package/dist/{integrations/pi/session-ui → app/session-shell}/stream-presentation-coalescer.d.ts +0 -0
  368. package/dist/{integrations/pi/session-ui → app/session-shell}/stream-presentation-coalescer.js +3 -3
  369. /package/dist/{integrations/pi/session-ui → app/session-shell}/text-paste.d.ts +0 -0
  370. /package/dist/{integrations/pi/session-ui → app/session-shell}/text-paste.js +0 -0
@@ -1,80 +1,23 @@
1
- import { execFile } from "node:child_process";
2
- import { existsSync, realpathSync } from "node:fs";
3
- import { mkdir, rm, writeFile } from "node:fs/promises";
4
- import { tmpdir } from "node:os";
5
- import { dirname, join, resolve } from "node:path";
6
- import { promisify } from "node:util";
7
- import { PRODUCT_IDENTITY } from "../../../product-identity.js";
8
- import { configureOwnedHttpDispatcher } from "./http-dispatcher.js";
9
- import { readPinnedCommandChangelog } from "./changelog.js";
10
- import { toolRenderingInput } from "./tool-rendering.js";
11
- import { TranscriptImageAssets } from "./transcript-image-assets.js";
12
- import { copyToClipboard, CredentialSynchronizationError, DefaultPackageManager, getAgentDir, ProjectTrustStore, SessionManager, VERSION, } from "../startup-public.js";
13
- import { OWNED_UI_EXTENSION_CONTRACT_VERSION, OWNED_UI_EXTENSION_RENDER_CALLBACKS, OWNED_UI_EXTENSION_UI_CALLBACKS, OWNED_UI_EXTENSION_UI_PROPERTIES, CONTEXTUAL_PROMPT_SUGGESTION_INSTRUCTION, acceptsTranscriptUpdate, transcriptToolState, assertOwnedUiCommand, assertOwnedUiExtensionUiPort, assertOwnedUiPromptSuggestionRequest, assertOwnedUiPromptSuggestionResult, assertOwnedUiSnapshot, normalizePromptSuggestionCandidate, } from "../../../contracts/owned-ui/index.js";
14
- import { PINNED_PI_SETTINGS_CALLBACKS, PINNED_PI_WORKFLOW_COMMAND_NAMES, } from "./workflows.js";
15
- import { createPiRuntimeIntegration } from "./runtime-integration.js";
16
- import { PiSessionCommandIntegration } from "./session-integration.js";
17
- import { observeCompactionProgress } from "./compaction-progress.js";
18
- import { PiSettingsIntegration } from "./settings-integration.js";
19
- import { PendingEngineDelivery } from "./pending-delivery.js";
1
+ import { PiTranscriptProjection } from "./transcript-projection.js";
2
+ import { isRecord, readModel, readStringArray, readThinkingLevel, stringProperty } from "./message-values.js";
3
+ import { getAgentDir } from "../startup-public.js";
4
+ import { assertOwnedUiSnapshot, } from "../../../contracts/owned-ui/index.js";
5
+ import { PiCommandDispatch } from "./command-dispatch.js";
6
+ import { PiEventDelivery } from "./event-delivery.js";
7
+ import { PiExtensionUiBinding } from "./extension-ui-binding.js";
8
+ import { PiPromptSuggestions } from "./prompt-suggestions.js";
9
+ import { PiResourceCatalog } from "./resource-catalog.js";
10
+ import { PiEngineRuntime } from "./session-runtime.js";
11
+ import { PiEngineSettings } from "./settings-port.js";
12
+ import { PiSessionEvents } from "./session-events.js";
13
+ import { readUsageView } from "./usage-view.js";
14
+ import { PiWorkflowContexts } from "./workflow-contexts.js";
15
+ import { PiWorkflowRunner } from "./workflow-runner.js";
16
+ import { defaultWorkflowHost } from "./workflow-support.js";
20
17
  /** Explicit flush failure when required delivery was interrupted rather than completed. */
21
18
  export class EngineDeliveryError extends Error {
22
19
  constructor() { super("Engine delivery did not complete"); this.name = "EngineDeliveryError"; }
23
20
  }
24
- const execFileAsync = promisify(execFile);
25
- /**
26
- * Engine events delivered before the queue hands the event loop a turn. Small enough
27
- * that a streaming burst never holds input, large enough that an ordinary turn is one
28
- * batch.
29
- */
30
- // Performance: deliver at most one engine event per event-loop turn. Transcript updates can
31
- // be expensive in long sessions; a larger synchronous batch starves terminal
32
- // input and makes an in-progress mouse selection appear frozen.
33
- const EVENT_DELIVERY_BATCH = 1;
34
- const AUTH_REFRESH_TIMEOUT_MS = 15_000;
35
- // Provenance: Pi 0.84.2 core/model-resolver.ts defaultModelPerProvider.
36
- const PINNED_DEFAULT_MODEL_BY_PROVIDER = Object.freeze({
37
- "amazon-bedrock": "us.anthropic.claude-opus-4-6-v1",
38
- "ant-ling": "Ring-2.6-1T",
39
- anthropic: "claude-opus-4-8",
40
- openai: "gpt-5.5",
41
- "azure-openai-responses": "gpt-5.4",
42
- "openai-codex": "gpt-5.5",
43
- radius: "auto",
44
- nvidia: "nvidia/nemotron-3-super-120b-a12b",
45
- deepseek: "deepseek-v4-pro",
46
- google: "gemini-3.1-pro-preview",
47
- "google-vertex": "gemini-3.1-pro-preview",
48
- "github-copilot": "gpt-5.4",
49
- openrouter: "moonshotai/kimi-k2.6",
50
- "vercel-ai-gateway": "zai/glm-5.1",
51
- xai: "grok-4.5",
52
- groq: "openai/gpt-oss-120b",
53
- cerebras: "zai-glm-4.7",
54
- zai: "glm-5.1",
55
- "zai-coding-cn": "glm-5.1",
56
- mistral: "devstral-medium-latest",
57
- minimax: "MiniMax-M2.7",
58
- "minimax-cn": "MiniMax-M2.7",
59
- moonshotai: "kimi-k2.6",
60
- "moonshotai-cn": "kimi-k2.6",
61
- huggingface: "moonshotai/Kimi-K2.6",
62
- fireworks: "accounts/fireworks/models/kimi-k2p6",
63
- together: "moonshotai/Kimi-K2.6",
64
- baseten: "zai-org/GLM-5.2",
65
- opencode: "kimi-k2.6",
66
- "opencode-go": "kimi-k2.6",
67
- "kimi-coding": "kimi-for-coding",
68
- "cloudflare-workers-ai": "@cf/moonshotai/kimi-k2.6",
69
- "cloudflare-ai-gateway": "workers-ai/@cf/moonshotai/kimi-k2.6",
70
- "qwen-token-plan": "qwen3.7-max",
71
- "qwen-token-plan-cn": "qwen3.7-max",
72
- "qwen-token-plan-individual": "qwen3.8-max",
73
- xiaomi: "mimo-v2.5-pro",
74
- "xiaomi-token-plan-cn": "mimo-v2.5-pro",
75
- "xiaomi-token-plan-ams": "mimo-v2.5-pro",
76
- "xiaomi-token-plan-sgp": "mimo-v2.5-pro",
77
- });
78
21
  const DEFAULT_SURFACE = {
79
22
  columns: 100,
80
23
  rows: 32,
@@ -83,22 +26,16 @@ const DEFAULT_SURFACE = {
83
26
  };
84
27
  /** Owns the pinned Pi session lifecycle and translates its events into the neutral agent-engine contract. */
85
28
  export class PiEngineAdapter {
86
- #runtimeFactory;
87
- #checkPackageUpdates;
88
- #cwd;
89
29
  #agentDir;
90
30
  #sessionId;
91
- #sessionPath;
92
- #sessionSelection;
93
- #sessionForkPrompt;
94
31
  #workflowHost;
95
- #clipboardWriter;
32
+ #engine;
96
33
  #workflowInteraction;
97
- #listeners = new Map();
98
- #runtime;
99
- #session;
100
- #unsubscribe;
101
- #sequence = 0;
34
+ #contexts;
35
+ #workflows;
36
+ #resources;
37
+ #settings;
38
+ #suggestions;
102
39
  #viewRevision = 0;
103
40
  #lifecycle = "starting";
104
41
  #editor = {
@@ -109,8 +46,6 @@ export class PiEngineAdapter {
109
46
  historyRevision: 0,
110
47
  submitEnabled: false,
111
48
  };
112
- #sessionGeneration = 0;
113
- #sessionBindingGeneration = 0;
114
49
  #status = {
115
50
  title: "Pi",
116
51
  workingMessage: null,
@@ -120,62 +55,161 @@ export class PiEngineAdapter {
120
55
  #terminal = DEFAULT_SURFACE;
121
56
  #activeModel = null;
122
57
  #thinkingLevel = "medium";
123
- #activeCommandIds = [];
124
- #completedCommands = new Map();
125
- #transcript = [];
126
- #transcriptIndex = new Map();
127
- #transcriptSnapshot;
128
- #messageBlockIds = new WeakMap();
129
- #messageFallbackIds = new Map();
130
- #toolBlockIds = new Map();
131
- #transcriptImageAssets = new TranscriptImageAssets();
58
+ #projection = new PiTranscriptProjection({
59
+ retryAttempt: () => this.#engine.session?.retryAttempt ?? 0,
60
+ blockChanged: block => this.#emitEvent({ type: "transcript-block", block }),
61
+ });
132
62
  #usageCache;
133
- #nextBlockSequence = 0;
134
63
  #diagnostics = [];
135
- #eventQueue = new PendingEngineDelivery(event => this.#transcriptImageAssets.retainEvent(event));
136
- #eventQueueProcessing;
137
- #overload;
138
- #overloads = 0;
139
- #deliveryFailed = false;
64
+ // Invariant: every port closes over adapter state read at call time, so delivery is built before a session binds.
65
+ #delivery = new PiEventDelivery({
66
+ sessionId: () => this.#sessionId,
67
+ sessionGeneration: () => this.#engine.generation,
68
+ transcriptBlock: id => this.#projection.block(id),
69
+ retainEvent: event => this.#projection.assets.retainEvent(event),
70
+ isPendingCommand: correlationId => this.#commands.isPending(correlationId),
71
+ cancelForOverload: () => this.#cancelForOverload(),
72
+ reconcileOverload: cancelled => this.#reconcileOverload(cancelled),
73
+ listenerFailed: message => { this.#recordDiagnostic("warning", "event-listener", message, true); },
74
+ });
140
75
  #admissionStopped = false;
141
76
  #runningCommands = 0;
142
- #invalidatedEvents = 0;
143
- #pendingCommands = new Map();
144
- #reservedOutcomes = new Map();
145
- #pendingWorkflows = new Set();
146
- #agentRunActive = false;
147
- #agentRunSequence = 0;
148
- #assistantResponseSequence = 0;
149
- #statusKind = null;
150
- #sessionCommands;
151
- #compactionProgress = null;
152
- #gitBranch = null;
153
- #extensionUi;
154
- #extensionShutdown;
155
- #extensionBound = false;
77
+ #commands;
78
+ #events = new PiSessionEvents(this.#projection, {
79
+ sessionGeneration: () => this.#engine.generation,
80
+ sessionMessages: () => this.#engine.session?.messages ?? [],
81
+ activeModel: () => this.#activeModel,
82
+ replaceTranscript: blocks => { this.#setTranscript(blocks); },
83
+ emit: value => { this.#emitEvent(value); },
84
+ emitView: () => { this.#emitView(); },
85
+ enterWork: message => {
86
+ const wasBusy = this.#lifecycle === "busy";
87
+ this.#lifecycle = "busy";
88
+ this.#status = { ...this.#status, workingMessage: message, workingProgress: null };
89
+ if (!wasBusy)
90
+ this.#emitEvent({ type: "session-lifecycle", lifecycle: "busy", reason: null });
91
+ this.#emitEvent({ type: "status", status: this.#status });
92
+ },
93
+ leaveWork: () => {
94
+ this.#lifecycle = "ready";
95
+ this.#status = { ...this.#status, workingMessage: null, workingProgress: null };
96
+ this.#emitEvent({ type: "session-lifecycle", lifecycle: "ready", reason: null });
97
+ this.#emitEvent({ type: "status", status: this.#status });
98
+ },
99
+ workProgress: percent => {
100
+ if (this.#status.workingProgress === percent)
101
+ return;
102
+ this.#status = { ...this.#status, workingProgress: percent };
103
+ this.#emitEvent({ type: "status", status: this.#status });
104
+ },
105
+ queueChanged: queuedSubmissions => {
106
+ this.#editor = { ...this.#editor, queuedSubmissions: [...queuedSubmissions], historyRevision: this.#editor.historyRevision + 1 };
107
+ this.#emitEvent({ type: "editor-state", editor: this.#editor });
108
+ },
109
+ thinkingLevelChanged: level => { this.#thinkingLevel = level; },
110
+ usageInvalidated: () => { this.#usageCache = undefined; },
111
+ compactionStarted: () => { this.#engine.compactionStarted(); },
112
+ compactionEnded: () => { this.#engine.compactionEnded(); },
113
+ deliverQueuedAfterCompaction: () => { this.#engine.deliverQueuedAfterCompaction(); },
114
+ });
115
+ #extensions = new PiExtensionUiBinding({
116
+ session: () => this.#engine.session,
117
+ diagnostic: (severity, code, message, recoverable) => { this.#addDiagnostic(severity, code, message, recoverable); },
118
+ emitView: () => { this.#emitView(); },
119
+ });
156
120
  #disposed = false;
157
- #settingsProductMode;
158
- #projectTrustPrompt;
159
- #settingsIntegration;
160
- #settingsIntegrationManager;
161
- #availableThemes;
162
121
  constructor(options = {}) {
163
- this.#cwd = options.cwd ?? process.cwd();
164
122
  this.#agentDir = options.agentDir ?? getAgentDir();
165
123
  this.#sessionId = options.sessionId ?? "owned-session-1";
166
- this.#sessionPath = options.sessionPath;
167
- this.#sessionSelection = options.sessionSelection;
168
- this.#sessionForkPrompt = options.sessionForkPrompt;
169
- this.#runtimeFactory = options.createRuntime ?? createDefaultPiRuntime;
170
- this.#checkPackageUpdates = options.checkPackageUpdates
171
- ?? (options.createRuntime
172
- ? async () => []
173
- : settingsManager => checkDefaultPiPackageUpdates(this.#cwd, this.#agentDir, settingsManager));
174
124
  this.#workflowHost = options.workflowHost ?? defaultWorkflowHost();
175
- this.#availableThemes = options.availableThemes ?? null;
176
- this.#settingsProductMode = options.settingsProductMode ?? "bare";
177
- this.#projectTrustPrompt = options.projectTrustPrompt;
178
125
  this.#workflowInteraction = { prompt: async () => null, notify() { } };
126
+ this.#engine = new PiEngineRuntime({
127
+ cwd: options.cwd ?? process.cwd(),
128
+ agentDir: this.#agentDir,
129
+ sessionId: this.#sessionId,
130
+ sessionPath: options.sessionPath,
131
+ sessionSelection: options.sessionSelection,
132
+ sessionForkPrompt: options.sessionForkPrompt,
133
+ projectTrustPrompt: options.projectTrustPrompt,
134
+ createRuntime: options.createRuntime,
135
+ checkPackageUpdates: options.checkPackageUpdates,
136
+ host: this.#workflowHost,
137
+ }, {
138
+ disposed: () => this.#disposed,
139
+ started: runtime => {
140
+ this.#terminal = {
141
+ ...this.#terminal,
142
+ hardwareCursor: runtime.services.settingsManager?.getShowHardwareCursor?.() ?? this.#terminal.hardwareCursor,
143
+ };
144
+ },
145
+ rebindBlocked: () => this.#delivery.overloaded || this.#admissionStopped || this.#disposed,
146
+ sessionReplacing: () => { this.#commands.cancelPending(["new-session", "resume-session"]); },
147
+ sessionReplaced: session => { this.#sessionReplaced(session); },
148
+ rebound: () => { this.#emitView(); },
149
+ event: event => { this.#events.handle(event); },
150
+ compactionProgress: percent => { this.#events.compactionProgress(percent); },
151
+ diagnostic: (severity, code, message, recoverable) => { this.#addDiagnostic(severity, code, message, recoverable); },
152
+ emitView: () => { this.#emitView(); },
153
+ });
154
+ this.#contexts = new PiWorkflowContexts({ agentDir: this.#agentDir }, {
155
+ cwd: () => this.#engine.cwd,
156
+ session: () => this.#engine.session,
157
+ runtime: () => this.#engine.runtime,
158
+ disposed: () => this.#disposed,
159
+ activeModel: () => this.#activeModel,
160
+ emitView: () => { this.#emitView(); },
161
+ });
162
+ this.#workflows = new PiWorkflowRunner({ agentDir: this.#agentDir, host: this.#workflowHost, contexts: this.#contexts }, {
163
+ session: () => this.#engine.session,
164
+ runtime: () => this.#engine.runtime,
165
+ disposed: () => this.#disposed,
166
+ sessionGeneration: () => this.#engine.generation,
167
+ interaction: () => this.#workflowInteraction,
168
+ admissionStopped: () => this.#delivery.overloaded || this.#admissionStopped || this.#disposed,
169
+ pendingCommandCount: () => this.#commands.pendingCount,
170
+ beginRunning: () => { this.#runningCommands++; },
171
+ endRunning: () => { this.#runningCommands--; },
172
+ activeModel: () => this.#activeModel,
173
+ setActiveModel: model => { this.#activeModel = model; },
174
+ thinkingLevel: () => this.#thinkingLevel,
175
+ setThinkingLevel: level => { this.#thinkingLevel = level; },
176
+ emitView: () => { this.#emitView(); },
177
+ reconcileActiveModelAvailability: () => { this.#reconcileActiveModelAvailability(); },
178
+ bindExtensionUiToSession: () => this.#extensions.rebind(),
179
+ applyPinnedSetting: selection => this.#settings.applyPinnedSetting(selection),
180
+ snapshot: () => this.snapshot(),
181
+ dispose: () => this.dispose(),
182
+ });
183
+ this.#commands = new PiCommandDispatch({
184
+ sessionId: () => this.#sessionId,
185
+ notRunning: () => this.#disposed || !this.#engine.runtime || !this.#engine.session,
186
+ admissionStopped: () => this.#delivery.overloaded || this.#admissionStopped,
187
+ pendingWorkflowCount: () => this.#workflows.pendingCount,
188
+ sessionGeneration: () => this.#engine.generation,
189
+ beginRunning: () => { this.#runningCommands++; },
190
+ endRunning: () => { this.#runningCommands--; },
191
+ perform: command => this.#perform(command),
192
+ emit: value => { this.#emitEvent(value); },
193
+ emitView: () => { this.#emitView(); },
194
+ diagnostic: (severity, code, message, recoverable) => { this.#addDiagnostic(severity, code, message, recoverable); },
195
+ });
196
+ this.#resources = new PiResourceCatalog({ contexts: this.#contexts }, {
197
+ session: () => this.#engine.session,
198
+ runtime: () => this.#engine.runtime,
199
+ });
200
+ this.#settings = new PiEngineSettings({ availableThemes: options.availableThemes ?? null, productMode: options.settingsProductMode ?? "bare" }, {
201
+ runtime: () => this.#engine.runtime,
202
+ requireSession: () => this.#requireWorkflowSession(),
203
+ thinkingLevelChanged: level => { this.#thinkingLevel = level; this.#emitView(); },
204
+ emitView: () => { this.#emitView(); },
205
+ });
206
+ this.#suggestions = new PiPromptSuggestions({
207
+ session: () => this.#engine.session,
208
+ runtime: () => this.#engine.runtime,
209
+ activeModel: () => this.#activeModel,
210
+ identity: () => ({ sessionId: this.#sessionId, sessionGeneration: this.#engine.generation, runSequence: this.#events.runSequence, responseSequence: this.#events.responseSequence }),
211
+ unavailable: () => this.#disposed || this.#delivery.overloaded || this.#admissionStopped,
212
+ });
179
213
  }
180
214
  setWorkflowInteractionHost(interaction) {
181
215
  this.#workflowInteraction = interaction;
@@ -184,855 +218,170 @@ export class PiEngineAdapter {
184
218
  return this.#sessionId;
185
219
  }
186
220
  get sessionGeneration() {
187
- return this.#sessionGeneration;
221
+ return this.#engine.generation;
188
222
  }
189
223
  /** Actual session replacements, excluding delivery-only invalidation of callbacks. */
190
- get sessionBindingGeneration() { return this.#sessionBindingGeneration; }
224
+ get sessionBindingGeneration() { return this.#engine.bindingGeneration; }
191
225
  get cwd() {
192
- return this.#runtime?.cwd ?? this.#cwd;
226
+ return this.#engine.cwd;
193
227
  }
194
228
  get agentDir() {
195
229
  return this.#agentDir;
196
230
  }
197
231
  resolveTranscriptImage(assetId) {
198
- return this.#transcriptImageAssets.resolve(assetId);
232
+ return this.#projection.assets.resolve(assetId);
199
233
  }
200
234
  currentSessionFile() {
201
- const value = this.#session?.sessionManager?.getSessionFile?.();
202
- return typeof value === "string" && value.length > 0 ? value : null;
235
+ return this.#engine.currentSessionFile();
203
236
  }
204
237
  currentSessionResumeMetadata() {
205
- const manager = this.#session?.sessionManager;
206
- if (manager === undefined
207
- || typeof manager.isPersisted !== "function"
208
- || typeof manager.getSessionId !== "function"
209
- || typeof manager.getSessionDir !== "function"
210
- || typeof manager.usesDefaultSessionDir !== "function"
211
- || manager.isPersisted() !== true
212
- || !existsSync(this.currentSessionFile() ?? ""))
213
- return null;
214
- const sessionId = manager.getSessionId();
215
- const sessionDir = manager.getSessionDir();
216
- if (!sessionId || !sessionDir)
217
- return null;
218
- return { sessionId, sessionDir, usesDefaultSessionDir: manager.usesDefaultSessionDir() };
238
+ return this.#engine.currentSessionResumeMetadata();
219
239
  }
220
240
  get disposed() {
221
241
  return this.#disposed;
222
242
  }
223
243
  suggestionReasoningPolicy() {
224
- const session = this.#session;
225
- if (!session?.model)
226
- return "unavailable";
227
- if (!session.model.reasoning)
228
- return "ordinary";
229
- // Performance: the run's own level keeps the provider's thinking parameters, and the cached prefix, identical.
230
- return readSuggestionReasoning(session.thinkingLevel);
231
- }
232
- async generate(request) {
233
- assertOwnedUiPromptSuggestionRequest(request);
234
- const identity = request.identity;
235
- const session = this.#session;
236
- const runtime = this.#runtime;
237
- const activeModel = this.#activeModel;
238
- if (this.#disposed || this.#overload !== undefined || this.#admissionStopped || session === undefined || runtime === undefined || request.signal.aborted
239
- || identity.sessionId !== this.#sessionId
240
- || identity.sessionGeneration !== this.#sessionGeneration
241
- || identity.runSequence !== this.#agentRunSequence
242
- || identity.responseSequence !== this.#assistantResponseSequence
243
- || activeModel === null
244
- || identity.model.providerId !== activeModel.providerId
245
- || identity.model.modelId !== activeModel.modelId) {
246
- return { identity, outcome: request.signal.aborted ? "cancelled" : "unavailable", text: null };
247
- }
248
- const model = session.model;
249
- const agent = session.agent;
250
- const agentState = agent.state;
251
- const policy = this.suggestionReasoningPolicy();
252
- if (model === undefined || policy === "unavailable" || typeof runtime.services.modelRuntime.completeSimple !== "function") {
253
- return { identity, outcome: "unavailable", text: null };
254
- }
255
- // Performance: mirror the primary loop's request shape so the provider serves the conversation prefix
256
- // from the run's prompt cache. `onResponse` stays out: extensions must not see a suggestion as a response.
257
- const reasoning = policy === "ordinary" || policy === "off" ? undefined : policy;
258
- let response;
259
- try {
260
- const transformed = typeof agent.transformContext === "function"
261
- ? await agent.transformContext(agentState.messages, request.signal)
262
- : agentState.messages;
263
- const messages = typeof agent.convertToLlm === "function"
264
- ? await agent.convertToLlm(transformed)
265
- : transformed.filter(message => message.role === "user" || message.role === "assistant" || message.role === "toolResult");
266
- response = await runtime.services.modelRuntime.completeSimple(model, {
267
- systemPrompt: agentState.systemPrompt,
268
- messages: [
269
- ...messages,
270
- { role: "user", content: CONTEXTUAL_PROMPT_SUGGESTION_INSTRUCTION, timestamp: Date.now() },
271
- ],
272
- tools: agentState.tools,
273
- }, {
274
- signal: request.signal,
275
- ...(reasoning === undefined ? {} : { reasoning }),
276
- ...(agent.sessionId === undefined ? {} : { sessionId: agent.sessionId }),
277
- ...(agent.thinkingBudgets === undefined ? {} : { thinkingBudgets: agent.thinkingBudgets }),
278
- ...(agent.transport === undefined ? {} : { transport: agent.transport }),
279
- ...(agent.onPayload === undefined ? {} : { onPayload: agent.onPayload }),
280
- });
281
- }
282
- catch {
283
- return { identity, outcome: request.signal.aborted ? "cancelled" : "provider-failure", text: null };
284
- }
285
- if (request.signal.aborted || (isRecord(response) && response.stopReason === "aborted")) {
286
- return { identity, outcome: "cancelled", text: null };
287
- }
288
- if (!isRecord(response) || stringValue(response.errorMessage) !== undefined || response.stopReason === "error") {
289
- return { identity, outcome: "provider-failure", text: null };
290
- }
291
- const content = Array.isArray(response.content) ? response.content : [];
292
- if (response.stopReason === "length" || content.some(block => isRecord(block) && block.type === "toolCall")) {
293
- return { identity, outcome: "rejected", text: null };
294
- }
295
- // Security: validate all text, not just the first block of a multi-part response.
296
- const rawText = content.filter(block => isRecord(block) && block.type === "text")
297
- .map(block => stringValue(block.text) ?? "").join("\n");
298
- const text = normalizePromptSuggestionCandidate(rawText);
299
- const result = text === null
300
- ? { identity, outcome: rawText.trim() ? "rejected" : "empty", text: null }
301
- : { identity, outcome: "candidate", text };
302
- assertOwnedUiPromptSuggestionResult(result);
303
- return result;
244
+ return this.#suggestions.reasoningPolicy();
245
+ }
246
+ generate(request) {
247
+ return this.#suggestions.generate(request);
304
248
  }
305
249
  async start() {
306
- if (this.#runtime)
250
+ if (this.#engine.started)
307
251
  return this.view();
308
- const runtime = await this.#runtimeFactory({
309
- cwd: this.#cwd,
310
- agentDir: this.#agentDir,
311
- sessionId: this.#sessionId,
312
- ...(this.#sessionPath === undefined ? {} : { sessionPath: this.#sessionPath }),
313
- ...(this.#sessionSelection === undefined ? {} : { sessionSelection: this.#sessionSelection }),
314
- ...(this.#sessionForkPrompt === undefined ? {} : { sessionForkPrompt: this.#sessionForkPrompt }),
315
- ...(this.#projectTrustPrompt === undefined ? {} : { projectTrustPrompt: this.#projectTrustPrompt }),
316
- }).catch(error => {
252
+ await this.#engine.start().catch(error => {
317
253
  this.#lifecycle = "failed";
318
254
  this.#addDiagnostic("error", "engine-startup", error instanceof Error ? error.message : String(error), false);
319
255
  throw error;
320
256
  });
321
- this.#runtime = runtime;
322
- this.#cwd = runtime.cwd ?? this.#cwd;
323
- this.#gitBranch = await readGitBranch(this.#cwd);
324
- this.#terminal = {
325
- ...this.#terminal,
326
- hardwareCursor: runtime.services.settingsManager?.getShowHardwareCursor?.() ?? this.#terminal.hardwareCursor,
327
- };
328
- runtime.setRebindSession(async (session) => {
329
- if (this.#overload !== undefined || this.#admissionStopped || this.#disposed)
330
- return;
331
- this.#bindSession(session);
332
- this.#emitView();
333
- });
334
- for (const diagnostic of [...runtime.diagnostics, ...runtime.services.diagnostics]) {
335
- this.#addDiagnostic(diagnostic.type === "error" ? "error" : diagnostic.type === "warning" ? "warning" : "info", "engine-startup", diagnostic.message, diagnostic.type !== "error");
336
- }
337
- this.#bindSession(runtime.session);
338
- await this.#announceChangelog(runtime.services.settingsManager);
339
257
  this.#lifecycle = "ready";
340
258
  this.#editor = { ...this.#editor, submitEnabled: true };
341
259
  this.#emitEvent({ type: "session-lifecycle", lifecycle: "ready", reason: null });
342
260
  this.#emitView();
343
- void this.#announcePackageUpdates(runtime.services.settingsManager);
261
+ void this.#engine.announcePackageUpdates();
344
262
  return this.view();
345
263
  }
346
- async #announceChangelog(settingsManager) {
347
- if (!settingsManager || typeof settingsManager.getLastChangelogVersion !== "function"
348
- || typeof settingsManager.setLastChangelogVersion !== "function")
349
- return;
350
- if ((this.#session?.messages.length ?? 0) > 0)
351
- return;
352
- const lastVersion = settingsManager.getLastChangelogVersion();
353
- if (lastVersion === VERSION)
354
- return;
355
- if (!lastVersion) {
356
- settingsManager.setLastChangelogVersion(VERSION);
357
- await settingsManager.flush();
358
- return;
359
- }
360
- const markdown = await this.#workflowHost.readChangelog(lastVersion).catch(() => "");
361
- if (markdown.trim().length > 0) {
362
- this.#addDiagnostic("info", settingsManager.getCollapseChangelog() ? "changelog-collapsed" : "changelog-expanded", markdown, true);
363
- settingsManager.setLastChangelogVersion(VERSION);
364
- await settingsManager.flush();
365
- }
366
- }
367
- async #announcePackageUpdates(settingsManager) {
368
- if (process.env.PI_OFFLINE)
369
- return;
370
- let updates;
371
- try {
372
- updates = await this.#checkPackageUpdates(settingsManager);
373
- }
374
- catch {
375
- return;
376
- }
377
- if (this.#disposed || updates.length === 0)
378
- return;
379
- const packages = updates.map(name => `- ${name}`).join("\n");
380
- this.#addDiagnostic("info", "package-updates", `Package updates are available. Run ${PRODUCT_IDENTITY.commandName} pi update --extensions\nPackages:\n${packages}`, true);
381
- this.#emitView();
382
- }
383
264
  onEvent(listener) {
384
- const initial = this.#event({ type: "session-view", view: this.view() });
385
- this.#listeners.set(listener, initial.sequence);
386
- listener(initial);
387
- return () => this.#listeners.delete(listener);
265
+ return this.#delivery.subscribe(listener, this.#delivery.stamp({ type: "session-view", view: this.view() }));
388
266
  }
389
267
  /** Developer-only pressure evidence; never mirrored into visible diagnostic/status arrays. */
390
268
  deliveryDiagnostics() {
391
- return { ...this.#eventQueue.diagnostics(), overloads: this.#overloads, recovering: this.#overload !== undefined,
392
- pendingCommands: this.#pendingCommands.size + this.#pendingWorkflows.size, reservedOutcomes: this.#reservedOutcomes.size, invalidatedEvents: this.#invalidatedEvents };
269
+ return { ...this.#delivery.diagnostics(), pendingCommands: this.#commands.pendingCount + this.#workflows.pendingCount };
393
270
  }
394
271
  async flushEvents() {
395
- const failed = this.#deliveryFailed;
396
- while (this.#eventQueueProcessing)
397
- await this.#eventQueueProcessing;
398
- if (failed || this.#deliveryFailed || this.#disposed && this.#lifecycle !== "stopped") {
399
- this.#deliveryFailed = false;
272
+ const failed = this.#delivery.failed;
273
+ await this.#delivery.settle();
274
+ if (failed || this.#delivery.failed || this.#disposed && this.#lifecycle !== "stopped") {
275
+ this.#delivery.clearFailure();
400
276
  throw new EngineDeliveryError();
401
277
  }
402
278
  }
403
279
  nonVisualResources() {
404
- const loader = this.#runtime?.services.resourceLoader;
405
- if (!loader)
406
- return [];
407
- const resources = [];
408
- const skills = collectionResult(loader.getSkills(), "skills");
409
- for (const [index, skill] of skills.values.entries()) {
410
- resources.push({
411
- kind: "skill",
412
- id: `skill-${index}-${stringProperty(skill, "name") ?? "unknown"}`,
413
- label: stringProperty(skill, "name") ?? "Unnamed skill",
414
- sourcePath: stringProperty(skill, "filePath") ?? stringProperty(skill, "path") ?? stringProperty(skill, "location") ?? null,
415
- diagnostic: null,
416
- });
417
- }
418
- resources.push(...skills.diagnostics.map((diagnostic, index) => ({
419
- kind: "skill",
420
- id: `skill-diagnostic-${index}`,
421
- label: "Skill diagnostic",
422
- sourcePath: null,
423
- diagnostic,
424
- })));
425
- const prompts = collectionResult(loader.getPrompts(), "prompts");
426
- for (const [index, prompt] of prompts.values.entries()) {
427
- resources.push({
428
- kind: "prompt-template",
429
- id: `prompt-${index}-${stringProperty(prompt, "name") ?? "unknown"}`,
430
- label: stringProperty(prompt, "name") ?? "Prompt template",
431
- sourcePath: stringProperty(prompt, "filePath") ?? stringProperty(prompt, "path") ?? null,
432
- diagnostic: null,
433
- });
434
- }
435
- resources.push(...prompts.diagnostics.map((diagnostic, index) => ({
436
- kind: "prompt-template",
437
- id: `prompt-diagnostic-${index}`,
438
- label: "Prompt diagnostic",
439
- sourcePath: null,
440
- diagnostic,
441
- })));
442
- const agentsFilesResult = loader.getAgentsFiles();
443
- const agentsFiles = unknownArray(isRecord(agentsFilesResult) ? agentsFilesResult.agentsFiles : undefined);
444
- for (const [index, file] of agentsFiles.entries()) {
445
- resources.push({
446
- kind: "agent-context",
447
- id: `agent-context-${index}`,
448
- label: "Agent context",
449
- sourcePath: stringProperty(file, "path") ?? null,
450
- diagnostic: null,
451
- });
452
- }
453
- const systemPrompt = loader.getSystemPromptSource();
454
- if (isRecord(systemPrompt) && typeof systemPrompt.path === "string") {
455
- resources.push({
456
- kind: "system-prompt",
457
- id: "system-prompt",
458
- label: "System prompt",
459
- sourcePath: systemPrompt.path,
460
- diagnostic: null,
461
- });
462
- }
463
- for (const [index, source] of unknownArray(loader.getAppendSystemPromptSources()).entries()) {
464
- resources.push({
465
- kind: "system-prompt",
466
- id: `append-system-prompt-${index}`,
467
- label: "Append system prompt",
468
- sourcePath: stringProperty(source, "path") ?? null,
469
- diagnostic: null,
470
- });
471
- }
472
- if (loader.getThemes !== undefined) {
473
- const themes = collectionResult(loader.getThemes(), "themes");
474
- for (const [index, theme] of themes.values.entries()) {
475
- const sourcePath = stringProperty(theme, "sourcePath");
476
- if (!sourcePath)
477
- continue;
478
- resources.push({
479
- kind: "theme",
480
- id: `theme-${index}-${stringProperty(theme, "name") ?? "unknown"}`,
481
- label: stringProperty(theme, "name") ?? compactResourceLabel(sourcePath),
482
- sourcePath,
483
- diagnostic: null,
484
- });
485
- }
486
- resources.push(...themes.diagnostics.map((diagnostic, index) => ({
487
- kind: "theme",
488
- id: `theme-diagnostic-${index}`,
489
- label: "Theme diagnostic",
490
- sourcePath: null,
491
- diagnostic,
492
- })));
493
- }
494
- return resources;
280
+ return this.#resources.nonVisualResources();
495
281
  }
496
282
  extensionResources() {
497
- const loader = this.#runtime?.services.resourceLoader;
498
- if (loader?.getExtensions === undefined)
499
- return [];
500
- let result;
501
- try {
502
- result = loader.getExtensions();
503
- }
504
- catch (error) {
505
- return [extensionResourceDiagnostic(0, null, `Extension discovery failed: ${error instanceof Error ? error.message : String(error)}`)];
506
- }
507
- if (!isRecord(result))
508
- return [extensionResourceDiagnostic(0, null, "Extension discovery returned a malformed result")];
509
- const resources = [];
510
- if (!Array.isArray(result.extensions)) {
511
- resources.push(extensionResourceDiagnostic(resources.length, null, "Extension discovery returned a malformed extensions collection"));
512
- }
513
- else {
514
- for (const extension of result.extensions) {
515
- if (!isRecord(extension) || typeof extension.path !== "string" || extension.path.length === 0
516
- || typeof extension.resolvedPath !== "string" || extension.resolvedPath.length === 0
517
- || (extension.hidden !== undefined && typeof extension.hidden !== "boolean")) {
518
- resources.push(extensionResourceDiagnostic(resources.length, null, "Extension discovery returned malformed extension metadata"));
519
- continue;
520
- }
521
- resources.push({
522
- kind: "extension",
523
- id: `extension-${resources.length}`,
524
- sourcePath: extension.path,
525
- resolvedPath: extension.resolvedPath,
526
- sourceInfo: extensionSourceSummary(extension.sourceInfo),
527
- loaded: true,
528
- hidden: extension.hidden === true,
529
- diagnostic: null,
530
- });
531
- }
532
- }
533
- if (!Array.isArray(result.errors)) {
534
- resources.push(extensionResourceDiagnostic(resources.length, null, "Extension discovery returned a malformed errors collection"));
535
- }
536
- else {
537
- for (const error of result.errors) {
538
- if (!isRecord(error) || typeof error.path !== "string" || typeof error.error !== "string") {
539
- resources.push(extensionResourceDiagnostic(resources.length, null, "Extension discovery returned malformed error metadata"));
540
- continue;
541
- }
542
- resources.push(extensionResourceDiagnostic(resources.length, error.path, error.error));
543
- }
544
- }
545
- return resources;
546
- }
547
- visualExtensionSupport() {
548
- return {
549
- available: this.#extensionBound,
550
- contractComplete: true,
551
- contractVersion: OWNED_UI_EXTENSION_CONTRACT_VERSION,
552
- binding: this.#extensionBound ? "bound" : "unbound",
553
- uiCallbacks: OWNED_UI_EXTENSION_UI_CALLBACKS,
554
- uiProperties: OWNED_UI_EXTENSION_UI_PROPERTIES,
555
- renderCallbacks: OWNED_UI_EXTENSION_RENDER_CALLBACKS,
556
- diagnostic: this.#extensionBound
557
- ? `Pinned public extension UI lifecycle is bound through the ${PRODUCT_IDENTITY.displayName}-owned bridge.`
558
- : `The complete ${PRODUCT_IDENTITY.displayName}-owned extension UI contract is available; the active session has not been bound to the owned UI bridge.`,
559
- };
560
- }
561
- async bindExtensionUi(ui, shutdown) {
562
- assertPiExtensionUiContext(ui);
563
- this.#extensionUi = ui;
564
- this.#extensionShutdown = shutdown;
565
- await this.#bindExtensionUiToSession();
566
- }
567
- async unbindExtensionUi() {
568
- this.#extensionUi = undefined;
569
- this.#extensionShutdown = undefined;
570
- this.#extensionBound = false;
283
+ return this.#resources.extensionResources();
571
284
  }
572
285
  workflowAutocompleteCommands() {
573
- const commands = [
574
- {
575
- name: "model",
576
- description: "Select model (opens selector UI)",
577
- argumentHint: "<provider/model>",
578
- argumentOptions: this.#modelOptions(),
579
- source: "builtin",
580
- },
581
- {
582
- name: "login",
583
- description: "Configure provider authentication",
584
- argumentHint: "<provider>",
585
- argumentOptions: this.#loginOptions().map(option => ({ ...option, id: option.id.split(":").at(-1) ?? option.id })),
586
- source: "builtin",
587
- },
588
- ];
589
- const usedNames = new Set(PINNED_PI_WORKFLOW_COMMAND_NAMES);
590
- const loader = this.#runtime?.services.resourceLoader;
591
- if (!loader)
592
- return commands;
593
- const prompts = collectionResult(loader.getPrompts(), "prompts");
594
- for (const prompt of prompts.values) {
595
- const name = stringProperty(prompt, "name");
596
- if (!name || usedNames.has(name))
597
- continue;
598
- const argumentHint = stringProperty(prompt, "argumentHint");
599
- commands.push({
600
- name,
601
- description: stringProperty(prompt, "description") ?? "Prompt template",
602
- ...(argumentHint === undefined ? {} : { argumentHint }),
603
- source: "prompt",
604
- });
605
- usedNames.add(name);
606
- }
607
- const settings = this.#runtime?.services.settingsManager;
608
- const skillsEnabled = settings?.getEnableSkillCommands?.() !== false;
609
- if (skillsEnabled) {
610
- const skills = collectionResult(loader.getSkills(), "skills");
611
- for (const skill of skills.values) {
612
- const resourceName = stringProperty(skill, "name");
613
- const name = resourceName ? `skill:${resourceName}` : undefined;
614
- if (!name || usedNames.has(name))
615
- continue;
616
- commands.push({ name, description: stringProperty(skill, "description") ?? "Skill", source: "skill" });
617
- usedNames.add(name);
618
- }
619
- }
620
- const extensionCommands = this.#session?.extensionRunner?.getRegisteredCommands?.();
621
- if (Array.isArray(extensionCommands)) {
622
- const registered = extensionCommands.filter(isRecord);
623
- for (const command of registered) {
624
- const name = stringProperty(command, "invocationName") ?? stringProperty(command, "name");
625
- if (!name || usedNames.has(name) || isPiPrefixedCompatibilityAlias(command, registered))
626
- continue;
627
- commands.push({ name, description: stringProperty(command, "description") ?? "Extension command", source: "extension" });
628
- usedNames.add(name);
629
- }
630
- }
631
- return commands;
632
- }
633
- async cycleModelWorkflow(direction) {
634
- try {
635
- const session = this.#requireWorkflowSession();
636
- const result = await requireCapability(session.cycleModel, "cycleModel").call(session, direction);
637
- if (!isRecord(result) || !isRecord(result.model)) {
638
- const scoped = Array.isArray(session.scopedModels) && session.scopedModels.length > 0;
639
- return workflowResult("model", "completed", scoped ? "Only one model in scope" : "Only one model available", undefined, "status");
640
- }
641
- const model = readModel(result.model);
642
- if (model)
643
- this.#activeModel = model;
644
- if (result.thinkingLevel !== undefined)
645
- this.#thinkingLevel = readThinkingLevel(result.thinkingLevel);
646
- this.#emitView();
647
- const modelName = stringProperty(result.model, "name") ?? stringProperty(result.model, "id") ?? "model";
648
- const reasoning = result.model.reasoning === true;
649
- const thinking = this.#thinkingLevel;
650
- const suffix = reasoning && thinking !== "off" ? ` (thinking: ${thinking})` : "";
651
- return workflowResult("model", "completed", `Switched to ${modelName}${suffix}`, undefined, "status");
652
- }
653
- catch (error) {
654
- return workflowResult("model", "failed", error instanceof Error ? error.message : String(error));
655
- }
286
+ return this.#resources.workflowAutocompleteCommands();
656
287
  }
657
- /**
658
- * The theme the engine is configured with, in the engine's own grammar: a
659
- * theme's name, or a `light/dark` pair meaning "follow the terminal".
660
- */
661
- configuredTheme() {
662
- return this.#runtime?.services.settingsManager.getThemeSetting();
288
+ visualExtensionSupport() {
289
+ return this.#extensions.support();
663
290
  }
664
- /** Settings port for the live runtime, or null before the runtime is available. */
665
- settingsPort() {
666
- const settings = this.#runtime?.services.settingsManager;
667
- if (!settings || typeof settings.getCompactionEnabled !== "function")
668
- return null;
669
- if (this.#settingsIntegration === undefined || this.#settingsIntegrationManager !== settings) {
670
- this.#settingsIntegrationManager = settings;
671
- configureOwnedHttpDispatcher(settings.getHttpIdleTimeoutMs());
672
- this.#settingsIntegration = new PiSettingsIntegration(settings, {
673
- ...(this.#availableThemes === null ? {} : { themes: this.#availableThemes }),
674
- thinkingLevels: () => {
675
- const levels = this.#runtime?.session?.getAvailableThinkingLevels?.();
676
- return Array.isArray(levels) ? levels.map(level => String(level)) : [];
677
- },
678
- productMode: this.#settingsProductMode,
679
- });
680
- this.#settingsIntegration.bindOwner("shell", {
681
- enableSkillCommands: { apply: value => {
682
- if (typeof value !== "boolean")
683
- throw new TypeError("Skill commands value is invalid");
684
- settings.setEnableSkillCommands(value);
685
- } },
686
- doubleEscapeAction: { apply: value => {
687
- if (value !== "tree" && value !== "fork" && value !== "none")
688
- throw new TypeError("Double-Escape action is invalid");
689
- settings.setDoubleEscapeAction(value);
690
- } },
691
- treeFilterMode: { apply: value => {
692
- if (value !== "default" && value !== "no-tools" && value !== "user-only" && value !== "labeled-only" && value !== "all")
693
- throw new TypeError("Tree filter mode is invalid");
694
- settings.setTreeFilterMode(value);
695
- } },
696
- showCacheMissNotices: { apply: value => {
697
- if (typeof value !== "boolean")
698
- throw new TypeError("Cache-miss notice setting is invalid");
699
- settings.setShowCacheMissNotices(value);
700
- } },
701
- });
702
- this.#settingsIntegration.bindOwner("startup", {
703
- // Invariant: deferred application is the owner operation: the next preflight reads
704
- // the persisted default before constructing project-backed services.
705
- defaultProjectTrust: { apply() { } },
706
- collapseChangelog: { apply() { } },
707
- });
708
- this.#settingsIntegration.bindOwner("agent", {
709
- autoCompact: { apply: value => {
710
- if (typeof value !== "boolean")
711
- throw new TypeError("Auto compact value is invalid");
712
- this.#requireWorkflowSession().setAutoCompactionEnabled(value);
713
- } },
714
- autoResizeImages: { apply: value => {
715
- if (typeof value !== "boolean")
716
- throw new TypeError("Auto-resize images value is invalid");
717
- settings.setImageAutoResize(value);
718
- } },
719
- blockImages: { apply: value => {
720
- if (typeof value !== "boolean")
721
- throw new TypeError("Block images value is invalid");
722
- settings.setBlockImages(value);
723
- } },
724
- steeringMode: { apply: value => {
725
- if (value !== "all" && value !== "one-at-a-time")
726
- throw new TypeError("Steering mode is invalid");
727
- this.#requireWorkflowSession().setSteeringMode(value);
728
- } },
729
- followUpMode: { apply: value => {
730
- if (value !== "all" && value !== "one-at-a-time")
731
- throw new TypeError("Follow-up mode is invalid");
732
- this.#requireWorkflowSession().setFollowUpMode(value);
733
- } },
734
- transport: { apply: value => {
735
- if (value !== "sse" && value !== "websocket" && value !== "websocket-cached" && value !== "auto")
736
- throw new TypeError("Transport is invalid");
737
- this.#requireWorkflowSession().agent.transport = value;
738
- } },
739
- httpIdleTimeoutMs: { apply: value => {
740
- if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0)
741
- throw new TypeError("HTTP idle timeout is invalid");
742
- // Invariant: provider streaming reads the manager per request; global fetch uses
743
- // the matching owned dispatcher and zero maps to disabled semantics.
744
- configureOwnedHttpDispatcher(value);
745
- settings.setHttpIdleTimeoutMs(value);
746
- } },
747
- thinkingLevel: { apply: value => {
748
- if (!isThinkingLevel(value))
749
- throw new TypeError("Thinking level is invalid");
750
- this.#requireWorkflowSession().setThinkingLevel(value);
751
- this.#thinkingLevel = readThinkingLevel(this.#requireWorkflowSession().thinkingLevel);
752
- this.#emitView();
753
- } },
754
- warnings: { apply: value => {
755
- if (!isRecord(value) || Object.values(value).some(flag => typeof flag !== "boolean"))
756
- throw new TypeError("Warnings setting is invalid");
757
- settings.setWarnings(value);
758
- } },
759
- });
760
- }
761
- return this.#settingsIntegration;
291
+ bindExtensionUi(ui, shutdown) {
292
+ return this.#extensions.bind(ui, shutdown);
762
293
  }
763
- /** Which settings surface this adapter serves; hidden-in-bare effects are unbindable in bare mode. */
764
- get settingsProductMode() {
765
- return this.#settingsProductMode;
294
+ unbindExtensionUi() {
295
+ return this.#extensions.unbind();
766
296
  }
767
- bindSettingsOwner(owner, handlers) {
768
- const settings = this.settingsPort();
769
- if (settings === null)
770
- return () => { };
771
- return settings.bindOwner(owner, handlers);
297
+ async cycleModelWorkflow(direction) {
298
+ return this.#workflows.cycleModelWorkflow(direction);
772
299
  }
773
300
  pinnedModelSelectorContext() {
774
- const session = this.#requireWorkflowSession();
775
- const runtime = this.#runtime;
776
- if (!runtime)
777
- throw new Error("engine runtime is unavailable");
778
- const scoped = session.scopedModels;
779
- const modelRuntime = runtime.services.modelRuntime;
780
- const selectorRuntime = typeof modelRuntime.getAvailableSnapshot === "function"
781
- ? modelRuntime
782
- : {
783
- getAvailableSnapshot: () => session.model === undefined ? [] : [session.model],
784
- getModel: (providerId, modelId) => modelRuntime.getModel(providerId, modelId),
785
- getError: () => undefined,
786
- refresh: async () => undefined,
787
- };
788
- const settingsManager = runtime.services.settingsManager;
789
- const selectorSettings = typeof settingsManager?.setDefaultModelAndProvider === "function"
790
- ? settingsManager
791
- : { setDefaultModelAndProvider() { } };
792
- return {
793
- currentModel: this.#activeModel === null ? undefined : session.model,
794
- settingsManager: selectorSettings,
795
- modelRuntime: selectorRuntime,
796
- scopedModels: Array.isArray(scoped) ? scoped : [],
797
- };
301
+ return this.#contexts.pinnedModelSelectorContext();
798
302
  }
799
303
  pinnedProjectTrustContext() {
800
- const runtime = this.#runtime;
801
- if (!runtime)
802
- throw new Error("engine runtime is unavailable");
803
- const cwd = resolve(this.#cwd);
804
- let trustPath = cwd;
805
- try {
806
- trustPath = realpathSync(cwd);
807
- }
808
- catch {
809
- // Compatibility: pinned Pi retains the resolved path when real-path lookup fails.
810
- }
811
- // Security: trust the target's parent, not the parent containing a directory alias.
812
- const parent = dirname(trustPath);
813
- const trustStore = new ProjectTrustStore(this.#agentDir);
814
- const trustOptions = [
815
- { label: "Trust", trusted: true, updates: [{ path: trustPath, decision: true }], savedPath: trustPath },
816
- ...(parent === trustPath ? [] : [{
817
- label: `Trust parent folder (${parent})`,
818
- trusted: true,
819
- updates: [{ path: parent, decision: true }, { path: trustPath, decision: null }],
820
- savedPath: parent,
821
- }]),
822
- { label: "Do not trust", trusted: false, updates: [{ path: trustPath, decision: false }], savedPath: trustPath },
823
- ];
824
- return {
825
- cwd,
826
- savedDecision: trustStore.getEntry(cwd),
827
- projectTrusted: runtime.services.settingsManager?.isProjectTrusted?.() === true,
828
- trustOptions,
829
- };
304
+ return this.#contexts.pinnedProjectTrustContext();
830
305
  }
831
306
  persistProjectTrust(updates) {
832
- new ProjectTrustStore(this.#agentDir).setMany([...updates]);
307
+ this.#contexts.persistProjectTrust(updates);
833
308
  }
834
309
  pinnedSessionSelectorContext() {
835
- const session = this.#requireWorkflowSession();
836
- const manager = session.sessionManager;
837
- const cwd = manager?.getCwd?.();
838
- const sessionDir = manager?.getSessionDir?.();
839
- const currentSessionFilePath = manager?.getSessionFile?.();
840
- const usesDefaultSessionDir = manager?.usesDefaultSessionDir?.() === true;
841
- const resolvedCwd = typeof cwd === "string" ? cwd : this.#cwd;
842
- const resolvedSessionDir = typeof sessionDir === "string" ? sessionDir : undefined;
843
- return {
844
- currentSessionFilePath: typeof currentSessionFilePath === "string" ? currentSessionFilePath : undefined,
845
- loadCurrentSessions: onProgress => SessionManager.list(resolvedCwd, resolvedSessionDir, onProgress),
846
- loadAllSessions: onProgress => usesDefaultSessionDir
847
- ? SessionManager.listAll(onProgress)
848
- : resolvedSessionDir === undefined ? SessionManager.listAll(onProgress) : SessionManager.listAll(resolvedSessionDir, onProgress),
849
- renameSession: async (sessionFilePath, nextName) => {
850
- const next = (nextName ?? "").trim();
851
- if (!next)
852
- return;
853
- SessionManager.open(sessionFilePath).appendSessionInfo(next);
854
- },
855
- };
310
+ return this.#contexts.pinnedSessionSelectorContext();
856
311
  }
857
312
  pinnedScopedModelsContext() {
858
- const session = this.#requireWorkflowSession();
859
- const runtime = this.#runtime;
860
- if (!runtime)
861
- throw new Error("engine runtime is unavailable");
862
- const models = scopedModelRecords(runtime.services.modelRuntime);
863
- const scoped = session.scopedModels;
864
- if (Array.isArray(scoped) && scoped.length > 0) {
865
- return {
866
- models: models.map(item => item.descriptor),
867
- enabledModelIds: scoped.map(scopedModelReference).filter((id) => id !== undefined),
868
- };
869
- }
870
- const patterns = runtime.services.settingsManager?.getEnabledModels?.();
871
- return {
872
- models: models.map(item => item.descriptor),
873
- enabledModelIds: Array.isArray(patterns)
874
- ? resolveConfiguredModelIds(patterns.filter((value) => typeof value === "string"), models)
875
- : null,
876
- };
313
+ return this.#contexts.pinnedScopedModelsContext();
877
314
  }
878
315
  updateScopedModels(enabledModelIds) {
879
- const session = this.#requireWorkflowSession();
880
- const runtime = this.#runtime;
881
- if (!runtime)
882
- throw new Error("engine runtime is unavailable");
883
- const models = scopedModelRecords(runtime.services.modelRuntime);
884
- const availableIds = new Set(models.map(item => `${item.descriptor.provider}/${item.descriptor.id}`));
885
- const selected = enabledModelIds?.filter(id => availableIds.has(id)) ?? [];
886
- const allAvailableEnabled = enabledModelIds !== null && availableIds.size > 0 && selected.length === availableIds.size;
887
- const scoped = enabledModelIds !== null && selected.length > 0 && !allAvailableEnabled
888
- ? selected.flatMap(id => {
889
- const item = models.find(candidate => `${candidate.descriptor.provider}/${candidate.descriptor.id}` === id);
890
- return item === undefined ? [] : [{ model: item.model }];
891
- })
892
- : [];
893
- requireCapability(session.setScopedModels, "setScopedModels").call(session, scoped);
894
- this.#emitView();
316
+ this.#contexts.updateScopedModels(enabledModelIds);
895
317
  }
896
318
  persistScopedModels(enabledModelIds) {
897
- const runtime = this.#runtime;
898
- if (!runtime)
899
- throw new Error("engine runtime is unavailable");
900
- const availableCount = scopedModelRecords(runtime.services.modelRuntime).length;
901
- const patterns = enabledModelIds === null || enabledModelIds.length === availableCount
902
- ? undefined
903
- : [...enabledModelIds];
904
- requireCapability(runtime.services.settingsManager?.setEnabledModels, "setEnabledModels").call(runtime.services.settingsManager, patterns === undefined ? undefined : [...patterns]);
905
- }
906
- async refreshScopedModels(signal) {
907
- const runtime = this.#runtime;
908
- if (!runtime)
909
- throw new Error("engine runtime is unavailable");
910
- const result = await runtime.services.modelRuntime.refresh?.({ signal });
911
- const context = this.pinnedScopedModelsContext();
912
- if (isRecord(result) && result.aborted === true) {
913
- return { ...context, status: "Model refresh timed out; showing cached models.", statusKind: "warning" };
914
- }
915
- const errors = isRecord(result) ? result.errors : undefined;
916
- if (errors instanceof Map && errors.size > 0) {
917
- return {
918
- ...context,
919
- status: `Could not refresh ${[...errors.keys()].join(", ")}; showing cached models.`,
920
- statusKind: "warning",
921
- };
922
- }
923
- return { ...context, status: "Model catalogs refreshed.", statusKind: "success" };
319
+ this.#contexts.persistScopedModels(enabledModelIds);
320
+ }
321
+ refreshScopedModels(signal) {
322
+ return this.#contexts.refreshScopedModels(signal);
924
323
  }
925
324
  pinnedLoginOptions(authType) {
926
- return this.#loginOptions(authType);
325
+ return this.#contexts.loginOptions(authType);
927
326
  }
928
327
  pinnedLoginMethodOptions(providerReference) {
929
- const runtime = this.#runtime;
930
- if (!runtime)
931
- throw new Error("engine runtime is unavailable");
932
- const modelRuntime = runtime.services.modelRuntime;
933
- const normalized = providerReference.trim().toLowerCase();
934
- const providers = modelRuntime.getProviders?.();
935
- const matches = Array.isArray(providers) ? providers.filter(isRecord).filter(candidate => stringProperty(candidate, "id")?.toLowerCase() === normalized
936
- || stringProperty(candidate, "name")?.toLowerCase() === normalized) : [];
937
- const providerId = matches.length === 1 ? stringProperty(matches[0], "id") ?? providerReference : providerReference;
938
- const provider = modelRuntime.getProvider?.(providerId);
939
- const providerName = stringProperty(provider, "name") ?? providerId;
940
- const oauth = dynamicObject(dynamicObject(provider, "auth"), "oauth");
941
- const loginLabel = stringProperty(oauth, "loginLabel") ?? "Sign in with an account";
942
- const options = this.#loginOptions().filter(option => option.id.endsWith(`:${providerId}`)).map(option => ({
943
- id: option.id,
944
- label: option.id.startsWith("api_key:") ? "Sign in with an API key" : loginLabel,
945
- ...(option.description === undefined ? {} : { description: option.description }),
946
- }));
947
- return { title: `Select authentication method for ${providerName}:`, options };
328
+ return this.#contexts.pinnedLoginMethodOptions(providerReference);
948
329
  }
949
330
  pinnedAmbientAuthentication(selection) {
950
- if (!selection.startsWith("api_key:"))
951
- return null;
952
- const providerId = selection.slice("api_key:".length);
953
- const provider = this.#runtime?.services.modelRuntime.getProvider?.(providerId);
954
- const method = dynamicObject(dynamicObject(provider, "auth"), "apiKey");
955
- if (!method || typeof method.login === "function")
956
- return null;
957
- const providerName = stringProperty(provider, "name") ?? providerId;
958
- return { providerId, providerName, title: `${providerName} setup`, message: `${stringProperty(method, "name") ?? "Authentication"} is configured outside pi.` };
331
+ return this.#contexts.pinnedAmbientAuthentication(selection);
959
332
  }
960
333
  pinnedLogoutOptions() {
961
- return this.#logoutOptions();
334
+ return this.#contexts.logoutOptions();
962
335
  }
963
336
  pinnedForkOptions() {
964
- const session = this.#requireWorkflowSession();
965
- const messages = requireCapability(session.getUserMessagesForForking, "getUserMessagesForForking").call(session);
966
- return workflowOptions(messages, "entryId", "text");
337
+ return this.#contexts.pinnedForkOptions();
967
338
  }
968
339
  pinnedTreeSelectorContext() {
969
- const manager = this.#requireWorkflowSession().sessionManager;
970
- const settings = this.#runtime?.services.settingsManager;
971
- const tree = manager?.getTree?.();
972
- const leaf = manager?.getLeafId?.();
973
- const configuredFilter = settings?.getTreeFilterMode?.();
974
- const filterMode = configuredFilter === "no-tools" || configuredFilter === "user-only" || configuredFilter === "labeled-only" || configuredFilter === "all"
975
- ? configuredFilter
976
- : "default";
977
- return {
978
- tree: Array.isArray(tree) ? tree : [],
979
- currentLeafId: typeof leaf === "string" ? leaf : null,
980
- filterMode,
981
- skipSummaryPrompt: settings?.getBranchSummarySkipPrompt?.() === true,
982
- appendLabelChange: (entryId, label) => {
983
- requireCapability(manager?.appendLabelChange, "appendLabelChange").call(manager, entryId, label);
984
- },
985
- };
340
+ return this.#contexts.pinnedTreeSelectorContext();
341
+ }
342
+ /** Bind the owned UI's clipboard lifecycle without changing the comparison host. True means acknowledged delivery. */
343
+ bindClipboardWriter(writer) {
344
+ return this.#workflows.bindClipboardWriter(writer);
345
+ }
346
+ /** Workflow and tree copying share the active owner's write fence and delivery acknowledgment. */
347
+ copyWorkflowText(text) {
348
+ return this.#workflows.copyWorkflowText(text);
349
+ }
350
+ abortBashWorkflow() {
351
+ this.#workflows.abortBashWorkflow();
352
+ }
353
+ executeBashWorkflow(command, excludeFromContext) {
354
+ return this.#workflows.executeBashWorkflow(command, excludeFromContext);
355
+ }
356
+ reloadBlockedResult() {
357
+ return this.#workflows.reloadBlockedResult();
358
+ }
359
+ executeWorkflow(request) {
360
+ return this.#workflows.executeWorkflow(request);
361
+ }
362
+ /**
363
+ * The theme the engine is configured with, in the engine's own grammar: a
364
+ * theme's name, or a `light/dark` pair meaning "follow the terminal".
365
+ */
366
+ configuredTheme() {
367
+ return this.#settings.configuredTheme();
368
+ }
369
+ /** Settings port for the live runtime, or null before the runtime is available. */
370
+ settingsPort() {
371
+ return this.#settings.settingsPort();
372
+ }
373
+ /** Which settings surface this adapter serves; hidden-in-bare effects are unbindable in bare mode. */
374
+ get settingsProductMode() {
375
+ return this.#settings.productMode;
376
+ }
377
+ bindSettingsOwner(owner, handlers) {
378
+ return this.#settings.bindSettingsOwner(owner, handlers);
986
379
  }
987
380
  pinnedSettingsSnapshot() {
988
- const session = this.#requireWorkflowSession();
989
- const settings = this.#runtime?.services.settingsManager;
990
- const setting = (getter, fallback) => {
991
- const value = getter?.call(settings);
992
- return value === undefined ? fallback : value;
993
- };
994
- const levels = session.getAvailableThinkingLevels?.();
995
- const themes = collectionResult(this.#runtime?.services.resourceLoader?.getThemes?.(), "themes").values
996
- .map(theme => stringProperty(theme, "name"))
997
- .filter((name) => !!name);
998
- return {
999
- autoCompact: typeof session.autoCompactionEnabled === "boolean"
1000
- ? session.autoCompactionEnabled
1001
- : setting(settings?.getCompactionEnabled, true),
1002
- showImages: setting(settings?.getShowImages, true),
1003
- imageWidthCells: setting(settings?.getImageWidthCells, 80),
1004
- autoResizeImages: setting(settings?.getImageAutoResize, true),
1005
- blockImages: setting(settings?.getBlockImages, false),
1006
- enableSkillCommands: setting(settings?.getEnableSkillCommands, true),
1007
- steeringMode: session.steeringMode ?? setting(settings?.getSteeringMode, "one-at-a-time"),
1008
- followUpMode: session.followUpMode ?? setting(settings?.getFollowUpMode, "one-at-a-time"),
1009
- transport: setting(settings?.getTransport, "sse"),
1010
- httpIdleTimeoutMs: setting(settings?.getHttpIdleTimeoutMs, 300_000),
1011
- thinkingLevel: readThinkingLevel(session.thinkingLevel),
1012
- availableThinkingLevels: Array.isArray(levels) ? levels.map(readThinkingLevel) : ["off", "minimal", "low", "medium", "high", "xhigh"],
1013
- currentTheme: setting(settings?.getThemeSetting, setting(settings?.getTheme, "dark")),
1014
- terminalTheme: "dark",
1015
- availableThemes: themes.length > 0 ? themes : ["dark", "light"],
1016
- hideThinkingBlock: setting(settings?.getHideThinkingBlock, false),
1017
- mermaidRenderingMode: setting(settings?.getMermaidRenderingMode, "off"),
1018
- showCacheMissNotices: setting(settings?.getShowCacheMissNotices, false),
1019
- collapseChangelog: setting(settings?.getCollapseChangelog, true),
1020
- enableInstallTelemetry: setting(settings?.getEnableInstallTelemetry, true),
1021
- doubleEscapeAction: setting(settings?.getDoubleEscapeAction, "tree"),
1022
- treeFilterMode: setting(settings?.getTreeFilterMode, "default"),
1023
- showHardwareCursor: setting(settings?.getShowHardwareCursor, false),
1024
- editorPaddingX: setting(settings?.getEditorPaddingX, 0),
1025
- outputPad: setting(settings?.getOutputPad, 1),
1026
- autocompleteMaxVisible: setting(settings?.getAutocompleteMaxVisible, 5),
1027
- quietStartup: setting(settings?.getQuietStartup, false),
1028
- defaultProjectTrust: setting(settings?.getDefaultProjectTrust, "ask"),
1029
- clearOnShrink: setting(settings?.getClearOnShrink, false),
1030
- showTerminalProgress: setting(settings?.getShowTerminalProgress, false),
1031
- tuiMode: setting(settings?.getTuiMode, "regular"),
1032
- fullscreenExitOutput: setting(settings?.getFullscreenExitOutput, "transcript"),
1033
- fullscreenScrollbar: setting(settings?.getFullscreenScrollbar, "auto"),
1034
- warnings: setting(settings?.getWarnings, { anthropicExtraUsage: true }),
1035
- };
381
+ return this.#settings.pinnedSettingsSnapshot();
382
+ }
383
+ applyPinnedSettingValue(callback, value) {
384
+ return this.#settings.applyPinnedSettingValue(callback, value);
1036
385
  }
1037
386
  pinnedMessageRenderer(customType) {
1038
387
  return this.#requireWorkflowSession().extensionRunner?.getMessageRenderer?.(customType);
@@ -1044,121 +393,38 @@ export class PiEngineAdapter {
1044
393
  pinnedToolDefinition(toolName) {
1045
394
  return this.#requireWorkflowSession().extensionRunner?.getToolDefinition?.(toolName);
1046
395
  }
1047
- /** Bind the owned UI's clipboard lifecycle without changing the comparison host. True means acknowledged delivery. */
1048
- bindClipboardWriter(writer) {
1049
- const previous = this.#clipboardWriter;
1050
- this.#clipboardWriter = writer;
1051
- return () => { if (this.#clipboardWriter === writer)
1052
- this.#clipboardWriter = previous; };
1053
- }
1054
- /** Workflow and tree copying share the active owner's write fence and delivery acknowledgment. */
1055
- async copyWorkflowText(text) {
1056
- if (this.#clipboardWriter !== undefined)
1057
- return this.#clipboardWriter(text);
1058
- await this.#workflowHost.copyText(text);
1059
- return true;
1060
- }
1061
396
  clearQueuedWorkflows() {
1062
397
  const session = this.#requireWorkflowSession();
1063
398
  const result = session.clearQueue?.();
1064
- this.#sessionCommands?.forgetQueuedImages();
399
+ this.#engine.sessionCommands?.forgetQueuedImages();
1065
400
  if (!isRecord(result))
1066
401
  return [];
1067
402
  return [...readStringArray(result.steering), ...readStringArray(result.followUp)];
1068
403
  }
1069
- abortBashWorkflow() {
1070
- this.#requireWorkflowSession().abortBash?.();
1071
- }
1072
- async executeBashWorkflow(command, excludeFromContext) {
1073
- const session = this.#requireWorkflowSession();
1074
- const result = await requireCapability(session.executeBash, "executeBash").call(session, command, undefined, { excludeFromContext });
1075
- if (!isRecord(result))
1076
- throw new Error("Pi bash workflow returned a malformed result");
1077
- return {
1078
- command,
1079
- output: typeof result.output === "string" ? result.output : "",
1080
- exitCode: typeof result.exitCode === "number" ? result.exitCode : undefined,
1081
- cancelled: result.cancelled === true,
1082
- truncated: result.truncated === true,
1083
- excludeFromContext,
1084
- };
1085
- }
1086
- async applyPinnedSettingValue(callback, value) {
1087
- try {
1088
- return await this.#applyPinnedSetting(callback, value, true);
1089
- }
1090
- catch (error) {
1091
- return workflowResult("settings", "failed", error instanceof Error ? error.message : String(error));
1092
- }
1093
- }
1094
- reloadBlockedResult() {
1095
- const session = this.#requireWorkflowSession();
1096
- if (session.isStreaming)
1097
- return workflowResult("reload", "failed", "Wait for the current response to finish before reloading.", undefined, "warning");
1098
- if (session.isCompacting)
1099
- return workflowResult("reload", "failed", "Wait for compaction to finish before reloading.", undefined, "warning");
1100
- return null;
1101
- }
1102
- async executeWorkflow(request) {
1103
- const cancelled = workflowResult(request.command, "cancelled", "", undefined, "silent");
1104
- if (this.#overload !== undefined || this.#admissionStopped || this.#disposed
1105
- || this.#pendingCommands.size + this.#pendingWorkflows.size >= 32)
1106
- return cancelled;
1107
- let cancel;
1108
- const cancellation = new Promise(resolve => { cancel = () => resolve(cancelled); });
1109
- const pending = { command: request.command, cancel };
1110
- this.#pendingWorkflows.add(pending);
1111
- this.#runningCommands++;
1112
- const operation = this.#runWorkflow(request).finally(() => { this.#runningCommands--; });
1113
- try {
1114
- return await Promise.race([operation, cancellation]);
1115
- }
1116
- finally {
1117
- this.#pendingWorkflows.delete(pending);
1118
- }
1119
- }
1120
- async #runWorkflow(request) {
1121
- try {
1122
- return await this.#performWorkflow(request);
1123
- }
1124
- catch (error) {
1125
- const message = error instanceof Error ? error.message : String(error);
1126
- const contextualMessage = request.command === "export"
1127
- ? `Failed to export session: ${errorMessage(error, "Unknown error")}`
1128
- : request.command === "import"
1129
- ? `Failed to import session: ${message}`
1130
- : request.command === "new"
1131
- ? `Failed to create session: ${message}`
1132
- : request.command === "resume"
1133
- ? `Failed to resume session: ${message}`
1134
- : request.command === "reload" ? `Reload failed: ${message}` : message;
1135
- return workflowResult(request.command, "failed", contextualMessage);
1136
- }
1137
- }
1138
404
  view() {
1139
405
  return {
1140
406
  contractVersion: 1,
1141
407
  sessionId: this.#sessionId,
1142
408
  revision: this.#viewRevision,
1143
409
  lifecycle: this.#lifecycle,
1144
- transcript: this.#transcriptSnapshot ??= Object.freeze([...this.#transcript]),
410
+ transcript: this.#projection.snapshot(),
1145
411
  editor: { ...this.#editor, queuedSubmissions: [...this.#editor.queuedSubmissions] },
1146
412
  status: {
1147
413
  ...this.#status,
1148
414
  diagnostics: [...this.#status.diagnostics],
1149
415
  badges: [...this.#status.badges],
1150
- usage: this.#usageCache ??= this.#readUsage(),
416
+ usage: this.#usageCache ??= readUsageView(this.#engine.session, this.#engine.runtime, this.#activeModel),
1151
417
  footer: {
1152
- branch: this.#gitBranch,
1153
- sessionName: this.#session?.sessionManager?.getSessionName() ?? null,
1154
- availableProviderCount: new Set(this.#runtime?.services.modelRuntime.getAvailableSnapshot?.().map(model => model.provider).filter(provider => provider !== undefined) ?? []).size,
418
+ branch: this.#engine.gitBranch,
419
+ sessionName: this.#engine.session?.sessionManager?.getSessionName() ?? null,
420
+ availableProviderCount: new Set(this.#engine.runtime?.services.modelRuntime.getAvailableSnapshot?.().map(model => model.provider).filter(provider => provider !== undefined) ?? []).size,
1155
421
  extensionStatuses: [],
1156
422
  },
1157
423
  },
1158
424
  terminal: { ...this.#terminal },
1159
425
  activeModel: this.#activeModel === null ? null : { ...this.#activeModel },
1160
426
  thinkingLevel: this.#thinkingLevel,
1161
- activeCommandIds: [...this.#activeCommandIds],
427
+ activeCommandIds: [...this.#commands.activeCommandIds],
1162
428
  dialog: null,
1163
429
  overlay: null,
1164
430
  customizations: [],
@@ -1170,99 +436,27 @@ export class PiEngineAdapter {
1170
436
  contractVersion: 1,
1171
437
  snapshotId: `snapshot-${this.#viewRevision}`,
1172
438
  sessionId: this.#sessionId,
1173
- sequence: this.#sequence,
439
+ sequence: this.#delivery.sequence,
1174
440
  view: this.view(),
1175
441
  };
1176
442
  assertOwnedUiSnapshot(snapshot);
1177
443
  return snapshot;
1178
444
  }
1179
- async execute(command) {
1180
- assertOwnedUiCommand(command);
1181
- if (command.sessionId !== this.#sessionId) {
1182
- return this.#finishCommand(command, "rejected", "command targets a different owned session");
1183
- }
1184
- if (this.#disposed || !this.#runtime || !this.#session) {
1185
- return this.#finishCommand(command, "rejected", "engine adapter is not running");
1186
- }
1187
- // Invariant: the bounded out-of-band cancellation path has one slot per admitted command.
1188
- if (this.#overload !== undefined || this.#admissionStopped || this.#pendingCommands.size + this.#pendingWorkflows.size >= 32)
1189
- return { outcome: "rejected", diagnostic: null };
1190
- const existing = this.#completedCommands.get(command.correlationId);
1191
- if (existing)
1192
- return existing;
1193
- if (this.#activeCommandIds.includes(command.correlationId)) {
1194
- return this.#finishCommand(command, "rejected", "duplicate engine command correlation id");
1195
- }
1196
- this.#activeCommandIds.push(command.correlationId);
1197
- const generation = this.#sessionGeneration;
1198
- let cancelled = false;
1199
- const cancellation = new Promise(resolve => {
1200
- this.#pendingCommands.set(command.correlationId, { type: command.type, cancel: () => {
1201
- if (cancelled)
1202
- return;
1203
- cancelled = true;
1204
- resolve(this.#recordCommand(command, "failed", null));
1205
- } });
1206
- });
1207
- this.#emitEvent({ type: "command-outcome", correlationId: command.correlationId, outcome: "accepted", diagnostic: null });
1208
- const operation = async () => {
1209
- try {
1210
- if (cancelled)
1211
- return { outcome: "failed", diagnostic: null };
1212
- this.#runningCommands++;
1213
- try {
1214
- await this.#perform(command);
1215
- }
1216
- finally {
1217
- this.#runningCommands--;
1218
- }
1219
- if (cancelled)
1220
- return { outcome: "failed", diagnostic: null };
1221
- if (generation === this.#sessionGeneration)
1222
- this.#emitView();
1223
- if (cancelled)
1224
- return { outcome: "failed", diagnostic: null };
1225
- return this.#recordCommand(command, "completed", null);
1226
- }
1227
- catch (error) {
1228
- if (cancelled)
1229
- return { outcome: "failed", diagnostic: null };
1230
- const diagnostic = error instanceof Error ? error.message : String(error);
1231
- this.#addDiagnostic("error", "engine-command", diagnostic, true);
1232
- if (generation === this.#sessionGeneration)
1233
- this.#emitView();
1234
- return this.#recordCommand(command, "failed", diagnostic);
1235
- }
1236
- };
1237
- try {
1238
- return await Promise.race([operation(), cancellation]);
1239
- }
1240
- finally {
1241
- this.#pendingCommands.delete(command.correlationId);
1242
- }
445
+ execute(command) {
446
+ return this.#commands.execute(command);
1243
447
  }
1244
448
  async dispose() {
1245
449
  if (this.#disposed)
1246
450
  return;
1247
451
  this.#disposed = true;
1248
- for (const pending of this.#pendingCommands.values())
1249
- if (pending.type !== "shutdown")
1250
- pending.cancel();
452
+ this.#commands.cancelPending(["shutdown"]);
1251
453
  // Compatibility: /quit owns normal disposal and must report its actual completion, not cancel itself.
1252
- for (const pending of this.#pendingWorkflows)
1253
- if (pending.command !== "quit")
1254
- pending.cancel();
1255
- this.#transcriptImageAssets.clear();
1256
- this.#extensionBound = false;
1257
- this.#extensionUi = undefined;
1258
- this.#extensionShutdown = undefined;
454
+ this.#workflows.cancelPending("quit");
455
+ this.#projection.assets.clear();
456
+ await this.#extensions.unbind();
1259
457
  this.#lifecycle = "stopping";
1260
458
  this.#emitEvent({ type: "session-lifecycle", lifecycle: "stopping", reason: null });
1261
- this.#unsubscribe?.();
1262
- this.#unsubscribe = undefined;
1263
- this.#compactionProgress?.dispose();
1264
- this.#compactionProgress = null;
1265
- await this.#runtime?.dispose();
459
+ await this.#engine.dispose();
1266
460
  this.#lifecycle = "stopped";
1267
461
  this.#emitEvent({ type: "session-lifecycle", lifecycle: "stopped", reason: null });
1268
462
  this.#emitView();
@@ -1274,716 +468,45 @@ export class PiEngineAdapter {
1274
468
  throw error;
1275
469
  }
1276
470
  }
1277
- async #performWorkflow(request) {
1278
- const session = this.#requireWorkflowSession();
1279
- const runtime = this.#runtime;
1280
- if (!runtime)
1281
- throw new Error("engine runtime is unavailable");
1282
- const argument = request.argument.trim();
1283
- const selected = request.selection?.trim();
1284
- const selection = selected && selected.length > 0 ? selected : undefined;
1285
- switch (request.command) {
1286
- case "settings": {
1287
- if (!selection)
1288
- return workflowResult(request.command, "failed", "Settings requires the owned settings controller");
1289
- return await this.#applyPinnedSetting(selection);
1290
- }
1291
- case "model": {
1292
- const reference = selection ?? argument;
1293
- if (!reference)
1294
- return workflowResult(request.command, "failed", "Model requires the owned model controller");
1295
- const scopedModels = Array.isArray(session.scopedModels)
1296
- ? session.scopedModels.map(item => item.model)
1297
- : [];
1298
- let availableModels = scopedModels.length > 0
1299
- ? scopedModels
1300
- : [...(runtime.services.modelRuntime.getAvailableSnapshot?.() ?? [])];
1301
- let model = findExactWorkflowModel(reference, availableModels);
1302
- const messages = [];
1303
- const generation = this.#sessionGeneration;
1304
- const current = () => !this.#disposed && generation === this.#sessionGeneration;
1305
- const publish = (message) => {
1306
- if (!current())
1307
- return;
1308
- if (this.#workflowInteraction.publish)
1309
- this.#workflowInteraction.publish(message);
1310
- else
1311
- messages.push(message);
1312
- };
1313
- if (model === undefined && scopedModels.length === 0) {
1314
- publish({ kind: "status", message: "Refreshing model catalogs…" });
1315
- const controller = new AbortController();
1316
- let timedOut = false;
1317
- const timeout = setTimeout(() => {
1318
- timedOut = true;
1319
- controller.abort();
1320
- }, AUTH_REFRESH_TIMEOUT_MS);
1321
- try {
1322
- const refreshed = await runtime.services.modelRuntime.refresh?.({ signal: controller.signal });
1323
- if (isRecord(refreshed) && refreshed.aborted === true && timedOut) {
1324
- publish({ kind: "warning", message: "Model refresh timed out; searching cached models." });
1325
- }
1326
- else if (isRecord(refreshed) && refreshed.errors instanceof Map && refreshed.errors.size > 0) {
1327
- publish({ kind: "warning", message: `Could not refresh ${[...refreshed.errors.keys()].join(", ")}; searching cached models.` });
1328
- }
1329
- }
1330
- catch (error) {
1331
- publish({
1332
- kind: "warning",
1333
- message: timedOut
1334
- ? "Model refresh timed out; searching cached models."
1335
- : `Could not refresh model catalogs: ${error instanceof Error ? error.message : String(error)}`,
1336
- });
1337
- }
1338
- finally {
1339
- clearTimeout(timeout);
1340
- }
1341
- availableModels = [...(runtime.services.modelRuntime.getAvailableSnapshot?.() ?? [])];
1342
- model = findExactWorkflowModel(reference, availableModels);
1343
- }
1344
- if (!current())
1345
- return workflowResult(request.command, "cancelled", "Model selection cancelled", undefined, "silent");
1346
- if (model === undefined) {
1347
- return {
1348
- ...workflowResult(request.command, "requires-selection", "Select a model", reference, "silent"),
1349
- ...(messages.length === 0 ? {} : { messages: Object.freeze(messages) }),
1350
- };
1351
- }
1352
- try {
1353
- await session.setModel(model);
1354
- }
1355
- catch (error) {
1356
- if (!current())
1357
- return workflowResult(request.command, "cancelled", "Model selection cancelled", undefined, "silent");
1358
- const failure = { kind: "error", message: error instanceof Error ? error.message : String(error) };
1359
- return workflowResult(request.command, "failed", failure.message, undefined, "error", messages.length === 0 ? undefined : [...messages, failure]);
1360
- }
1361
- if (!current())
1362
- return workflowResult(request.command, "cancelled", "Model selection cancelled", undefined, "silent");
1363
- const providerId = stringProperty(model, "provider") ?? "unknown";
1364
- const modelId = stringProperty(model, "id") ?? reference;
1365
- this.#activeModel = { providerId, modelId, displayName: stringProperty(model, "name") ?? modelId };
1366
- this.#emitView();
1367
- const resultMessage = { kind: "status", message: `Model: ${modelId}` };
1368
- return messages.length === 0
1369
- ? workflowResult(request.command, "completed", resultMessage.message)
1370
- : workflowResult(request.command, "completed", resultMessage.message, undefined, "status", [...messages, resultMessage]);
1371
- }
1372
- case "scoped-models": {
1373
- if (!selection)
1374
- return workflowResult(request.command, "failed", "Scoped models requires the owned scoped-model controller");
1375
- const [providerId, modelId] = selection.split("/", 2);
1376
- const model = providerId && modelId ? runtime.services.modelRuntime.getModel(providerId, modelId) : undefined;
1377
- if (!model)
1378
- return workflowResult(request.command, "failed", `Model is unavailable: ${selection}`);
1379
- requireCapability(session.setScopedModels, "setScopedModels").call(session, [{ model }]);
1380
- return workflowResult(request.command, "completed", `Enabled scoped model ${selection}`);
1381
- }
1382
- case "export": {
1383
- const path = pathArgument(argument);
1384
- const file = path?.toLowerCase().endsWith(".jsonl")
1385
- ? requireCapability(session.exportToJsonl, "exportToJsonl").call(session, path)
1386
- : await requireCapability(session.exportToHtml, "exportToHtml").call(session, path);
1387
- return workflowResult(request.command, "completed", `Session exported to: ${String(file)}`);
1388
- }
1389
- case "import": {
1390
- const path = pathArgument(argument);
1391
- if (!path)
1392
- return workflowResult(request.command, "failed", "Usage: /import <path.jsonl>");
1393
- if (request.confirmed === undefined) {
1394
- return workflowConfirmation(request.command, `Replace current session with ${path}?`);
1395
- }
1396
- if (!request.confirmed)
1397
- return workflowResult(request.command, "cancelled", "Import cancelled", undefined, "status");
1398
- try {
1399
- const result = await requireCapability(runtime.importFromJsonl, "importFromJsonl").call(runtime, path, request.cwdOverride);
1400
- if (isRecord(result) && result.cancelled === true)
1401
- return workflowResult(request.command, "cancelled", "Import cancelled", undefined, "status");
1402
- return workflowResult(request.command, "completed", `Session imported from: ${path}`);
1403
- }
1404
- catch (error) {
1405
- const issue = isRecord(error) && isRecord(error.issue) ? error.issue : undefined;
1406
- const fallbackCwd = issue === undefined ? undefined : stringProperty(issue, "fallbackCwd");
1407
- if (fallbackCwd !== undefined && request.cwdOverride === undefined) {
1408
- const sessionCwd = stringProperty(issue, "sessionCwd") ?? "the session working directory";
1409
- return workflowConfirmation(request.command, `cwd from session file does not exist\n${sessionCwd}\n\ncontinue in current cwd\n${fallbackCwd}`, fallbackCwd);
1410
- }
1411
- throw error;
1412
- }
1413
- }
1414
- case "share": {
1415
- const signal = request.signal;
1416
- if (signal?.aborted)
1417
- return workflowResult(request.command, "cancelled", "Share cancelled", undefined, "status");
1418
- try {
1419
- await this.#workflowHost.runCommand("gh", ["auth", "status"], signal === undefined ? undefined : { signal });
1420
- }
1421
- catch (error) {
1422
- if (signal?.aborted || isAbortError(error))
1423
- return workflowResult(request.command, "cancelled", "Share cancelled", undefined, "status");
1424
- const message = commandMissing(error)
1425
- ? "GitHub CLI (gh) is not installed. Install it from https://cli.github.com/"
1426
- : "GitHub CLI is not logged in. Run 'gh auth login' first.";
1427
- return workflowResult(request.command, "failed", message);
1428
- }
1429
- const temporary = join(tmpdir(), `${PRODUCT_IDENTITY.filesystem.temporaryPrefix}pi-session-${process.pid}.html`);
1430
- try {
1431
- try {
1432
- await requireCapability(session.exportToHtml, "exportToHtml").call(session, temporary);
1433
- }
1434
- catch (error) {
1435
- return workflowResult(request.command, "failed", `Failed to export session: ${errorMessage(error, "Unknown error")}`);
1436
- }
1437
- if (signal?.aborted)
1438
- return workflowResult(request.command, "cancelled", "Share cancelled", undefined, "status");
1439
- let gist;
1440
- try {
1441
- gist = await this.#workflowHost.runCommand("gh", ["gist", "create", "--public=false", temporary], signal === undefined ? undefined : { signal });
1442
- }
1443
- catch (error) {
1444
- if (signal?.aborted || isAbortError(error))
1445
- return workflowResult(request.command, "cancelled", "Share cancelled", undefined, "status");
1446
- return workflowResult(request.command, "failed", `Failed to create gist: ${shareCommandFailureDetail(error)}`);
471
+ #requireWorkflowSession() {
472
+ if (this.#disposed || !this.#engine.runtime || !this.#engine.session)
473
+ throw new Error("engine adapter is not running");
474
+ return this.#engine.session;
475
+ }
476
+ async #perform(command) {
477
+ const runtime = this.#engine.runtime;
478
+ const session = this.#engine.session;
479
+ const generation = this.#engine.generation;
480
+ if (!runtime || !session)
481
+ throw new Error("engine session is unavailable");
482
+ switch (command.type) {
483
+ case "prompt":
484
+ case "steer":
485
+ case "follow-up":
486
+ case "abort":
487
+ case "retry":
488
+ case "compact": {
489
+ const result = await this.#engine.sessionCommands?.execute(command.type === "prompt" || command.type === "steer" || command.type === "follow-up"
490
+ ? {
491
+ type: command.type,
492
+ text: command.text,
493
+ ...(command.images === undefined ? {} : { images: [...command.images] }),
1447
494
  }
1448
- if (signal?.aborted)
1449
- return workflowResult(request.command, "cancelled", "Share cancelled", undefined, "status");
1450
- const gistUrl = gist.stdout.trim();
1451
- const gistId = gistUrl.split("/").at(-1);
1452
- if (!gistId)
1453
- return workflowResult(request.command, "failed", "Failed to parse gist ID from gh output");
1454
- return workflowResult(request.command, "completed", `Share URL: ${shareViewerUrl(gistId)}`, gistUrl);
1455
- }
1456
- finally {
1457
- await rm(temporary, { force: true });
495
+ : { type: command.type });
496
+ if (!result || result.outcome === "rejected" || result.outcome === "failed") {
497
+ throw new Error(command.type === "retry" ? "no previous prompt is available to retry" : `Pi session command failed: ${command.type}`);
1458
498
  }
499
+ return;
1459
500
  }
1460
- case "copy": {
1461
- const text = requireCapability(session.getLastAssistantText, "getLastAssistantText").call(session);
1462
- if (typeof text !== "string" || text.length === 0)
1463
- return workflowResult(request.command, "failed", "No agent messages to copy yet.");
1464
- const acknowledged = await this.copyWorkflowText(text);
1465
- return workflowResult(request.command, "completed", acknowledged ? "Copied last agent message to clipboard" : "Submitted last agent message to clipboard");
1466
- }
1467
- case "name": {
1468
- const manager = session.sessionManager;
1469
- if (!argument) {
1470
- const current = manager?.getSessionName();
1471
- return typeof current === "string"
1472
- ? workflowResult(request.command, "completed", `Session name: ${current}`)
1473
- : workflowResult(request.command, "failed", "Usage: /name <name>", undefined, "warning");
501
+ case "set-model": {
502
+ const model = runtime.services.modelRuntime.getModel(command.model.providerId, command.model.modelId);
503
+ if (!model) {
504
+ throw new Error(`model is unavailable: ${command.model.providerId}/${command.model.modelId}`);
1474
505
  }
1475
- requireCapability(session.setSessionName, "setSessionName").call(session, argument);
1476
- const normalized = manager?.getSessionName();
1477
- const actual = typeof normalized === "string" ? normalized : argument;
1478
- return workflowResult(request.command, "completed", `Session name set: ${actual}`, actual === argument ? undefined : `Session name was normalized from ${JSON.stringify(argument)} to ${JSON.stringify(actual)}`);
1479
- }
1480
- case "session": {
1481
- const manager = session.sessionManager;
1482
- const stats = requireCapability(session.getSessionStats, "getSessionStats").call(session);
1483
- const entries = manager?.getEntries();
1484
- return {
1485
- ...workflowResult(request.command, "completed", "Session Info"),
1486
- presentation: pinnedSessionInfoPresentation(stats, manager?.getSessionName(), Array.isArray(entries) ? entries : [], runtime.services.modelRuntime),
1487
- };
1488
- }
1489
- case "changelog": {
1490
- const changelog = await this.#workflowHost.readChangelog();
1491
- return workflowResult(request.command, "completed", "What's New", changelog);
1492
- }
1493
- case "hotkeys":
1494
- return workflowResult(request.command, "completed", "Keyboard Shortcuts", pinnedHotkeySummary());
1495
- case "fork": {
1496
- if (!selection)
1497
- return workflowResult(request.command, "failed", "Fork requires the owned user-message controller");
1498
- const result = await requireCapability(runtime.fork, "fork").call(runtime, selection, { position: "before" });
1499
- if (isRecord(result) && result.cancelled === true)
1500
- return workflowResult(request.command, "cancelled", "Fork cancelled", undefined, "silent");
1501
- return workflowResult(request.command, "completed", "Forked to new session");
1502
- }
1503
- case "clone": {
1504
- const manager = session.sessionManager;
1505
- const leaf = manager?.getLeafId?.();
1506
- if (typeof leaf !== "string")
1507
- return workflowResult(request.command, "completed", "Nothing to clone yet", undefined, "status");
1508
- const result = await requireCapability(runtime.fork, "fork").call(runtime, leaf, { position: "at" });
1509
- if (isRecord(result) && result.cancelled === true)
1510
- return workflowResult(request.command, "cancelled", "Clone cancelled", undefined, "silent");
1511
- return workflowResult(request.command, "completed", "Cloned to new session");
1512
- }
1513
- case "tree": {
1514
- if (!selection)
1515
- return workflowResult(request.command, "failed", "Tree navigation requires the owned tree controller");
1516
- const navigateTree = requireCapability(session.navigateTree, "navigateTree");
1517
- const result = request.treeSummary === undefined
1518
- ? await navigateTree.call(session, selection)
1519
- : await navigateTree.call(session, selection, {
1520
- summarize: request.treeSummary.summarize,
1521
- ...(request.treeSummary.customInstructions === undefined ? {} : { customInstructions: request.treeSummary.customInstructions }),
1522
- });
1523
- if (isRecord(result) && result.aborted === true)
1524
- return workflowResult(request.command, "cancelled", "Branch summarization cancelled", undefined, "status");
1525
- if (isRecord(result) && result.cancelled === true)
1526
- return workflowResult(request.command, "cancelled", "Navigation cancelled", undefined, "status");
1527
- return workflowResult(request.command, "completed", "Navigated to selected point");
1528
- }
1529
- case "trust": {
1530
- if (!selection)
1531
- return workflowResult(request.command, "failed", "Trust requires the owned trust controller");
1532
- requireCapability(runtime.services.settingsManager?.setProjectTrusted, "setProjectTrusted").call(runtime.services.settingsManager, selection === "trust");
1533
- return workflowResult(request.command, "completed", selection === "trust" ? "Project trusted" : "Project trust removed");
1534
- }
1535
- case "login": {
1536
- if (!selection && !argument)
1537
- return workflowResult(request.command, "failed", "Login requires the owned authentication controller");
1538
- const modelRuntime = runtime.services.modelRuntime;
1539
- let loginSelection = selection ?? argument;
1540
- if (!selection && argument && !argument.includes(":")) {
1541
- const normalized = argument.toLowerCase();
1542
- const providers = modelRuntime.getProviders?.();
1543
- const providerMatches = Array.isArray(providers) ? providers.filter(isRecord).filter(candidate => stringProperty(candidate, "id")?.toLowerCase() === normalized
1544
- || stringProperty(candidate, "name")?.toLowerCase() === normalized) : [];
1545
- const providerReference = providerMatches.length === 1 ? stringProperty(providerMatches[0], "id") ?? argument : argument;
1546
- const matching = this.#loginOptions().filter(option => option.id.endsWith(`:${providerReference}`));
1547
- if (matching.length > 1) {
1548
- const provider = modelRuntime.getProvider?.(providerReference);
1549
- const providerName = stringProperty(provider, "name") ?? providerReference;
1550
- return workflowResult(request.command, "failed", `Authentication method for ${providerName} requires the owned authentication controller`);
1551
- }
1552
- if (matching[0])
1553
- loginSelection = matching[0].id;
1554
- }
1555
- const [authTypeValue = "oauth", providerId = loginSelection] = loginSelection.includes(":")
1556
- ? loginSelection.split(":", 2)
1557
- : ["oauth", loginSelection];
1558
- const authType = authTypeValue === "api_key" ? "api_key" : "oauth";
1559
- const provider = modelRuntime.getProvider?.(providerId);
1560
- const providerName = stringProperty(provider, "name") ?? providerId;
1561
- const previousModel = session.model;
1562
- this.#workflowInteraction.startLogin?.({ providerId, providerName, authType });
1563
- try {
1564
- await requireCapability(modelRuntime.login, "login").call(modelRuntime, providerId, authType, {
1565
- signal: AbortSignal.timeout(120_000),
1566
- prompt: async (prompt) => {
1567
- const promptType = stringProperty(prompt, "type");
1568
- const options = isRecord(prompt) && Array.isArray(prompt.options)
1569
- ? prompt.options.filter(isRecord).flatMap(option => {
1570
- const id = stringProperty(option, "id");
1571
- const label = stringProperty(option, "label");
1572
- return id && label ? [{ id, label }] : [];
1573
- })
1574
- : [];
1575
- const response = await this.#workflowInteraction.prompt({
1576
- type: promptType === "select"
1577
- ? "select"
1578
- : promptType === "manual_code"
1579
- ? "manual-code"
1580
- : authType === "api_key"
1581
- ? "secret"
1582
- : "text",
1583
- message: stringProperty(prompt, "message") ?? `Authenticate ${providerName}`,
1584
- ...(stringProperty(prompt, "placeholder") === undefined ? {} : { placeholder: stringProperty(prompt, "placeholder") }),
1585
- ...(options.length === 0 ? {} : { options }),
1586
- });
1587
- if (response === null)
1588
- throw new Error("Login cancelled");
1589
- return response;
1590
- },
1591
- notify: (event) => {
1592
- const notification = workflowLoginNotification(event);
1593
- if (notification)
1594
- this.#workflowInteraction.notify(notification);
1595
- },
1596
- });
1597
- }
1598
- catch (error) {
1599
- if (error instanceof Error && error.message === "Login cancelled") {
1600
- return workflowResult(request.command, "cancelled", "Login cancelled", undefined, "silent");
1601
- }
1602
- const detail = error instanceof Error ? error.message : String(error);
1603
- const actionLabel = authType === "api_key" ? `Saved API key for ${providerName}` : `Logged in to ${providerName}`;
1604
- const message = error instanceof CredentialSynchronizationError
1605
- ? `${actionLabel}, but local model state could not be synchronized: ${detail}`
1606
- : authType === "api_key"
1607
- ? `Failed to save API key for ${providerName}: ${detail}`
1608
- : `Failed to login to ${providerName}: ${detail}`;
1609
- return workflowResult(request.command, "failed", message);
1610
- }
1611
- finally {
1612
- this.#workflowInteraction.finishLogin?.();
1613
- }
1614
- return await this.#completeProviderAuthentication(providerId, providerName, authType, previousModel);
1615
- }
1616
- case "logout": {
1617
- if (!selection)
1618
- return workflowResult(request.command, "failed", "Logout requires the owned authentication controller");
1619
- const [credentialType = "oauth", providerId = selection] = selection.includes(":") ? selection.split(":", 2) : ["oauth", selection];
1620
- const modelRuntime = runtime.services.modelRuntime;
1621
- const provider = modelRuntime.getProvider?.(providerId);
1622
- const providerName = stringProperty(provider, "name") ?? providerId;
1623
- try {
1624
- await requireCapability(modelRuntime.logout, "logout").call(modelRuntime, providerId, { signal: AbortSignal.timeout(15_000) });
1625
- }
1626
- catch (error) {
1627
- const detail = error instanceof Error ? error.message : String(error);
1628
- const message = error instanceof CredentialSynchronizationError
1629
- ? `Credentials removed for ${providerName}, but local model state could not be synchronized: ${detail}`
1630
- : `Logout failed: ${detail}`;
1631
- return workflowResult(request.command, "failed", message);
1632
- }
1633
- this.#reconcileActiveModelAvailability();
1634
- this.#emitView();
1635
- return workflowResult(request.command, "completed", credentialType === "api_key"
1636
- ? `Removed stored API key for ${providerName}. Environment variables and models.json config are unchanged.`
1637
- : `Logged out of ${providerName}`);
1638
- }
1639
- case "new": {
1640
- const result = await runtime.newSession();
1641
- if (isRecord(result) && result.cancelled === true)
1642
- return workflowResult(request.command, "cancelled", "New session cancelled", undefined, "silent");
1643
- return workflowResult(request.command, "completed", "✓ New session started", undefined, "accent");
1644
- }
1645
- case "compact": {
1646
- try {
1647
- await session.compact(argument || undefined);
1648
- return workflowResult(request.command, "completed", "Compaction requested", undefined, "silent");
1649
- }
1650
- catch (error) {
1651
- return workflowResult(request.command, "failed", error instanceof Error ? error.message : String(error), undefined, "silent");
1652
- }
1653
- }
1654
- case "resume": {
1655
- if (!selection && !argument)
1656
- return workflowResult(request.command, "failed", "Resume requires the owned session controller");
1657
- const sessionPath = selection ?? argument;
1658
- try {
1659
- const result = await runtime.switchSession(sessionPath);
1660
- if (isRecord(result) && result.cancelled === true)
1661
- return workflowResult(request.command, "cancelled", "Resume cancelled", undefined, "silent");
1662
- return workflowResult(request.command, "completed", "Resumed session");
1663
- }
1664
- catch (error) {
1665
- const issue = isRecord(error) && isRecord(error.issue) ? error.issue : undefined;
1666
- const fallbackCwd = issue === undefined ? undefined : stringProperty(issue, "fallbackCwd");
1667
- if (fallbackCwd === undefined)
1668
- throw error;
1669
- if (request.confirmed === undefined) {
1670
- const sessionCwd = stringProperty(issue, "sessionCwd") ?? "the session working directory";
1671
- return workflowConfirmation(request.command, `cwd from session file does not exist\n${sessionCwd}\n\ncontinue in current cwd\n${fallbackCwd}`);
1672
- }
1673
- if (!request.confirmed)
1674
- return workflowResult(request.command, "cancelled", "Resume cancelled", undefined, "status");
1675
- const result = await runtime.switchSession(sessionPath, { cwdOverride: fallbackCwd });
1676
- if (isRecord(result) && result.cancelled === true)
1677
- return workflowResult(request.command, "cancelled", "Resume cancelled", undefined, "silent");
1678
- return workflowResult(request.command, "completed", "Resumed session in current cwd");
1679
- }
1680
- }
1681
- case "reload": {
1682
- const blocked = this.reloadBlockedResult();
1683
- if (blocked)
1684
- return blocked;
1685
- await requireCapability(session.reload, "reload").call(session);
1686
- await this.#bindExtensionUiToSession();
1687
- const message = "Reloaded keybindings, extensions, skills, prompts, themes, and context files";
1688
- const modelError = runtime.services.modelRuntime.getError?.();
1689
- return workflowResult(request.command, "completed", message, undefined, "status", modelError ? [
1690
- { kind: "error", message: `models.json error: ${modelError}` },
1691
- { kind: "status", message },
1692
- ] : undefined);
1693
- }
1694
- case "quit": {
1695
- await this.dispose();
1696
- return workflowResult(request.command, "completed", "Shutdown complete");
1697
- }
1698
- case "debug": {
1699
- const debugPath = join(this.#agentDir, "pi-debug.log");
1700
- await mkdir(dirname(debugPath), { recursive: true });
1701
- await writeFile(debugPath, `${JSON.stringify(this.snapshot(), null, 2)}\n`, "utf8");
1702
- return workflowResult(request.command, "completed", "✓ Debug log written", debugPath);
1703
- }
1704
- case "arminsayshi":
1705
- return workflowResult(request.command, "completed", "Armin says hi");
1706
- case "dementedelves":
1707
- return workflowResult(request.command, "completed", "Demented elves announcement");
1708
- }
1709
- }
1710
- async #completeProviderAuthentication(providerId, providerName, authType, previousModel) {
1711
- const session = this.#requireWorkflowSession();
1712
- const modelRuntime = this.#runtime?.services.modelRuntime;
1713
- if (!modelRuntime)
1714
- throw new Error("engine runtime is unavailable");
1715
- const actionLabel = authType === "oauth" ? `Logged in to ${providerName}` : `Saved API key for ${providerName}`;
1716
- let selectedModelId;
1717
- let selectionError;
1718
- if (isUnknownModel(previousModel)) {
1719
- const available = modelRuntime.getAvailableSnapshot?.() ?? [];
1720
- const providerModels = available.filter(model => stringProperty(model, "provider") === providerId);
1721
- const defaultModelId = PINNED_DEFAULT_MODEL_BY_PROVIDER[providerId];
1722
- if (defaultModelId === undefined) {
1723
- selectionError = `${actionLabel}, but no default model is configured for provider "${providerId}". Use /model to select a model.`;
1724
- }
1725
- else if (providerModels.length === 0) {
1726
- selectionError = `${actionLabel}, but no models are available for that provider. Use /model to select a model.`;
1727
- }
1728
- else {
1729
- const selectedModel = providerModels.find(model => stringProperty(model, "id") === defaultModelId);
1730
- if (selectedModel === undefined) {
1731
- selectionError = `${actionLabel}, but its default model "${defaultModelId}" is not available. Use /model to select a model.`;
1732
- }
1733
- else {
1734
- try {
1735
- await session.setModel(selectedModel);
1736
- selectedModelId = stringProperty(selectedModel, "id") ?? defaultModelId;
1737
- this.#activeModel = {
1738
- providerId,
1739
- modelId: selectedModelId,
1740
- displayName: stringProperty(selectedModel, "name") ?? selectedModelId,
1741
- };
1742
- }
1743
- catch (error) {
1744
- selectionError = `${actionLabel}, but selecting its default model failed: ${error instanceof Error ? error.message : String(error)}. Use /model to select a model.`;
1745
- }
1746
- }
1747
- }
1748
- }
1749
- this.#reconcileActiveModelAvailability();
1750
- this.#emitView();
1751
- const status = `${actionLabel}.${selectedModelId ? ` Selected ${selectedModelId}.` : ""} Credentials saved to ${join(this.#agentDir, "auth.json")}`;
1752
- const messages = [
1753
- { kind: "status", message: status },
1754
- ...(selectionError === undefined ? [] : [{ kind: "error", message: selectionError }]),
1755
- ];
1756
- this.#scheduleAuthenticatedProviderRefresh(providerId, actionLabel);
1757
- return workflowResult("login", "completed", status, undefined, "status", messages);
1758
- }
1759
- #scheduleAuthenticatedProviderRefresh(providerId, actionLabel) {
1760
- const runtime = this.#runtime;
1761
- const refresh = runtime?.services.modelRuntime.refresh;
1762
- if (!runtime || typeof refresh !== "function")
1763
- return;
1764
- const generation = this.#sessionGeneration;
1765
- const publish = this.#workflowInteraction.publish;
1766
- const controller = new AbortController();
1767
- const timeout = setTimeout(() => controller.abort(), AUTH_REFRESH_TIMEOUT_MS);
1768
- // Compatibility: post-authentication refresh starts on the next turn so its notices follow the saved-credential status.
1769
- void new Promise(resolve => setTimeout(resolve, 0))
1770
- .then(() => refresh.call(runtime.services.modelRuntime, { providers: [providerId], signal: controller.signal }))
1771
- .then(result => {
1772
- if (this.#disposed || this.#sessionGeneration !== generation)
1773
- return;
1774
- if (isRecord(result) && result.aborted === true) {
1775
- publish?.({ kind: "warning", message: `${actionLabel}, but its model catalog refresh timed out; using cached models.` });
1776
- }
1777
- else if (isRecord(result) && result.errors instanceof Map && result.errors.size > 0) {
1778
- publish?.({ kind: "warning", message: `${actionLabel}, but its model catalog could not be refreshed; using cached models.` });
1779
- }
1780
- this.#reconcileActiveModelAvailability();
1781
- this.#emitView();
1782
- })
1783
- .catch(error => {
1784
- if (this.#disposed || this.#sessionGeneration !== generation)
1785
- return;
1786
- publish?.({
1787
- kind: "warning",
1788
- message: controller.signal.aborted
1789
- ? `${actionLabel}, but its model catalog refresh timed out; using cached models.`
1790
- : `${actionLabel}, but its model catalog could not be refreshed: ${error instanceof Error ? error.message : String(error)}`,
1791
- });
1792
- })
1793
- .finally(() => clearTimeout(timeout));
1794
- }
1795
- #requireWorkflowSession() {
1796
- if (this.#disposed || !this.#runtime || !this.#session)
1797
- throw new Error("engine adapter is not running");
1798
- return this.#session;
1799
- }
1800
- #modelOptions() {
1801
- const runtime = this.#runtime;
1802
- if (!runtime)
1803
- return [];
1804
- const models = runtime.services.modelRuntime.getAvailableSnapshot?.();
1805
- if (!Array.isArray(models)) {
1806
- const active = this.#activeModel;
1807
- return active ? [{ id: `${active.providerId}/${active.modelId}`, label: active.displayName, description: `${active.providerId}/${active.modelId}` }] : [];
1808
- }
1809
- return models.filter(isRecord).flatMap(model => {
1810
- const provider = stringProperty(model, "provider");
1811
- const id = stringProperty(model, "id");
1812
- if (!provider || !id)
1813
- return [];
1814
- return [{ id: `${provider}/${id}`, label: stringProperty(model, "name") ?? id, description: `${provider}/${id}` }];
1815
- });
1816
- }
1817
- #loginOptions(authType) {
1818
- const runtime = this.#runtime;
1819
- const modelRuntime = runtime?.services.modelRuntime;
1820
- if (!modelRuntime)
1821
- return [];
1822
- const providers = modelRuntime.getProviders?.();
1823
- if (!Array.isArray(providers))
1824
- return [];
1825
- return providers.filter(isRecord).flatMap(provider => {
1826
- const id = stringProperty(provider, "id");
1827
- if (!id)
1828
- return [];
1829
- const name = stringProperty(provider, "name") ?? id;
1830
- const auth = isRecord(provider.auth) ? provider.auth : {};
1831
- const authStatus = modelRuntime.getProviderAuthStatus?.(id);
1832
- const source = authStatus?.label ?? authStatus?.source;
1833
- const status = authStatus?.configured === true
1834
- ? {
1835
- type: modelRuntime.isUsingOAuth?.(id) === true ? "oauth" : "api_key",
1836
- ...(source === undefined ? {} : { source }),
1837
- }
1838
- : undefined;
1839
- return [
1840
- ...(authType !== "api_key" && auth.oauth ? [{
1841
- id: `oauth:${id}`,
1842
- providerId: id,
1843
- label: name,
1844
- description: "Account / OAuth",
1845
- authType: "oauth",
1846
- ...(status === undefined ? {} : { status }),
1847
- }] : []),
1848
- ...(authType !== "oauth" && auth.apiKey ? [{
1849
- id: `api_key:${id}`,
1850
- providerId: id,
1851
- label: name,
1852
- description: "API key",
1853
- authType: "api_key",
1854
- ...(status === undefined ? {} : { status }),
1855
- }] : []),
1856
- ];
1857
- }).sort((left, right) => left.label.localeCompare(right.label));
1858
- }
1859
- async #logoutOptions() {
1860
- const runtime = this.#runtime;
1861
- if (!runtime)
1862
- return [];
1863
- const modelRuntime = runtime.services.modelRuntime;
1864
- const credentials = await requireCapability(modelRuntime.listCredentials, "listCredentials").call(modelRuntime, { signal: AbortSignal.timeout(15_000) });
1865
- if (!Array.isArray(credentials))
1866
- return [];
1867
- return credentials.filter(isRecord).flatMap(credential => {
1868
- const providerId = stringProperty(credential, "providerId");
1869
- if (!providerId)
1870
- return [];
1871
- const credentialType = stringProperty(credential, "type") === "api_key" ? "api_key" : "oauth";
1872
- const provider = modelRuntime.getProvider?.(providerId);
1873
- return [{
1874
- id: `${credentialType}:${providerId}`,
1875
- providerId,
1876
- label: stringProperty(provider, "name") ?? providerId,
1877
- description: credentialType,
1878
- authType: credentialType,
1879
- status: { type: credentialType, source: "stored credential" },
1880
- }];
1881
- });
1882
- }
1883
- async #sessionOptions() {
1884
- const session = this.#requireWorkflowSession();
1885
- const manager = session.sessionManager;
1886
- const cwd = manager?.getCwd?.();
1887
- const sessionDir = manager?.getSessionDir?.();
1888
- if (typeof cwd !== "string")
1889
- return [];
1890
- return sessionInfoOptions(await SessionManager.list(cwd, typeof sessionDir === "string" ? sessionDir : undefined));
1891
- }
1892
- async #applyPinnedSetting(selection, selectedValue, hasSelectedValue = false) {
1893
- if (!PINNED_PI_SETTINGS_CALLBACKS.includes(selection)) {
1894
- return workflowResult("settings", "failed", `Unknown setting callback: ${selection}`);
1895
- }
1896
- const callback = selection;
1897
- if (callback === "onCancel")
1898
- return workflowResult("settings", "cancelled", "Settings cancelled");
1899
- if (callback === "onThemePreview")
1900
- return workflowResult("settings", "completed", "Theme preview refreshed");
1901
- if (!hasSelectedValue) {
1902
- const snapshot = this.pinnedSettingsSnapshot();
1903
- const currentValues = {
1904
- onAutoCompactChange: snapshot.autoCompact,
1905
- onShowImagesChange: snapshot.showImages,
1906
- onImageWidthCellsChange: snapshot.imageWidthCells,
1907
- onAutoResizeImagesChange: snapshot.autoResizeImages,
1908
- onBlockImagesChange: snapshot.blockImages,
1909
- onEnableSkillCommandsChange: snapshot.enableSkillCommands,
1910
- onSteeringModeChange: snapshot.steeringMode,
1911
- onFollowUpModeChange: snapshot.followUpMode,
1912
- onTransportChange: snapshot.transport,
1913
- onHttpIdleTimeoutMsChange: snapshot.httpIdleTimeoutMs,
1914
- onThinkingLevelChange: snapshot.thinkingLevel,
1915
- onThemeChange: snapshot.currentTheme,
1916
- onHideThinkingBlockChange: snapshot.hideThinkingBlock,
1917
- onMermaidRenderingModeChange: snapshot.mermaidRenderingMode,
1918
- onShowCacheMissNoticesChange: snapshot.showCacheMissNotices,
1919
- onCollapseChangelogChange: snapshot.collapseChangelog,
1920
- onEnableInstallTelemetryChange: snapshot.enableInstallTelemetry,
1921
- onQuietStartupChange: snapshot.quietStartup,
1922
- onDefaultProjectTrustChange: snapshot.defaultProjectTrust,
1923
- onDoubleEscapeActionChange: snapshot.doubleEscapeAction,
1924
- onTreeFilterModeChange: snapshot.treeFilterMode,
1925
- onShowHardwareCursorChange: snapshot.showHardwareCursor,
1926
- onEditorPaddingXChange: snapshot.editorPaddingX,
1927
- onOutputPadChange: snapshot.outputPad,
1928
- onAutocompleteMaxVisibleChange: snapshot.autocompleteMaxVisible,
1929
- onClearOnShrinkChange: snapshot.clearOnShrink,
1930
- onShowTerminalProgressChange: snapshot.showTerminalProgress,
1931
- onTuiModeChange: snapshot.tuiMode,
1932
- onFullscreenExitOutputChange: snapshot.fullscreenExitOutput,
1933
- onFullscreenScrollbarChange: snapshot.fullscreenScrollbar,
1934
- onWarningsChange: snapshot.warnings,
1935
- };
1936
- selectedValue = currentValues[callback];
1937
- hasSelectedValue = true;
1938
- }
1939
- const port = this.settingsPort();
1940
- if (port === null)
1941
- return workflowResult("settings", "failed", "Settings are unavailable");
1942
- const key = settingKeyForCallback(callback);
1943
- if (key === null)
1944
- return workflowResult("settings", "failed", `${settingLabel(callback)} is unavailable in this runtime`);
1945
- const result = await port.writeSetting(key, agentJsonValue(selectedValue));
1946
- if (result.status === "failed" || result.status === "unavailable") {
1947
- return workflowResult("settings", "failed", result.failure ?? result.limitationReason ?? `${settingLabel(callback)} is unavailable`);
1948
- }
1949
- this.#emitView();
1950
- const suffix = result.status === "deferred" ? ` (${result.application})` : "";
1951
- return workflowResult("settings", "completed", `${settingLabel(callback)}: ${String(selectedValue)}${suffix}`);
1952
- }
1953
- async #perform(command) {
1954
- const runtime = this.#runtime;
1955
- const session = this.#session;
1956
- const generation = this.#sessionGeneration;
1957
- if (!runtime || !session)
1958
- throw new Error("engine session is unavailable");
1959
- switch (command.type) {
1960
- case "prompt":
1961
- case "steer":
1962
- case "follow-up":
1963
- case "abort":
1964
- case "retry":
1965
- case "compact": {
1966
- const result = await this.#sessionCommands?.execute(command.type === "prompt" || command.type === "steer" || command.type === "follow-up"
1967
- ? {
1968
- type: command.type,
1969
- text: command.text,
1970
- ...(command.images === undefined ? {} : { images: [...command.images] }),
1971
- }
1972
- : { type: command.type });
1973
- if (!result || result.outcome === "rejected" || result.outcome === "failed") {
1974
- throw new Error(command.type === "retry" ? "no previous prompt is available to retry" : `Pi session command failed: ${command.type}`);
1975
- }
1976
- return;
1977
- }
1978
- case "set-model": {
1979
- const model = runtime.services.modelRuntime.getModel(command.model.providerId, command.model.modelId);
1980
- if (!model) {
1981
- throw new Error(`model is unavailable: ${command.model.providerId}/${command.model.modelId}`);
1982
- }
1983
- await session.setModel(model);
1984
- if (generation === this.#sessionGeneration && this.#overload === undefined && !this.#admissionStopped)
1985
- this.#activeModel = { ...command.model };
1986
- return;
506
+ await session.setModel(model);
507
+ if (generation === this.#engine.generation && !this.#delivery.overloaded && !this.#admissionStopped)
508
+ this.#activeModel = { ...command.model };
509
+ return;
1987
510
  }
1988
511
  case "set-thinking-level":
1989
512
  session.setThinkingLevel(command.thinkingLevel);
@@ -2008,24 +531,10 @@ export class PiEngineAdapter {
2008
531
  throw new Error("owned UI state commands belong to the owned UI layer, not the Pi engine adapter");
2009
532
  }
2010
533
  }
2011
- #bindSession(session) {
2012
- this.#unsubscribe?.();
2013
- for (const pending of this.#pendingCommands.values()) {
2014
- if (pending.type !== "new-session" && pending.type !== "resume-session")
2015
- pending.cancel();
2016
- }
2017
- this.#sessionGeneration += 1;
2018
- this.#sessionBindingGeneration += 1;
2019
- this.#invalidatedEvents += this.#eventQueue.discardObsolete(this.#sessionGeneration);
2020
- this.#session = session;
2021
- this.#activeCommandIds = [];
2022
- this.#completedCommands.clear();
2023
- this.#sessionCommands = new PiSessionCommandIntegration(session);
2024
- this.#compactionProgress?.dispose();
2025
- this.#compactionProgress = observeCompactionProgress(session, percent => {
2026
- if (this.#session === session && !this.#disposed)
2027
- this.#publishCompactionProgress(percent);
2028
- });
534
+ // Invariant: the runtime already advanced the generation and subscribed; this rebuilds what the view derives from a session.
535
+ #sessionReplaced(session) {
536
+ this.#delivery.discardObsolete(this.#engine.generation);
537
+ this.#commands.reset();
2029
538
  this.#editor = {
2030
539
  text: "",
2031
540
  queuedSubmissions: [],
@@ -2035,33 +544,25 @@ export class PiEngineAdapter {
2035
544
  submitEnabled: true,
2036
545
  };
2037
546
  this.#status = { ...this.#status, workingMessage: null, workingProgress: null, badges: [] };
2038
- this.#statusKind = null;
2039
- this.#agentRunActive = false;
2040
- this.#agentRunSequence = 0;
2041
- this.#assistantResponseSequence = 0;
547
+ this.#events.reset();
2042
548
  this.#activeModel = readModel(session.model);
2043
549
  this.#reconcileActiveModelAvailability();
2044
550
  this.#thinkingLevel = readThinkingLevel(session.thinkingLevel);
2045
- this.#transcriptImageAssets.clear();
551
+ this.#projection.assets.clear();
2046
552
  // Invariant: a new binding cannot inherit arguments/results from a reused invocation id.
2047
553
  this.#setTranscript([]);
2048
- this.#rebuildTranscript(session.messages, "finalized");
2049
- const generation = this.#sessionGeneration;
2050
- this.#unsubscribe = session.subscribe(event => {
2051
- if (generation === this.#sessionGeneration && !this.#disposed)
2052
- this.#handlePiEvent(event);
2053
- });
2054
- if (this.#extensionUi !== undefined)
2055
- void this.#bindExtensionUiToSession();
554
+ this.#setTranscript(this.#projection.rebuild(session.messages, "finalized"));
555
+ if (this.#extensions.attached)
556
+ void this.#extensions.rebind();
2056
557
  }
2057
558
  #reconcileActiveModelAvailability() {
2058
- const modelRuntime = this.#runtime?.services.modelRuntime;
559
+ const modelRuntime = this.#engine.runtime?.services.modelRuntime;
2059
560
  if (!modelRuntime) {
2060
561
  this.#activeModel = null;
2061
562
  return;
2062
563
  }
2063
564
  const available = modelRuntime.getAvailableSnapshot?.() ?? [];
2064
- const sessionModel = readModel(this.#session?.model);
565
+ const sessionModel = readModel(this.#engine.session?.model);
2065
566
  const authoritative = this.#activeModel ?? sessionModel;
2066
567
  if (authoritative === null)
2067
568
  return;
@@ -2069,694 +570,13 @@ export class PiEngineAdapter {
2069
570
  && stringProperty(model, "id") === authoritative.modelId);
2070
571
  this.#activeModel = remainsAvailable ? authoritative : null;
2071
572
  }
2072
- async #bindExtensionUiToSession() {
2073
- const session = this.#session;
2074
- const ui = this.#extensionUi;
2075
- if (session === undefined || ui === undefined || session.bindExtensions === undefined) {
2076
- this.#extensionBound = false;
2077
- return;
2078
- }
2079
- try {
2080
- await session.bindExtensions({
2081
- uiContext: ui,
2082
- mode: "tui",
2083
- shutdownHandler: () => this.#extensionShutdown?.(),
2084
- onError: error => {
2085
- const message = isRecord(error) && typeof error.error === "string"
2086
- ? error.error
2087
- : error instanceof Error ? error.message : String(error);
2088
- this.#addDiagnostic("warning", "extension-ui", message, true);
2089
- this.#emitView();
2090
- },
2091
- });
2092
- this.#extensionBound = true;
2093
- this.#emitView();
2094
- }
2095
- catch (error) {
2096
- this.#extensionBound = false;
2097
- this.#addDiagnostic("error", "extension-ui-bind", error instanceof Error ? error.message : String(error), true);
2098
- this.#emitView();
2099
- }
2100
- }
2101
- // Performance: replacing the transcript rebuilds its index so block lookup stays constant-time.
2102
573
  #setTranscript(blocks) {
2103
574
  // Invariant: lazy delivery snapshots must freeze before their authoritative source can be removed.
2104
- if (!this.#eventQueue.seal()) {
2105
- this.#beginOverload();
2106
- return;
2107
- }
2108
- for (const block of blocks)
2109
- this.#transcriptImageAssets.retain(block);
2110
- for (const block of this.#transcript)
2111
- this.#transcriptImageAssets.release(block);
2112
- this.#transcript = blocks;
2113
- this.#transcriptIndex.clear();
2114
- for (const [index, block] of blocks.entries())
2115
- this.#transcriptIndex.set(block.id, index);
2116
- this.#transcriptImageAssets.discardUnowned();
2117
- this.#transcriptSnapshot = undefined;
2118
- }
2119
- #transcriptBlock(id) {
2120
- const index = this.#transcriptIndex.get(id);
2121
- return index === undefined ? undefined : this.#transcript[index];
2122
- }
2123
- // Invariant: the named work state is the only state a matching end may clear.
2124
- #enterWorkState(kind, message) {
2125
- const wasBusy = this.#lifecycle === "busy";
2126
- this.#statusKind = kind;
2127
- this.#lifecycle = "busy";
2128
- this.#status = { ...this.#status, workingMessage: message, workingProgress: null };
2129
- if (!wasBusy)
2130
- this.#emitEvent({ type: "session-lifecycle", lifecycle: "busy", reason: null });
2131
- this.#emitEvent({ type: "status", status: this.#status });
2132
- }
2133
- // Invariant: ending retry or compaction cannot clear a different active work state.
2134
- #endWorkState(kind) {
2135
- if (this.#statusKind !== kind)
2136
- return;
2137
- if (this.#agentRunActive) {
2138
- this.#enterWorkState("working", "Working");
2139
- return;
2140
- }
2141
- this.#leaveWorkStates();
2142
- }
2143
- #leaveWorkStates() {
2144
- this.#statusKind = null;
2145
- this.#lifecycle = "ready";
2146
- this.#status = { ...this.#status, workingMessage: null, workingProgress: null };
2147
- this.#emitEvent({ type: "session-lifecycle", lifecycle: "ready", reason: null });
2148
- this.#emitEvent({ type: "status", status: this.#status });
2149
- }
2150
- // Invariant: progress belongs to the compaction state only and is published when the integer changes.
2151
- #publishCompactionProgress(percent) {
2152
- if (this.#statusKind !== "compaction" || this.#status.workingProgress === percent)
2153
- return;
2154
- this.#status = { ...this.#status, workingProgress: percent };
2155
- this.#emitEvent({ type: "status", status: this.#status });
2156
- }
2157
- // Rationale: a manual compaction ends with an idle session, so the engine would never consume
2158
- // the messages queued during it; automatic compaction returns to a run or a pending prompt.
2159
- #deliverQueuedAfterCompaction() {
2160
- const commands = this.#sessionCommands;
2161
- const generation = this.#sessionGeneration;
2162
- if (commands === undefined)
575
+ if (!this.#delivery.seal()) {
576
+ this.#delivery.beginOverload();
2163
577
  return;
2164
- const report = (error) => {
2165
- if (generation !== this.#sessionGeneration || this.#disposed)
2166
- return;
2167
- this.#addDiagnostic("error", "engine-command", `Queued input could not be sent after compaction: ${error instanceof Error ? error.message : String(error)}`, true);
2168
- this.#emitView();
2169
- };
2170
- commands.deliverQueuedAfterCompaction(report).catch(report);
2171
- }
2172
- #handlePiEvent(event) {
2173
- try {
2174
- this.#applyPiEvent(event);
2175
- }
2176
- finally {
2177
- this.#transcriptImageAssets.discardUnowned();
2178
- }
2179
- }
2180
- #applyPiEvent(event) {
2181
- if (!isRecord(event) || typeof event.type !== "string")
2182
- return;
2183
- // Invariant: usage moves at message and lifecycle boundaries, not with stream chunks, so the
2184
- // two streaming event kinds keep the memo and everything else drops it.
2185
- if (event.type !== "message_update" && event.type !== "tool_execution_update")
2186
- this.#usageCache = undefined;
2187
- switch (event.type) {
2188
- case "agent_start":
2189
- this.#agentRunActive = true;
2190
- this.#agentRunSequence += 1;
2191
- this.#emitEvent({ type: "agent-run-started" });
2192
- this.#enterWorkState("working", "Working");
2193
- return;
2194
- case "message_start":
2195
- this.#upsertMessageBlock(event.message, "live");
2196
- return;
2197
- case "message_update": {
2198
- const delta = isRecord(event.assistantMessageEvent) && typeof event.assistantMessageEvent.delta === "string"
2199
- ? event.assistantMessageEvent.delta
2200
- : undefined;
2201
- // Invariant: the delta is folded in before the block is stored, so a chunk is one update to
2202
- // one block rather than a store without the delta followed by a store with it.
2203
- const blocks = this.#messageBlocks(event.message, "live", this.#transcript.length);
2204
- for (const [index, block] of blocks.entries()) {
2205
- this.#upsertTranscriptBlock(index === 0 && delta !== undefined && !block.text.endsWith(delta)
2206
- ? { ...block, text: `${block.text}${delta}` }
2207
- : block);
2208
- }
2209
- return;
2210
- }
2211
- case "message_end":
2212
- this.#upsertMessageBlock(event.message, "finalized");
2213
- this.#settleFailedDeclarations(event.message);
2214
- // Compatibility: preserve the same semantic boundary v2 counted. Transcript block
2215
- // finalization is intentionally not a substitute: rebuilds, retries,
2216
- // thinking parts, and tool rows can all finalize independently.
2217
- if (isRecord(event.message) && event.message.role === "assistant") {
2218
- this.#assistantResponseSequence += 1;
2219
- const content = Array.isArray(event.message.content) ? event.message.content : [];
2220
- const stopReason = stringValue(event.message.stopReason) ?? null;
2221
- const toolContinuation = stopReason === "toolUse"
2222
- || content.some(item => isRecord(item) && item.type === "toolCall");
2223
- const successful = stringValue(event.message.errorMessage) === undefined
2224
- && stopReason !== "error"
2225
- && stopReason !== "aborted"
2226
- && textFromContent(content).trim().length > 0;
2227
- this.#emitEvent({
2228
- type: "assistant-message-completed",
2229
- sessionGeneration: this.#sessionGeneration,
2230
- runSequence: this.#agentRunSequence,
2231
- responseSequence: this.#assistantResponseSequence,
2232
- model: this.#activeModel,
2233
- assistantMessageCount: this.#transcript.filter(block => block.kind === "assistant").length,
2234
- successful,
2235
- stopReason,
2236
- toolContinuation,
2237
- });
2238
- }
2239
- return;
2240
- case "turn_end":
2241
- this.#upsertMessageBlock(event.message, "finalized");
2242
- if (Array.isArray(event.toolResults)) {
2243
- for (const result of event.toolResults)
2244
- this.#upsertMessageBlock(result, "finalized");
2245
- }
2246
- return;
2247
- case "tool_execution_start":
2248
- case "tool_execution_end": {
2249
- this.#upsertToolExecutionBlock(event);
2250
- return;
2251
- }
2252
- case "tool_execution_update":
2253
- this.#upsertToolExecutionBlock(event);
2254
- return;
2255
- case "agent_settled":
2256
- case "agent_end": {
2257
- if (event.type === "agent_end" && event.willRetry === true)
2258
- return;
2259
- // Protocol: agent_end is run-local; only settlement reads the complete session scope.
2260
- const finalMessages = event.type === "agent_settled"
2261
- ? this.#session?.messages ?? []
2262
- : Array.isArray(event.messages) ? event.messages : [];
2263
- if (event.type === "agent_end")
2264
- this.#mergeRunTranscript(finalMessages);
2265
- else if (finalMessages.length > 0)
2266
- this.#rebuildTranscript(finalMessages, "finalized");
2267
- // Invariant: missing final messages must not erase accumulated content or invent tool outcomes.
2268
- if (finalMessages.length === 0)
2269
- this.#setTranscript(this.#transcript.map(block => block.status === "live" && transcriptToolState(block) === undefined
2270
- ? { ...block, status: "finalized", revision: block.revision + 1 } : block));
2271
- // Compatibility: ending a turn leaves the working state, as the recorded pinned baseline does, but
2272
- // it leaves only that state: a compaction or retry being shown outlives the turn
2273
- // that ended under it. Settlement ends the run, and with it every state — the
2274
- // engine ends a turn for each continuation it makes and settles once.
2275
- if (event.type === "agent_settled") {
2276
- this.#agentRunActive = false;
2277
- this.#leaveWorkStates();
2278
- }
2279
- else if (this.#statusKind === null || this.#statusKind === "working") {
2280
- this.#leaveWorkStates();
2281
- }
2282
- this.#emitView();
2283
- if (event.type === "agent_settled") {
2284
- const assistants = finalMessages.filter(message => isRecord(message) && message.role === "assistant");
2285
- const lastAssistant = assistants.at(-1);
2286
- const successful = lastAssistant !== undefined
2287
- && stringValue(lastAssistant.errorMessage) === undefined
2288
- && stringValue(lastAssistant.stopReason) !== "error"
2289
- && stringValue(lastAssistant.stopReason) !== "aborted";
2290
- this.#emitEvent({
2291
- type: "agent-run-settled",
2292
- sessionGeneration: this.#sessionGeneration,
2293
- runSequence: this.#agentRunSequence,
2294
- responseSequence: this.#assistantResponseSequence,
2295
- model: this.#activeModel,
2296
- assistantMessageCount: assistants.length,
2297
- successful,
2298
- });
2299
- }
2300
- return;
2301
- }
2302
- case "queue_update": {
2303
- const steering = readStringArray(event.steering);
2304
- const followUp = readStringArray(event.followUp);
2305
- this.#editor = {
2306
- ...this.#editor,
2307
- queuedSubmissions: [...steering, ...followUp],
2308
- historyRevision: this.#editor.historyRevision + 1,
2309
- };
2310
- this.#emitEvent({ type: "editor-state", editor: this.#editor });
2311
- return;
2312
- }
2313
- case "auto_retry_start":
2314
- this.#enterWorkState("retry", "Retrying");
2315
- return;
2316
- case "auto_retry_end":
2317
- this.#endWorkState("retry");
2318
- return;
2319
- case "compaction_start":
2320
- this.#enterWorkState("compaction", "Compacting");
2321
- this.#compactionProgress?.begin();
2322
- return;
2323
- case "compaction_end":
2324
- this.#compactionProgress?.end();
2325
- this.#endWorkState("compaction");
2326
- if (event.reason === "manual")
2327
- this.#deliverQueuedAfterCompaction();
2328
- return;
2329
- case "thinking_level_changed":
2330
- this.#thinkingLevel = readThinkingLevel(event.level);
2331
- return;
2332
- default:
2333
- return;
2334
- }
2335
- }
2336
- #readUsage() {
2337
- let input = 0;
2338
- let output = 0;
2339
- let cacheRead = 0;
2340
- let cacheWrite = 0;
2341
- let cost = 0;
2342
- let latestCacheHitRate = null;
2343
- let latestPrompt = null;
2344
- const entries = this.#session?.sessionManager?.getEntries?.()
2345
- ?? (this.#session?.messages ?? []).map(message => ({ type: "message", message }));
2346
- for (const entry of entries) {
2347
- if (!isRecord(entry))
2348
- continue;
2349
- const message = entry.type === "message" && isRecord(entry.message) ? entry.message : undefined;
2350
- const usage = message !== undefined && isRecord(message.usage)
2351
- ? message.usage
2352
- : (entry.type === "branch_summary" || entry.type === "compaction") && isRecord(entry.usage) ? entry.usage : undefined;
2353
- if (usage === undefined)
2354
- continue;
2355
- input += finiteNumber(usage.input);
2356
- output += finiteNumber(usage.output);
2357
- cacheRead += finiteNumber(usage.cacheRead);
2358
- cacheWrite += finiteNumber(usage.cacheWrite);
2359
- cost += isRecord(usage.cost) ? finiteNumber(usage.cost.total) : 0;
2360
- if (message?.role === "assistant") {
2361
- latestPrompt = {
2362
- input: finiteNumber(usage.input),
2363
- cacheRead: finiteNumber(usage.cacheRead),
2364
- cacheWrite: finiteNumber(usage.cacheWrite),
2365
- };
2366
- const promptTokens = latestPrompt.input + latestPrompt.cacheRead + latestPrompt.cacheWrite;
2367
- latestCacheHitRate = promptTokens > 0 ? (latestPrompt.cacheRead / promptTokens) * 100 : null;
2368
- }
2369
578
  }
2370
- const context = this.#session?.getContextUsage?.();
2371
- const providerId = this.#activeModel?.providerId;
2372
- const usingSubscription = providerId === "kimi-coding"
2373
- || (providerId !== undefined && this.#runtime?.services.modelRuntime.isUsingSubscription?.(providerId) === true);
2374
- return {
2375
- input,
2376
- output,
2377
- cacheRead,
2378
- cacheWrite,
2379
- cost,
2380
- latestCacheHitRate,
2381
- latestPrompt,
2382
- contextAvailable: context !== undefined,
2383
- contextTokens: context?.tokens ?? null,
2384
- contextWindow: context?.contextWindow ?? 0,
2385
- contextPercent: context?.percent ?? null,
2386
- usingSubscription,
2387
- autoCompactEnabled: this.#runtime?.services.settingsManager?.getCompactionEnabled?.() ?? true,
2388
- };
2389
- }
2390
- // Invariant: run-local completion may restate messages, but never owns transcript membership.
2391
- #mergeRunTranscript(messages) {
2392
- const positions = new Map();
2393
- const sessionOccurrences = new Map();
2394
- for (const [index, message] of (this.#session?.messages ?? []).entries()) {
2395
- const key = messageFallbackKey(message, index);
2396
- const occurrence = sessionOccurrences.get(key) ?? 0;
2397
- sessionOccurrences.set(key, occurrence + 1);
2398
- positions.set(message, { index, occurrence });
2399
- }
2400
- const runOccurrences = new Map();
2401
- for (const [index, message] of messages.entries()) {
2402
- const key = messageFallbackKey(message, index);
2403
- const occurrence = runOccurrences.get(key) ?? 0;
2404
- runOccurrences.set(key, occurrence + 1);
2405
- const position = positions.get(message) ?? { index, occurrence };
2406
- for (const block of this.#messageBlocks(message, "finalized", position.index, position.occurrence)) {
2407
- this.#upsertTranscriptBlock(block);
2408
- }
2409
- this.#settleFailedDeclarations(message);
2410
- }
2411
- }
2412
- // Invariant: full replacement is reserved for session-authoritative scope (bind/settlement).
2413
- #rebuildTranscript(messages, status) {
2414
- const blocks = [];
2415
- const blockIndexes = new Map();
2416
- const occurrences = new Map();
2417
- for (const [index, message] of messages.entries()) {
2418
- const key = messageFallbackKey(message, index);
2419
- const occurrence = occurrences.get(key) ?? 0;
2420
- occurrences.set(key, occurrence + 1);
2421
- for (const block of this.#messageBlocks(message, status, index, occurrence, id => blocks[blockIndexes.get(id) ?? -1] ?? this.#transcriptBlock(id))) {
2422
- const existingIndex = blockIndexes.get(block.id);
2423
- if (existingIndex === undefined) {
2424
- blockIndexes.set(block.id, blocks.length);
2425
- blocks.push(block);
2426
- continue;
2427
- }
2428
- const existing = blocks[existingIndex];
2429
- if (existing !== undefined) {
2430
- blocks[existingIndex] = {
2431
- ...block,
2432
- payload: {
2433
- ...(isRecord(existing.payload) ? existing.payload : {}),
2434
- ...(isRecord(block.payload) ? block.payload : {}),
2435
- },
2436
- };
2437
- }
2438
- }
2439
- }
2440
- // Performance: an authoritative rebuild restates most of what is already there. Reusing the block
2441
- // that already says it keeps its revision, and with it the rows the shell rendered for
2442
- // it — otherwise every turn that ends re-renders the whole session.
2443
- this.#setTranscript(blocks.map(block => {
2444
- const existing = this.#transcriptBlock(block.id);
2445
- const next = existing === undefined ? block : retainCompletedArguments(existing, block);
2446
- return existing !== undefined && sameBlockContent(existing, next) ? existing : next;
2447
- }));
2448
- }
2449
- #upsertMessageBlock(message, status) {
2450
- const blocks = this.#messageBlocks(message, status, this.#transcript.length);
2451
- const first = blocks[0];
2452
- for (const block of blocks)
2453
- this.#upsertTranscriptBlock(block);
2454
- return first;
2455
- }
2456
- #messageBlocks(message, status, fallbackIndex, occurrence, currentBlock = id => this.#transcriptBlock(id)) {
2457
- if (!isRecord(message) || typeof message.role !== "string")
2458
- return [];
2459
- const baseId = this.#messageBlockId(message, fallbackIndex, status, occurrence);
2460
- if (message.role === "user") {
2461
- return [{
2462
- id: baseId,
2463
- kind: "user",
2464
- status,
2465
- revision: this.#nextBlockRevision(baseId),
2466
- title: "User",
2467
- text: textFromContent(message.content),
2468
- imageReferences: this.#imageReferences(message.content, "user"),
2469
- payload: {
2470
- role: "user",
2471
- imageCount: contentImageCount(message.content),
2472
- timestamp: typeof message.timestamp === "number" && Number.isFinite(message.timestamp) ? message.timestamp : null,
2473
- },
2474
- }];
2475
- }
2476
- if (message.role === "bashExecution") {
2477
- return [{
2478
- id: baseId,
2479
- kind: "bash",
2480
- status,
2481
- revision: this.#nextBlockRevision(baseId),
2482
- title: stringValue(message.command) ?? "Bash",
2483
- text: stringValue(message.output) ?? "",
2484
- payload: {
2485
- role: "bashExecution",
2486
- command: stringValue(message.command) ?? "",
2487
- exitCode: typeof message.exitCode === "number" ? message.exitCode : null,
2488
- cancelled: message.cancelled === true,
2489
- truncated: message.truncated === true,
2490
- fullOutputPath: stringValue(message.fullOutputPath) ?? null,
2491
- excludeFromContext: message.excludeFromContext === true,
2492
- },
2493
- }];
2494
- }
2495
- if (message.role === "custom") {
2496
- if (message.display === false)
2497
- return [];
2498
- return [{
2499
- id: baseId,
2500
- kind: "custom",
2501
- status,
2502
- revision: this.#nextBlockRevision(baseId),
2503
- title: stringValue(message.customType) ?? "Custom",
2504
- text: textFromContent(message.content),
2505
- payload: {
2506
- role: "custom",
2507
- customType: stringValue(message.customType) ?? "custom",
2508
- display: true,
2509
- details: message.details,
2510
- timestamp: typeof message.timestamp === "number" ? message.timestamp : 0,
2511
- },
2512
- }];
2513
- }
2514
- if (message.role === "compactionSummary" || message.role === "branchSummary") {
2515
- return [{
2516
- id: baseId,
2517
- kind: "compaction",
2518
- status,
2519
- revision: this.#nextBlockRevision(baseId),
2520
- title: message.role === "branchSummary" ? "Branch summary" : "Compaction summary",
2521
- text: stringValue(message.summary) ?? "",
2522
- payload: {
2523
- role: message.role,
2524
- tokensBefore: typeof message.tokensBefore === "number" ? message.tokensBefore : 0,
2525
- fromId: stringValue(message.fromId) ?? null,
2526
- timestamp: typeof message.timestamp === "number" ? message.timestamp : 0,
2527
- },
2528
- }];
2529
- }
2530
- if (message.role === "toolResult") {
2531
- const toolCallId = stringValue(message.toolCallId);
2532
- const blockId = toolCallId === undefined
2533
- ? baseId
2534
- : this.#toolBlockIds.get(toolCallId) ?? `tool-${toolCallId}`;
2535
- if (toolCallId !== undefined)
2536
- this.#toolBlockIds.set(toolCallId, blockId);
2537
- const existing = currentBlock(blockId);
2538
- const existingPayload = isRecord(existing?.payload) ? existing.payload : undefined;
2539
- const payload = {
2540
- role: "toolResult", toolCallId: toolCallId ?? null,
2541
- toolName: stringValue(message.toolName) ?? stringValue(existingPayload?.toolName) ?? "unknown",
2542
- argsComplete: true, partialResult: status === "live", isError: message.isError === true,
2543
- };
2544
- const rendering = toolRenderingInput({ args: undefined, previousArgs: existing?.toolRendering,
2545
- result: message, payload, image: part => this.#imageReferences([part], "tool-result")[0] });
2546
- return [{
2547
- id: blockId,
2548
- kind: "tool-result",
2549
- status,
2550
- toolState: { argsComplete: true, execution: status === "live" ? "running" : message.isError === true ? "failed" : "succeeded" },
2551
- revision: this.#nextBlockRevision(blockId),
2552
- title: stringValue(message.toolName) ?? existing?.title ?? "Tool result",
2553
- ...rendering,
2554
- payload,
2555
- }];
2556
- }
2557
- if (message.role !== "assistant" || !Array.isArray(message.content))
2558
- return [];
2559
- const blocks = [{
2560
- id: baseId,
2561
- kind: "assistant",
2562
- status,
2563
- revision: this.#nextBlockRevision(baseId),
2564
- title: "Assistant",
2565
- text: textFromContent(message.content),
2566
- payload: {
2567
- role: "assistant",
2568
- content: assistantContent(message.content),
2569
- provider: stringValue(message.provider) ?? null,
2570
- model: stringValue(message.model) ?? null,
2571
- api: stringValue(message.api) ?? null,
2572
- usage: sanitizeJson(message.usage),
2573
- stopReason: stringValue(message.stopReason) ?? null,
2574
- errorMessage: stringValue(message.errorMessage) ?? null,
2575
- timestamp: typeof message.timestamp === "number" ? message.timestamp : 0,
2576
- },
2577
- }];
2578
- for (const item of message.content) {
2579
- if (!isRecord(item) || item.type !== "toolCall")
2580
- continue;
2581
- const toolCallId = stringValue(item.id) ?? `${baseId}:${blocks.length}`;
2582
- const blockId = this.#toolBlockIds.get(toolCallId) ?? `tool-${toolCallId}`;
2583
- this.#toolBlockIds.set(toolCallId, blockId);
2584
- const failure = status === "finalized" ? this.#declarationFailure(message) : undefined;
2585
- const payload = { toolCallId, toolName: stringValue(item.name) ?? "unknown",
2586
- argsComplete: failure === undefined && status === "finalized",
2587
- ...(failure === undefined ? {} : { isError: true }) };
2588
- const rendering = toolRenderingInput({ args: item.arguments, payload,
2589
- ...(failure === undefined ? {} : { result: { content: [{ type: "text", text: failure.text }] } }),
2590
- image: () => undefined });
2591
- blocks.push({
2592
- id: blockId,
2593
- kind: failure === undefined ? "tool-call" : "tool-result",
2594
- status: failure === undefined ? "live" : "finalized",
2595
- toolState: {
2596
- argsComplete: failure === undefined && status === "finalized",
2597
- execution: failure?.execution ?? "pending",
2598
- },
2599
- revision: this.#nextBlockRevision(blockId),
2600
- title: stringValue(item.name) ?? "Tool",
2601
- ...rendering,
2602
- text: failure?.text ?? jsonSummary(item.arguments).summary,
2603
- payload,
2604
- });
2605
- }
2606
- return blocks;
2607
- }
2608
- #declarationFailure(message) {
2609
- // Provenance: pinned interactive-mode message_end and history reconstruction settle pending tools.
2610
- if (message.role !== "assistant")
2611
- return undefined;
2612
- if (message.stopReason === "aborted") {
2613
- const attempts = this.#session?.retryAttempt ?? 0;
2614
- return { execution: "aborted", text: attempts > 0
2615
- ? `Aborted after ${attempts} retry attempt${attempts > 1 ? "s" : ""}` : "Operation aborted" };
2616
- }
2617
- return message.stopReason === "error"
2618
- ? { execution: "failed", text: stringValue(message.errorMessage) || "Error" } : undefined;
2619
- }
2620
- #settleFailedDeclarations(message) {
2621
- if (!isRecord(message))
2622
- return;
2623
- const failure = this.#declarationFailure(message);
2624
- if (failure === undefined)
2625
- return;
2626
- for (const block of this.#transcript) {
2627
- const state = transcriptToolState(block);
2628
- if (state === undefined || state.execution !== "pending" && state.execution !== "running")
2629
- continue;
2630
- const payload = { ...(isRecord(block.payload) ? block.payload : {}), partialResult: false, isError: true };
2631
- this.#upsertTranscriptBlock({
2632
- ...block, kind: "tool-result", status: "finalized", revision: block.revision + 1,
2633
- ...toolRenderingInput({ args: undefined, previousArgs: block.toolRendering, payload,
2634
- result: { content: [{ type: "text", text: failure.text }] }, image: () => undefined }),
2635
- toolState: { ...state, execution: failure.execution }, payload,
2636
- });
2637
- }
2638
- }
2639
- #imageReferences(content, source) {
2640
- if (!Array.isArray(content))
2641
- return [];
2642
- const references = [];
2643
- for (const item of content) {
2644
- if (references.length >= 16)
2645
- break;
2646
- const reference = this.#transcriptImageAssets.reference(item, source);
2647
- if (reference !== undefined)
2648
- references.push(reference);
2649
- }
2650
- return references;
2651
- }
2652
- #upsertToolExecutionBlock(event) {
2653
- const toolCallId = stringValue(event.toolCallId);
2654
- if (!toolCallId)
2655
- return;
2656
- const blockId = this.#toolBlockIds.get(toolCallId) ?? `tool-${toolCallId}`;
2657
- this.#toolBlockIds.set(toolCallId, blockId);
2658
- const ended = event.type === "tool_execution_end";
2659
- const existing = this.#transcriptBlock(blockId);
2660
- const state = existing === undefined ? undefined : transcriptToolState(existing);
2661
- // Invariant: duplicate starts cannot blank accumulated output; late events cannot reopen a settled invocation.
2662
- if (state !== undefined && (state.execution !== "pending" && state.execution !== "running"
2663
- || event.type === "tool_execution_start" && state.execution === "running"))
2664
- return;
2665
- const source = ended ? event.result : event.partialResult;
2666
- const payload = { toolCallId, toolName: stringValue(event.toolName) ?? "unknown",
2667
- partialResult: event.type === "tool_execution_update", argsComplete: true, isError: event.isError === true };
2668
- const rendering = toolRenderingInput({ args: event.args, previousArgs: existing?.toolRendering, result: source,
2669
- payload, image: part => this.#imageReferences([part], "tool-result")[0] });
2670
- this.#upsertTranscriptBlock({
2671
- id: blockId,
2672
- kind: ended ? "tool-result" : "tool-call",
2673
- status: ended ? "finalized" : "live",
2674
- toolState: { argsComplete: true, execution: ended ? event.isError === true ? "failed" : "succeeded" : "running" },
2675
- revision: this.#nextBlockRevision(blockId),
2676
- title: stringValue(event.toolName) ?? "Tool",
2677
- ...rendering,
2678
- payload,
2679
- });
2680
- }
2681
- #upsertTranscriptBlock(block) {
2682
- const index = this.#transcriptIndex.get(block.id);
2683
- if (index !== undefined) {
2684
- const existing = this.#transcript[index];
2685
- // Invariant: argument completion is not execution finality; stale phases still stay rejected.
2686
- if (existing !== undefined && !acceptsTranscriptUpdate(existing, block))
2687
- return;
2688
- if (existing !== undefined)
2689
- block = retainCompletedArguments(existing, block);
2690
- // Performance: nothing is emitted for a block that repeats itself, and keeping the
2691
- // revision keeps the rows it already rendered.
2692
- if (existing !== undefined && sameBlockContent(existing, block))
2693
- return;
2694
- this.#transcriptImageAssets.retain(block);
2695
- if (existing !== undefined)
2696
- this.#transcriptImageAssets.release(existing);
2697
- this.#transcript[index] = block;
2698
- }
2699
- else {
2700
- this.#transcriptImageAssets.retain(block);
2701
- this.#transcriptIndex.set(block.id, this.#transcript.length);
2702
- this.#transcript.push(block);
2703
- }
2704
- this.#transcriptSnapshot = undefined;
2705
- this.#emitEvent({ type: "transcript-block", block });
2706
- }
2707
- #messageBlockId(message, fallbackIndex, status, occurrence) {
2708
- const existing = this.#messageBlockIds.get(message);
2709
- if (existing)
2710
- return existing;
2711
- const fallback = messageFallbackKey(message, fallbackIndex);
2712
- const cached = this.#messageFallbackIds.get(fallback) ?? [];
2713
- let id;
2714
- if (occurrence !== undefined) {
2715
- id = cached[occurrence];
2716
- if (id === undefined) {
2717
- id = `${fallback}-${occurrence}`;
2718
- cached[occurrence] = id;
2719
- }
2720
- }
2721
- else {
2722
- id = [...cached].reverse().find(candidate => this.#transcriptBlock(candidate)?.status === "live");
2723
- if (id === undefined && status === "finalized")
2724
- id = cached.at(-1);
2725
- if (id === undefined) {
2726
- id = `${fallback}-${cached.length}`;
2727
- cached.push(id);
2728
- }
2729
- }
2730
- this.#messageFallbackIds.set(fallback, cached);
2731
- this.#messageBlockIds.set(message, id);
2732
- return id;
2733
- }
2734
- #nextBlockRevision(id) {
2735
- const existing = this.#transcriptBlock(id);
2736
- if (existing)
2737
- return existing.revision + 1;
2738
- this.#nextBlockSequence += 1;
2739
- return this.#nextBlockSequence;
2740
- }
2741
- #recordCommand(command, outcome, diagnostic) {
2742
- const result = this.#finishCommand(command, outcome, diagnostic);
2743
- this.#completedCommands.set(command.correlationId, result);
2744
- if (this.#completedCommands.size > 256) {
2745
- const oldest = this.#completedCommands.keys().next().value;
2746
- if (oldest)
2747
- this.#completedCommands.delete(oldest);
2748
- }
2749
- return result;
2750
- }
2751
- #finishCommand(command, outcome, diagnostic) {
2752
- this.#activeCommandIds = this.#activeCommandIds.filter(id => id !== command.correlationId);
2753
- this.#emitEvent({
2754
- type: "command-outcome",
2755
- correlationId: command.correlationId,
2756
- outcome,
2757
- diagnostic,
2758
- });
2759
- return { outcome, diagnostic };
579
+ this.#projection.replace(blocks);
2760
580
  }
2761
581
  #addDiagnostic(severity, code, message, recoverable) {
2762
582
  const diagnostic = this.#recordDiagnostic(severity, code, message, recoverable);
@@ -2768,138 +588,40 @@ export class PiEngineAdapter {
2768
588
  this.#emitEvent({ type: "session-view", view: this.view() });
2769
589
  }
2770
590
  #emitEvent(value) {
2771
- const event = this.#event(value);
2772
- if (event.type !== "session-view")
591
+ if (value.type !== "session-view")
2773
592
  this.#viewRevision += 1;
2774
- this.#enqueueEvent(event);
2775
- }
2776
- #enqueueEvent(event) {
2777
- if (this.#overload !== undefined) {
2778
- if (event.type === "command-outcome" && event.outcome !== "accepted" && this.#pendingCommands.has(event.correlationId)) {
2779
- this.#reservedOutcomes.set(event.correlationId, { outcome: event.outcome, diagnostic: event.diagnostic });
2780
- }
2781
- return;
2782
- }
2783
- if (!this.#eventQueue.push(event, this.#sessionGeneration, event.type === "transcript-block"
2784
- ? this.#blockReconciliation(event.block.id, event.sequence) : undefined)) {
2785
- this.#beginOverload();
2786
- if (event.type === "command-outcome" && event.outcome !== "accepted" && this.#pendingCommands.has(event.correlationId)
2787
- && !this.#reservedOutcomes.has(event.correlationId)) {
2788
- this.#reservedOutcomes.set(event.correlationId, { outcome: event.outcome, diagnostic: event.diagnostic });
2789
- }
2790
- }
2791
- this.#eventQueueProcessing ??= Promise.resolve().then(() => this.#processEventQueue());
2792
- }
2793
- // Performance: capture only identity, not the complete block/event retained by an earlier revision.
2794
- #blockReconciliation(id, sequence) {
2795
- return () => ({ type: "transcript-block", sessionId: this.#sessionId, sequence, block: this.#transcriptBlock(id) });
2796
- }
2797
- #beginOverload() {
2798
- if (this.#overload !== undefined)
2799
- return;
2800
- this.#overloads = Math.min(Number.MAX_SAFE_INTEGER, this.#overloads + 1);
2801
- this.#deliveryFailed = true;
2802
- // Concurrency: reserve before cancellation; outcomes produced reentrantly must not enter the saturated queue.
2803
- this.#overload = Promise.resolve(false);
2804
- for (const pending of this.#pendingCommands.values())
2805
- pending.cancel();
2806
- for (const pending of this.#pendingWorkflows)
2807
- pending.cancel();
2808
- const session = this.#session;
2809
- this.#overload = new Promise(resolve => {
2810
- const timer = setTimeout(() => resolve(false), 2000);
2811
- void Promise.resolve().then(() => session?.abort()).then(() => {
2812
- clearTimeout(timer);
2813
- resolve(true);
2814
- }, () => { clearTimeout(timer); resolve(false); });
2815
- });
593
+ this.#delivery.emit(value);
2816
594
  }
2817
- #deliver(event) {
2818
- for (const [listener, subscribedAt] of this.#listeners) {
2819
- if (event.sequence <= subscribedAt)
2820
- continue;
2821
- try {
2822
- listener(event);
2823
- }
2824
- catch (error) {
2825
- this.#deliveryFailed = true;
2826
- this.#recordDiagnostic("warning", "event-listener", error instanceof Error ? error.message : String(error), true);
2827
- }
2828
- }
595
+ // Concurrency: cancellation runs synchronously so the overload reservation already guards reentrant outcomes.
596
+ #cancelForOverload() {
597
+ this.#commands.cancelPending();
598
+ this.#workflows.cancelPending();
599
+ const session = this.#engine.session;
600
+ return Promise.resolve().then(() => session?.abort()).then(() => undefined);
2829
601
  }
2830
- async #reconcileOverload() {
2831
- const cancelled = await this.#overload;
602
+ async #reconcileOverload(cancelled) {
2832
603
  const canResume = cancelled === true && this.#runningCommands === 0;
2833
604
  this.#admissionStopped = !canResume;
2834
- this.#unsubscribe?.();
2835
- this.#unsubscribe = undefined;
2836
- this.#compactionProgress?.dispose();
2837
- this.#compactionProgress = null;
2838
- ++this.#sessionGeneration;
2839
- this.#agentRunActive = false;
2840
- this.#statusKind = null;
605
+ this.#engine.suspend();
606
+ this.#events.abandonRun();
2841
607
  this.#status = { ...this.#status, workingMessage: null, workingProgress: null };
2842
608
  this.#lifecycle = this.#disposed ? "stopped" : canResume ? "ready" : "failed";
2843
609
  this.#editor = { ...this.#editor, submitEnabled: canResume && !this.#disposed };
2844
610
  this.#viewRevision += 1;
2845
611
  // Invariant: overload publishes one authoritative view; settle tool phases before finalizing other live blocks.
2846
- this.#settleFailedDeclarations({ role: "assistant", stopReason: cancelled ? "aborted" : "error",
612
+ this.#projection.settleFailedDeclarations({ role: "assistant", stopReason: cancelled ? "aborted" : "error",
2847
613
  errorMessage: "Tool result unavailable after UI delivery overload" });
2848
- this.#setTranscript(this.#transcript.map(block => block.status === "live" ? { ...block, status: "finalized", revision: block.revision + 1 } : block));
2849
- for (const [correlationId, result] of this.#reservedOutcomes) {
2850
- this.#deliver(this.#event({ type: "command-outcome", correlationId, ...result }));
614
+ this.#setTranscript(this.#projection.blocks.map(block => block.status === "live" ? { ...block, status: "finalized", revision: block.revision + 1 } : block));
615
+ for (const [correlationId, result] of this.#delivery.takeReservedOutcomes()) {
616
+ this.#delivery.deliverNow(this.#delivery.stamp({ type: "command-outcome", correlationId, ...result }));
2851
617
  await new Promise(resolve => setImmediate(resolve));
2852
618
  }
2853
- this.#reservedOutcomes.clear();
2854
619
  // Invariant: one out-of-band authoritative reconciliation, not an emergency transcript/event log.
2855
- this.#deliver(this.#event({ type: "session-view", view: this.view() }));
620
+ this.#delivery.deliverNow(this.#delivery.stamp({ type: "session-view", view: this.view() }));
2856
621
  if (this.#disposed)
2857
- this.#deliver(this.#event({ type: "session-lifecycle", lifecycle: "stopped", reason: null }));
2858
- if (canResume && !this.#disposed && this.#session !== undefined) {
2859
- const generation = this.#sessionGeneration;
2860
- this.#unsubscribe = this.#session.subscribe(event => {
2861
- if (generation === this.#sessionGeneration && !this.#disposed)
2862
- this.#handlePiEvent(event);
2863
- });
2864
- }
2865
- this.#overload = undefined;
2866
- }
2867
- async #processEventQueue() {
2868
- try {
2869
- let deliveredSinceYield = 0;
2870
- while (this.#eventQueue.size > 0) {
2871
- const pending = this.#eventQueue.shift();
2872
- if (pending === undefined)
2873
- continue;
2874
- if (pending.generation !== this.#sessionGeneration && pending.event.type !== "command-outcome") {
2875
- this.#invalidatedEvents = Math.min(Number.MAX_SAFE_INTEGER, this.#invalidatedEvents + 1);
2876
- pending.release?.();
2877
- continue;
2878
- }
2879
- try {
2880
- this.#deliver(pending.event);
2881
- }
2882
- finally {
2883
- pending.release?.();
2884
- }
2885
- deliveredSinceYield += 1;
2886
- // Concurrency: a microtask chain runs to exhaustion before the loop turns, so a streaming
2887
- // burst would hold typed input, pointer reports, and timed indicators until it
2888
- // drained. Yielding on a macrotask hands those their turn between batches.
2889
- if (deliveredSinceYield >= EVENT_DELIVERY_BATCH && this.#eventQueue.size > 0) {
2890
- deliveredSinceYield = 0;
2891
- await new Promise(resolve => { setImmediate(resolve); });
2892
- }
2893
- }
2894
- if (this.#overload !== undefined)
2895
- await this.#reconcileOverload();
2896
- }
2897
- finally {
2898
- this.#eventQueueProcessing = undefined;
2899
- if (this.#eventQueue.size > 0) {
2900
- this.#eventQueueProcessing = Promise.resolve().then(() => this.#processEventQueue());
2901
- }
2902
- }
622
+ this.#delivery.deliverNow(this.#delivery.stamp({ type: "session-lifecycle", lifecycle: "stopped", reason: null }));
623
+ if (canResume && !this.#disposed)
624
+ this.#engine.resume();
2903
625
  }
2904
626
  #recordDiagnostic(severity, code, message, recoverable) {
2905
627
  const diagnostic = {
@@ -2918,619 +640,9 @@ export class PiEngineAdapter {
2918
640
  };
2919
641
  return diagnostic;
2920
642
  }
2921
- #event(value) {
2922
- this.#sequence += 1;
2923
- return { ...value, sessionId: this.#sessionId, sequence: this.#sequence };
2924
- }
2925
643
  }
2926
644
  export async function createPiEngineAdapter(options = {}) {
2927
645
  const adapter = new PiEngineAdapter(options);
2928
646
  await adapter.start();
2929
647
  return adapter;
2930
648
  }
2931
- async function createDefaultPiRuntime(input) {
2932
- return createPiRuntimeIntegration({
2933
- cwd: input.cwd,
2934
- agentDir: input.agentDir,
2935
- ...(input.sessionPath === undefined ? {} : { sessionPath: input.sessionPath }),
2936
- ...(input.sessionSelection === undefined ? {} : { sessionSelection: input.sessionSelection }),
2937
- ...(input.sessionForkPrompt === undefined ? {} : { sessionForkPrompt: input.sessionForkPrompt }),
2938
- ...(input.projectTrustPrompt === undefined ? {} : { projectTrustPrompt: input.projectTrustPrompt }),
2939
- });
2940
- }
2941
- async function checkDefaultPiPackageUpdates(cwd, agentDir, settingsManager) {
2942
- const packageManager = new DefaultPackageManager({ cwd, agentDir, settingsManager });
2943
- const updates = await packageManager.checkForAvailableUpdates();
2944
- return updates.map(update => update.displayName);
2945
- }
2946
- function pinnedSessionInfoPresentation(value, sessionName, entries, modelRuntime) {
2947
- const stats = isRecord(value) ? value : {};
2948
- const tokens = dynamicObject(stats, "tokens");
2949
- return {
2950
- kind: "session-info",
2951
- ...(sessionName === undefined ? {} : { sessionName }),
2952
- stats: {
2953
- ...(stringProperty(stats, "sessionFile") === undefined ? {} : { sessionFile: stringProperty(stats, "sessionFile") }),
2954
- sessionId: stringProperty(stats, "sessionId") ?? "unknown",
2955
- userMessages: finiteNumber(stats.userMessages),
2956
- assistantMessages: finiteNumber(stats.assistantMessages),
2957
- toolCalls: finiteNumber(stats.toolCalls),
2958
- toolResults: finiteNumber(stats.toolResults),
2959
- totalMessages: finiteNumber(stats.totalMessages),
2960
- tokens: {
2961
- input: finiteNumber(tokens.input),
2962
- output: finiteNumber(tokens.output),
2963
- cacheRead: finiteNumber(tokens.cacheRead),
2964
- cacheWrite: finiteNumber(tokens.cacheWrite),
2965
- total: finiteNumber(tokens.total),
2966
- },
2967
- cost: finiteNumber(stats.cost),
2968
- },
2969
- cacheWaste: pinnedCacheWaste(entries, modelRuntime),
2970
- usageBreakdown: pinnedUsageCostBreakdown(entries),
2971
- };
2972
- }
2973
- function pinnedUsageCostBreakdown(entries) {
2974
- const totals = new Map();
2975
- for (const entry of entries) {
2976
- if (!isRecord(entry))
2977
- continue;
2978
- let key;
2979
- let usage;
2980
- const message = dynamicObject(entry, "message");
2981
- if (entry.type === "message" && message.role === "assistant") {
2982
- const provider = stringProperty(message, "provider");
2983
- const model = stringProperty(message, "responseModel") ?? stringProperty(message, "model");
2984
- if (provider && model)
2985
- key = `${provider}/${model}`;
2986
- usage = dynamicObject(message, "usage");
2987
- }
2988
- else if (entry.type === "message" && message.role === "toolResult" && isRecord(message.usage)) {
2989
- key = "Tools/summaries";
2990
- usage = message.usage;
2991
- }
2992
- else if ((entry.type === "branch_summary" || entry.type === "compaction") && isRecord(entry.usage)) {
2993
- key = "Tools/summaries";
2994
- usage = entry.usage;
2995
- }
2996
- if (!key || !usage)
2997
- continue;
2998
- const cost = finiteNumber(dynamicObject(usage, "cost").total);
2999
- const tokens = finiteNumber(usage.input) + finiteNumber(usage.output)
3000
- + finiteNumber(usage.cacheRead) + finiteNumber(usage.cacheWrite);
3001
- const current = totals.get(key) ?? { cost: 0, tokens: 0 };
3002
- current.cost += cost;
3003
- current.tokens += tokens;
3004
- totals.set(key, current);
3005
- }
3006
- return [...totals].map(([key, total]) => ({ key, ...total }))
3007
- .filter(entry => entry.cost > 0 || entry.tokens > 0)
3008
- .sort((a, b) => b.cost - a.cost);
3009
- }
3010
- function pinnedCacheWaste(entries, modelRuntime) {
3011
- let previous;
3012
- const totals = { missedTokens: 0, missedCost: 0, missCount: 0 };
3013
- for (const entry of entries) {
3014
- if (!isRecord(entry))
3015
- continue;
3016
- if (entry.type === "compaction" || entry.type === "branch_summary") {
3017
- previous = undefined;
3018
- continue;
3019
- }
3020
- const message = dynamicObject(entry, "message");
3021
- if (entry.type !== "message" || message.role !== "assistant")
3022
- continue;
3023
- const usage = dynamicObject(message, "usage");
3024
- const input = finiteNumber(usage.input);
3025
- const cacheRead = finiteNumber(usage.cacheRead);
3026
- const cacheWrite = finiteNumber(usage.cacheWrite);
3027
- const promptTokens = input + cacheRead + cacheWrite;
3028
- if (previous && promptTokens > 0 && (cacheRead + cacheWrite > 0 || previous.reportedCache)) {
3029
- const missedTokens = Math.min(previous.promptTokens, promptTokens) - cacheRead;
3030
- if (missedTokens > 1024) {
3031
- const cost = dynamicObject(usage, "cost");
3032
- const paidTokens = input + cacheWrite;
3033
- const paidRate = paidTokens > 0 ? (finiteNumber(cost.input) + finiteNumber(cost.cacheWrite)) / paidTokens : 0;
3034
- const provider = stringProperty(message, "provider") ?? "";
3035
- const modelId = stringProperty(message, "model") ?? "";
3036
- const model = modelRuntime.getModel(provider, modelId);
3037
- const modelCost = dynamicObject(dynamicObject(model, "cost"));
3038
- const readRate = cacheRead > 0
3039
- ? finiteNumber(cost.cacheRead) / cacheRead
3040
- : finiteNumber(modelCost.cacheRead) / 1_000_000;
3041
- totals.missedTokens += missedTokens;
3042
- totals.missedCost += missedTokens * Math.max(0, paidRate - readRate);
3043
- totals.missCount += 1;
3044
- }
3045
- }
3046
- if (promptTokens > 0) {
3047
- const provider = stringProperty(message, "provider") ?? "";
3048
- const model = stringProperty(message, "model") ?? "";
3049
- previous = {
3050
- promptTokens,
3051
- modelKey: `${provider}/${model}`,
3052
- timestamp: finiteNumber(message.timestamp),
3053
- reportedCache: (previous?.reportedCache ?? false) || cacheRead + cacheWrite > 0,
3054
- };
3055
- }
3056
- }
3057
- return totals;
3058
- }
3059
- function defaultWorkflowHost() {
3060
- return {
3061
- copyText: copyToClipboard,
3062
- async runCommand(command, arguments_, options) {
3063
- const result = await execFileAsync(command, [...arguments_], { encoding: "utf8", signal: options?.signal });
3064
- return { stdout: result.stdout, stderr: result.stderr };
3065
- },
3066
- readChangelog: readPinnedCommandChangelog,
3067
- };
3068
- }
3069
- function workflowLoginNotification(event) {
3070
- if (!isRecord(event))
3071
- return undefined;
3072
- if (event.type === "auth_url") {
3073
- const url = stringProperty(event, "url");
3074
- if (!url)
3075
- return undefined;
3076
- const instructions = stringProperty(event, "instructions");
3077
- return { type: "auth_url", url, ...(instructions === undefined ? {} : { instructions }) };
3078
- }
3079
- if (event.type === "device_code") {
3080
- const verificationUri = stringProperty(event, "verificationUri");
3081
- const userCode = stringProperty(event, "userCode");
3082
- return verificationUri && userCode ? { type: "device_code", verificationUri, userCode } : undefined;
3083
- }
3084
- const message = stringProperty(event, "message");
3085
- if (!message)
3086
- return undefined;
3087
- if (event.type === "info") {
3088
- const links = Array.isArray(event.links) ? event.links.filter(isRecord).flatMap(link => {
3089
- const url = stringProperty(link, "url");
3090
- if (!url)
3091
- return [];
3092
- const label = stringProperty(link, "label");
3093
- return [{ ...(label === undefined ? {} : { label }), url }];
3094
- }) : [];
3095
- return { type: "info", message, ...(links.length === 0 ? {} : { links }) };
3096
- }
3097
- return { type: event.type === "waiting" ? "waiting" : "progress", message };
3098
- }
3099
- function workflowResult(command, outcome, message, detail, messageKind, messages) {
3100
- return {
3101
- command,
3102
- outcome,
3103
- message,
3104
- ...(detail === undefined ? {} : { detail }),
3105
- ...(messageKind === undefined ? {} : { messageKind }),
3106
- ...(messages === undefined ? {} : { messages: Object.freeze([...messages]) }),
3107
- };
3108
- }
3109
- function workflowConfirmation(command, message, detail) {
3110
- return {
3111
- command,
3112
- outcome: "requires-confirmation",
3113
- message,
3114
- ...(detail === undefined ? {} : { detail }),
3115
- selectorTitle: "Confirm",
3116
- options: [
3117
- { id: "yes", label: "Yes" },
3118
- { id: "no", label: "No" },
3119
- ],
3120
- };
3121
- }
3122
- function errorMessage(error, fallback) {
3123
- return error instanceof Error ? error.message : fallback;
3124
- }
3125
- function shareCommandFailureDetail(error) {
3126
- // Compatibility: Pi reports a failed child's stderr, not execFile's command wrapper.
3127
- if (isRecord(error) && (typeof error.code === "number" || typeof error.signal === "string") && typeof error.stderr === "string") {
3128
- return error.stderr.trim() || "Unknown error";
3129
- }
3130
- return errorMessage(error, "Unknown error");
3131
- }
3132
- function commandMissing(error) {
3133
- return isRecord(error) && error.code === "ENOENT";
3134
- }
3135
- function isAbortError(error) {
3136
- return isRecord(error) && (error.name === "AbortError" || error.code === "ABORT_ERR");
3137
- }
3138
- function shareViewerUrl(gistId) {
3139
- const baseUrl = process.env.PI_SHARE_VIEWER_URL || "https://pi.dev/session/";
3140
- return `${baseUrl}#${gistId}`;
3141
- }
3142
- function workflowOptions(value, idKey, labelKey) {
3143
- if (!Array.isArray(value))
3144
- return [];
3145
- return value.filter(isRecord).flatMap(item => {
3146
- const id = stringProperty(item, idKey);
3147
- if (!id)
3148
- return [];
3149
- return [{ id, label: stringProperty(item, labelKey) ?? id }];
3150
- });
3151
- }
3152
- function sessionInfoOptions(value) {
3153
- return value.filter(isRecord).flatMap(info => {
3154
- const path = stringProperty(info, "path");
3155
- if (!path)
3156
- return [];
3157
- const modified = info.modified instanceof Date ? info.modified.toISOString() : stringProperty(info, "modified") ?? "unknown time";
3158
- const messageCount = typeof info.messageCount === "number" ? info.messageCount : 0;
3159
- return [{
3160
- id: path,
3161
- label: stringProperty(info, "name") ?? stringProperty(info, "firstMessage") ?? stringProperty(info, "id") ?? path,
3162
- description: `${messageCount} messages · ${modified}`,
3163
- }];
3164
- });
3165
- }
3166
- function assertPiExtensionUiContext(value) {
3167
- assertOwnedUiExtensionUiPort(value);
3168
- }
3169
- function dynamicObject(value, key) {
3170
- const candidate = key === undefined ? value : isRecord(value) ? value[key] : undefined;
3171
- return isRecord(candidate) ? candidate : {};
3172
- }
3173
- function requireCapability(capability, name) {
3174
- if (typeof capability !== "function")
3175
- throw new Error(`Pi workflow capability is unavailable: ${name}`);
3176
- return capability;
3177
- }
3178
- function pathArgument(value) {
3179
- if (!value)
3180
- return undefined;
3181
- const quote = value[0];
3182
- if (quote === '"' || quote === "'") {
3183
- const closing = value.indexOf(quote, 1);
3184
- return closing < 0 ? undefined : value.slice(1, closing);
3185
- }
3186
- return value.split(/\s/, 1)[0] || undefined;
3187
- }
3188
- function settingKeyForCallback(callback) {
3189
- const keys = {
3190
- onAutoCompactChange: "autoCompact", onShowImagesChange: "showImages", onImageWidthCellsChange: "imageWidthCells",
3191
- onAutoResizeImagesChange: "autoResizeImages", onBlockImagesChange: "blockImages", onEnableSkillCommandsChange: "enableSkillCommands",
3192
- onSteeringModeChange: "steeringMode", onFollowUpModeChange: "followUpMode", onTransportChange: "transport",
3193
- onHttpIdleTimeoutMsChange: "httpIdleTimeoutMs", onThinkingLevelChange: "thinkingLevel", onThemeChange: "theme", onThemePreview: "theme",
3194
- onHideThinkingBlockChange: "hideThinkingBlock", onMermaidRenderingModeChange: "mermaidRenderingMode",
3195
- onShowCacheMissNoticesChange: "showCacheMissNotices", onCollapseChangelogChange: "collapseChangelog",
3196
- onEnableInstallTelemetryChange: "enableInstallTelemetry", onQuietStartupChange: "quietStartup",
3197
- onDefaultProjectTrustChange: "defaultProjectTrust", onDoubleEscapeActionChange: "doubleEscapeAction",
3198
- onTreeFilterModeChange: "treeFilterMode", onShowHardwareCursorChange: "showHardwareCursor",
3199
- onEditorPaddingXChange: "editorPaddingX", onOutputPadChange: "outputPad", onAutocompleteMaxVisibleChange: "autocompleteMaxVisible",
3200
- onClearOnShrinkChange: "clearOnShrink", onShowTerminalProgressChange: "showTerminalProgress", onTuiModeChange: "tuiMode",
3201
- onFullscreenExitOutputChange: "fullscreenExitOutput", onFullscreenScrollbarChange: "fullscreenScrollbar", onWarningsChange: "warnings",
3202
- };
3203
- return keys[callback] ?? null;
3204
- }
3205
- function agentJsonValue(value) {
3206
- if (value === null || typeof value === "string" || typeof value === "boolean" || (typeof value === "number" && Number.isFinite(value)))
3207
- return value;
3208
- if (Array.isArray(value))
3209
- return value.map(agentJsonValue);
3210
- if (isRecord(value))
3211
- return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined).map(([key, item]) => [key, agentJsonValue(item)]));
3212
- throw new TypeError("setting value must be JSON serializable");
3213
- }
3214
- function isThinkingLevel(value) {
3215
- return typeof value === "string" && ["off", "minimal", "low", "medium", "high", "xhigh"].includes(value);
3216
- }
3217
- function settingLabel(callback) {
3218
- return callback
3219
- .replace(/^on/, "")
3220
- .replace(/Change$|Preview$|Cancel$/, "")
3221
- .replace(/([a-z])([A-Z])/g, "$1 $2")
3222
- .replace(/^./, character => character.toUpperCase());
3223
- }
3224
- function pinnedHotkeySummary() {
3225
- return [
3226
- "Enter: send message · Alt+Enter: queue follow-up",
3227
- "Escape: cancel/abort · Ctrl+C: clear/exit · Ctrl+D: exit when empty",
3228
- "Shift+Tab: cycle thinking · Ctrl+P/Shift+Ctrl+P: cycle models · Ctrl+L: select model",
3229
- "Ctrl+O: expand tools · Ctrl+T: toggle thinking · Ctrl+X: copy message",
3230
- "Alt+Up: restore queued messages · /: commands · !/!!: bash",
3231
- ].join("\n");
3232
- }
3233
- function scopedModelRecords(modelRuntime) {
3234
- return modelRuntime.getAvailableSnapshot().map(model => ({
3235
- descriptor: { provider: model.provider, id: model.id, name: model.name ?? model.id },
3236
- model,
3237
- }));
3238
- }
3239
- function scopedModelReference(value) {
3240
- if (!isRecord(value) || !isRecord(value.model))
3241
- return undefined;
3242
- const provider = stringValue(value.model.provider);
3243
- const id = stringValue(value.model.id);
3244
- return provider && id ? `${provider}/${id}` : undefined;
3245
- }
3246
- function resolveConfiguredModelIds(patterns, models) {
3247
- const references = models.map(item => `${item.descriptor.provider}/${item.descriptor.id}`);
3248
- const resolved = [];
3249
- for (const pattern of patterns) {
3250
- const thinkingSuffix = /:(?:off|minimal|low|medium|high|xhigh)$/.exec(pattern);
3251
- const modelPattern = thinkingSuffix === null ? pattern : pattern.slice(0, -thinkingSuffix[0].length);
3252
- const matcher = wildcardMatcher(modelPattern);
3253
- const matches = references.filter((reference, index) => matcher.test(reference) || matcher.test(models[index]?.descriptor.id ?? ""));
3254
- if (matches.length === 0) {
3255
- resolved.push(pattern);
3256
- }
3257
- else {
3258
- for (const match of matches)
3259
- if (!resolved.includes(match))
3260
- resolved.push(match);
3261
- }
3262
- }
3263
- return resolved;
3264
- }
3265
- function wildcardMatcher(pattern) {
3266
- let source = "";
3267
- for (const character of pattern) {
3268
- if (character === "*")
3269
- source += ".*";
3270
- else if (character === "?")
3271
- source += ".";
3272
- else
3273
- source += character.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
3274
- }
3275
- return new RegExp(`^${source}$`, "i");
3276
- }
3277
- function readModel(value) {
3278
- if (!isRecord(value))
3279
- return null;
3280
- const providerId = stringValue(value.provider) ?? stringValue(value.providerId);
3281
- const modelId = stringValue(value.id) ?? stringValue(value.modelId);
3282
- if (!providerId || !modelId)
3283
- return null;
3284
- return {
3285
- providerId,
3286
- modelId,
3287
- displayName: stringValue(value.name) ?? modelId,
3288
- };
3289
- }
3290
- function readSuggestionReasoning(value) {
3291
- return value === "minimal" || value === "low" || value === "medium" || value === "high" || value === "xhigh" || value === "max"
3292
- ? value
3293
- : "off";
3294
- }
3295
- function readThinkingLevel(value) {
3296
- return value === "off" || value === "minimal" || value === "low" || value === "medium"
3297
- || value === "high" || value === "xhigh"
3298
- ? value
3299
- : "medium";
3300
- }
3301
- function readStringArray(value) {
3302
- return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
3303
- }
3304
- function stringValue(value) {
3305
- return typeof value === "string" && value.length > 0 ? value : undefined;
3306
- }
3307
- function messageFallbackKey(message, fallbackIndex) {
3308
- if (!isRecord(message))
3309
- return `message-unknown-${fallbackIndex}`;
3310
- const timestamp = typeof message.timestamp === "number" && Number.isSafeInteger(message.timestamp)
3311
- ? message.timestamp
3312
- : `index-${fallbackIndex}`;
3313
- return `message-${stringValue(message.role) ?? "unknown"}-${timestamp}`;
3314
- }
3315
- function extensionResourceDiagnostic(index, sourcePath, diagnostic) {
3316
- return {
3317
- kind: "extension",
3318
- id: `extension-diagnostic-${index}`,
3319
- sourcePath,
3320
- resolvedPath: null,
3321
- sourceInfo: null,
3322
- loaded: false,
3323
- hidden: false,
3324
- diagnostic,
3325
- };
3326
- }
3327
- function extensionSourceSummary(value) {
3328
- if (!isRecord(value))
3329
- return null;
3330
- const source = stringProperty(value, "source");
3331
- const scope = value.scope;
3332
- const origin = value.origin;
3333
- const baseDir = value.baseDir;
3334
- if (!source
3335
- || (scope !== "user" && scope !== "project" && scope !== "temporary")
3336
- || (origin !== "package" && origin !== "top-level")
3337
- || (baseDir !== undefined && typeof baseDir !== "string"))
3338
- return null;
3339
- return { source, scope, origin, baseDir: baseDir ?? null };
3340
- }
3341
- function collectionResult(value, key) {
3342
- if (!isRecord(value))
3343
- return { values: [], diagnostics: [] };
3344
- const diagnostics = unknownArray(value.diagnostics).map(diagnostic => {
3345
- if (typeof diagnostic === "string")
3346
- return diagnostic;
3347
- if (isRecord(diagnostic)) {
3348
- const message = stringProperty(diagnostic, "message") ?? String(diagnostic);
3349
- const path = stringProperty(diagnostic, "path");
3350
- return path ? `${path}: ${message}` : message;
3351
- }
3352
- return String(diagnostic);
3353
- });
3354
- return { values: unknownArray(value[key]), diagnostics };
3355
- }
3356
- function unknownArray(value) {
3357
- return Array.isArray(value) ? value : [];
3358
- }
3359
- function stringProperty(value, key) {
3360
- if (!isRecord(value))
3361
- return undefined;
3362
- const item = value[key];
3363
- return typeof item === "string" && item.length > 0 ? item : undefined;
3364
- }
3365
- function isUnknownModel(value) {
3366
- return isRecord(value)
3367
- && value.provider === "unknown"
3368
- && value.id === "unknown"
3369
- && value.api === "unknown";
3370
- }
3371
- function findExactWorkflowModel(reference, models) {
3372
- const normalized = reference.trim().toLowerCase();
3373
- const canonical = models.filter(model => {
3374
- const provider = stringProperty(model, "provider");
3375
- const id = stringProperty(model, "id");
3376
- return provider !== undefined && id !== undefined && `${provider}/${id}`.toLowerCase() === normalized;
3377
- });
3378
- if (canonical.length === 1)
3379
- return canonical[0];
3380
- const bare = models.filter(model => stringProperty(model, "id")?.toLowerCase() === normalized);
3381
- return bare.length === 1 ? bare[0] : undefined;
3382
- }
3383
- /**
3384
- * Some ecosystem extensions retain a `pi-<name>` slash-command alias beside
3385
- * their unprefixed command. A1 presents the product-neutral command once while
3386
- * leaving Pi's runner free to accept the compatibility alias when typed.
3387
- */
3388
- function isPiPrefixedCompatibilityAlias(command, commands) {
3389
- const name = stringProperty(command, "name");
3390
- if (!name?.startsWith("pi-") || name.length === 3)
3391
- return false;
3392
- const canonicalName = name.slice(3);
3393
- const description = stringProperty(command, "description");
3394
- const sourcePath = extensionCommandSourcePath(command);
3395
- return commands.some(candidate => candidate !== command
3396
- && stringProperty(candidate, "name") === canonicalName
3397
- && stringProperty(candidate, "description") === description
3398
- && extensionCommandSourcePath(candidate) === sourcePath);
3399
- }
3400
- function extensionCommandSourcePath(command) {
3401
- return isRecord(command) ? stringProperty(command.sourceInfo, "path") : undefined;
3402
- }
3403
- function compactResourceLabel(path) {
3404
- const segments = path.replaceAll("\\", "/").split("/").filter(Boolean);
3405
- return segments.at(-1) ?? path;
3406
- }
3407
- /** A later error may restate a declaration, but cannot make completed arguments incomplete. */
3408
- function retainCompletedArguments(current, next) {
3409
- if (current.toolState?.argsComplete !== true || next.toolState === undefined || next.toolState.argsComplete)
3410
- return next;
3411
- return { ...next, toolState: { ...next.toolState, argsComplete: true },
3412
- ...(next.toolRendering === undefined || current.toolRendering === undefined ? {} : {
3413
- toolRendering: { ...next.toolRendering, arguments: current.toolRendering.arguments },
3414
- }),
3415
- payload: { ...(isRecord(next.payload) ? next.payload : {}), argsComplete: true } };
3416
- }
3417
- /**
3418
- * Whether two blocks say the same thing. A block that says what it already said is not a
3419
- * new revision: the shell renders a block once per revision, so bumping one it did not
3420
- * need re-renders it for nothing.
3421
- */
3422
- function sameBlockContent(left, right) {
3423
- return left.kind === right.kind
3424
- && left.status === right.status
3425
- && left.title === right.title
3426
- && left.text === right.text
3427
- && sameValue(left.toolState, right.toolState)
3428
- && sameValue(left.toolRendering, right.toolRendering)
3429
- && sameValue(left.payload, right.payload)
3430
- && sameValue(left.imageReferences ?? [], right.imageReferences ?? []);
3431
- }
3432
- function sameValue(left, right) {
3433
- if (left === right)
3434
- return true;
3435
- if (typeof left !== typeof right || left === null || right === null)
3436
- return false;
3437
- if (Array.isArray(left) || Array.isArray(right)) {
3438
- if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length)
3439
- return false;
3440
- return left.every((value, index) => sameValue(value, right[index]));
3441
- }
3442
- if (typeof left !== "object")
3443
- return false;
3444
- const leftKeys = Object.keys(left);
3445
- const rightRecord = right;
3446
- if (leftKeys.length !== Object.keys(rightRecord).length)
3447
- return false;
3448
- return leftKeys.every(key => Object.hasOwn(rightRecord, key) && sameValue(left[key], rightRecord[key]));
3449
- }
3450
- function textFromContent(content) {
3451
- if (typeof content === "string")
3452
- return content;
3453
- if (!Array.isArray(content))
3454
- return "";
3455
- return content
3456
- .map(item => isRecord(item) && item.type === "text" ? stringValue(item.text) ?? "" : "")
3457
- .filter(text => text.length > 0)
3458
- .join("\n");
3459
- }
3460
- function assistantContent(content) {
3461
- const result = [];
3462
- for (const item of content) {
3463
- if (!isRecord(item))
3464
- continue;
3465
- if (item.type === "text")
3466
- result.push({ type: "text", text: stringValue(item.text) ?? "" });
3467
- else if (item.type === "thinking") {
3468
- result.push({
3469
- type: "thinking",
3470
- thinking: stringValue(item.thinking) ?? "",
3471
- ...(item.redacted === true ? { redacted: true } : {}),
3472
- });
3473
- }
3474
- else if (item.type === "toolCall") {
3475
- result.push({
3476
- type: "toolCall",
3477
- id: stringValue(item.id) ?? "",
3478
- name: stringValue(item.name) ?? "unknown",
3479
- arguments: sanitizeJson(item.arguments),
3480
- });
3481
- }
3482
- }
3483
- return result;
3484
- }
3485
- function contentImageCount(content) {
3486
- return Array.isArray(content)
3487
- ? content.filter(item => isRecord(item) && item.type === "image").length
3488
- : 0;
3489
- }
3490
- function jsonSummary(value) {
3491
- const json = sanitizeJson(value);
3492
- let summary = "";
3493
- try {
3494
- summary = JSON.stringify(json);
3495
- }
3496
- catch {
3497
- summary = String(value);
3498
- }
3499
- if (summary.length > 512)
3500
- summary = `${summary.slice(0, 509)}...`;
3501
- return { summary, json };
3502
- }
3503
- function sanitizeJson(value, depth = 0) {
3504
- if (typeof value === "bigint")
3505
- return value.toString();
3506
- if (value === null || ["string", "number", "boolean"].includes(typeof value))
3507
- return value;
3508
- if (depth >= 8)
3509
- return "[truncated]";
3510
- if (Array.isArray(value))
3511
- return value.slice(0, 100).map(item => sanitizeJson(item, depth + 1));
3512
- if (isRecord(value)) {
3513
- const output = {};
3514
- for (const [key, item] of Object.entries(value).slice(0, 100)) {
3515
- output[key] = sanitizeJson(item, depth + 1);
3516
- }
3517
- return output;
3518
- }
3519
- return String(value);
3520
- }
3521
- async function readGitBranch(cwd) {
3522
- try {
3523
- const { stdout } = await execFileAsync("git", ["branch", "--show-current"], { cwd, windowsHide: true });
3524
- const branch = stdout.trim();
3525
- return branch.length > 0 ? branch : null;
3526
- }
3527
- catch {
3528
- return null;
3529
- }
3530
- }
3531
- function finiteNumber(value) {
3532
- return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : 0;
3533
- }
3534
- function isRecord(value) {
3535
- return typeof value === "object" && value !== null;
3536
- }