@pellux/goodvibes-agent 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (398) hide show
  1. package/.goodvibes/GOODVIBES.md +35 -0
  2. package/.goodvibes/agents/reviewer.md +48 -0
  3. package/.goodvibes/skills/add-provider/SKILL.md +199 -0
  4. package/CHANGELOG.md +25 -0
  5. package/README.md +74 -0
  6. package/bin/goodvibes-agent.ts +2 -0
  7. package/docs/README.md +23 -0
  8. package/docs/deployment-and-services.md +57 -0
  9. package/docs/getting-started.md +53 -0
  10. package/docs/release-and-publishing.md +46 -0
  11. package/package.json +134 -0
  12. package/scripts/check-bun.sh +20 -0
  13. package/src/audio/player.ts +156 -0
  14. package/src/audio/spoken-turn-controller.ts +203 -0
  15. package/src/audio/spoken-turn-model-routing.ts +117 -0
  16. package/src/audio/spoken-turn-wiring.ts +44 -0
  17. package/src/audio/text-chunker.ts +110 -0
  18. package/src/cli/bundle-command.ts +227 -0
  19. package/src/cli/completion.ts +90 -0
  20. package/src/cli/config-overrides.ts +159 -0
  21. package/src/cli/endpoints.ts +63 -0
  22. package/src/cli/entrypoint.ts +172 -0
  23. package/src/cli/help.ts +299 -0
  24. package/src/cli/index.ts +11 -0
  25. package/src/cli/management-commands.ts +426 -0
  26. package/src/cli/management.ts +744 -0
  27. package/src/cli/network-posture.ts +46 -0
  28. package/src/cli/package-verification.ts +123 -0
  29. package/src/cli/parser.ts +369 -0
  30. package/src/cli/provider-auth-routes.ts +22 -0
  31. package/src/cli/provider-classification.ts +107 -0
  32. package/src/cli/redaction.ts +105 -0
  33. package/src/cli/service-command.ts +26 -0
  34. package/src/cli/service-posture.ts +482 -0
  35. package/src/cli/status.ts +383 -0
  36. package/src/cli/surface-command.ts +247 -0
  37. package/src/cli/tui-startup.ts +32 -0
  38. package/src/cli/types.ts +69 -0
  39. package/src/cli-flags.ts +21 -0
  40. package/src/config/goodvibes-home-audit.ts +465 -0
  41. package/src/config/index.ts +57 -0
  42. package/src/config/provider-model.ts +23 -0
  43. package/src/config/secret-config.ts +119 -0
  44. package/src/config/secrets.ts +71 -0
  45. package/src/config/surface.ts +1 -0
  46. package/src/core/composer-state.ts +61 -0
  47. package/src/core/conversation-rendering.ts +359 -0
  48. package/src/core/conversation.ts +551 -0
  49. package/src/core/history.ts +45 -0
  50. package/src/core/orchestrator.ts +7 -0
  51. package/src/core/system-message-router.ts +171 -0
  52. package/src/daemon/cli.ts +55 -0
  53. package/src/daemon/safe-serve.ts +61 -0
  54. package/src/input/agent-workspace.ts +428 -0
  55. package/src/input/autocomplete.ts +96 -0
  56. package/src/input/bookmark-modal.ts +115 -0
  57. package/src/input/command-args-hint.ts +36 -0
  58. package/src/input/command-registry.ts +329 -0
  59. package/src/input/commands/agent-externalized-tui.ts +73 -0
  60. package/src/input/commands/agent-workspace-runtime.ts +17 -0
  61. package/src/input/commands/branch-runtime.ts +72 -0
  62. package/src/input/commands/cloudflare-runtime.ts +370 -0
  63. package/src/input/commands/config.ts +18 -0
  64. package/src/input/commands/control-room-runtime.ts +255 -0
  65. package/src/input/commands/conversation-runtime.ts +207 -0
  66. package/src/input/commands/discovery-runtime.ts +52 -0
  67. package/src/input/commands/eval.ts +204 -0
  68. package/src/input/commands/experience-runtime.ts +278 -0
  69. package/src/input/commands/guidance-runtime.ts +106 -0
  70. package/src/input/commands/health-runtime.ts +434 -0
  71. package/src/input/commands/hooks-runtime.ts +148 -0
  72. package/src/input/commands/incident-runtime.ts +95 -0
  73. package/src/input/commands/integration-runtime.ts +394 -0
  74. package/src/input/commands/intelligence-runtime.ts +223 -0
  75. package/src/input/commands/knowledge.ts +531 -0
  76. package/src/input/commands/local-auth-runtime.ts +105 -0
  77. package/src/input/commands/local-provider-runtime.ts +170 -0
  78. package/src/input/commands/local-runtime.ts +392 -0
  79. package/src/input/commands/local-setup-review.ts +199 -0
  80. package/src/input/commands/local-setup-transfer.ts +135 -0
  81. package/src/input/commands/local-setup.ts +282 -0
  82. package/src/input/commands/managed-runtime.ts +209 -0
  83. package/src/input/commands/marketplace-runtime.ts +290 -0
  84. package/src/input/commands/mcp-runtime.ts +432 -0
  85. package/src/input/commands/memory-product-runtime.ts +111 -0
  86. package/src/input/commands/memory.ts +151 -0
  87. package/src/input/commands/notify-runtime.ts +83 -0
  88. package/src/input/commands/onboarding-runtime.ts +14 -0
  89. package/src/input/commands/operator-panel-runtime.ts +146 -0
  90. package/src/input/commands/operator-runtime.ts +392 -0
  91. package/src/input/commands/planning-runtime.ts +205 -0
  92. package/src/input/commands/platform-access-runtime.ts +422 -0
  93. package/src/input/commands/platform-services-runtime.ts +246 -0
  94. package/src/input/commands/policy-dispatch.ts +339 -0
  95. package/src/input/commands/policy.ts +17 -0
  96. package/src/input/commands/product-runtime.ts +351 -0
  97. package/src/input/commands/profile-sync-runtime.ts +99 -0
  98. package/src/input/commands/provider-accounts-runtime.ts +113 -0
  99. package/src/input/commands/provider.ts +363 -0
  100. package/src/input/commands/qrcode-runtime.ts +20 -0
  101. package/src/input/commands/quit-shared.ts +162 -0
  102. package/src/input/commands/recall-bundle.ts +132 -0
  103. package/src/input/commands/recall-capture.ts +152 -0
  104. package/src/input/commands/recall-query.ts +229 -0
  105. package/src/input/commands/recall-review.ts +98 -0
  106. package/src/input/commands/recall-shared.ts +22 -0
  107. package/src/input/commands/remote-runtime-pool.ts +106 -0
  108. package/src/input/commands/remote-runtime-setup.ts +199 -0
  109. package/src/input/commands/remote-runtime.ts +431 -0
  110. package/src/input/commands/replay-runtime.ts +18 -0
  111. package/src/input/commands/runtime-services.ts +291 -0
  112. package/src/input/commands/schedule-runtime.ts +91 -0
  113. package/src/input/commands/services-runtime.ts +209 -0
  114. package/src/input/commands/session-content.ts +408 -0
  115. package/src/input/commands/session-workflow.ts +464 -0
  116. package/src/input/commands/session.ts +375 -0
  117. package/src/input/commands/settings-sync-runtime.ts +174 -0
  118. package/src/input/commands/share-runtime.ts +119 -0
  119. package/src/input/commands/shell-core.ts +307 -0
  120. package/src/input/commands/skills-runtime.ts +221 -0
  121. package/src/input/commands/subscription-runtime.ts +434 -0
  122. package/src/input/commands/tasks-runtime.ts +230 -0
  123. package/src/input/commands/teamwork-runtime.ts +339 -0
  124. package/src/input/commands/teleport-runtime.ts +57 -0
  125. package/src/input/commands/tts-runtime.ts +29 -0
  126. package/src/input/commands/work-plan-runtime.ts +169 -0
  127. package/src/input/commands.ts +131 -0
  128. package/src/input/feed-context-factory.ts +254 -0
  129. package/src/input/file-picker.ts +192 -0
  130. package/src/input/handler-command-route.ts +180 -0
  131. package/src/input/handler-content-actions.ts +497 -0
  132. package/src/input/handler-feed-routes.ts +648 -0
  133. package/src/input/handler-feed.ts +452 -0
  134. package/src/input/handler-interactions.ts +281 -0
  135. package/src/input/handler-modal-routes.ts +418 -0
  136. package/src/input/handler-modal-stack.ts +263 -0
  137. package/src/input/handler-modal-token-routes.ts +329 -0
  138. package/src/input/handler-onboarding-cloudflare.ts +391 -0
  139. package/src/input/handler-onboarding.ts +620 -0
  140. package/src/input/handler-picker-routes.ts +472 -0
  141. package/src/input/handler-prompt-buffer.ts +320 -0
  142. package/src/input/handler-shortcuts.ts +213 -0
  143. package/src/input/handler-ui-state.ts +372 -0
  144. package/src/input/handler.ts +729 -0
  145. package/src/input/input-history.ts +297 -0
  146. package/src/input/keybindings.ts +292 -0
  147. package/src/input/mcp-workspace.ts +554 -0
  148. package/src/input/model-picker-provider-filter.ts +28 -0
  149. package/src/input/model-picker-types.ts +137 -0
  150. package/src/input/model-picker.ts +797 -0
  151. package/src/input/onboarding/handler-onboarding-routes.ts +125 -0
  152. package/src/input/onboarding/onboarding-runtime-status.ts +87 -0
  153. package/src/input/onboarding/onboarding-wizard-apply.ts +277 -0
  154. package/src/input/onboarding/onboarding-wizard-cloudflare-step.ts +494 -0
  155. package/src/input/onboarding/onboarding-wizard-cloudflare.ts +204 -0
  156. package/src/input/onboarding/onboarding-wizard-constants.ts +158 -0
  157. package/src/input/onboarding/onboarding-wizard-external-surface-extra-specs.ts +130 -0
  158. package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +762 -0
  159. package/src/input/onboarding/onboarding-wizard-helpers.ts +167 -0
  160. package/src/input/onboarding/onboarding-wizard-rules.ts +256 -0
  161. package/src/input/onboarding/onboarding-wizard-state.ts +365 -0
  162. package/src/input/onboarding/onboarding-wizard-steps.ts +798 -0
  163. package/src/input/onboarding/onboarding-wizard-types.ts +195 -0
  164. package/src/input/onboarding/onboarding-wizard.ts +711 -0
  165. package/src/input/panel-integration-actions.ts +78 -0
  166. package/src/input/profile-picker-modal.ts +222 -0
  167. package/src/input/search.ts +100 -0
  168. package/src/input/selection-modal.ts +163 -0
  169. package/src/input/selection.ts +135 -0
  170. package/src/input/session-picker-modal.ts +136 -0
  171. package/src/input/settings-modal-behavior.ts +37 -0
  172. package/src/input/settings-modal-secrets.ts +41 -0
  173. package/src/input/settings-modal-subscriptions.ts +95 -0
  174. package/src/input/settings-modal-types.ts +91 -0
  175. package/src/input/settings-modal.ts +793 -0
  176. package/src/input/submission-intent.ts +17 -0
  177. package/src/input/submission-router.ts +59 -0
  178. package/src/input/tts-settings-actions.ts +100 -0
  179. package/src/main.ts +792 -0
  180. package/src/mcp/runtime-reload.ts +81 -0
  181. package/src/panels/agent-inspector-panel.ts +521 -0
  182. package/src/panels/agent-inspector-shared.ts +94 -0
  183. package/src/panels/agent-logs-panel.ts +559 -0
  184. package/src/panels/agent-logs-shared.ts +129 -0
  185. package/src/panels/approval-panel.ts +150 -0
  186. package/src/panels/automation-control-panel.ts +212 -0
  187. package/src/panels/base-panel.ts +254 -0
  188. package/src/panels/builtin/agent.ts +117 -0
  189. package/src/panels/builtin/development.ts +31 -0
  190. package/src/panels/builtin/knowledge.ts +26 -0
  191. package/src/panels/builtin/operations.ts +349 -0
  192. package/src/panels/builtin/session.ts +129 -0
  193. package/src/panels/builtin/shared.ts +274 -0
  194. package/src/panels/builtin-panels.ts +23 -0
  195. package/src/panels/cockpit-panel.ts +183 -0
  196. package/src/panels/communication-panel.ts +153 -0
  197. package/src/panels/confirm-state.ts +61 -0
  198. package/src/panels/context-visualizer-panel.ts +204 -0
  199. package/src/panels/control-plane-panel.ts +211 -0
  200. package/src/panels/cost-tracker-panel.ts +444 -0
  201. package/src/panels/debug-panel.ts +432 -0
  202. package/src/panels/diff-panel.ts +520 -0
  203. package/src/panels/docs-panel.ts +283 -0
  204. package/src/panels/eval-panel.ts +399 -0
  205. package/src/panels/file-explorer-panel.ts +584 -0
  206. package/src/panels/file-preview-panel.ts +434 -0
  207. package/src/panels/forensics-panel.ts +364 -0
  208. package/src/panels/git-panel.ts +638 -0
  209. package/src/panels/hooks-panel.ts +239 -0
  210. package/src/panels/incident-review-panel.ts +197 -0
  211. package/src/panels/index.ts +46 -0
  212. package/src/panels/intelligence-panel.ts +176 -0
  213. package/src/panels/knowledge-panel.ts +345 -0
  214. package/src/panels/local-auth-panel.ts +130 -0
  215. package/src/panels/marketplace-panel.ts +212 -0
  216. package/src/panels/memory-panel.ts +225 -0
  217. package/src/panels/ops-control-panel.ts +150 -0
  218. package/src/panels/ops-strategy-panel.ts +235 -0
  219. package/src/panels/orchestration-panel.ts +273 -0
  220. package/src/panels/panel-list-panel.ts +509 -0
  221. package/src/panels/panel-manager.ts +570 -0
  222. package/src/panels/panel-picker.ts +106 -0
  223. package/src/panels/plan-dashboard-panel.ts +274 -0
  224. package/src/panels/plugins-panel.ts +178 -0
  225. package/src/panels/policy-panel.ts +308 -0
  226. package/src/panels/polish.ts +717 -0
  227. package/src/panels/project-planning-panel.ts +711 -0
  228. package/src/panels/provider-account-snapshot.ts +259 -0
  229. package/src/panels/provider-accounts-panel.ts +218 -0
  230. package/src/panels/provider-health-domains.ts +215 -0
  231. package/src/panels/provider-health-panel.ts +727 -0
  232. package/src/panels/provider-health-tracker.ts +115 -0
  233. package/src/panels/provider-stats-panel.ts +366 -0
  234. package/src/panels/qr-panel.ts +182 -0
  235. package/src/panels/remote-panel.ts +449 -0
  236. package/src/panels/routes-panel.ts +178 -0
  237. package/src/panels/sandbox-panel.ts +283 -0
  238. package/src/panels/schedule-panel.ts +329 -0
  239. package/src/panels/scrollable-list-panel.ts +491 -0
  240. package/src/panels/search-focus.ts +32 -0
  241. package/src/panels/security-panel.ts +295 -0
  242. package/src/panels/services-panel.ts +231 -0
  243. package/src/panels/session-browser-panel.ts +400 -0
  244. package/src/panels/session-maintenance.ts +125 -0
  245. package/src/panels/settings-sync-panel.ts +120 -0
  246. package/src/panels/skills-panel.ts +431 -0
  247. package/src/panels/subscription-panel.ts +263 -0
  248. package/src/panels/symbol-outline-panel.ts +486 -0
  249. package/src/panels/system-messages-panel.ts +230 -0
  250. package/src/panels/tasks-panel.ts +399 -0
  251. package/src/panels/thinking-panel.ts +304 -0
  252. package/src/panels/token-budget-panel.ts +475 -0
  253. package/src/panels/tool-inspector-panel.ts +429 -0
  254. package/src/panels/types.ts +54 -0
  255. package/src/panels/watchers-panel.ts +193 -0
  256. package/src/panels/work-plan-panel.ts +175 -0
  257. package/src/panels/worktree-panel.ts +182 -0
  258. package/src/panels/wrfc-panel.ts +609 -0
  259. package/src/permissions/prompt.ts +165 -0
  260. package/src/planning/project-planning-coordinator.ts +543 -0
  261. package/src/plugins/loader.ts +15 -0
  262. package/src/renderer/agent-detail-modal.ts +331 -0
  263. package/src/renderer/agent-workspace.ts +238 -0
  264. package/src/renderer/ansi-sanitize.ts +76 -0
  265. package/src/renderer/autocomplete-overlay.ts +154 -0
  266. package/src/renderer/block-actions.ts +76 -0
  267. package/src/renderer/bookmark-modal.ts +101 -0
  268. package/src/renderer/bottom-bar.ts +58 -0
  269. package/src/renderer/buffer.ts +113 -0
  270. package/src/renderer/code-block.ts +373 -0
  271. package/src/renderer/compositor.ts +283 -0
  272. package/src/renderer/context-inspector.ts +219 -0
  273. package/src/renderer/conversation-layout.ts +67 -0
  274. package/src/renderer/conversation-overlays.ts +140 -0
  275. package/src/renderer/conversation-surface.ts +260 -0
  276. package/src/renderer/diff-view.ts +132 -0
  277. package/src/renderer/diff.ts +130 -0
  278. package/src/renderer/file-picker-overlay.ts +101 -0
  279. package/src/renderer/file-tree.ts +153 -0
  280. package/src/renderer/fullscreen-primitives.ts +130 -0
  281. package/src/renderer/fullscreen-workspace.ts +199 -0
  282. package/src/renderer/git-status.ts +89 -0
  283. package/src/renderer/help-overlay.ts +267 -0
  284. package/src/renderer/history-search-overlay.ts +73 -0
  285. package/src/renderer/layout-engine.ts +97 -0
  286. package/src/renderer/layout.ts +32 -0
  287. package/src/renderer/live-tail-modal.ts +156 -0
  288. package/src/renderer/markdown.ts +635 -0
  289. package/src/renderer/mcp-workspace.ts +237 -0
  290. package/src/renderer/modal-factory.ts +467 -0
  291. package/src/renderer/modal-utils.ts +24 -0
  292. package/src/renderer/model-picker-overlay.ts +473 -0
  293. package/src/renderer/model-workspace.ts +488 -0
  294. package/src/renderer/onboarding/onboarding-wizard.ts +615 -0
  295. package/src/renderer/overlay-box.ts +146 -0
  296. package/src/renderer/overlay-viewport.ts +104 -0
  297. package/src/renderer/panel-composite.ts +158 -0
  298. package/src/renderer/panel-picker-overlay.ts +202 -0
  299. package/src/renderer/panel-tab-bar.ts +69 -0
  300. package/src/renderer/panel-workspace-bar.ts +42 -0
  301. package/src/renderer/process-indicator.ts +96 -0
  302. package/src/renderer/process-modal.ts +656 -0
  303. package/src/renderer/process-summary.ts +67 -0
  304. package/src/renderer/profile-picker-modal.ts +129 -0
  305. package/src/renderer/progress.ts +98 -0
  306. package/src/renderer/qr-renderer.ts +120 -0
  307. package/src/renderer/search-overlay.ts +54 -0
  308. package/src/renderer/selection-modal-overlay.ts +214 -0
  309. package/src/renderer/semantic-diff.ts +369 -0
  310. package/src/renderer/session-picker-modal.ts +127 -0
  311. package/src/renderer/settings-modal-helpers.ts +193 -0
  312. package/src/renderer/settings-modal.ts +537 -0
  313. package/src/renderer/shell-surface.ts +88 -0
  314. package/src/renderer/status-glyphs.ts +21 -0
  315. package/src/renderer/status-token.ts +67 -0
  316. package/src/renderer/surface-layout.ts +101 -0
  317. package/src/renderer/syntax-highlighter.ts +542 -0
  318. package/src/renderer/system-message.ts +83 -0
  319. package/src/renderer/tab-strip.ts +108 -0
  320. package/src/renderer/text-layout.ts +31 -0
  321. package/src/renderer/thinking.ts +17 -0
  322. package/src/renderer/tool-call.ts +234 -0
  323. package/src/renderer/ui-factory.ts +524 -0
  324. package/src/renderer/ui-primitives.ts +96 -0
  325. package/src/runtime/bootstrap-command-context.ts +278 -0
  326. package/src/runtime/bootstrap-command-parts.ts +386 -0
  327. package/src/runtime/bootstrap-core.ts +540 -0
  328. package/src/runtime/bootstrap-hook-bridge.ts +112 -0
  329. package/src/runtime/bootstrap-shell.ts +283 -0
  330. package/src/runtime/bootstrap.ts +575 -0
  331. package/src/runtime/cloudflare-control-plane.ts +349 -0
  332. package/src/runtime/context.ts +142 -0
  333. package/src/runtime/diagnostics/panels/index.ts +24 -0
  334. package/src/runtime/diagnostics/panels/ops.ts +156 -0
  335. package/src/runtime/diagnostics/panels/panel-resources.ts +118 -0
  336. package/src/runtime/diagnostics/panels/policy.ts +177 -0
  337. package/src/runtime/index.ts +662 -0
  338. package/src/runtime/onboarding/apply.ts +642 -0
  339. package/src/runtime/onboarding/derivation.ts +534 -0
  340. package/src/runtime/onboarding/index.ts +7 -0
  341. package/src/runtime/onboarding/markers.ts +148 -0
  342. package/src/runtime/onboarding/snapshot.ts +406 -0
  343. package/src/runtime/onboarding/state.ts +141 -0
  344. package/src/runtime/onboarding/types.ts +404 -0
  345. package/src/runtime/onboarding/verify.ts +171 -0
  346. package/src/runtime/operator-token-cleanup.ts +27 -0
  347. package/src/runtime/perf/panel-contracts.ts +32 -0
  348. package/src/runtime/perf/panel-health-monitor.ts +18 -0
  349. package/src/runtime/sandbox-public-gaps.ts +358 -0
  350. package/src/runtime/services.ts +670 -0
  351. package/src/runtime/store/domains/domain-read-matrix.ts +15 -0
  352. package/src/runtime/store/domains/index.ts +222 -0
  353. package/src/runtime/store/domains/panels.ts +117 -0
  354. package/src/runtime/store/domains/ui-perf.ts +103 -0
  355. package/src/runtime/store/index.ts +305 -0
  356. package/src/runtime/store/selectors/index.ts +359 -0
  357. package/src/runtime/store/state.ts +145 -0
  358. package/src/runtime/surface-feature-flags.ts +65 -0
  359. package/src/runtime/terminal-output-guard.ts +228 -0
  360. package/src/runtime/ui/index.ts +39 -0
  361. package/src/runtime/ui/model-picker/data-provider.ts +182 -0
  362. package/src/runtime/ui/model-picker/health-enrichment.ts +228 -0
  363. package/src/runtime/ui/model-picker/index.ts +59 -0
  364. package/src/runtime/ui/model-picker/types.ts +149 -0
  365. package/src/runtime/ui/provider-health/data-provider.ts +244 -0
  366. package/src/runtime/ui/provider-health/fallback-visualizer.ts +71 -0
  367. package/src/runtime/ui/provider-health/index.ts +46 -0
  368. package/src/runtime/ui/provider-health/types.ts +146 -0
  369. package/src/runtime/ui-events.ts +1 -0
  370. package/src/runtime/ui-read-model-helpers.ts +1 -0
  371. package/src/runtime/ui-read-models-observability-maintenance.ts +1 -0
  372. package/src/runtime/ui-read-models-observability-options.ts +1 -0
  373. package/src/runtime/ui-read-models-observability-remote.ts +1 -0
  374. package/src/runtime/ui-read-models-observability-security.ts +1 -0
  375. package/src/runtime/ui-read-models-observability-system.ts +1 -0
  376. package/src/runtime/ui-read-models-observability.ts +1 -0
  377. package/src/runtime/ui-read-models.ts +61 -0
  378. package/src/runtime/ui-service-queries.ts +1 -0
  379. package/src/runtime/ui-services.ts +190 -0
  380. package/src/scripts/process-messages.ts +42 -0
  381. package/src/shell/blocking-input.ts +98 -0
  382. package/src/shell/service-settings-sync.ts +273 -0
  383. package/src/shell/ui-openers.ts +352 -0
  384. package/src/tools/index.ts +1 -0
  385. package/src/tools/wrfc-agent-guard.ts +49 -0
  386. package/src/types/grid.ts +48 -0
  387. package/src/types/sql-js.d.ts +15 -0
  388. package/src/utils/clipboard.ts +22 -0
  389. package/src/utils/splash-lines.ts +46 -0
  390. package/src/utils/terminal-width.ts +185 -0
  391. package/src/verification/live-verifier.ts +430 -0
  392. package/src/verification/verification-ledger.ts +242 -0
  393. package/src/version.ts +17 -0
  394. package/src/widget/index.ts +2 -0
  395. package/src/widget/types.ts +9 -0
  396. package/src/widget/widget.ts +8 -0
  397. package/src/work-plans/work-plan-store.ts +374 -0
  398. package/tsconfig.json +18 -0
@@ -0,0 +1,537 @@
1
+ /**
2
+ * Fullscreen configuration workspace.
3
+ *
4
+ * This intentionally does not use ModalFactory. Configuration needs a stable,
5
+ * roomy workspace with contextual documentation, not a cramped modal list.
6
+ */
7
+
8
+ import type { Line } from '../types/grid.ts';
9
+ import type { SettingsModal, SettingEntry, FlagEntry, McpEntry, SubscriptionEntry, SettingsCategory } from '../input/settings-modal.ts';
10
+ import { SETTINGS_CATEGORIES, SETTINGS_CATEGORY_GROUPS } from '../input/settings-modal.ts';
11
+ import { getDisplayWidth, wrapText } from '../utils/terminal-width.ts';
12
+ import { CATEGORY_LABELS, describeUiRouting, formatValue, getSettingLabel, inferSubscriptionRouteReason, valueColor } from './settings-modal-helpers.ts';
13
+ import { isSecretConfigKey } from '../config/secret-config.ts';
14
+ import { GLYPHS } from './ui-primitives.ts';
15
+ import {
16
+ clamp,
17
+ getFullscreenWorkspaceMetrics,
18
+ padDisplay,
19
+ renderFullscreenWorkspace,
20
+ stableWindow,
21
+ WORKSPACE_PALETTE as PALETTE,
22
+ type WorkspaceRow,
23
+ } from './fullscreen-workspace.ts';
24
+
25
+ const CATEGORY_INFO: Record<SettingsCategory, string> = {
26
+ display: 'Presentation settings for the terminal transcript: streaming, line numbers, thinking visibility, reasoning summaries, token speed, and tool previews.',
27
+ ui: 'Controls where operational messages render and whether voice surfaces are enabled. These settings change visibility, not provider behavior.',
28
+ provider: 'Default model routing for normal chat turns, embeddings, reasoning effort, and persistent system prompt file.',
29
+ subscriptions: 'Provider subscription login state and routing posture. Active sessions can be reviewed or signed out here; API keys remain managed through secrets.',
30
+ behavior: 'Day-to-day shell behavior: approval posture, compaction, history, guidance, notifications, stale-context warnings, return context, and Human-in-the-Loop mode.',
31
+ storage: 'Local storage posture, including secret storage policy and maximum artifact size for knowledge/home graph/document ingestion.',
32
+ permissions: 'Permission mode and tool-class policy. These settings decide whether the shell prompts before read/write/exec/network/agent actions.',
33
+ orchestration: 'Agent orchestration limits and recursion controls.',
34
+ wrfc: 'WRFC is external to normal Agent operation. Review these copied compatibility values only for explicit GoodVibes TUI build delegation.',
35
+ helper: 'Helper model defaults used by helper subsystems when they do not use the main chat route.',
36
+ tts: 'Text-to-speech provider, voice, and optional spoken-turn LLM overrides.',
37
+ service: 'Background service posture: enabled state, autostart, restart behavior, service name, platform, and logs.',
38
+ controlPlane: 'Daemon control-plane settings for local admin/API access.',
39
+ httpListener: 'HTTP listener settings for webhook and integration ingress.',
40
+ web: 'Browser surface settings for the local or network web UI.',
41
+ batch: 'Batch execution settings, including local vs Cloudflare queue behavior.',
42
+ automation: 'Scheduled and automated run settings, concurrency, timeout, catch-up, cooldown, and retention behavior.',
43
+ watchers: 'File/process watcher heartbeat, polling, and recovery-window behavior.',
44
+ runtime: 'Runtime guardrails such as companion chat limiter and event bus listener caps.',
45
+ telemetry: 'Telemetry payload policy.',
46
+ cache: 'Provider and model cache behavior, TTL, and hit-rate monitoring.',
47
+ mcp: 'MCP server trust and scope review. Trust changes can expose local files, tools, databases, browsers, or remote automation depending on the server.',
48
+ sandbox: 'Sandbox/QEMU execution is externalized to GoodVibes TUI for delegated build/runtime isolation. Agent does not start sandbox sessions.',
49
+ surfaces: 'External app surfaces such as Slack, Discord, ntfy, Home Assistant, Telegram, webhooks, chat bridges, and messaging providers.',
50
+ cloudflare: 'Optional Cloudflare control plane, batch queue, Worker, Tunnel, Access, DNS, KV, Durable Objects, Secrets Store, and R2 settings.',
51
+ release: 'Release-channel preference.',
52
+ danger: 'High-impact switches for daemon and HTTP listener behavior. These are operational overrides, not normal preferences.',
53
+ tools: 'Tool LLM and helper model routing. Empty provider/model values inherit the active chat route unless a specific helper/tool route is set.',
54
+ flags: 'Feature flags are SDK runtime gates. They are separate from normal config keys because they enable or disable staged runtime behavior.',
55
+ network: 'Combined network view for daemon control-plane, HTTP listener, browser web surface, and general outbound network settings.',
56
+ };
57
+
58
+ const ENUM_VALUE_DESCRIPTIONS: Record<string, Record<string, string>> = {
59
+ 'behavior.hitlMode': {
60
+ quiet: 'Minimize operational interruptions and surface fewer Human-in-the-Loop prompts.',
61
+ balanced: 'Show important Human-in-the-Loop prompts without turning routine work into noise.',
62
+ operator: 'Surface more operational detail for users actively supervising agents, tools, services, and automation.',
63
+ },
64
+ 'behavior.guidanceMode': {
65
+ off: 'Do not add extra guidance beyond direct command output.',
66
+ minimal: 'Show concise guidance only when it helps avoid mistakes.',
67
+ guided: 'Provide more explanation and next-step context during configuration and operations.',
68
+ },
69
+ 'permissions.mode': {
70
+ prompt: 'Ask before powerful or risky actions according to tool policy.',
71
+ 'allow-all': 'Allow actions without prompting. This is fast but removes an important safety gate.',
72
+ custom: 'Use per-tool-class permission settings from the rows below.',
73
+ },
74
+ 'storage.secretPolicy': {
75
+ preferred_secure: 'Use secure secret storage when available, with supported fallback behavior.',
76
+ require_secure: 'Require secure secret storage and reject plaintext fallback.',
77
+ plaintext_allowed: 'Allow plaintext fallback when secure storage is unavailable.',
78
+ },
79
+ 'batch.mode': {
80
+ off: 'Keep daemon work on the immediate local path.',
81
+ explicit: 'Use batch only when callers explicitly request batch execution.',
82
+ 'eligible-by-default': 'Allow eligible daemon work to use the batch path unless callers opt out.',
83
+ },
84
+ 'controlPlane.hostMode': {
85
+ localhost: 'Bind only to this computer.',
86
+ network: 'Bind for LAN access using the default network host.',
87
+ custom: 'Use the explicit host value in the related host setting.',
88
+ },
89
+ 'httpListener.hostMode': {
90
+ localhost: 'Bind only to this computer.',
91
+ network: 'Bind for LAN/webhook access using the default network host.',
92
+ custom: 'Use the explicit host value in the related host setting.',
93
+ },
94
+ 'web.hostMode': {
95
+ localhost: 'Serve the browser UI only on this computer.',
96
+ network: 'Serve the browser UI on the LAN.',
97
+ custom: 'Use the explicit host value in the related host setting.',
98
+ },
99
+ 'ui.systemMessages': {
100
+ panel: 'Show system messages in panels only.',
101
+ conversation: 'Show system messages inline in the transcript.',
102
+ both: 'Show system messages in both panels and the transcript.',
103
+ },
104
+ 'ui.operationalMessages': {
105
+ panel: 'Show operational messages in panels only.',
106
+ conversation: 'Show operational messages inline in the transcript.',
107
+ both: 'Show operational messages in both panels and the transcript.',
108
+ },
109
+ 'ui.wrfcMessages': {
110
+ panel: 'Show explicit delegated-build WRFC messages in panels only.',
111
+ conversation: 'Show explicit delegated-build WRFC messages inline in the transcript.',
112
+ both: 'Show explicit delegated-build WRFC messages in both panels and the transcript.',
113
+ },
114
+ 'surfaces.telegram.mode': {
115
+ webhook: 'Receive Telegram updates through webhook delivery.',
116
+ polling: 'Poll Telegram for updates from the service.',
117
+ },
118
+ 'surfaces.whatsapp.provider': {
119
+ 'meta-cloud': 'Use Meta Cloud API credentials and identifiers.',
120
+ bridge: 'Use a bridge service URL/token flow instead of direct Meta Cloud API delivery.',
121
+ },
122
+ };
123
+
124
+ function paddedWrapped(text: string, width: number, prefix = ''): string[] {
125
+ const safeWidth = Math.max(1, width - getDisplayWidth(prefix));
126
+ const wrapped = wrapText(text, safeWidth);
127
+ if (prefix.length === 0) return wrapped;
128
+ return wrapped.map((line, index) => `${index === 0 ? prefix : ' '.repeat(getDisplayWidth(prefix))}${line}`);
129
+ }
130
+
131
+ function formatDefaultValue(value: unknown): string {
132
+ if (value === '') return '(empty)';
133
+ if (value === null || value === undefined) return '(unset)';
134
+ return String(value);
135
+ }
136
+
137
+ function currentSettingValue(modal: SettingsModal, entry: SettingEntry, selected: boolean): string {
138
+ if (selected && modal.editingMode) return `${modal.editBuffer}${GLYPHS.surface.cursor}`;
139
+ return formatValue(entry);
140
+ }
141
+
142
+ function buildSettingContext(modal: SettingsModal, entry: SettingEntry): string[] {
143
+ const lines: string[] = [
144
+ getSettingLabel(entry),
145
+ `Key: ${entry.setting.key}`,
146
+ `Current: ${currentSettingValue(modal, entry, true)}`,
147
+ `Default: ${formatDefaultValue(entry.setting.default)}`,
148
+ `Type: ${entry.setting.type}${entry.setting.enumValues ? ` with ${entry.setting.enumValues.length} possible value(s)` : ''}`,
149
+ `Source: ${entry.effectiveSource ?? 'default'}${entry.sourceLabel ? ` from ${entry.sourceLabel}` : ''}`,
150
+ ];
151
+
152
+ if (entry.locked) lines.push(`Locked: ${entry.lockReason ?? 'This setting is locked by a higher-priority layer.'}`);
153
+ if (entry.conflict) lines.push(`Conflict: resolve with /settingssync resolve ${entry.setting.key} local|synced.`);
154
+
155
+ lines.push('', entry.setting.description);
156
+
157
+ if (
158
+ entry.setting.key === 'ui.systemMessages'
159
+ || entry.setting.key === 'ui.operationalMessages'
160
+ || entry.setting.key === 'ui.wrfcMessages'
161
+ ) {
162
+ lines.push(`Routing meaning: ${describeUiRouting(String(entry.currentValue))}.`);
163
+ }
164
+
165
+ if (entry.setting.type === 'boolean') {
166
+ lines.push('');
167
+ lines.push('Possible values:');
168
+ lines.push('true: enabled or allowed for this setting.');
169
+ lines.push('false: disabled or not allowed for this setting.');
170
+ }
171
+
172
+ if (entry.setting.type === 'enum' && entry.setting.enumValues) {
173
+ lines.push('');
174
+ lines.push('Possible values:');
175
+ const descriptions = ENUM_VALUE_DESCRIPTIONS[entry.setting.key] ?? {};
176
+ for (const value of entry.setting.enumValues) {
177
+ lines.push(`${value}: ${descriptions[value] ?? `Use ${value} for this setting.`}`);
178
+ }
179
+ }
180
+
181
+ if (isSecretConfigKey(entry.setting.key)) {
182
+ lines.push('');
183
+ lines.push('Secret handling: raw values entered here are stored through the secret manager and the config receives a goodvibes:// secret reference. Empty input clears the config value.');
184
+ }
185
+
186
+ if (entry.setting.type === 'number') {
187
+ lines.push('');
188
+ lines.push('Editing: Enter opens inline edit, then type the value and press Enter to save. Arrow keys only navigate.');
189
+ }
190
+
191
+ if (entry.setting.type === 'string' && !isSecretConfigKey(entry.setting.key)) {
192
+ lines.push('');
193
+ lines.push('Editing: Enter opens inline edit. Delete the current text to save an empty value when that is valid for the setting.');
194
+ }
195
+
196
+ return lines;
197
+ }
198
+
199
+ function buildFlagContext(entry: FlagEntry | null): string[] {
200
+ if (!entry) return ['Feature flags', 'No feature flag is selected.'];
201
+ return [
202
+ entry.flag.name,
203
+ `ID: ${entry.flag.id}`,
204
+ `State: ${entry.state}`,
205
+ `Default: ${entry.flag.defaultState}`,
206
+ `Tier: ${entry.flag.tier}`,
207
+ `Runtime toggleable: ${entry.flag.runtimeToggleable ? 'yes' : 'no'}`,
208
+ '',
209
+ entry.flag.description,
210
+ ...(entry.state === 'killed' && entry.flag.killReason ? ['', `Kill reason: ${entry.flag.killReason}`] : []),
211
+ '',
212
+ entry.flag.runtimeToggleable
213
+ ? 'Impact: changes apply immediately and are also persisted as an override when they differ from the default.'
214
+ : 'Impact: this flag is persisted as an override and requires restart before startup-only code sees the new state.',
215
+ ];
216
+ }
217
+
218
+ function buildMcpContext(modal: SettingsModal, entry: McpEntry | null): string[] {
219
+ if (!entry) return ['MCP trust', 'No MCP server is selected.'];
220
+ const scope = entry.allowedPaths.length > 0
221
+ ? `Allowed paths: ${entry.allowedPaths.join(', ')}`
222
+ : entry.allowedHosts.length > 0
223
+ ? `Allowed hosts: ${entry.allowedHosts.join(', ')}`
224
+ : 'No explicit path or host scope is configured.';
225
+ const confirmation = modal.mcpAllowAllConfirmationTarget === entry.name
226
+ ? `Confirmation required: type ALLOW ALL ${entry.name} to grant unrestricted trust.`
227
+ : 'Enter edits the trust mode. Valid values are constrained, ask-on-risk, allow-all, and blocked.';
228
+ return [
229
+ entry.name,
230
+ `Connection: ${entry.connected ? 'connected' : 'disconnected'}`,
231
+ `Role: ${entry.role}`,
232
+ `Trust mode: ${entry.trustMode}`,
233
+ confirmation,
234
+ '',
235
+ scope,
236
+ '',
237
+ 'Trust meanings:',
238
+ 'constrained: keep MCP activity inside declared paths/hosts and prompt on risk.',
239
+ 'ask-on-risk: allow routine MCP operations but ask before risky behavior.',
240
+ 'allow-all: allow unrestricted MCP operations for this server after explicit confirmation.',
241
+ 'blocked: prevent this MCP server from being used.',
242
+ ];
243
+ }
244
+
245
+ function buildSubscriptionContext(modal: SettingsModal, entry: SubscriptionEntry | null): string[] {
246
+ if (!entry) return ['Subscriptions', 'No subscription provider is selected.'];
247
+ const expires = entry.expiresAt ? new Date(entry.expiresAt).toISOString() : 'not reported';
248
+ const routeReason = inferSubscriptionRouteReason(entry);
249
+ const logout = entry.state === 'active' || entry.state === 'pending'
250
+ ? modal.subscriptionLogoutConfirmationTarget === entry.provider
251
+ ? `Press Enter again to sign out ${entry.provider}. Move selection or close config to cancel.`
252
+ : 'Press Enter to review sign-out for this provider session.'
253
+ : `Use /subscription login ${entry.provider} start to begin OAuth sign-in for this provider.`;
254
+ return [
255
+ entry.provider,
256
+ `State: ${entry.state}`,
257
+ ...(routeReason ? [routeReason] : []),
258
+ logout,
259
+ `Active route: ${entry.activeRoute ?? 'n/a'}`,
260
+ `Preferred route: ${entry.preferredRoute ?? 'n/a'}`,
261
+ `OAuth configured: ${entry.oauthConfigured ? 'yes' : 'no'}`,
262
+ `Freshness: ${entry.authFreshness ?? 'n/a'}`,
263
+ `Expires: ${expires}`,
264
+ ...((entry.issues ?? []).length > 0 ? ['', 'Issues:', ...(entry.issues ?? [])] : []),
265
+ ...((entry.nextActions ?? []).length > 0 ? ['', 'Next actions:', ...(entry.nextActions ?? [])] : []),
266
+ ];
267
+ }
268
+
269
+ function buildContextLines(modal: SettingsModal, width: number): string[] {
270
+ const category = modal.currentCategory;
271
+ const lines: string[] = [
272
+ `${CATEGORY_LABELS[category]} configuration`,
273
+ ];
274
+
275
+ if (category === 'flags') {
276
+ lines.push(...buildFlagContext(modal.getSelectedFlag()));
277
+ } else if (category === 'mcp') {
278
+ lines.push(...buildMcpContext(modal, modal.getSelectedMcp()));
279
+ } else if (category === 'subscriptions') {
280
+ lines.push(...buildSubscriptionContext(modal, modal.getSelectedSubscription()));
281
+ } else {
282
+ const selected = modal.getSelected();
283
+ if (selected) lines.push(...buildSettingContext(modal, selected));
284
+ else lines.push('No setting is selected in this category.');
285
+ }
286
+
287
+ lines.push('', `Category purpose: ${CATEGORY_INFO[category]}`);
288
+
289
+ const wrapped: string[] = [];
290
+ for (const line of lines) {
291
+ if (line === '') {
292
+ wrapped.push('');
293
+ continue;
294
+ }
295
+ wrapped.push(...paddedWrapped(line, width));
296
+ }
297
+ return wrapped;
298
+ }
299
+
300
+ function categoryItemCount(modal: SettingsModal, category: SettingsCategory): number {
301
+ if (category === 'flags') return modal.flagEntries.length;
302
+ if (category === 'mcp') return modal.mcpEntries.length;
303
+ if (category === 'subscriptions') return modal.subscriptionEntries.length;
304
+ return modal.groups.get(category)?.length ?? 0;
305
+ }
306
+
307
+ type CategoryRailEntry =
308
+ | { readonly type: 'group'; readonly label: string }
309
+ | { readonly type: 'category'; readonly category: SettingsCategory; readonly index: number };
310
+
311
+ type CategoryRailRow = {
312
+ readonly text: string;
313
+ readonly type: CategoryRailEntry['type'] | 'more' | 'empty';
314
+ readonly selected: boolean;
315
+ };
316
+
317
+ function buildCategoryRailEntries(): CategoryRailEntry[] {
318
+ const entries: CategoryRailEntry[] = [];
319
+ for (const group of SETTINGS_CATEGORY_GROUPS) {
320
+ const categories = group.categories.filter(category => SETTINGS_CATEGORIES.includes(category));
321
+ if (categories.length === 0) continue;
322
+ entries.push({ type: 'group', label: group.label });
323
+ for (const category of categories) {
324
+ entries.push({
325
+ type: 'category',
326
+ category,
327
+ index: SETTINGS_CATEGORIES.indexOf(category),
328
+ });
329
+ }
330
+ }
331
+ return entries;
332
+ }
333
+
334
+ function renderCategories(modal: SettingsModal, width: number, height: number): CategoryRailRow[] {
335
+ const rows: CategoryRailRow[] = [];
336
+ const entries = buildCategoryRailEntries();
337
+ const selectedEntryIndex = Math.max(0, entries.findIndex(entry => entry.type === 'category' && entry.index === modal.categoryIndex));
338
+ const window = stableWindow(entries.length, selectedEntryIndex, height);
339
+ if (window.start > 0) rows.push({ text: `${GLYPHS.navigation.moreAbove} ${window.start} more row(s) above`, type: 'more', selected: false });
340
+ for (let railIndex = window.start; railIndex < window.end; railIndex += 1) {
341
+ const entry = entries[railIndex]!;
342
+ if (entry.type === 'group') {
343
+ rows.push({ text: entry.label.toUpperCase(), type: 'group', selected: false });
344
+ continue;
345
+ }
346
+ const category = entry.category;
347
+ const active = entry.index === modal.categoryIndex;
348
+ const count = categoryItemCount(modal, category);
349
+ const cursor = active ? (modal.focusPane === 'categories' ? GLYPHS.navigation.selected : '•') : ' ';
350
+ rows.push({ text: ` ${cursor} ${CATEGORY_LABELS[category]} (${count})`, type: 'category', selected: active });
351
+ }
352
+ if (window.end < entries.length) rows.push({ text: `${GLYPHS.navigation.moreBelow} ${entries.length - window.end} more row(s) below`, type: 'more', selected: false });
353
+ while (rows.length < height) rows.push({ text: '', type: 'empty', selected: false });
354
+ return rows.slice(0, height);
355
+ }
356
+
357
+ function renderSettingRows(modal: SettingsModal, width: number, height: number): string[] {
358
+ const rows: string[] = [];
359
+ const items = modal.currentItems;
360
+ if (items.length === 0) return ['No settings in this category.'];
361
+ const selectedIndex = clamp(modal.selectedIndex, 0, items.length - 1);
362
+ const typeWidth = 9;
363
+ const sourceWidth = 12;
364
+ const defaultWidth = 12;
365
+ const available = Math.max(24, width - typeWidth - sourceWidth - defaultWidth - 13);
366
+ const keyWidth = clamp(Math.floor(available * 0.56), 18, 52);
367
+ const valueWidth = Math.max(10, available - keyWidth);
368
+ rows.push(` ${padDisplay('Setting', keyWidth)} ${padDisplay('Value', valueWidth)} ${padDisplay('Type', typeWidth)} ${padDisplay('Source', sourceWidth)} ${padDisplay('Default', defaultWidth)}`);
369
+ const visibleCount = Math.max(1, height - 2);
370
+ const window = stableWindow(items.length, selectedIndex, visibleCount);
371
+ if (window.start > 0) rows.push(`${GLYPHS.navigation.moreAbove} ${window.start} more setting(s) above`);
372
+
373
+ for (let index = window.start; index < window.end; index += 1) {
374
+ const entry = items[index]!;
375
+ const selected = index === selectedIndex;
376
+ const marker = selected ? (modal.focusPane === 'settings' ? GLYPHS.navigation.selected : '•') : entry.isDefault ? ' ' : '◇';
377
+ const value = currentSettingValue(modal, entry, selected);
378
+ const source = `${entry.effectiveSource ?? 'default'}${entry.locked ? ' locked' : ''}${entry.conflict ? ' conflict' : ''}`;
379
+ const label = getSettingLabel(entry);
380
+ rows.push(`${marker} ${padDisplay(label, keyWidth)} ${padDisplay(value, valueWidth)} ${padDisplay(entry.setting.type, typeWidth)} ${padDisplay(source, sourceWidth)} ${padDisplay(formatDefaultValue(entry.setting.default), defaultWidth)}`);
381
+ }
382
+
383
+ if (window.end < items.length) rows.push(`${GLYPHS.navigation.moreBelow} ${items.length - window.end} more setting(s) below`);
384
+ return rows.slice(0, height);
385
+ }
386
+
387
+ function renderFlagRows(modal: SettingsModal, width: number, height: number): string[] {
388
+ const rows: string[] = [];
389
+ const items = modal.flagEntries;
390
+ if (items.length === 0) return ['No feature flags registered.'];
391
+ const selectedIndex = clamp(modal.selectedIndex, 0, items.length - 1);
392
+ const nameWidth = clamp(Math.floor(width * 0.40), 24, 58);
393
+ const stateWidth = 10;
394
+ const tierWidth = 6;
395
+ const runtimeWidth = 9;
396
+ const defaultWidth = 9;
397
+ const idWidth = Math.max(12, width - nameWidth - stateWidth - tierWidth - runtimeWidth - defaultWidth - 14);
398
+ rows.push(` ${padDisplay('Feature Flag', nameWidth)} ${padDisplay('State', stateWidth)} ${padDisplay('Tier', tierWidth)} ${padDisplay('Runtime', runtimeWidth)} ${padDisplay('Default', defaultWidth)} ${padDisplay('ID', idWidth)}`);
399
+ const visibleCount = Math.max(1, height - 2);
400
+ const window = stableWindow(items.length, selectedIndex, visibleCount);
401
+ if (window.start > 0) rows.push(`${GLYPHS.navigation.moreAbove} ${window.start} more flag(s) above`);
402
+ for (let index = window.start; index < window.end; index += 1) {
403
+ const entry = items[index]!;
404
+ const selected = index === selectedIndex;
405
+ const marker = selected ? (modal.focusPane === 'settings' ? GLYPHS.navigation.selected : '•') : ' ';
406
+ rows.push(`${marker} ${padDisplay(entry.flag.name, nameWidth)} ${padDisplay(entry.state, stateWidth)} ${padDisplay(String(entry.flag.tier), tierWidth)} ${padDisplay(entry.flag.runtimeToggleable ? 'yes' : 'restart', runtimeWidth)} ${padDisplay(entry.flag.defaultState, defaultWidth)} ${padDisplay(entry.flag.id, idWidth)}`);
407
+ }
408
+ if (window.end < items.length) rows.push(`${GLYPHS.navigation.moreBelow} ${items.length - window.end} more flag(s) below`);
409
+ return rows.slice(0, height);
410
+ }
411
+
412
+ function renderMcpRows(modal: SettingsModal, width: number, height: number): string[] {
413
+ const rows: string[] = [];
414
+ const items = modal.mcpEntries;
415
+ if (items.length === 0) return ['No MCP servers registered.'];
416
+ const selectedIndex = clamp(modal.selectedIndex, 0, items.length - 1);
417
+ const nameWidth = clamp(Math.floor(width * 0.32), 18, 44);
418
+ const trustWidth = 14;
419
+ const roleWidth = 12;
420
+ const statusWidth = 12;
421
+ const scopeWidth = Math.max(12, width - nameWidth - trustWidth - roleWidth - statusWidth - 10);
422
+ rows.push(` ${padDisplay('Server', nameWidth)} ${padDisplay('Trust', trustWidth)} ${padDisplay('Role', roleWidth)} ${padDisplay('Status', statusWidth)} ${padDisplay('Scope', scopeWidth)}`);
423
+ const window = stableWindow(items.length, selectedIndex, Math.max(1, height - 2));
424
+ if (window.start > 0) rows.push(`${GLYPHS.navigation.moreAbove} ${window.start} more MCP server(s) above`);
425
+ for (let index = window.start; index < window.end; index += 1) {
426
+ const entry = items[index]!;
427
+ const selected = index === selectedIndex;
428
+ const trust = selected && modal.editingMode ? `${modal.editBuffer}${GLYPHS.surface.cursor}` : entry.trustMode;
429
+ const scope = entry.allowedPaths.length > 0 ? entry.allowedPaths.join(', ') : entry.allowedHosts.length > 0 ? entry.allowedHosts.join(', ') : 'none';
430
+ const marker = selected ? (modal.focusPane === 'settings' ? GLYPHS.navigation.selected : '•') : ' ';
431
+ rows.push(`${marker} ${padDisplay(entry.name, nameWidth)} ${padDisplay(trust, trustWidth)} ${padDisplay(entry.role, roleWidth)} ${padDisplay(entry.connected ? 'connected' : 'offline', statusWidth)} ${padDisplay(scope, scopeWidth)}`);
432
+ }
433
+ if (window.end < items.length) rows.push(`${GLYPHS.navigation.moreBelow} ${items.length - window.end} more MCP server(s) below`);
434
+ return rows.slice(0, height);
435
+ }
436
+
437
+ function renderSubscriptionRows(modal: SettingsModal, width: number, height: number): string[] {
438
+ const rows: string[] = [];
439
+ const items = modal.subscriptionEntries;
440
+ if (items.length === 0) return ['No provider subscriptions available or configured.'];
441
+ const selectedIndex = clamp(modal.selectedIndex, 0, items.length - 1);
442
+ const providerWidth = clamp(Math.floor(width * 0.28), 14, 36);
443
+ const stateWidth = 10;
444
+ const routeWidth = 16;
445
+ const freshnessWidth = 14;
446
+ const oauthWidth = 8;
447
+ const noteWidth = Math.max(12, width - providerWidth - stateWidth - routeWidth - freshnessWidth - oauthWidth - 12);
448
+ rows.push(` ${padDisplay('Provider', providerWidth)} ${padDisplay('State', stateWidth)} ${padDisplay('Route', routeWidth)} ${padDisplay('Freshness', freshnessWidth)} ${padDisplay('OAuth', oauthWidth)} ${padDisplay('Note', noteWidth)}`);
449
+ const window = stableWindow(items.length, selectedIndex, Math.max(1, height - 2));
450
+ if (window.start > 0) rows.push(`${GLYPHS.navigation.moreAbove} ${window.start} more subscription provider(s) above`);
451
+ for (let index = window.start; index < window.end; index += 1) {
452
+ const entry = items[index]!;
453
+ const selected = index === selectedIndex;
454
+ const marker = selected ? (modal.focusPane === 'settings' ? GLYPHS.navigation.selected : '•') : ' ';
455
+ rows.push(`${marker} ${padDisplay(entry.provider, providerWidth)} ${padDisplay(entry.state, stateWidth)} ${padDisplay(entry.activeRoute ?? 'n/a', routeWidth)} ${padDisplay(entry.authFreshness ?? 'n/a', freshnessWidth)} ${padDisplay(entry.oauthConfigured ? 'yes' : 'no', oauthWidth)} ${padDisplay(inferSubscriptionRouteReason(entry) ?? '', noteWidth)}`);
456
+ }
457
+ if (window.end < items.length) rows.push(`${GLYPHS.navigation.moreBelow} ${items.length - window.end} more subscription provider(s) below`);
458
+ return rows.slice(0, height);
459
+ }
460
+
461
+ function renderControlRows(modal: SettingsModal, width: number, height: number): string[] {
462
+ if (modal.currentCategory === 'flags') return renderFlagRows(modal, width, height);
463
+ if (modal.currentCategory === 'mcp') return renderMcpRows(modal, width, height);
464
+ if (modal.currentCategory === 'subscriptions') return renderSubscriptionRows(modal, width, height);
465
+ return renderSettingRows(modal, width, height);
466
+ }
467
+
468
+ function rowColorForSetting(modal: SettingsModal, rowText: string): string {
469
+ if (modal.currentCategory === 'danger') return PALETTE.bad;
470
+ if (rowText.startsWith(GLYPHS.navigation.selected)) return PALETTE.text;
471
+ const selected = modal.getSelected();
472
+ if (!selected) return PALETTE.text;
473
+ return valueColor(selected);
474
+ }
475
+
476
+ function footerText(modal: SettingsModal): string {
477
+ if (modal.editingMode) return 'Enter Confirm edit · Esc Cancel edit · text keys edit the selected field';
478
+ if (modal.focusPane === 'categories') return 'Focus categories · Up/Down choose · Right/Enter settings · Tab pane · Esc close';
479
+ if (modal.currentCategory === 'subscriptions') return 'Focus settings · Up/Down provider · Left categories · Tab pane · Enter review/sign out · Esc close';
480
+ if (modal.currentCategory === 'mcp') return 'Focus settings · Up/Down server · Left categories · Tab pane · Enter edit trust · Esc close';
481
+ if (modal.currentCategory === 'flags') return 'Focus feature flags · Up/Down flag · Left categories · Tab pane · Enter/Space toggle · Esc close';
482
+ return 'Focus settings · Up/Down setting · Left categories · Tab pane · Enter/Space edit/toggle · R reset · Esc close';
483
+ }
484
+
485
+ export function renderSettingsModal(
486
+ modal: SettingsModal,
487
+ width: number,
488
+ viewportHeight = 24,
489
+ ): Line[] {
490
+ const notices = [
491
+ ...(modal.lastSaveTriggeredRestart ? [`Restarting ${modal.lastSaveTriggeredRestart}`] : []),
492
+ ...(modal.lastSettingEffectMessage ? [modal.lastSettingEffectMessage] : []),
493
+ ];
494
+ const metrics = getFullscreenWorkspaceMetrics({ width, height: viewportHeight });
495
+ const categoryRows = renderCategories(modal, metrics.leftWidth - 2, metrics.bodyRows);
496
+ const contextRows = buildContextLines(modal, metrics.contextWidth).map((text, row): WorkspaceRow => {
497
+ const selectedSetting = modal.getSelected();
498
+ const isTitle = row === 0 || (selectedSetting !== null && text === getSettingLabel(selectedSetting));
499
+ return {
500
+ text,
501
+ fg: row === 0 ? PALETTE.title : text.endsWith(':') ? PALETTE.subtitle : PALETTE.text,
502
+ bold: isTitle,
503
+ dim: text.length === 0,
504
+ };
505
+ });
506
+ const controlRows = renderControlRows(modal, metrics.contextWidth, metrics.controlRows).map((text): WorkspaceRow => {
507
+ const selected = text.startsWith(GLYPHS.navigation.selected);
508
+ return {
509
+ text,
510
+ selected,
511
+ fg: selected
512
+ ? PALETTE.text
513
+ : text.startsWith('value:') || text.trimStart().startsWith('value:')
514
+ ? PALETTE.info
515
+ : rowColorForSetting(modal, text),
516
+ bold: selected,
517
+ dim: text.length === 0,
518
+ };
519
+ });
520
+
521
+ return renderFullscreenWorkspace({
522
+ width,
523
+ height: viewportHeight,
524
+ title: 'Configuration Workspace / Settings',
525
+ leftHeader: 'Categories',
526
+ mainHeader: `${CATEGORY_LABELS[modal.currentCategory]} (${categoryItemCount(modal, modal.currentCategory)})${notices.length > 0 ? ` · ${notices.join(' · ')}` : ''}`,
527
+ leftRows: categoryRows.map((row): WorkspaceRow => ({
528
+ text: row.text,
529
+ selected: row.selected,
530
+ kind: row.type === 'group' ? 'group' : row.type === 'more' ? 'more' : row.type === 'empty' ? 'empty' : 'item',
531
+ bold: row.selected || row.type === 'group',
532
+ })),
533
+ contextRows,
534
+ controlRows,
535
+ footer: footerText(modal),
536
+ });
537
+ }
@@ -0,0 +1,88 @@
1
+ import type { Line } from '../types/grid.ts';
2
+ import { renderProcessIndicator } from './process-indicator.ts';
3
+ import { UIFactory } from './ui-factory.ts';
4
+
5
+ export interface ShellFooterBuildOptions {
6
+ readonly width: number;
7
+ readonly promptText: string;
8
+ readonly promptLineCount: number;
9
+ readonly promptCursorPos?: number;
10
+ readonly promptFocused?: boolean;
11
+ readonly usage: { up: number; down: number };
12
+ readonly showExitNotice: boolean;
13
+ readonly lastCopyTime: number;
14
+ readonly model?: string;
15
+ readonly toolCount?: number;
16
+ readonly workingDir?: string;
17
+ readonly provider?: string;
18
+ readonly contextWindow?: number;
19
+ readonly compactThreshold?: number;
20
+ readonly dangerMode?: boolean;
21
+ readonly lastInputTokens?: number;
22
+ readonly commandArgsHint?: string;
23
+ readonly hitlMode?: string;
24
+ readonly runningAgentCount: number;
25
+ readonly runningProcessCount: number;
26
+ readonly indicatorFocused: boolean;
27
+ readonly runningAgentProgress?: string;
28
+ readonly composerMode?: string;
29
+ readonly composerStatus?: string;
30
+ readonly composerFlags?: readonly string[];
31
+ readonly composerPendingRisk?: 'none' | 'approval-wait' | 'shell' | 'command' | 'remote';
32
+ }
33
+
34
+ export interface ShellFooterBuildResult {
35
+ readonly lines: Line[];
36
+ readonly height: number;
37
+ }
38
+
39
+ const FOOTER_BASE_ROWS = 9;
40
+ const CONTEXT_PROGRESS_ROWS = 2;
41
+ const PROCESS_INDICATOR_ROWS = 1;
42
+
43
+ export function estimateShellFooterHeight(
44
+ promptLineCount: number,
45
+ contextWindow?: number,
46
+ ): number {
47
+ const safePromptLines = Math.max(1, promptLineCount);
48
+ const progressRows = contextWindow && contextWindow > 0 ? CONTEXT_PROGRESS_ROWS : 0;
49
+ return FOOTER_BASE_ROWS + safePromptLines + progressRows + PROCESS_INDICATOR_ROWS;
50
+ }
51
+
52
+ export function buildShellFooter(
53
+ options: ShellFooterBuildOptions,
54
+ ): ShellFooterBuildResult {
55
+ const lines = UIFactory.createFooter(
56
+ options.width,
57
+ options.promptText,
58
+ options.usage,
59
+ options.showExitNotice,
60
+ options.lastCopyTime,
61
+ options.model,
62
+ options.toolCount,
63
+ options.promptCursorPos,
64
+ options.workingDir,
65
+ options.provider,
66
+ options.contextWindow,
67
+ options.compactThreshold,
68
+ options.dangerMode,
69
+ options.lastInputTokens,
70
+ options.commandArgsHint,
71
+ options.hitlMode,
72
+ options.promptFocused ?? !options.indicatorFocused,
73
+ options.composerMode,
74
+ options.composerStatus,
75
+ options.composerFlags,
76
+ options.composerPendingRisk,
77
+ );
78
+ const processIndicator = renderProcessIndicator(
79
+ options.width,
80
+ options.runningAgentCount,
81
+ options.runningProcessCount,
82
+ options.indicatorFocused,
83
+ options.runningAgentProgress,
84
+ );
85
+ const inputBoxRows = Math.max(1, options.promptLineCount) + 2;
86
+ lines.splice(inputBoxRows, 0, ...processIndicator);
87
+ return { lines, height: lines.length };
88
+ }
@@ -0,0 +1,21 @@
1
+ // ---------------------------------------------------------------------------
2
+ // status-glyphs.ts — canonical glyph map for status states.
3
+ //
4
+ // Extracted as a neutral module so both status-token.ts and polish.ts can
5
+ // import from here without creating a circular ESM dependency.
6
+ //
7
+ // Glyphs:
8
+ // good ✓ (CHECK MARK U+2713)
9
+ // warn ⚠ (WARNING SIGN U+26A0)
10
+ // bad ✕ (MULTIPLICATION X U+2715)
11
+ // info ○ (WHITE CIRCLE U+25CB)
12
+ // ---------------------------------------------------------------------------
13
+
14
+ export type StatusState = 'good' | 'warn' | 'bad' | 'info';
15
+
16
+ export const STATE_GLYPHS: Record<StatusState, string> = {
17
+ good: '\u2713', // ✓
18
+ warn: '\u26a0', // ⚠
19
+ bad: '\u2715', // ✕
20
+ info: '\u25cb', // ○
21
+ };