@pellux/goodvibes-tui 1.16.1 → 1.18.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 (257) hide show
  1. package/CHANGELOG.md +138 -9
  2. package/README.md +1 -1
  3. package/docs/foundation-artifacts/operator-contract.json +5490 -1806
  4. package/package.json +7 -7
  5. package/src/cli/completions/generate.ts +5 -4
  6. package/src/cli/config-overrides.ts +51 -11
  7. package/src/cli/doctor.ts +4 -5
  8. package/src/cli/entrypoint.ts +13 -8
  9. package/src/cli/help.ts +7 -6
  10. package/src/cli/launch-auto-update.ts +214 -0
  11. package/src/cli/management-commands.ts +82 -15
  12. package/src/cli/management.ts +0 -2
  13. package/src/cli/parser.ts +5 -0
  14. package/src/cli/status.ts +13 -0
  15. package/src/cli/surface-command.ts +5 -5
  16. package/src/cli/tui-startup.ts +60 -97
  17. package/src/cli/types.ts +2 -0
  18. package/src/config/index.ts +1 -1
  19. package/src/config/tui-extension-settings.ts +33 -0
  20. package/src/core/composer-state.ts +1 -1
  21. package/src/core/consolidation-receipt.ts +41 -0
  22. package/src/core/conversation-line-cache.ts +3 -3
  23. package/src/core/conversation-rendering.ts +18 -9
  24. package/src/core/conversation.ts +2 -2
  25. package/src/core/long-task-notifier.ts +7 -7
  26. package/src/core/memory-provenance.ts +67 -0
  27. package/src/core/pairing-devices.ts +49 -0
  28. package/src/core/pairing-handoff.ts +101 -0
  29. package/src/core/pairing-offers.ts +83 -0
  30. package/src/core/pairing-origin.ts +78 -0
  31. package/src/core/pairing-tailscale-gateway.ts +56 -0
  32. package/src/core/power-chip-source.ts +91 -0
  33. package/src/core/power-status.ts +95 -0
  34. package/src/core/stable-host.ts +171 -0
  35. package/src/core/stream-event-wiring.ts +29 -6
  36. package/src/core/system-message-noise.ts +1 -1
  37. package/src/core/system-message-router.ts +6 -6
  38. package/src/core/turn-budget-outcome.ts +60 -0
  39. package/src/core/turn-cancellation.ts +40 -1
  40. package/src/core/turn-event-wiring.ts +14 -8
  41. package/src/daemon/cli.ts +64 -58
  42. package/src/daemon/lifecycle.ts +41 -0
  43. package/src/daemon/service-commands.ts +5 -5
  44. package/src/export/cost-utils.ts +128 -6
  45. package/src/input/autocomplete.ts +2 -2
  46. package/src/input/command-registry.ts +55 -9
  47. package/src/input/commands/acp-runtime.ts +72 -0
  48. package/src/input/commands/ci-runtime.ts +5 -1
  49. package/src/input/commands/codebase-runtime.ts +6 -6
  50. package/src/input/commands/cost-runtime.ts +1 -1
  51. package/src/input/commands/devices-runtime.ts +109 -0
  52. package/src/input/commands/git-runtime.ts +28 -4
  53. package/src/input/commands/graph-runtime.ts +45 -0
  54. package/src/input/commands/health-runtime.ts +2 -2
  55. package/src/input/commands/intelligence-runtime.ts +1 -1
  56. package/src/input/commands/local-auth-runtime.ts +1 -1
  57. package/src/input/commands/local-provider-runtime.ts +49 -10
  58. package/src/input/commands/local-runtime.ts +53 -5
  59. package/src/input/commands/memory.ts +2 -2
  60. package/src/input/commands/operator-panel-runtime.ts +5 -5
  61. package/src/input/commands/operator-rpc.ts +32 -11
  62. package/src/input/commands/permissions-provenance.ts +1 -1
  63. package/src/input/commands/permissions-runtime.ts +99 -8
  64. package/src/input/commands/planning-runtime.ts +3 -3
  65. package/src/input/commands/platform-sandbox-runtime.ts +1 -1
  66. package/src/input/commands/power-runtime.ts +56 -0
  67. package/src/input/commands/provider-accounts-runtime.ts +71 -6
  68. package/src/input/commands/provider.ts +7 -5
  69. package/src/input/commands/qrcode-runtime.ts +14 -19
  70. package/src/input/commands/queue-runtime.ts +70 -0
  71. package/src/input/commands/recall-files-git.ts +13 -51
  72. package/src/input/commands/recall-files.ts +8 -11
  73. package/src/input/commands/recall-review.ts +2 -2
  74. package/src/input/commands/relay-runtime.ts +1 -1
  75. package/src/input/commands/remote-runtime.ts +1 -1
  76. package/src/input/commands/runtime-services.ts +4 -4
  77. package/src/input/commands/schedule-runtime.ts +14 -7
  78. package/src/input/commands/services-runtime.ts +1 -1
  79. package/src/input/commands/session-workflow.ts +2 -2
  80. package/src/input/commands/settings-sync-runtime.ts +2 -2
  81. package/src/input/commands/share-runtime.ts +1 -1
  82. package/src/input/commands/shell-core.ts +46 -68
  83. package/src/input/commands/subscription-runtime.ts +1 -1
  84. package/src/input/commands/update-runtime.ts +153 -96
  85. package/src/input/commands/work-plan-runtime.ts +1 -1
  86. package/src/input/commands/workstream-attempts.ts +6 -1
  87. package/src/input/commands/workstream-runtime.ts +46 -27
  88. package/src/input/commands/worktree-runtime.ts +35 -8
  89. package/src/input/commands.ts +10 -2
  90. package/src/input/config-modal-types.ts +14 -3
  91. package/src/input/config-modal.ts +75 -13
  92. package/src/input/feature-unit-layout.ts +93 -82
  93. package/src/input/feed-context-factory.ts +2 -2
  94. package/src/input/handler-command-route.ts +2 -2
  95. package/src/input/handler-content-actions.ts +1 -1
  96. package/src/input/handler-feed-routes.ts +7 -7
  97. package/src/input/handler-feed.ts +4 -4
  98. package/src/input/handler-interactions.ts +3 -2
  99. package/src/input/handler-modal-routes.ts +12 -4
  100. package/src/input/handler-modal-token-routes.ts +1 -1
  101. package/src/input/handler-onboarding-daemon-adopt.ts +2 -2
  102. package/src/input/handler-onboarding.ts +3 -2
  103. package/src/input/handler-picker-routes.ts +26 -13
  104. package/src/input/handler-shortcuts.ts +29 -6
  105. package/src/input/handler.ts +2 -2
  106. package/src/input/keybindings.ts +21 -3
  107. package/src/input/onboarding/onboarding-feature-units.ts +34 -27
  108. package/src/input/onboarding/onboarding-provider-key-fields.ts +112 -0
  109. package/src/input/onboarding/onboarding-wizard-apply.ts +22 -21
  110. package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +17 -0
  111. package/src/input/onboarding/onboarding-wizard-network-adopt.ts +2 -2
  112. package/src/input/onboarding/onboarding-wizard-steps.ts +30 -42
  113. package/src/input/panel-integration-actions.ts +2 -2
  114. package/src/input/panel-mouse-geometry.ts +1 -1
  115. package/src/input/panel-paste-flood-guard.ts +1 -1
  116. package/src/input/provider-key-intake.ts +167 -0
  117. package/src/input/sandbox-exec-config.ts +2 -2
  118. package/src/input/selection-modal.ts +1 -1
  119. package/src/input/settings-modal-activation.ts +5 -0
  120. package/src/input/settings-modal-adjustment.ts +4 -31
  121. package/src/input/settings-modal-data.ts +52 -57
  122. package/src/input/settings-modal-mutations.ts +12 -68
  123. package/src/input/settings-modal-types.ts +27 -17
  124. package/src/input/settings-modal.ts +74 -55
  125. package/src/main.ts +90 -90
  126. package/src/panels/agent-inspector-shared.ts +4 -4
  127. package/src/panels/base-panel.ts +2 -2
  128. package/src/panels/builtin/agent.ts +4 -4
  129. package/src/panels/builtin/development.ts +4 -4
  130. package/src/panels/builtin/knowledge.ts +1 -1
  131. package/src/panels/builtin/operations.ts +83 -11
  132. package/src/panels/builtin/session.ts +5 -5
  133. package/src/panels/builtin/shared.ts +16 -4
  134. package/src/panels/builtin-modals.ts +59 -42
  135. package/src/panels/builtin-panels.ts +1 -1
  136. package/src/panels/cost-tracker-panel.ts +124 -7
  137. package/src/panels/diff-panel.ts +39 -6
  138. package/src/panels/eval-registry.ts +2 -2
  139. package/src/panels/fleet-acts.ts +446 -0
  140. package/src/panels/fleet-deep-link.ts +1 -1
  141. package/src/panels/fleet-gateway.ts +117 -0
  142. package/src/panels/fleet-observed-render.ts +128 -0
  143. package/src/panels/fleet-panel-format.ts +163 -11
  144. package/src/panels/fleet-panel-worktree-detail.ts +14 -0
  145. package/src/panels/fleet-panel.ts +73 -57
  146. package/src/panels/fleet-read-model.ts +84 -14
  147. package/src/panels/fleet-spawn.ts +266 -0
  148. package/src/panels/fleet-steer.ts +2 -2
  149. package/src/panels/fleet-stop.ts +41 -5
  150. package/src/panels/fleet-tabs.ts +15 -16
  151. package/src/panels/fleet-transcript.ts +5 -5
  152. package/src/panels/git-panel.ts +2 -2
  153. package/src/panels/index.ts +4 -1
  154. package/src/panels/local-auth-panel.ts +1 -1
  155. package/src/panels/memory-consolidation-gateway.ts +94 -0
  156. package/src/panels/modals/devices-modal.ts +161 -0
  157. package/src/panels/modals/hooks-modal.ts +1 -1
  158. package/src/panels/modals/keybindings-modal.ts +2 -2
  159. package/src/panels/modals/knowledge-modal.ts +1 -1
  160. package/src/panels/modals/marketplace-modal.ts +4 -4
  161. package/src/panels/modals/memory-modal.ts +153 -9
  162. package/src/panels/modals/modal-surface-helpers.ts +1 -1
  163. package/src/panels/modals/modal-theme.ts +1 -1
  164. package/src/panels/modals/pairing-modal.ts +198 -68
  165. package/src/panels/modals/planning-modal.ts +2 -2
  166. package/src/panels/modals/plugins-modal.ts +1 -1
  167. package/src/panels/modals/policy-modal.ts +1 -1
  168. package/src/panels/modals/security-modal.ts +7 -1
  169. package/src/panels/modals/skills-modal.ts +1 -1
  170. package/src/panels/modals/work-plan-modal.ts +3 -3
  171. package/src/panels/notifications-feed.ts +127 -0
  172. package/src/panels/notifications-panel.ts +160 -0
  173. package/src/panels/panel-manager.ts +9 -9
  174. package/src/panels/plugins-panel.ts +4 -4
  175. package/src/panels/project-planning-answer-actions.ts +1 -1
  176. package/src/panels/provider-health-routes.ts +2 -2
  177. package/src/panels/skills-panel.ts +4 -5
  178. package/src/panels/token-budget-panel.ts +11 -3
  179. package/src/panels/types.ts +5 -5
  180. package/src/panels/workstream-graph-render.ts +123 -0
  181. package/src/permissions/broker-approval-card.ts +201 -0
  182. package/src/permissions/hunk-selection.ts +28 -4
  183. package/src/permissions/prompt.ts +244 -29
  184. package/src/renderer/autocomplete-overlay.ts +107 -54
  185. package/src/renderer/code-block.ts +2 -2
  186. package/src/renderer/compaction-history-modal.ts +1 -1
  187. package/src/renderer/compaction-preview.ts +1 -1
  188. package/src/renderer/compaction-quality.ts +1 -1
  189. package/src/renderer/composer-fragments.ts +69 -0
  190. package/src/renderer/compositor.ts +2 -2
  191. package/src/renderer/config-modal.ts +1 -1
  192. package/src/renderer/conversation-overlays.ts +1 -1
  193. package/src/renderer/footer-tips.ts +3 -3
  194. package/src/renderer/hint-grammar.ts +1 -1
  195. package/src/renderer/modal-factory.ts +1 -1
  196. package/src/renderer/model-picker-overlay.ts +1 -1
  197. package/src/renderer/model-workspace.ts +7 -2
  198. package/src/renderer/panel-composite.ts +2 -2
  199. package/src/renderer/panel-workspace-bar.ts +1 -1
  200. package/src/renderer/process-indicator.ts +1 -1
  201. package/src/renderer/selection-modal-overlay.ts +1 -1
  202. package/src/renderer/session-picker-modal.ts +2 -2
  203. package/src/renderer/settings-modal-helpers.ts +23 -5
  204. package/src/renderer/settings-modal.ts +129 -39
  205. package/src/renderer/shell-surface.ts +6 -0
  206. package/src/renderer/terminal-bg-probe.ts +1 -1
  207. package/src/renderer/theme.ts +3 -3
  208. package/src/renderer/tool-call.ts +13 -7
  209. package/src/renderer/tool-result-summary.ts +1 -1
  210. package/src/renderer/turn-injection.ts +7 -6
  211. package/src/renderer/ui-factory.ts +38 -1
  212. package/src/runtime/bootstrap-command-context.ts +7 -2
  213. package/src/runtime/bootstrap-command-parts.ts +9 -5
  214. package/src/runtime/bootstrap-core.ts +41 -41
  215. package/src/runtime/bootstrap-hook-bridge.ts +1 -1
  216. package/src/runtime/bootstrap-shell.ts +13 -5
  217. package/src/runtime/bootstrap.ts +34 -32
  218. package/src/runtime/channel-composition.ts +46 -0
  219. package/src/runtime/code-index-services.ts +3 -4
  220. package/src/runtime/daemon-attach-notices.ts +120 -0
  221. package/src/runtime/durability-services.ts +90 -0
  222. package/src/runtime/embedded-service-factories.ts +40 -0
  223. package/src/runtime/feature-settings.ts +147 -0
  224. package/src/runtime/fleet-services.ts +41 -0
  225. package/src/runtime/idle-power-services.ts +85 -0
  226. package/src/runtime/index.ts +6 -21
  227. package/src/runtime/interaction-seams.ts +109 -0
  228. package/src/runtime/legacy-daemon-migration.ts +29 -19
  229. package/src/runtime/memory-fold.ts +1 -1
  230. package/src/runtime/notification-dispatch.ts +110 -0
  231. package/src/runtime/onboarding/apply.ts +3 -29
  232. package/src/runtime/onboarding/fast-path.ts +39 -6
  233. package/src/runtime/onboarding/provider-key-capture.ts +126 -0
  234. package/src/runtime/onboarding/snapshot.ts +0 -1
  235. package/src/runtime/onboarding/types.ts +9 -3
  236. package/src/runtime/power-keepawake-remote.ts +75 -0
  237. package/src/runtime/process-lifecycle.ts +3 -1
  238. package/src/runtime/provider-fallback.ts +55 -0
  239. package/src/runtime/provider-optimizer-wiring.ts +55 -0
  240. package/src/runtime/release-artifacts.ts +19 -103
  241. package/src/runtime/resume-notice.ts +1 -1
  242. package/src/runtime/services.ts +113 -113
  243. package/src/runtime/session-continuity-hints.ts +38 -0
  244. package/src/runtime/surface-feature-flags.ts +40 -9
  245. package/src/runtime/terminal-output-guard.ts +2 -2
  246. package/src/runtime/trust/workspace-registration.ts +10 -2
  247. package/src/runtime/trust/workspace-trust.ts +58 -31
  248. package/src/runtime/ui-services.ts +6 -1
  249. package/src/runtime/update-check.ts +15 -69
  250. package/src/runtime/workstream-services.ts +2 -2
  251. package/src/shell/blocking-input.ts +108 -132
  252. package/src/shell/recovery-input-helpers.ts +53 -166
  253. package/src/shell/ui-openers.ts +38 -6
  254. package/src/utils/format-duration.ts +1 -1
  255. package/src/verification/verification-ledger.ts +21 -4
  256. package/src/version.ts +1 -1
  257. package/src/input/commands/flags-runtime.ts +0 -328
@@ -1,57 +1,19 @@
1
1
  /**
2
- * recall-files-git.ts — the synchronous git seam for memory file projection.
2
+ * recall-files-git.ts — the git seam for memory file projection.
3
3
  *
4
- * `projectMemoryToFiles(records, dir, { git })` (SDK, platform/state) is a
5
- * pure synchronous function and its `MemoryProjectionGit` seam is declared
6
- * synchronous (`add(dir): void`, `commit(dir, message): void`) — it throws on
7
- * failure rather than returning a result. The TUI's existing git wrapper,
8
- * `GitService` (platform/git), is async (wraps simple-git), so it cannot
9
- * satisfy this seam directly. `GitService`'s own static helpers
10
- * (initRepo/isGitRepo/getRepoRoot) ARE synchronous, using `Bun.spawnSync`
11
- * directly (see service.ts) — this file follows the exact same convention
12
- * for `add`/`commit` so the seam matches the SDK's synchronous contract.
4
+ * The SDK owns the production seam now (`createMemoryProjectionGit`,
5
+ * platform/state): it shells out to the system git, commits with a neutral
6
+ * projection identity (never the operator's), tolerates an unchanged tree,
7
+ * and — through `projectMemoryToFiles`'s ownership gate — only ever commits
8
+ * to a repository whose toplevel IS the projection directory, initializing
9
+ * one there when needed. A projection directory nested inside some other
10
+ * checkout can never pollute that checkout. This module is a thin local
11
+ * seam factory so command code keeps a single import site.
13
12
  */
13
+ import { createMemoryProjectionGit } from '@pellux/goodvibes-sdk/platform/state';
14
14
  import type { MemoryProjectionGit } from '@pellux/goodvibes-sdk/platform/state';
15
- import { GitService } from '@pellux/goodvibes-sdk/platform/git';
16
15
 
17
- function decode(bytes: Uint8Array | undefined): string {
18
- return bytes ? new TextDecoder().decode(bytes).trim() : '';
19
- }
20
-
21
- /**
22
- * Build a synchronous git seam rooted at `repoRoot`. `commit()` treats a
23
- * "nothing to commit" exit as a benign no-op (not an error) so
24
- * `/recall files sync` stays safely re-runnable when the projection files
25
- * did not actually change since the last sync — any OTHER commit failure
26
- * (no git identity configured, hook rejection, etc.) still throws, since the
27
- * seam's contract has no way to report partial failure back to the caller
28
- * other than throwing.
29
- */
30
- export function createSyncGitSeam(repoRoot: string): MemoryProjectionGit {
31
- return {
32
- add(dir: string): void {
33
- const result = Bun.spawnSync(['git', 'add', dir], { cwd: repoRoot });
34
- if (result.exitCode !== 0) {
35
- throw new Error(`git add failed: ${decode(result.stderr) || 'unknown error'}`);
36
- }
37
- },
38
- commit(dir: string, message: string): void {
39
- const result = Bun.spawnSync(['git', 'commit', '-m', message, '--', dir], { cwd: repoRoot });
40
- if (result.exitCode === 0) return;
41
- const output = `${decode(result.stdout)} ${decode(result.stderr)}`;
42
- if (/nothing to commit/i.test(output)) return; // benign — no new/changed files this run
43
- throw new Error(`git commit failed: ${output.trim() || 'unknown error'}`);
44
- },
45
- };
46
- }
47
-
48
- /**
49
- * Resolve the git seam for `dir`, or null when `dir` is not inside a git
50
- * repository. Never auto-initializes a repository — that is a much bigger
51
- * side effect than a memory sync command should take unasked.
52
- */
53
- export function resolveSyncGitSeam(dir: string): { seam: MemoryProjectionGit; repoRoot: string } | null {
54
- const repoRoot = GitService.getRepoRoot(dir);
55
- if (!repoRoot) return null;
56
- return { seam: createSyncGitSeam(repoRoot), repoRoot };
16
+ /** The projection git seam (see module note — SDK-owned behavior). */
17
+ export function createSyncGitSeam(): MemoryProjectionGit {
18
+ return createMemoryProjectionGit();
57
19
  }
@@ -39,7 +39,7 @@ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
39
39
  import { requireShellPaths } from './runtime-services.ts';
40
40
  import { getMemorySpine } from './recall-query.ts';
41
41
  import { readMemoryProjectionDir } from './recall-files-config.ts';
42
- import { resolveSyncGitSeam } from './recall-files-git.ts';
42
+ import { createSyncGitSeam } from './recall-files-git.ts';
43
43
 
44
44
  const PROJECTION_LIST_LIMIT = 5000;
45
45
 
@@ -58,19 +58,16 @@ export async function handleRecallFilesSync(args: string[], context: CommandCont
58
58
 
59
59
  const dir = resolveProjectionDir(args, context);
60
60
  const records = await memory.list({ limit: PROJECTION_LIST_LIMIT });
61
- // Resolve the git seam from the project working directory, not `dir` itself
62
- // — `dir` may not exist yet on a first sync, and `git -C <dir>` requires the
63
- // directory to already exist. The working directory is always real.
64
- const gitSeam = resolveSyncGitSeam(requireShellPaths(context).workingDirectory);
65
61
 
66
62
  try {
67
- const report = projectMemoryToFiles(records, dir, gitSeam ? { git: gitSeam.seam } : {});
63
+ // The SDK's ownership gate commits only to a repository whose toplevel IS
64
+ // the projection directory (initializing one there when needed) — never
65
+ // to a checkout the directory merely sits inside.
66
+ const report = projectMemoryToFiles(records, dir, { git: createSyncGitSeam() });
68
67
  context.print(`[recall] Projected ${report.written.length} record(s) to ${report.dir}`);
69
- if (gitSeam) {
70
- context.print(report.committed ? ` committed in ${gitSeam.repoRoot}` : ' not committed (git seam did not run)');
71
- } else {
72
- context.print(` not committed: ${dir} is not inside a git repository`);
73
- }
68
+ context.print(report.committed
69
+ ? ` committed in ${report.dir} (the projection's own repository)`
70
+ : ' not committed (git seam did not run)');
74
71
  } catch (error) {
75
72
  context.print(`[recall] Projection sync failed: ${summarizeError(error)}`);
76
73
  }
@@ -112,9 +112,9 @@ export function handleRecallExplain(args: string[], context: CommandContext): vo
112
112
  * `exportState()` returns the same full `AgentRecord[]` (it's what
113
113
  * compactConversation() already uses for the same reason).
114
114
  *
115
- * With NO agent id (wo805 wiring), renders the MAIN interactive session's own
115
+ * With NO agent id (wiring), renders the MAIN interactive session's own
116
116
  * per-turn injection ring — `Orchestrator.getTurnInjections()`, threaded onto
117
- * the command context as `session.getMainSessionTurnInjections`. As of wo805 the
117
+ * the command context as `session.getMainSessionTurnInjections`. As of the
118
118
  * main session DOES route through the passive-injection engine, so this is the
119
119
  * honest main-session default (previously there was no record source, so the
120
120
  * no-id path could only print a usage hint). When the accessor isn't wired
@@ -47,7 +47,7 @@ async function renderStatus(ctx: CommandContext): Promise<void> {
47
47
  async function renderPair(ctx: CommandContext): Promise<void> {
48
48
  const cfg = relayConfigured(ctx);
49
49
  if (!cfg.enabled || !cfg.flagOn) {
50
- ctx.print('Relay is disabled — enable relay.enabled and the relay-connect feature flag first (see /config relay, /flags).');
50
+ ctx.print('Relay is disabled — turn on relay.enabled in /config relay first.');
51
51
  return;
52
52
  }
53
53
  const externalServices = ctx.platform.externalServices;
@@ -48,7 +48,7 @@ export function registerRemoteRuntimeCommands(registry: CommandRegistry): void {
48
48
  async handler(args, ctx) {
49
49
  if (args.length === 0) {
50
50
  if (ctx.openModal) {
51
- ctx.openModal('remote-modal'); // W6.1: remote panel -> config modal
51
+ ctx.openModal('remote-modal'); // remote panel -> config modal
52
52
  return;
53
53
  }
54
54
  ctx.print('Remote surface is not available in this runtime.');
@@ -104,8 +104,8 @@ export function openCommandPanel(
104
104
  }
105
105
 
106
106
  /**
107
- * W6.1 (group-B migration): open a config-modal surface by name via the
108
- * ctx.openModal seam (ui-openers wires it; the WO-A host replaces the interim
107
+ * (group-B migration): open a config-modal surface by name via the
108
+ * ctx.openModal seam (ui-openers wires it; the host replaces the interim
109
109
  * "not available yet" implementation with real dispatch). Front-doors for
110
110
  * panels that migrated to modals call this instead of openCommandPanel — the
111
111
  * modal is the surface's new home. Stubbed in tests by setting ctx.openModal.
@@ -256,7 +256,7 @@ export function requireProviderApi(context: CommandContext): ProviderApi {
256
256
  }
257
257
 
258
258
  /** Result of a manual /compact run: the SDK's CompactionEvent, plus an
259
- * out-of-band quality score (W5.4/B28) when one could be computed. */
259
+ * out-of-band quality score () when one could be computed. */
260
260
  export interface CompactionRunResult {
261
261
  readonly event: CompactionEvent;
262
262
  readonly qualityScore: CompactionQualityScore | null;
@@ -328,7 +328,7 @@ export async function compactConversation(context: CommandContext): Promise<Comp
328
328
  const eventAfter = getLastCompactionEvent();
329
329
  // Return the new event only if it differs from the one recorded before the call.
330
330
  if (eventAfter !== null && eventAfter !== eventBefore) {
331
- // W5.4/B28: score this run out-of-band (see compaction-quality.ts for why
331
+ //: score this run out-of-band (see compaction-quality.ts for why
332
332
  // this can't just subscribe to the SDK's own CompactionManager pipeline)
333
333
  // and keep the score keyed by this event's timestamp for /compact-history.
334
334
  const qualityScore = scoreCompactionRun({
@@ -10,6 +10,7 @@ import type { AutomationManager } from '@pellux/goodvibes-sdk/platform/automatio
10
10
  import type { AutomationJob } from '@pellux/goodvibes-sdk/platform/automation';
11
11
  import type { AutomationScheduleDefinition } from '@pellux/goodvibes-sdk/platform/automation';
12
12
  import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
13
+ import { buildAutomationEmptyState } from '@pellux/goodvibes-sdk/platform/runtime/feature-announcements';
13
14
  import { parseNaturalLanguageSchedule } from './schedule-nl.ts';
14
15
  import type {
15
16
  AutomationExecutionPolicy,
@@ -106,13 +107,19 @@ export function registerScheduleRuntimeCommands(registry: CommandRegistry): void
106
107
  if (!sub || sub === 'list') {
107
108
  const jobs = manager.listJobs();
108
109
  if (jobs.length === 0) {
109
- ctx.print(
110
- 'No automation jobs.\n'
111
- + 'Use:\n'
112
- + ' /schedule add cron "*/30 * * * *" "check build status"\n'
113
- + ' /schedule add every 15m "summarize open PRs"\n'
114
- + ' /schedule add at 2026-04-10T09:00:00 "send release reminder"'
115
- );
110
+ // The SDK owns the presence + title of the how-to-create-your-first-
111
+ // routine empty state (shown while automation is on and idle). Its
112
+ // body points at a command name this registry doesn't have, so we
113
+ // render its title with THIS surface's real /schedule commands — a
114
+ // command pointer that actually exists in the registry.
115
+ const emptyState = buildAutomationEmptyState({ enabled: true, routineCount: jobs.length });
116
+ ctx.print([
117
+ emptyState ? emptyState.title : 'No automation jobs.',
118
+ 'Automation is on and idle. Create your first routine with:',
119
+ ' /schedule add cron "*/30 * * * *" "check build status"',
120
+ ' /schedule add every 15m "summarize open PRs"',
121
+ ' /schedule add at 2026-04-10T09:00:00 "send release reminder"',
122
+ ].join('\n'));
116
123
  return;
117
124
  }
118
125
  const lines = ['Automation jobs:', ''];
@@ -16,7 +16,7 @@ export function registerServicesRuntimeCommands(registry: CommandRegistry): void
16
16
  const sub = args[0] ?? 'open';
17
17
  const shellPaths = requireShellPaths(ctx);
18
18
  if (sub === 'open' || sub === 'panel') {
19
- ctx.openModal?.('services-modal'); // W6.1: services panel -> config modal
19
+ ctx.openModal?.('services-modal'); // services panel -> config modal
20
20
  return;
21
21
  }
22
22
  const svcRegistry = requireServiceRegistry(ctx);
@@ -89,7 +89,7 @@ export function reopenPanelsFromReturnContext(ctx: CommandContext, summary: Sess
89
89
  const reopened: string[] = [];
90
90
  const movedToModal: string[] = [];
91
91
  for (const panelId of summary.openPanels.slice(0, 4)) {
92
- // W6.1 (the purge): a MIGRATE-TO-MODAL id has no panel to restore — a modal
92
+ // (the purge): a MIGRATE-TO-MODAL id has no panel to restore — a modal
93
93
  // is not part of the saved panel layout. Skip it (don't pop a modal
94
94
  // mid-resume) and note it once, rather than firing openModal + revealing an
95
95
  // empty workspace during resume.
@@ -283,7 +283,7 @@ export async function handleSessionWorkflowCommand(args: string[], ctx: CommandC
283
283
  },
284
284
  });
285
285
  // Hydrate the footer's token counters from the resumed (+ journal-replayed)
286
- // history now, before ctx.renderRequest() below (W0.9).
286
+ // history now, before ctx.renderRequest() below.
287
287
  ctx.session.hydrateSessionUsage?.();
288
288
 
289
289
  if (meta.model) {
@@ -32,7 +32,7 @@ export function registerSettingsSyncRuntimeCommands(registry: CommandRegistry):
32
32
  const shellPaths = requireShellPaths(ctx);
33
33
  const controlPlaneConfigDir = ctx.platform.configManager.getControlPlaneConfigDir();
34
34
  const sub = (args[0] ?? '').toLowerCase();
35
- // DEBT-5 item 3 (report-vs-modal front doors): settings-sync has a full
35
+ // item 3 (report-vs-modal front doors): settings-sync has a full
36
36
  // modal surface (settings-sync-modal), so the bare command now opens it
37
37
  // — the old bare/`review` transcript report moved to an explicit
38
38
  // `report` subcommand (scriptability preserved: /settings-sync report).
@@ -41,7 +41,7 @@ export function registerSettingsSyncRuntimeCommands(registry: CommandRegistry):
41
41
  return;
42
42
  }
43
43
  if (sub === 'panel' || sub === 'open') {
44
- ctx.openModal?.('settings-sync-modal'); // W6.1: settings-sync panel -> config modal
44
+ ctx.openModal?.('settings-sync-modal'); // settings-sync panel -> config modal
45
45
  return;
46
46
  }
47
47
  if (sub === 'report' || sub === 'review') {
@@ -115,7 +115,7 @@ export function registerShareRuntimeCommands(registry: CommandRegistry): void {
115
115
  );
116
116
  // The exported document has no field for "this cost is a placeholder" —
117
117
  // omit `cost` entirely rather than embed a misleading 0 when the model
118
- // never resolved to a real price (WO-315).
118
+ // never resolved to a real price.
119
119
  const costIsUnpriced = !isModelPriced(sessionModel);
120
120
  if (costIsUnpriced) {
121
121
  ctx.print(`Note: cost omitted from export — no pricing data for model "${sessionModel}".`);
@@ -6,6 +6,7 @@ import { executeWriteQuit } from './quit-shared.ts';
6
6
  import { compactConversation, requireKeybindingsManager, requireProviderApi, requireSessionMemoryStore } from './runtime-services.ts';
7
7
  import { buildCompactionPreview, buildCompactionAfterNotice, buildPinUsageText, buildPinSuccessText } from '../../renderer/compaction-preview.ts';
8
8
  import { buildCompactionHistoryText } from '../../renderer/compaction-history-modal.ts';
9
+ import { ensureProviderKeyThenSelect } from '../provider-key-intake.ts';
9
10
  import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
10
11
  import { logger } from '@pellux/goodvibes-sdk/platform/utils';
11
12
 
@@ -32,11 +33,15 @@ export function registerShellCoreCommands(registry: CommandRegistry): void {
32
33
  const modelId = args[0];
33
34
  try {
34
35
  const selected = await providerApi.selectModel(modelId);
35
- ctx.session.runtime.model = selected.registryKey;
36
- ctx.session.runtime.provider = selected.providerId;
37
- ctx.platform.configManager.set('provider.model', selected.registryKey);
38
- ctx.print(`Switched to model: ${selected.displayName} (${selected.providerId})`);
39
- void providerApi.recordModelUsage(selected.registryKey).catch((err) => { logger.debug('model usage record failed', { err }); });
36
+ // Capture the key for an unconfigured provider inline before the
37
+ // selection is announced, so the switch lands ready to chat.
38
+ ensureProviderKeyThenSelect(ctx, selected.providerId, () => {
39
+ ctx.session.runtime.model = selected.registryKey;
40
+ ctx.session.runtime.provider = selected.providerId;
41
+ ctx.platform.configManager.set('provider.model', selected.registryKey);
42
+ ctx.print(`Switched to model: ${selected.displayName} (${selected.providerId})`);
43
+ void providerApi.recordModelUsage(selected.registryKey).catch((err) => { logger.debug('model usage record failed', { err }); });
44
+ });
40
45
  } catch (e) {
41
46
  ctx.print(`Error: ${summarizeError(e)}`);
42
47
  }
@@ -112,75 +117,48 @@ export function registerShellCoreCommands(registry: CommandRegistry): void {
112
117
  registry.register({
113
118
  name: 'help',
114
119
  aliases: ['h', '?'],
115
- description: 'Show available commands and keyboard shortcuts',
116
- argsHint: '[command]',
120
+ description: 'Browse every command with its description; picking one runs it',
117
121
  handler(_args, ctx) {
122
+ // Generated live from the command registry — never a hand-maintained
123
+ // list — so /help can never drift from the real command set. Category
124
+ // labels come from the same single source of truth as the generated
125
+ // reference and the palette (ctx.getCommandCategories; see its doc for
126
+ // why it is threaded through the context rather than imported here).
127
+ const commands = registry
128
+ .getAll()
129
+ .slice()
130
+ .sort((a, b) => a.name.localeCompare(b.name));
118
131
  if (ctx.openSelection) {
119
- const items: SelectionItem[] = [
120
- { id: '/model', label: '/model [id]', detail: 'Select LLM model', category: 'Model & Provider' },
121
- { id: '/provider', label: '/provider [name]', detail: 'Switch provider', category: 'Model & Provider' },
122
- { id: '/effort', label: '/effort [level]', detail: 'Reasoning effort (instant/low/medium/high)', category: 'Model & Provider' },
123
- { id: '/context window', label: '/context window [<size>|clear]', detail: 'Show, set, or clear a custom context window for the current model', category: 'Model & Provider' },
124
- { id: '/config', label: '/config [category|key]', detail: 'Open fullscreen configuration workspace', category: 'Config & Display' },
125
- { id: '/debug', label: '/debug', detail: 'Toggle debug mode', category: 'Config & Display' },
126
- { id: '/expand', label: '/expand [type]', detail: 'Expand blocks (all|thinking|tool|code)', category: 'Config & Display' },
127
- { id: '/collapse', label: '/collapse [type]', detail: 'Collapse blocks', category: 'Config & Display' },
128
- { id: '/bookmarks', label: '/bookmarks', detail: 'List bookmarked blocks', category: 'Config & Display' },
129
- { id: '/clear', label: '/clear', detail: 'Clear display (keep context)', category: 'Conversation' },
130
- { id: '/reset', label: '/reset', detail: 'Clear display + context', category: 'Conversation' },
131
- { id: '/compact', label: '/compact', detail: 'Summarize to free context', category: 'Conversation' },
132
- { id: '/export', label: '/export [file]', detail: 'Export as markdown', category: 'Conversation' },
133
- { id: '/title', label: '/title [text]', detail: 'Show or set title', category: 'Conversation' },
134
- { id: '/save', label: '/save [name]', detail: 'Save session', category: 'Conversation' },
135
- { id: '/load', label: '/load <name>', detail: 'Load session', category: 'Conversation' },
136
- { id: '/sessions', label: '/sessions', detail: 'List saved sessions', category: 'Conversation' },
137
- { id: '/session', label: '/session', detail: 'Current session info', category: 'Conversation' },
138
- { id: '/session list', label: '/session list', detail: 'List all sessions', category: 'Conversation' },
139
- { id: '/session rename', label: '/session rename <name>', detail: 'Rename current session', category: 'Conversation' },
140
- { id: '/resume', label: '/resume [id]', detail: 'Resume a previous session (picker when no id)', category: 'Conversation' },
141
- { id: '/session resume', label: '/session resume <id>', detail: 'Load and resume a session', category: 'Conversation' },
142
- { id: '/session fork', label: '/session fork [name]', detail: 'Fork current session to new ID', category: 'Conversation' },
143
- { id: '/session save', label: '/session save [name]', detail: 'Force-save current session', category: 'Conversation' },
144
- { id: '/session info', label: '/session info [id]', detail: 'Show session details', category: 'Conversation' },
145
- { id: '/session export', label: '/session export <id> [format]', detail: 'Export session as markdown/text', category: 'Conversation' },
146
- { id: '/session search', label: '/session search <query>', detail: 'Search across all sessions', category: 'Conversation' },
147
- { id: '/session delete', label: '/session delete <id>', detail: 'Delete a session', category: 'Conversation' },
148
- { id: '/undo', label: '/undo', detail: 'Remove last turn', category: 'Conversation' },
149
- { id: '/redo', label: '/redo', detail: 'Restore undone turn', category: 'Conversation' },
150
- { id: '/retry', label: '/retry [text]', detail: 'Re-send last message', category: 'Conversation' },
151
- { id: '/fork', label: '/fork [name]', detail: 'Snapshot conversation as a named branch', category: 'Conversation' },
152
- { id: '/branch', label: '/branch [name]', detail: 'List branches or switch to one', category: 'Conversation' },
153
- { id: '/merge', label: '/merge <name>', detail: 'Append messages from a branch', category: 'Conversation' },
154
- { id: '/template', label: '/template', detail: 'Browse templates', category: 'Templates' },
155
- { id: '/template save', label: '/template save <name>', detail: 'Save prompt as template', category: 'Templates' },
156
- { id: '/template use', label: '/template use <name>', detail: 'Execute template', category: 'Templates' },
157
- { id: '/tools', label: '/tools', detail: 'List available tools', category: 'Tools & System' },
158
- { id: '/search', label: '/search <query> [--limit <n>]', detail: 'Search the web and show ranked results', category: 'Tools & System' },
159
- { id: '/imagine', label: '/imagine <prompt>', detail: 'Generate an image from a prompt', category: 'Tools & System' },
160
- { id: '/paste', label: '/paste', detail: 'Insert clipboard text or image into the prompt', category: 'Tools & System' },
161
- { id: '/shortcuts', label: '/shortcuts', detail: 'View keyboard shortcuts reference', category: 'Tools & System' },
162
- { id: '/commands', label: '/commands', detail: 'Browse all commands in a scrollable list', category: 'Tools & System' },
163
- { id: '/secrets', label: '/secrets set|link|get|test|list|delete', detail: 'Manage encrypted and provider-backed secrets', category: 'Tools & System' },
164
- { id: '/help', label: '/help', detail: 'This help', category: 'Tools & System' },
165
- { id: '/quit', label: '/quit', detail: 'Exit', category: 'Tools & System' },
166
- { id: '/wq', label: '/wq', detail: 'Commit all git changes and then exit', category: 'Tools & System' },
167
- ];
168
- // UX-C: every item here is a command, and picking one RUNS it — label
132
+ const categoryByName = ctx.getCommandCategories?.() ?? new Map<string, string>();
133
+ const items: SelectionItem[] = commands.map((cmd) => {
134
+ const argsHint = cmd.argsHint ?? cmd.usage;
135
+ const aliases = cmd.aliases ?? [];
136
+ return {
137
+ id: cmd.name,
138
+ label: argsHint ? `/${cmd.name} ${argsHint}` : `/${cmd.name}`,
139
+ detail: aliases.length > 0 ? `${cmd.description} [aka ${aliases.map((a) => `/${a}`).join(', ')}]` : cmd.description,
140
+ category: categoryByName.get(cmd.name) ?? 'Other',
141
+ };
142
+ });
143
+ // every item here is a command, and picking one RUNS it — label
169
144
  // the verb "Run" (matching the slash-command palette) instead of the
170
145
  // generic "Select" default.
171
146
  ctx.openSelection('Help — Commands', items, { allowSearch: true, primaryVerbLabel: 'Run' }, (result) => {
172
147
  if (!result) return;
173
- const command = result.item.id;
174
- if (command.startsWith('/')) {
175
- const parts = command.slice(1).trim().split(/\s+/);
176
- const name = parts[0];
177
- const cmdArgs = parts.slice(1);
178
- void (ctx.executeCommand?.(name, cmdArgs) ?? registry.execute(name, cmdArgs, ctx));
179
- }
148
+ const name = result.item.id;
149
+ void (ctx.executeCommand?.(name, []) ?? registry.execute(name, [], ctx));
180
150
  });
181
151
  return;
182
152
  }
183
- ctx.print('Use /help to open the help modal. Commands: /model, /provider, /config, /template, /tools, /paste, /sessions, /bookmarks, /save, /load, /undo, /redo, /retry, /clear, /reset, /compact, /export, /title, /effort, /expand, /collapse, /debug, /quit, /wq');
153
+ ctx.print(
154
+ [
155
+ 'Commands:',
156
+ ...commands.map((cmd) => {
157
+ const argsHint = cmd.argsHint ?? cmd.usage;
158
+ return ` /${cmd.name}${argsHint ? ` ${argsHint}` : ''} — ${cmd.description}`;
159
+ }),
160
+ ].join('\n'),
161
+ );
184
162
  },
185
163
  });
186
164
 
@@ -214,7 +192,7 @@ export function registerShellCoreCommands(registry: CommandRegistry): void {
214
192
  description: 'Summarize conversation to free context window',
215
193
  async handler(_args, ctx) {
216
194
  const messages = ctx.session.conversationManager.getMessagesForLLM();
217
- // W5.4 (wo803): contextWindow IS reachable from CommandContext — the same
195
+ // contextWindow IS reachable from CommandContext — the same
218
196
  // call compactConversation() (runtime-services.ts) already makes to scale
219
197
  // its own behaviour by window size. Previously hardcoded to 0 here, which
220
198
  // silently suppressed buildCompactionPreview()'s capacity-% clause even
@@ -230,7 +208,7 @@ export function registerShellCoreCommands(registry: CommandRegistry): void {
230
208
  const result = await compactConversation(ctx);
231
209
  if (result) {
232
210
  // Post-compact notice: uses real CompactionEvent figures, plus an
233
- // out-of-band quality-score grade line when one was computed (W5.4/B28).
211
+ // out-of-band quality-score grade line when one was computed ().
234
212
  ctx.print(buildCompactionAfterNotice({ event: result.event, pinnedMemoryCount, qualityScore: result.qualityScore }));
235
213
  } else {
236
214
  ctx.print('[Context] Compact complete.');
@@ -300,7 +278,7 @@ export function registerShellCoreCommands(registry: CommandRegistry): void {
300
278
  ctx.session.runtime.debugMode = !ctx.session.runtime.debugMode;
301
279
  ctx.print(`Debug mode: ${ctx.session.runtime.debugMode ? 'ON' : 'OFF'}`);
302
280
  // Honest quiet counter for direct terminal writes the guard intercepted
303
- // (routed here instead of spamming the transcript). (UX-B item 1a.)
281
+ // (routed here instead of spamming the transcript). (item 1a.)
304
282
  const intercepted = ctx.session.runtime.terminalWritesIntercepted ?? 0;
305
283
  if (intercepted > 0) {
306
284
  ctx.print(`Terminal writes intercepted this session: ${intercepted} (details in the activity log)`);
@@ -80,7 +80,7 @@ export function registerSubscriptionRuntimeCommands(registry: CommandRegistry):
80
80
  async handler(args, ctx) {
81
81
  const shellPaths = requireShellPaths(ctx);
82
82
  if (args.length === 0 && ctx.openModal) {
83
- ctx.openModal('subscription-modal'); // W6.1: subscription panel -> config modal
83
+ ctx.openModal('subscription-modal'); // subscription panel -> config modal
84
84
  return;
85
85
  }
86
86
  const sub = (args[0] ?? 'review').toLowerCase();