@pellux/goodvibes-tui 1.27.1 → 2.0.2

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 (405) hide show
  1. package/CHANGELOG.md +127 -2
  2. package/README.md +7 -9
  3. package/bin/goodvibes +2 -2
  4. package/bin/launcher-support.js +8 -4
  5. package/docs/README.md +11 -2
  6. package/docs/foundation-artifacts/operator-contract.json +6403 -2347
  7. package/package.json +14 -25
  8. package/scripts/postinstall.js +35 -35
  9. package/src/audio/spoken-turn-model-routing.ts +1 -1
  10. package/src/cli/bundle-command.ts +5 -5
  11. package/src/cli/completions/generate.ts +1 -1
  12. package/src/cli/doctor.ts +3 -3
  13. package/src/cli/entrypoint.ts +12 -7
  14. package/src/cli/help.ts +4 -5
  15. package/src/cli/hooks-command.ts +1 -1
  16. package/src/cli/index.ts +1 -4
  17. package/src/cli/launch-auto-update.ts +59 -7
  18. package/src/cli/management-commands.ts +6 -6
  19. package/src/cli/management-utils.ts +5 -5
  20. package/src/cli/management.ts +7 -7
  21. package/src/cli/network-posture.ts +1 -1
  22. package/src/cli/package-verification.ts +7 -6
  23. package/src/cli/plugin-bundles-command.ts +1 -1
  24. package/src/cli/plugin-command.ts +2 -2
  25. package/src/cli/service-command.ts +2 -2
  26. package/src/cli/service-posture.ts +4 -4
  27. package/src/cli/status.ts +9 -8
  28. package/src/cli/surface-command.ts +4 -4
  29. package/src/cli/tui-startup.ts +3 -3
  30. package/src/cli-flags.ts +3 -3
  31. package/src/config/goodvibes-home-audit.ts +0 -2
  32. package/src/config/secret-config.ts +37 -5
  33. package/src/config/tui-extension-settings.ts +1 -1
  34. package/src/core/approval-alert.ts +2 -2
  35. package/src/core/budget-breach-notifier.ts +3 -3
  36. package/src/core/conversation-compaction-render.ts +1 -1
  37. package/src/core/conversation-line-cache.ts +1 -1
  38. package/src/core/conversation-render-context.ts +1 -1
  39. package/src/core/conversation-rendering.ts +2 -2
  40. package/src/core/conversation.ts +10 -10
  41. package/src/core/long-task-notifier.ts +2 -2
  42. package/src/core/memory-diagnostics-gateway.ts +1 -1
  43. package/src/core/scriptable-statusline.ts +1 -1
  44. package/src/core/session-auto-titler.ts +1 -1
  45. package/src/core/session-resume-core.ts +2 -2
  46. package/src/core/stream-event-wiring.ts +24 -44
  47. package/src/core/terminal-notifier.ts +3 -3
  48. package/src/core/turn-cancellation.ts +1 -1
  49. package/src/core/turn-event-wiring.ts +5 -5
  50. package/src/input/command-registry.ts +6 -6
  51. package/src/input/commands/channel-pairing.ts +1 -1
  52. package/src/input/commands/cluster-runtime.ts +1 -1
  53. package/src/input/commands/codebase-runtime.ts +1 -1
  54. package/src/input/commands/devices-runtime.ts +1 -1
  55. package/src/input/commands/hosted-runtime.ts +330 -0
  56. package/src/input/commands/local-setup-review.ts +1 -1
  57. package/src/input/commands/local-setup-transfer.ts +2 -2
  58. package/src/input/commands/local-setup.ts +3 -3
  59. package/src/input/commands/managed-runtime.ts +2 -2
  60. package/src/input/commands/operator-rpc.ts +24 -110
  61. package/src/input/commands/payment-card-intake.ts +1 -1
  62. package/src/input/commands/permissions-provenance.ts +2 -2
  63. package/src/input/commands/plugin-runtime.ts +1 -1
  64. package/src/input/commands/provider.ts +1 -1
  65. package/src/input/commands/relay-runtime.ts +18 -9
  66. package/src/input/commands/rewind-runtime.ts +1 -1
  67. package/src/input/commands/runtime-services.ts +1 -1
  68. package/src/input/commands/services-runtime.ts +1 -1
  69. package/src/input/commands/session-workflow.ts +1 -1
  70. package/src/input/commands/settings-sync-runtime.ts +2 -2
  71. package/src/input/commands/share-runtime.ts +1 -1
  72. package/src/input/commands/tasks-runtime.ts +145 -55
  73. package/src/input/commands/update-runtime.ts +31 -28
  74. package/src/input/commands/work-plan-runtime.ts +2 -2
  75. package/src/input/commands/workstream-attempts.ts +1 -1
  76. package/src/input/commands/workstream-runtime.ts +2 -2
  77. package/src/input/commands.ts +2 -0
  78. package/src/input/feature-unit-layout.ts +1 -1
  79. package/src/input/feed-context-factory.ts +3 -3
  80. package/src/input/handler-content-actions.ts +2 -2
  81. package/src/input/handler-feed-routes.ts +1 -1
  82. package/src/input/handler-feed.ts +3 -3
  83. package/src/input/handler-modal-token-routes.ts +1 -1
  84. package/src/input/handler-onboarding-daemon-adopt.ts +3 -2
  85. package/src/input/handler-onboarding.ts +1 -1
  86. package/src/input/handler-types.ts +2 -2
  87. package/src/input/handler-ui-state.ts +1 -1
  88. package/src/input/handler.ts +4 -4
  89. package/src/input/input-history.ts +1 -1
  90. package/src/input/onboarding/onboarding-feature-units.ts +2 -2
  91. package/src/input/onboarding/onboarding-runtime-status.ts +15 -1
  92. package/src/input/onboarding/onboarding-wizard-apply.ts +17 -8
  93. package/src/input/onboarding/onboarding-wizard-cloudflare-step.ts +5 -3
  94. package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +1 -1
  95. package/src/input/onboarding/onboarding-wizard-types.ts +1 -1
  96. package/src/input/payments-config.ts +6 -6
  97. package/src/input/search.ts +1 -1
  98. package/src/input/session-picker-modal.ts +46 -0
  99. package/src/input/settings-modal-activation.ts +1 -1
  100. package/src/input/settings-modal-behavior.ts +4 -4
  101. package/src/input/settings-modal-commit.ts +11 -0
  102. package/src/input/settings-modal-data.ts +9 -122
  103. package/src/input/settings-modal-mutations.ts +33 -0
  104. package/src/input/settings-modal-secrets.ts +34 -0
  105. package/src/input/settings-modal-subscriptions.ts +1 -1
  106. package/src/input/settings-modal-types.ts +15 -1
  107. package/src/input/settings-modal.ts +28 -3
  108. package/src/input/worktree-setup-config.ts +1 -1
  109. package/src/main.ts +11 -9
  110. package/src/panels/agent-inspector-shared.ts +2 -2
  111. package/src/panels/base-panel.ts +2 -2
  112. package/src/panels/builtin/operations.ts +6 -8
  113. package/src/panels/builtin/session.ts +17 -0
  114. package/src/panels/builtin/shared.ts +5 -5
  115. package/src/panels/builtin-modals.ts +3 -3
  116. package/src/panels/confirm-state.ts +1 -1
  117. package/src/panels/cost-tracker-panel.ts +4 -4
  118. package/src/panels/diff-panel.ts +2 -2
  119. package/src/panels/diff-review-panel.ts +2 -2
  120. package/src/panels/expandable-list-panel.ts +2 -2
  121. package/src/panels/fleet-acts.ts +1 -1
  122. package/src/panels/fleet-observed-render.ts +1 -1
  123. package/src/panels/fleet-panel-format.ts +1 -1
  124. package/src/panels/fleet-panel.ts +1 -1
  125. package/src/panels/fleet-spawn.ts +1 -1
  126. package/src/panels/fleet-steer.ts +1 -1
  127. package/src/panels/fleet-transcript.ts +1 -1
  128. package/src/panels/git-panel.ts +1 -1
  129. package/src/panels/hosted-session-feed.ts +352 -0
  130. package/src/panels/hosted-session-panel.ts +221 -0
  131. package/src/panels/local-auth-panel.ts +3 -3
  132. package/src/panels/modals/devices-modal.ts +1 -1
  133. package/src/panels/modals/local-auth-modal.ts +1 -1
  134. package/src/panels/modals/pairing-modal.ts +1 -1
  135. package/src/panels/modals/services-modal.ts +1 -1
  136. package/src/panels/modals/settings-sync-modal.ts +1 -1
  137. package/src/panels/modals/subscription-modal.ts +1 -1
  138. package/src/panels/modals/work-plan-modal.ts +1 -1
  139. package/src/panels/notifications-panel.ts +1 -1
  140. package/src/panels/panel-confirm-overlay.ts +1 -1
  141. package/src/panels/plugins-panel.ts +2 -2
  142. package/src/panels/polish-core.ts +2 -2
  143. package/src/panels/polish-tables.ts +1 -1
  144. package/src/panels/polish.ts +2 -2
  145. package/src/panels/scrollable-list-panel.ts +3 -3
  146. package/src/panels/skills-panel.ts +3 -3
  147. package/src/panels/token-budget-panel.ts +5 -5
  148. package/src/panels/types.ts +1 -2
  149. package/src/permissions/prompt.ts +1 -1
  150. package/src/renderer/autocomplete-overlay.ts +1 -1
  151. package/src/renderer/block-actions-overlay.ts +1 -1
  152. package/src/renderer/bookmark-modal.ts +1 -1
  153. package/src/renderer/buffer.ts +1 -1
  154. package/src/renderer/code-block.ts +1 -1
  155. package/src/renderer/composer-fragments.ts +1 -1
  156. package/src/renderer/compositor.ts +2 -2
  157. package/src/renderer/config-modal.ts +1 -1
  158. package/src/renderer/context-inspector.ts +1 -1
  159. package/src/renderer/conversation-layout.ts +2 -2
  160. package/src/renderer/conversation-overlays.ts +1 -1
  161. package/src/renderer/conversation-surface.ts +2 -2
  162. package/src/renderer/diff-view.ts +1 -1
  163. package/src/renderer/diff.ts +1 -1
  164. package/src/renderer/file-picker-overlay.ts +1 -1
  165. package/src/renderer/fleet-tab-strip.ts +1 -1
  166. package/src/renderer/fullscreen-primitives.ts +2 -2
  167. package/src/renderer/fullscreen-workspace.ts +1 -1
  168. package/src/renderer/header-line.ts +1 -1
  169. package/src/renderer/help-overlay.ts +1 -1
  170. package/src/renderer/history-search-overlay.ts +2 -2
  171. package/src/renderer/layout.ts +5 -8
  172. package/src/renderer/markdown-inline.ts +1 -1
  173. package/src/renderer/markdown-table.ts +1 -1
  174. package/src/renderer/markdown.ts +1 -1
  175. package/src/renderer/mcp-workspace.ts +1 -1
  176. package/src/renderer/modal-factory.ts +2 -2
  177. package/src/renderer/model-workspace.ts +2 -2
  178. package/src/renderer/onboarding/onboarding-wizard.ts +1 -1
  179. package/src/renderer/overlay-box.ts +1 -1
  180. package/src/renderer/panel-composite.ts +1 -1
  181. package/src/renderer/panel-workspace-bar.ts +1 -1
  182. package/src/renderer/process-indicator.ts +1 -1
  183. package/src/renderer/profile-picker-modal.ts +1 -1
  184. package/src/renderer/search-overlay.ts +2 -2
  185. package/src/renderer/selection-modal-overlay.ts +1 -1
  186. package/src/renderer/session-picker-modal.ts +124 -3
  187. package/src/renderer/settings-modal-helpers.ts +2 -0
  188. package/src/renderer/settings-modal.ts +2 -2
  189. package/src/renderer/shell-surface.ts +2 -2
  190. package/src/renderer/system-message.ts +1 -1
  191. package/src/renderer/tab-strip.ts +1 -1
  192. package/src/renderer/theme-mode-config.ts +21 -33
  193. package/src/renderer/theme.ts +11 -12
  194. package/src/renderer/thinking.ts +1 -1
  195. package/src/renderer/tool-call.ts +2 -2
  196. package/src/renderer/tool-result-expanded-lines.ts +1 -1
  197. package/src/renderer/turn-injection.ts +7 -8
  198. package/src/renderer/ui-factory.ts +3 -3
  199. package/src/runtime/bootstrap-command-context.ts +4 -4
  200. package/src/runtime/bootstrap-command-parts.ts +5 -5
  201. package/src/runtime/bootstrap-core.ts +30 -30
  202. package/src/runtime/bootstrap-shell.ts +4 -4
  203. package/src/runtime/bootstrap.ts +128 -157
  204. package/src/runtime/client/approval-updates.ts +82 -0
  205. package/src/runtime/client/build-floors.ts +167 -0
  206. package/src/runtime/client/daemon-attach-handshake.ts +115 -0
  207. package/src/runtime/client/fleet-union.ts +147 -0
  208. package/src/runtime/client/host-service-status.ts +71 -0
  209. package/src/runtime/client/hosted-exit.ts +86 -0
  210. package/src/runtime/client/hosted-roster.ts +110 -0
  211. package/src/runtime/client/hosted-session-stream.ts +178 -0
  212. package/src/runtime/client/hosted-sessions.ts +187 -0
  213. package/src/runtime/client/operator-endpoint.ts +213 -0
  214. package/src/runtime/client/spine-adoption.ts +99 -0
  215. package/src/runtime/cloudflare-control-plane.ts +1 -1
  216. package/src/runtime/context-accounting-source.ts +1 -1
  217. package/src/runtime/context.ts +2 -2
  218. package/src/runtime/conversation-rewind-port.ts +42 -14
  219. package/src/runtime/daemon-attach-notices.ts +55 -19
  220. package/src/runtime/diagnostics/panels/ops.ts +1 -1
  221. package/src/runtime/disposal-wiring.ts +4 -24
  222. package/src/runtime/index.ts +119 -105
  223. package/src/runtime/install-self-check-startup.ts +2 -2
  224. package/src/runtime/interaction-seams.ts +1 -1
  225. package/src/runtime/knowledge-services.ts +8 -2
  226. package/src/runtime/legacy-daemon-migration.ts +4 -3
  227. package/src/runtime/legacy-daemon-reconcile.ts +3 -2
  228. package/src/runtime/memory-spine-transport.ts +37 -267
  229. package/src/runtime/notification-dispatch.ts +2 -2
  230. package/src/runtime/onboarding/apply.ts +2 -2
  231. package/src/runtime/onboarding/derivation.ts +2 -2
  232. package/src/runtime/onboarding/markers.ts +2 -2
  233. package/src/runtime/onboarding/progress.ts +2 -2
  234. package/src/runtime/onboarding/snapshot.ts +1 -1
  235. package/src/runtime/onboarding/state.ts +2 -2
  236. package/src/runtime/onboarding/types.ts +2 -2
  237. package/src/runtime/orchestrator-core-services.ts +1 -1
  238. package/src/runtime/path-shadow-startup.ts +28 -221
  239. package/src/runtime/power-keepawake-remote.ts +3 -2
  240. package/src/runtime/process-lifecycle.ts +2 -2
  241. package/src/runtime/provider-fallback.ts +10 -66
  242. package/src/runtime/recovery-autosave.ts +1 -1
  243. package/src/runtime/recovery-offer-wiring.ts +39 -23
  244. package/src/runtime/recovery-prompt.ts +31 -17
  245. package/src/runtime/relay-reachability-bridge.ts +53 -42
  246. package/src/runtime/release-artifacts.ts +2 -1
  247. package/src/runtime/runtime-services-types.ts +82 -35
  248. package/src/runtime/services.ts +305 -300
  249. package/src/runtime/session-resume-liveness-confirm.ts +1 -1
  250. package/src/runtime/ui-services.ts +7 -6
  251. package/src/runtime/update-check.ts +23 -43
  252. package/src/runtime/workspace-checkpointing.ts +1 -1
  253. package/src/runtime/wrfc-persistence.ts +2 -2
  254. package/src/shell/ui-openers.ts +30 -7
  255. package/src/utils/clipboard.ts +1 -1
  256. package/src/verification/verification-ledger.ts +10 -8
  257. package/src/version.ts +1 -1
  258. package/bin/goodvibes-daemon +0 -90
  259. package/src/cli/config-overrides.ts +0 -267
  260. package/src/cli/endpoints.ts +0 -109
  261. package/src/cli/parser.ts +0 -395
  262. package/src/cli/redaction.ts +0 -170
  263. package/src/cli/types.ts +0 -84
  264. package/src/cluster/commands.ts +0 -382
  265. package/src/cluster/remote-daemon-target.ts +0 -225
  266. package/src/cluster/render.ts +0 -268
  267. package/src/config/atomic-write.ts +0 -70
  268. package/src/config/credential-availability.ts +0 -122
  269. package/src/config/goodvibes-home.ts +0 -149
  270. package/src/config/index.ts +0 -88
  271. package/src/config/provider-model.ts +0 -23
  272. package/src/config/read-versioned.ts +0 -228
  273. package/src/core/alert-gating.ts +0 -67
  274. package/src/core/consolidation-receipt.ts +0 -41
  275. package/src/core/focus-tracker.ts +0 -41
  276. package/src/core/history.ts +0 -45
  277. package/src/core/memory-status.ts +0 -118
  278. package/src/core/orchestrator.ts +0 -7
  279. package/src/core/pairing-devices.ts +0 -49
  280. package/src/core/pairing-handoff.ts +0 -101
  281. package/src/core/pairing-offers.ts +0 -83
  282. package/src/core/pairing-origin.ts +0 -78
  283. package/src/core/rewind-turn-anchors.ts +0 -311
  284. package/src/core/session-recovery.ts +0 -210
  285. package/src/core/stable-host.ts +0 -171
  286. package/src/core/transcript-journal.ts +0 -503
  287. package/src/daemon/cli.ts +0 -693
  288. package/src/daemon/fatal-boot-report.ts +0 -111
  289. package/src/daemon/handlers/context.ts +0 -29
  290. package/src/daemon/handlers/contracts.ts +0 -77
  291. package/src/daemon/handlers/credentials.ts +0 -139
  292. package/src/daemon/handlers/drafts/draft-store.ts +0 -427
  293. package/src/daemon/handlers/drafts/index.ts +0 -17
  294. package/src/daemon/handlers/drafts/register.ts +0 -331
  295. package/src/daemon/handlers/errors.ts +0 -18
  296. package/src/daemon/handlers/inbox/cursor-store.ts +0 -457
  297. package/src/daemon/handlers/inbox/index.ts +0 -284
  298. package/src/daemon/handlers/inbox/mapping.ts +0 -192
  299. package/src/daemon/handlers/inbox/poller.ts +0 -212
  300. package/src/daemon/handlers/inbox/provider-adapter.ts +0 -156
  301. package/src/daemon/handlers/inbox/providers/discord.ts +0 -252
  302. package/src/daemon/handlers/inbox/providers/email.ts +0 -152
  303. package/src/daemon/handlers/inbox/providers/imap-client.ts +0 -300
  304. package/src/daemon/handlers/inbox/providers/route-util.ts +0 -24
  305. package/src/daemon/handlers/inbox/providers/slack.ts +0 -263
  306. package/src/daemon/handlers/index.ts +0 -117
  307. package/src/daemon/handlers/register.ts +0 -168
  308. package/src/daemon/handlers/remote/backends/cloud-terminal.ts +0 -142
  309. package/src/daemon/handlers/remote/backends/docker.ts +0 -79
  310. package/src/daemon/handlers/remote/backends/index.ts +0 -40
  311. package/src/daemon/handlers/remote/backends/local-process.ts +0 -113
  312. package/src/daemon/handlers/remote/backends/process-runner.ts +0 -127
  313. package/src/daemon/handlers/remote/backends/ssh.ts +0 -125
  314. package/src/daemon/handlers/remote/backends/types.ts +0 -97
  315. package/src/daemon/handlers/remote/dispatcher.ts +0 -181
  316. package/src/daemon/handlers/remote/index.ts +0 -119
  317. package/src/daemon/handlers/remote/peer-registry.ts +0 -357
  318. package/src/daemon/handlers/remote/service.ts +0 -191
  319. package/src/daemon/handlers/routing/inbox-bridge.ts +0 -71
  320. package/src/daemon/handlers/routing/index.ts +0 -261
  321. package/src/daemon/handlers/routing/route-store.ts +0 -319
  322. package/src/daemon/handlers/routing/routing-resolver.ts +0 -75
  323. package/src/daemon/handlers/sqlite-store.ts +0 -278
  324. package/src/daemon/handlers/triage/index.ts +0 -57
  325. package/src/daemon/handlers/triage/integration.ts +0 -213
  326. package/src/daemon/handlers/triage/pipeline.ts +0 -274
  327. package/src/daemon/handlers/triage/scorer.ts +0 -287
  328. package/src/daemon/handlers/triage/tagger/discord.ts +0 -187
  329. package/src/daemon/handlers/triage/tagger/imap.ts +0 -384
  330. package/src/daemon/handlers/triage/tagger/index.ts +0 -184
  331. package/src/daemon/handlers/triage/tagger/shared.ts +0 -70
  332. package/src/daemon/handlers/triage/tagger/slack.ts +0 -69
  333. package/src/daemon/handlers/triage/types.ts +0 -50
  334. package/src/daemon/lifecycle.ts +0 -41
  335. package/src/daemon/provision-wake-model.ts +0 -81
  336. package/src/daemon/safe-serve.ts +0 -61
  337. package/src/daemon/send/channels.ts +0 -200
  338. package/src/daemon/send/command.ts +0 -332
  339. package/src/daemon/send/composition.ts +0 -99
  340. package/src/daemon/send/failure-text.ts +0 -93
  341. package/src/daemon/send/inert-text.ts +0 -225
  342. package/src/daemon/send/stdin.ts +0 -24
  343. package/src/daemon/service-commands.ts +0 -329
  344. package/src/export/cost-utils.ts +0 -319
  345. package/src/input/selection.ts +0 -135
  346. package/src/plugins/loader.ts +0 -15
  347. package/src/renderer/bottom-bar.ts +0 -58
  348. package/src/renderer/conversation-tree.ts +0 -228
  349. package/src/runtime/agent-graph-composition.ts +0 -78
  350. package/src/runtime/channel-composition.ts +0 -46
  351. package/src/runtime/cluster-composition.ts +0 -124
  352. package/src/runtime/cluster-group-composition.ts +0 -284
  353. package/src/runtime/code-index-services.ts +0 -167
  354. package/src/runtime/daemon-handler-composition.ts +0 -76
  355. package/src/runtime/device-posture-composition.ts +0 -110
  356. package/src/runtime/durability-housekeeping.ts +0 -183
  357. package/src/runtime/durability-services.ts +0 -155
  358. package/src/runtime/embedded-service-factories.ts +0 -74
  359. package/src/runtime/feature-settings.ts +0 -154
  360. package/src/runtime/fleet-needs-input-push.ts +0 -70
  361. package/src/runtime/fleet-services.ts +0 -41
  362. package/src/runtime/idle-power-services.ts +0 -85
  363. package/src/runtime/mail-composition.ts +0 -65
  364. package/src/runtime/memory-fold.ts +0 -55
  365. package/src/runtime/operator-token-cleanup.ts +0 -109
  366. package/src/runtime/path-shadow.ts +0 -418
  367. package/src/runtime/perf/panel-health-monitor.ts +0 -18
  368. package/src/runtime/provider-optimizer-wiring.ts +0 -55
  369. package/src/runtime/reachability-notice.ts +0 -100
  370. package/src/runtime/remote-execution-composition.ts +0 -62
  371. package/src/runtime/session-liveness-marker.ts +0 -257
  372. package/src/runtime/session-pointer-surface.ts +0 -22
  373. package/src/runtime/session-storage-services.ts +0 -41
  374. package/src/runtime/surface-feature-flags.ts +0 -96
  375. package/src/runtime/terminal-output-guard.ts +0 -235
  376. package/src/runtime/trust/workspace-registration.ts +0 -192
  377. package/src/runtime/trust/workspace-trust.ts +0 -229
  378. package/src/runtime/ui/index.ts +0 -39
  379. package/src/runtime/ui/model-picker/data-provider.ts +0 -182
  380. package/src/runtime/ui/model-picker/health-enrichment.ts +0 -231
  381. package/src/runtime/ui/model-picker/index.ts +0 -59
  382. package/src/runtime/ui/model-picker/types.ts +0 -153
  383. package/src/runtime/ui/provider-health/data-provider.ts +0 -244
  384. package/src/runtime/ui/provider-health/fallback-visualizer.ts +0 -71
  385. package/src/runtime/ui/provider-health/index.ts +0 -46
  386. package/src/runtime/ui/provider-health/types.ts +0 -146
  387. package/src/runtime/ui-events.ts +0 -1
  388. package/src/runtime/ui-read-model-helpers.ts +0 -1
  389. package/src/runtime/ui-read-models-observability-maintenance.ts +0 -1
  390. package/src/runtime/ui-read-models-observability-options.ts +0 -1
  391. package/src/runtime/ui-read-models-observability-remote.ts +0 -1
  392. package/src/runtime/ui-read-models-observability-security.ts +0 -1
  393. package/src/runtime/ui-read-models-observability-system.ts +0 -1
  394. package/src/runtime/ui-read-models-observability.ts +0 -1
  395. package/src/runtime/ui-service-queries.ts +0 -1
  396. package/src/runtime/voice-setup-services.ts +0 -98
  397. package/src/runtime/workstream-attempts-validation.ts +0 -77
  398. package/src/runtime/workstream-draft-edits.ts +0 -95
  399. package/src/runtime/workstream-draft-store.ts +0 -274
  400. package/src/runtime/workstream-draft-types.ts +0 -42
  401. package/src/runtime/workstream-services.ts +0 -373
  402. package/src/tools/index.ts +0 -1
  403. package/src/types/grid.ts +0 -48
  404. package/src/types/sql-js.d.ts +0 -15
  405. package/src/work-plans/work-plan-store.ts +0 -702
@@ -1,257 +0,0 @@
1
- /**
2
- * session-liveness-marker.ts — best-effort "this session is open in another
3
- * terminal" signal for multi-instance safety.
4
- *
5
- * Two independent TUI instances resuming or writing recovery snapshots for
6
- * the SAME session can silently step on each other: one instance's periodic
7
- * recovery write looks, to a second instance, indistinguishable from a
8
- * genuine crash snapshot, and resuming a session another instance still has
9
- * open forks its live state without warning.
10
- *
11
- * This module writes a small marker file (session id + pid), refreshed on
12
- * the SAME 60s cadence as the existing recovery-file write (main.ts), so a
13
- * second instance can do a best-effort liveness check before either warning
14
- * about a stale-looking recovery snapshot or resuming a live session out
15
- * from under its owner.
16
- *
17
- * Every check here is best-effort by design: a missing or stale marker never
18
- * blocks anything — it just means "we can't tell, proceed as before". This is
19
- * a convenience signal, not a lock.
20
- *
21
- * Location: <homeDirectory>/.goodvibes/<surfaceRoot>/liveness/<sessionId>.json.
22
- * Both halves come off the caller's SessionSurface rather than being spelled
23
- * out here, so the markers land under the same scope as the sessions and
24
- * recovery snapshots they describe — mirrors the transcript-journal.ts
25
- * convention (homeDirectory-scoped, under the surface's own directory).
26
- */
27
- import { existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from 'node:fs';
28
- import { dirname, join } from 'node:path';
29
- import type { SessionSurface } from '@/runtime/index.ts';
30
-
31
- export interface LivenessMarker {
32
- readonly sessionId: string;
33
- readonly pid: number;
34
- readonly updatedAt: number;
35
- }
36
-
37
- /**
38
- * A marker older than this is treated as stale even if its pid happens to
39
- * still resolve to a running process (pid reuse after a crash) — the marker
40
- * is refreshed every 60s (main.ts's recovery cadence), so anything past
41
- * ~2.5x that cadence means the writer stopped refreshing it.
42
- */
43
- export const LIVENESS_STALE_AFTER_MS = 150_000;
44
-
45
- /** Absolute path to the directory holding every session's liveness marker. */
46
- export function livenessMarkerDirFor(surface: SessionSurface): string {
47
- return join(surface.homeDirectory, '.goodvibes', surface.surfaceRoot, 'liveness');
48
- }
49
-
50
- /** Absolute path to a session's liveness marker file. */
51
- export function livenessMarkerPathFor(surface: SessionSurface, sessionId: string): string {
52
- return join(livenessMarkerDirFor(surface), `${sessionId}.json`);
53
- }
54
-
55
- /** Refresh (creating if needed) the liveness marker for this session/pid. Best-effort — never throws. */
56
- export function writeLivenessMarker(surface: SessionSurface, sessionId: string, pid: number = process.pid): void {
57
- try {
58
- const path = livenessMarkerPathFor(surface, sessionId);
59
- mkdirSync(dirname(path), { recursive: true });
60
- const marker: LivenessMarker = { sessionId, pid, updatedAt: Date.now() };
61
- writeFileSync(path, JSON.stringify(marker), { mode: 0o600 });
62
- } catch {
63
- // Best-effort — a missed liveness refresh never blocks anything.
64
- }
65
- }
66
-
67
- /** Best-effort removal on exit. Never throws. */
68
- export function removeLivenessMarker(surface: SessionSurface, sessionId: string): void {
69
- try {
70
- const path = livenessMarkerPathFor(surface, sessionId);
71
- if (existsSync(path)) unlinkSync(path);
72
- } catch {
73
- // Best-effort.
74
- }
75
- }
76
-
77
- /** Parse one marker file by absolute path. Returns null for missing, empty, torn, or wrong-shaped content. */
78
- function parseMarkerFile(path: string): LivenessMarker | null {
79
- try {
80
- if (!existsSync(path)) return null;
81
- const text = readFileSync(path, 'utf-8');
82
- // A crash between create and write leaves a zero-byte file. Content, not
83
- // existence, decides whether this marker says anything at all.
84
- if (text.trim().length === 0) return null;
85
- const parsed = JSON.parse(text) as Partial<LivenessMarker>;
86
- if (typeof parsed.sessionId !== 'string' || typeof parsed.pid !== 'number' || typeof parsed.updatedAt !== 'number') return null;
87
- if (!Number.isFinite(parsed.pid) || !Number.isFinite(parsed.updatedAt)) return null;
88
- return { sessionId: parsed.sessionId, pid: parsed.pid, updatedAt: parsed.updatedAt };
89
- } catch {
90
- return null;
91
- }
92
- }
93
-
94
- function readLivenessMarker(surface: SessionSurface, sessionId: string): LivenessMarker | null {
95
- return parseMarkerFile(livenessMarkerPathFor(surface, sessionId));
96
- }
97
-
98
- /** Best-effort check that a pid still refers to a running process. Injectable for tests. */
99
- export function isPidAlive(pid: number, kill: (pid: number, signal: 0) => void = process.kill.bind(process)): boolean {
100
- try {
101
- kill(pid, 0);
102
- return true;
103
- } catch (err) {
104
- // ESRCH = no such process. Any other error (e.g. EPERM — exists but this
105
- // process lacks permission to signal it) is treated as "still alive" —
106
- // best-effort here means erring toward not silently dropping a real warning.
107
- return (err as NodeJS.ErrnoException | undefined)?.code === 'EPERM';
108
- }
109
- }
110
-
111
- export interface SessionLivenessCheck {
112
- readonly live: boolean;
113
- readonly pid: number | null;
114
- }
115
-
116
- /**
117
- * Best-effort: is `sessionId` apparently open in another still-running
118
- * process right now? A missing, stale (older than LIVENESS_STALE_AFTER_MS),
119
- * or unreadable marker all resolve to `{ live: false, pid: null }` — never a
120
- * throw, never a block on the caller.
121
- */
122
- export function checkSessionLiveness(
123
- surface: SessionSurface,
124
- sessionId: string,
125
- opts: { readonly now?: () => number; readonly isPidAliveFn?: typeof isPidAlive } = {},
126
- ): SessionLivenessCheck {
127
- const marker = readLivenessMarker(surface, sessionId);
128
- if (!marker) return { live: false, pid: null };
129
- const now = opts.now?.() ?? Date.now();
130
- if (now - marker.updatedAt > LIVENESS_STALE_AFTER_MS) return { live: false, pid: null };
131
- const aliveCheck = opts.isPidAliveFn ?? isPidAlive;
132
- if (!aliveCheck(marker.pid)) return { live: false, pid: null };
133
- return { live: true, pid: marker.pid };
134
- }
135
-
136
- // ─── Reaping ────────────────────────────────────────────────────────────────
137
- //
138
- // `removeLivenessMarker` only runs on a CLEAN exit (process-lifecycle.ts). A
139
- // SIGKILL, a panic, or a pulled power cord leaves the marker behind forever,
140
- // and `checkSessionLiveness` treating it as not-live does not remove the file
141
- // — so the liveness directory grows one file per session id ever opened. The
142
- // sweep below is the reclaim half of that lifecycle.
143
-
144
- /**
145
- * Hard ceiling on marker files kept after a sweep, oldest dropped first.
146
- *
147
- * The liveness rule below already reclaims ordinary crash residue, but it can
148
- * be defeated: `isPidAlive` deliberately reports EPERM (a pid this user cannot
149
- * signal) as alive, so on a busy host a reused pid can keep a dead session's
150
- * marker "live" indefinitely. This cap is the second, unconditional bound so
151
- * the directory can never grow without limit whatever the pid probe says.
152
- */
153
- export const LIVENESS_MARKER_MAX_FILES = 200;
154
-
155
- export interface LivenessReapResult {
156
- /** Marker files examined this sweep. */
157
- readonly scanned: number;
158
- /** Marker files deleted this sweep. */
159
- readonly reaped: number;
160
- }
161
-
162
- export interface LivenessReapOptions {
163
- readonly now?: () => number;
164
- readonly isPidAliveFn?: typeof isPidAlive;
165
- /**
166
- * Session ids that must never be reaped regardless of what the marker says
167
- * — normally just the current session's id. Belt-and-braces: the current
168
- * session refreshes its own marker every 60s, so the liveness rule already
169
- * keeps it.
170
- */
171
- readonly keepSessionIds?: readonly string[];
172
- /** Override the count cap (tests). */
173
- readonly maxFiles?: number;
174
- }
175
-
176
- /**
177
- * Delete liveness markers that no longer describe a running session.
178
- *
179
- * A marker is reaped when it is either:
180
- * - unreadable, empty, or not shaped like a marker (crash residue that can
181
- * never resolve to a live session again — validated by parsing the
182
- * content, not by the file merely existing); or
183
- * - definitively not live: older than `LIVENESS_STALE_AFTER_MS` AND its pid
184
- * no longer resolves to a running process.
185
- *
186
- * Both halves of that second rule are required on purpose. Deleting is
187
- * destructive while a liveness answer is only advisory, so a marker that is
188
- * stale-but-pid-alive (a wedged owner still holding the session) or
189
- * fresh-but-pid-dead (a process that died seconds ago, or a pid probe that
190
- * failed transiently) is left for a later sweep. A genuinely crashed session
191
- * satisfies both conditions within `LIVENESS_STALE_AFTER_MS` and is reclaimed
192
- * on the next sweep after that.
193
- *
194
- * Idempotent and safe to run concurrently from several processes: a marker
195
- * another sweeper unlinked between the directory listing and this unlink
196
- * (ENOENT) counts as reaped, never as an error.
197
- */
198
- export function reapStaleLivenessMarkers(surface: SessionSurface, opts: LivenessReapOptions = {}): LivenessReapResult {
199
- const dir = livenessMarkerDirFor(surface);
200
- let names: string[];
201
- try {
202
- names = readdirSync(dir).filter((name) => name.endsWith('.json'));
203
- } catch {
204
- // No liveness directory yet, or it is unreadable — nothing to reclaim.
205
- return { scanned: 0, reaped: 0 };
206
- }
207
-
208
- const now = opts.now?.() ?? Date.now();
209
- const aliveCheck = opts.isPidAliveFn ?? isPidAlive;
210
- const keep = new Set(opts.keepSessionIds ?? []);
211
- const maxFiles = opts.maxFiles ?? LIVENESS_MARKER_MAX_FILES;
212
-
213
- let reaped = 0;
214
- /** Survivors, so the count cap below can drop the oldest of them. */
215
- const survivors: { readonly path: string; readonly updatedAt: number }[] = [];
216
-
217
- for (const name of names) {
218
- const sessionId = name.slice(0, -'.json'.length);
219
- if (keep.has(sessionId)) continue;
220
- const path = join(dir, name);
221
- const marker = parseMarkerFile(path);
222
- if (marker === null) {
223
- if (unlinkIfPresent(path)) reaped++;
224
- continue;
225
- }
226
- const stale = now - marker.updatedAt > LIVENESS_STALE_AFTER_MS;
227
- if (stale && !aliveCheck(marker.pid)) {
228
- if (unlinkIfPresent(path)) reaped++;
229
- continue;
230
- }
231
- survivors.push({ path, updatedAt: marker.updatedAt });
232
- }
233
-
234
- if (survivors.length > maxFiles) {
235
- survivors.sort((a, b) => a.updatedAt - b.updatedAt); // oldest first
236
- for (const victim of survivors.slice(0, survivors.length - maxFiles)) {
237
- if (unlinkIfPresent(victim.path)) reaped++;
238
- }
239
- }
240
-
241
- return { scanned: names.length, reaped };
242
- }
243
-
244
- /**
245
- * Best-effort unlink shared by the sweep. Returns true when the file is gone
246
- * because of (or by the time of) this call. A concurrent sweeper winning the
247
- * race (ENOENT) is success, not failure; any other error leaves the file for
248
- * the next sweep.
249
- */
250
- function unlinkIfPresent(path: string): boolean {
251
- try {
252
- unlinkSync(path);
253
- return true;
254
- } catch (err) {
255
- return (err as NodeJS.ErrnoException | undefined)?.code === 'ENOENT';
256
- }
257
- }
@@ -1,22 +0,0 @@
1
- import { writeLastSessionPointer } from '@/runtime/index.ts';
2
- import type { SessionSurface } from '@/runtime/index.ts';
3
-
4
- /**
5
- * Binds the SDK's two-argument `writeLastSessionPointer(sessionId, options?)`
6
- * to one `SessionSurface`, returning a plain `(sessionId: string) => void`.
7
- *
8
- * Never pass the raw SDK function reference into a `(sessionId: string) =>
9
- * void` slot. A function with an extra optional parameter is structurally
10
- * assignable there, so it type-checks, but every downstream caller
11
- * (bootstrap-shell.ts -> bootstrap-hook-bridge.ts's resume handler) invokes
12
- * it with exactly one argument: `options` arrives as `undefined`, the legacy
13
- * compat path's `requireWorkingDirectory(undefined)` throws, and
14
- * `writeLastSessionPointer`'s own try/catch swallows that into a logged
15
- * warning. The pointer file then silently never gets written after a resume,
16
- * and the next launch's `--continue` / boot notice sees nothing. That was the
17
- * live defect at bootstrap.ts's `createBootstrapShell({ writeLastSessionPointer })`
18
- * call. See session-surface-pointer-journey.test.ts for the on-disk proof.
19
- */
20
- export function bindWriteLastSessionPointerToSurface(surface: SessionSurface): (sessionId: string) => void {
21
- return (sessionId: string): void => writeLastSessionPointer(sessionId, { surface });
22
- }
@@ -1,41 +0,0 @@
1
- /**
2
- * session-storage-services.ts — the declare-once storage handle and the
3
- * session store built on it.
4
- *
5
- * Every path the app uses for session state (the sessions directory, the
6
- * last-session pointer, crash-recovery snapshots, the checkpoint store, the
7
- * transcript journal, the multi-instance liveness markers) derives from ONE
8
- * `SessionSurface`, built here, once, at composition time.
9
- *
10
- * The reason it is one object rather than a scope argument repeated at each
11
- * call site: when each site derived its own scope, they drifted. A resume
12
- * wrote the last-session pointer with no scope at all — into the shared,
13
- * unscoped `.goodvibes/` directory — while `--continue` and the boot notice
14
- * read a scoped path under `.goodvibes/tui/`. Nothing errored; the pointer
15
- * simply never existed where anyone looked. `createSessionSurface` also
16
- * throws synchronously on a bad scope instead of silently resolving to the
17
- * unscoped fallback on first use, so a mistake here fails at boot rather than
18
- * six months later as missing state.
19
- */
20
- import { SessionManager } from '@pellux/goodvibes-sdk/platform/sessions';
21
- import { createSessionSurface, type SessionSurface } from '@/runtime/index.ts';
22
- import { GOODVIBES_TUI_SURFACE_ROOT } from '../config/surface.ts';
23
-
24
- export interface SessionStorageServices {
25
- readonly surface: SessionSurface;
26
- readonly sessionManager: SessionManager;
27
- }
28
-
29
- export function createSessionStorageServices(input: {
30
- readonly workingDirectory: string;
31
- readonly homeDirectory: string;
32
- }): SessionStorageServices {
33
- // Also performs the SDK's one-time migration of pre-surface state (pointer
34
- // copy-forward, journal moves) into the scoped location.
35
- const surface = createSessionSurface({
36
- surfaceRoot: GOODVIBES_TUI_SURFACE_ROOT,
37
- workingDirectory: input.workingDirectory,
38
- homeDirectory: input.homeDirectory,
39
- });
40
- return { surface, sessionManager: new SessionManager(input.workingDirectory, { surface }) };
41
- }
@@ -1,96 +0,0 @@
1
- /**
2
- * surface-feature-flags — which platform capabilities a surface/server setup
3
- * needs, and how to switch them on through their real domain settings keys.
4
- *
5
- * Capabilities are configured through first-class domain settings (see
6
- * feature-settings.ts); enabling a surface writes keys like
7
- * `controlPlane.gateway` or `surfaces.slack.enabled`, never a separate
8
- * enablement namespace.
9
- */
10
-
11
- import type { ConfigManager } from '../config/index.ts';
12
- import { surfaceFeatureGateId } from '@/runtime/index.ts';
13
- import { featureEnablementWrite, getFeatureSetting, isFeatureConfigEnabled } from './feature-settings.ts';
14
-
15
- export const CONTROL_PLANE_FEATURE_FLAG = 'control-plane-gateway';
16
- export const ROUTE_BINDING_FEATURE_FLAG = 'route-binding';
17
- export const DELIVERY_ENGINE_FEATURE_FLAG = 'delivery-engine';
18
- export const SERVICE_MANAGEMENT_FEATURE_FLAG = 'service-management';
19
-
20
- const CORE_CHANNEL_FEATURE_FLAGS = [
21
- CONTROL_PLANE_FEATURE_FLAG,
22
- ROUTE_BINDING_FEATURE_FLAG,
23
- DELIVERY_ENGINE_FEATURE_FLAG,
24
- ] as const;
25
-
26
- export function getSurfaceFeatureFlag(surfaceId: string): string | null {
27
- // Onboarding surface ids may be camelCase ('googleChat'); the SDK's gate
28
- // map is keyed by the kebab-case ChannelSurface id ('google-chat'). Try the
29
- // literal id first, then the kebab-case form, so choosing Google Chat in
30
- // onboarding really flips google-chat-surface.
31
- const literal = surfaceFeatureGateId(surfaceId);
32
- if (literal) return literal;
33
- const kebab = surfaceId.replace(/[A-Z]/g, (ch) => `-${ch.toLowerCase()}`);
34
- return kebab === surfaceId ? null : surfaceFeatureGateId(kebab);
35
- }
36
-
37
- export function getServerSurfaceFeatureFlags(options: {
38
- readonly serverBacked?: boolean;
39
- readonly web?: boolean;
40
- readonly externalSurfaces?: readonly string[];
41
- }): readonly string[] {
42
- const flags = new Set<string>();
43
- const hasExternalSurfaces = (options.externalSurfaces?.length ?? 0) > 0;
44
-
45
- if (options.serverBacked || options.web || hasExternalSurfaces) {
46
- flags.add(CONTROL_PLANE_FEATURE_FLAG);
47
- flags.add(SERVICE_MANAGEMENT_FEATURE_FLAG);
48
- }
49
- if (options.web) {
50
- const webFlag = getSurfaceFeatureFlag('web');
51
- if (webFlag) flags.add(webFlag);
52
- }
53
-
54
- if (hasExternalSurfaces) {
55
- for (const flag of CORE_CHANNEL_FEATURE_FLAGS) flags.add(flag);
56
- for (const surfaceId of options.externalSurfaces ?? []) {
57
- const surfaceFlag = getSurfaceFeatureFlag(surfaceId);
58
- if (surfaceFlag) flags.add(surfaceFlag);
59
- }
60
- }
61
-
62
- return [...flags].sort((left, right) => left.localeCompare(right));
63
- }
64
-
65
- /** Whether the live config has the capability on, derived from its domain settings key. */
66
- export function isFeatureFlagEnabled(config: Pick<ConfigManager, 'get'>, flagId: string): boolean {
67
- return isFeatureConfigEnabled(config, flagId);
68
- }
69
-
70
- /**
71
- * Translate internal feature-gate ids (e.g. 'control-plane-gateway') to the
72
- * canonical domain settings keys the user actually sets (e.g.
73
- * 'controlPlane.gateway') — matching the SDK's own gate errors, which name the
74
- * settings key. A gate with no mapped setting falls back to its id. Deduped and
75
- * sorted for stable output.
76
- */
77
- export function surfaceFeatureGateSettingsKeys(flagIds: readonly string[]): string[] {
78
- const keys = new Set<string>();
79
- for (const flagId of flagIds) keys.add(getFeatureSetting(flagId)?.enablement.key ?? flagId);
80
- return [...keys].sort((left, right) => left.localeCompare(right));
81
- }
82
-
83
- export function getMissingSurfaceFeatureFlags(config: Pick<ConfigManager, 'get'>, surfaceId: string): readonly string[] {
84
- const required = surfaceId === 'web'
85
- ? getServerSurfaceFeatureFlags({ web: true })
86
- : getServerSurfaceFeatureFlags({ externalSurfaces: [surfaceId] });
87
- return required.filter((flagId) => !isFeatureFlagEnabled(config, flagId));
88
- }
89
-
90
- /** Switch each capability on by writing its real enablement settings key. */
91
- export function enableFeatureFlags(config: Pick<ConfigManager, 'setDynamic'>, flagIds: readonly string[]): void {
92
- for (const flagId of flagIds) {
93
- const write = featureEnablementWrite(flagId, true);
94
- if (write) config.setDynamic(write.key, write.value);
95
- }
96
- }
@@ -1,235 +0,0 @@
1
- import { format } from 'node:util';
2
- import { logger } from '@pellux/goodvibes-sdk/platform/utils';
3
-
4
- type WritableStreamLike = {
5
- write: {
6
- (buffer: string | Uint8Array, cb?: (error?: Error | null) => void): boolean;
7
- (buffer: string | Uint8Array, encoding?: BufferEncoding, cb?: (error?: Error | null) => void): boolean;
8
- };
9
- };
10
-
11
- export type TerminalOutputInterceptSource =
12
- | 'stdout'
13
- | 'stderr'
14
- | 'console.debug'
15
- | 'console.error'
16
- | 'console.info'
17
- | 'console.log'
18
- | 'console.warn';
19
-
20
- export type TerminalOutputIntercept = {
21
- readonly source: TerminalOutputInterceptSource;
22
- readonly text: string;
23
- readonly preview: string;
24
- };
25
-
26
- export type TerminalOutputGuard = {
27
- setActive(active: boolean): void;
28
- allowTerminalWrite<T>(fn: () => T): T;
29
- dispose(): void;
30
- };
31
-
32
- export type TerminalOutputGuardOptions = {
33
- readonly stdout: WritableStreamLike;
34
- readonly stderr?: WritableStreamLike;
35
- readonly active?: boolean;
36
- readonly onIntercept?: (event: TerminalOutputIntercept) => void;
37
- };
38
-
39
- export type TuiTerminalOutputGuardOptions = {
40
- readonly stdout: WritableStreamLike;
41
- readonly stderr?: WritableStreamLike;
42
- readonly active?: boolean;
43
- /**
44
- * Called (rate-limited) after intercepts with the cumulative count of direct
45
- * writes captured this session, so an honest quiet counter (e.g. /debug) can
46
- * refresh. The per-write detail is already recorded to the activity log via
47
- * logger.warn — this callback intentionally does NOT push transcript lines.
48
- * (item 1a.)
49
- */
50
- readonly onCapture?: (total: number) => void;
51
- };
52
-
53
- const MAX_LOG_TEXT = 4_000;
54
- const MAX_PREVIEW_TEXT = 180;
55
- const ANSI_RE = /\x1b\[[0-?]*[ -/]*[@-~]/g;
56
-
57
- let currentGuard: TerminalOutputGuard | null = null;
58
-
59
- function writeCallback(args: unknown[]): ((error?: Error | null) => void) | undefined {
60
- const maybeCallback = args[args.length - 1];
61
- return typeof maybeCallback === 'function'
62
- ? maybeCallback as (error?: Error | null) => void
63
- : undefined;
64
- }
65
-
66
- function chunkToText(chunk: unknown): string {
67
- if (typeof chunk === 'string') return chunk;
68
- if (chunk instanceof Uint8Array) return Buffer.from(chunk).toString('utf8');
69
- return String(chunk);
70
- }
71
-
72
- function normalizeText(text: string): string {
73
- return text.replace(ANSI_RE, '').replace(/\r/g, '').trim();
74
- }
75
-
76
- function previewText(text: string): string {
77
- const singleLine = normalizeText(text).replace(/\s+/g, ' ');
78
- if (singleLine.length <= MAX_PREVIEW_TEXT) return singleLine;
79
- return `${singleLine.slice(0, MAX_PREVIEW_TEXT - 1)}...`;
80
- }
81
-
82
- function truncateForLog(text: string): string {
83
- if (text.length <= MAX_LOG_TEXT) return text;
84
- return `${text.slice(0, MAX_LOG_TEXT)}\n[truncated ${text.length - MAX_LOG_TEXT} byte(s)]`;
85
- }
86
-
87
- function invokeSuppressedCallback(args: unknown[]): void {
88
- const callback = writeCallback(args);
89
- if (callback) {
90
- queueMicrotask(() => callback(null));
91
- }
92
- }
93
-
94
- export function allowTerminalWrite<T>(fn: () => T): T {
95
- return currentGuard ? currentGuard.allowTerminalWrite(fn) : fn();
96
- }
97
-
98
- export function installTerminalOutputGuard(options: TerminalOutputGuardOptions): TerminalOutputGuard {
99
- const stdout = options.stdout;
100
- const stderr = options.stderr ?? process.stderr;
101
- const originalStdoutWriteMethod = stdout.write;
102
- const originalStderrWriteMethod = stderr.write;
103
- const originalStdoutWrite = (...args: unknown[]): boolean =>
104
- Reflect.apply(originalStdoutWriteMethod, stdout, args) as boolean;
105
- const originalStderrWrite = (...args: unknown[]): boolean =>
106
- Reflect.apply(originalStderrWriteMethod, stderr, args) as boolean;
107
- const originalConsole = {
108
- debug: console.debug.bind(console),
109
- error: console.error.bind(console),
110
- info: console.info.bind(console),
111
- log: console.log.bind(console),
112
- warn: console.warn.bind(console),
113
- };
114
-
115
- let active = options.active ?? true;
116
- let disposed = false;
117
- let allowDepth = 0;
118
- let captureDepth = 0;
119
-
120
- const record = (source: TerminalOutputInterceptSource, text: string): void => {
121
- if (disposed || !active) return;
122
- const normalized = normalizeText(text);
123
- if (!normalized) return;
124
- if (normalized.startsWith('[ActivityLogger]')) return;
125
- if (captureDepth > 0) return;
126
-
127
- captureDepth++;
128
- try {
129
- const event: TerminalOutputIntercept = {
130
- source,
131
- text: truncateForLog(normalized),
132
- preview: previewText(normalized),
133
- };
134
- logger.warn('Intercepted terminal output while TUI renderer was active', {
135
- source: event.source,
136
- text: event.text,
137
- });
138
- options.onIntercept?.(event);
139
- } finally {
140
- captureDepth--;
141
- }
142
- };
143
-
144
- const shouldPassThrough = (): boolean => !active || allowDepth > 0 || disposed;
145
-
146
- stdout.write = ((...args: unknown[]) => {
147
- if (shouldPassThrough()) {
148
- return originalStdoutWrite(...args);
149
- }
150
- record('stdout', chunkToText(args[0]));
151
- invokeSuppressedCallback(args);
152
- return true;
153
- }) as WritableStreamLike['write'];
154
-
155
- stderr.write = ((...args: unknown[]) => {
156
- if (shouldPassThrough()) {
157
- return originalStderrWrite(...args);
158
- }
159
- record('stderr', chunkToText(args[0]));
160
- invokeSuppressedCallback(args);
161
- return true;
162
- }) as WritableStreamLike['write'];
163
-
164
- console.debug = (...args: unknown[]) => {
165
- if (!active || disposed) return originalConsole.debug(...args);
166
- record('console.debug', format(...args));
167
- };
168
- console.error = (...args: unknown[]) => {
169
- if (!active || disposed) return originalConsole.error(...args);
170
- record('console.error', format(...args));
171
- };
172
- console.info = (...args: unknown[]) => {
173
- if (!active || disposed) return originalConsole.info(...args);
174
- record('console.info', format(...args));
175
- };
176
- console.log = (...args: unknown[]) => {
177
- if (!active || disposed) return originalConsole.log(...args);
178
- record('console.log', format(...args));
179
- };
180
- console.warn = (...args: unknown[]) => {
181
- if (!active || disposed) return originalConsole.warn(...args);
182
- record('console.warn', format(...args));
183
- };
184
-
185
- const guard: TerminalOutputGuard = {
186
- setActive(nextActive) {
187
- active = nextActive;
188
- },
189
- allowTerminalWrite<T>(fn: () => T): T {
190
- allowDepth++;
191
- try {
192
- return fn();
193
- } finally {
194
- allowDepth--;
195
- }
196
- },
197
- dispose() {
198
- if (disposed) return;
199
- disposed = true;
200
- stdout.write = originalStdoutWriteMethod;
201
- stderr.write = originalStderrWriteMethod;
202
- console.debug = originalConsole.debug;
203
- console.error = originalConsole.error;
204
- console.info = originalConsole.info;
205
- console.log = originalConsole.log;
206
- console.warn = originalConsole.warn;
207
- if (currentGuard === guard) {
208
- currentGuard = null;
209
- }
210
- },
211
- };
212
-
213
- currentGuard = guard;
214
- return guard;
215
- }
216
-
217
- export function installTuiTerminalOutputGuard(options: TuiTerminalOutputGuardOptions): TerminalOutputGuard {
218
- let totalInterceptedWrites = 0;
219
- let lastNoticeAt = 0;
220
- return installTerminalOutputGuard({
221
- stdout: options.stdout,
222
- stderr: options.stderr,
223
- active: options.active,
224
- onIntercept: () => {
225
- // Each intercept is already logged (logger.warn in record()). Here we only
226
- // maintain a cumulative counter and, rate-limited, refresh a quiet counter
227
- // surface — no repeated transcript lines. (item 1a.)
228
- totalInterceptedWrites++;
229
- const now = Date.now();
230
- if (now - lastNoticeAt < 5_000) return;
231
- lastNoticeAt = now;
232
- options.onCapture?.(totalInterceptedWrites);
233
- },
234
- });
235
- }