@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
@@ -5,7 +5,7 @@ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
5
5
 
6
6
  /**
7
7
  * Parse `--target <id>[:<kind>]` out of the args following `<panel-id>
8
- * [top|bottom]` (DEBT-5 item 4). Splits the args array in place (removing the
8
+ * [top|bottom]` (item 4). Splits the args array in place (removing the
9
9
  * flag + its value) so positional `pane` parsing downstream is unaffected by
10
10
  * where the flag appears. Only `open fleet` currently has a deep-link
11
11
  * consumer (FleetPanel.receiveDeepLink) — other panel ids just ignore an
@@ -32,7 +32,7 @@ export function registerOperatorPanelCommand(registry: CommandRegistry): void {
32
32
  const pm = requirePanelManager(ctx);
33
33
  const sub = args[0]?.toLowerCase() ?? '';
34
34
  if (!sub || sub === 'toggle') {
35
- // W6.1 (the purge): 'panel-list' (the browse-all-panels picker) was
35
+ // (the purge): 'panel-list' (the browse-all-panels picker) was
36
36
  // DELETE-disposition — a picker over a handful of panels is dead
37
37
  // weight now (see the Ctrl+P selectionModal repoint in
38
38
  // shell/ui-openers.ts for its interactive replacement). Bare
@@ -71,9 +71,9 @@ export function registerOperatorPanelCommand(registry: CommandRegistry): void {
71
71
  // no panel lands in the workspace. Report that honestly rather than
72
72
  // claiming "Panel opened: <id>".
73
73
  const redirectTarget = pm.getModalRedirect(id);
74
- // UX-C focus rule 1a: the command path leaves focus in the composer
74
+ // focus rule 1a: the command path leaves focus in the composer
75
75
  // ("mid-command-flow") — showPanel does not grab panel focus here.
76
- // DEBT-5: forward the deep-link target so the panel lands on the row.
76
+ // forward the deep-link target so the panel lands on the row.
77
77
  if (ctx.showPanel) ctx.showPanel(id, pane as 'top' | 'bottom' | undefined, target);
78
78
  else {
79
79
  pm.open(id, pane as 'top' | 'bottom' | undefined, target);
@@ -169,7 +169,7 @@ export function registerOperatorPanelCommand(registry: CommandRegistry): void {
169
169
  } else {
170
170
  const id = args[0]!;
171
171
  try {
172
- // UX-C: bare `/panel <id>` is the same command path as `/panel open
172
+ // bare `/panel <id>` is the same command path as `/panel open
173
173
  // <id>` — composer stays focused.
174
174
  if (ctx.showPanel) ctx.showPanel(id);
175
175
  else {
@@ -27,6 +27,7 @@ import { join } from 'node:path';
27
27
  import { createGoodVibesSdk } from '@pellux/goodvibes-sdk';
28
28
  import type { GoodVibesSdk } from '@pellux/goodvibes-sdk';
29
29
  import { GoodVibesSdkError } from '@pellux/goodvibes-sdk';
30
+ import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
30
31
  import { resolveDaemonEnabled } from '@pellux/goodvibes-sdk/platform/config';
31
32
  import { getOrCreateCompanionToken } from '@pellux/goodvibes-sdk/platform/pairing';
32
33
  import type { CommandContext } from '../command-registry.ts';
@@ -44,8 +45,7 @@ export interface OperatorRpcAvailable {
44
45
 
45
46
  export type OperatorRpc = OperatorRpcUnavailable | OperatorRpcAvailable;
46
47
 
47
- function resolveControlPlaneBaseUrl(context: CommandContext): string | null {
48
- const configManager = context.platform.configManager;
48
+ function resolveControlPlaneBaseUrl(configManager: ConfigManager): string | null {
49
49
  const explicit = String(configManager.get('controlPlane.baseUrl') ?? '').trim();
50
50
  if (explicit) return explicit;
51
51
  const host = String(configManager.get('controlPlane.host') ?? '').trim();
@@ -55,27 +55,48 @@ function resolveControlPlaneBaseUrl(context: CommandContext): string | null {
55
55
  }
56
56
 
57
57
  /**
58
- * Resolve (or honestly refuse to resolve) an operator SDK client wired to
59
- * this workspace's control-plane daemon. Reasons for refusal are surfaced
60
- * verbatim to the caller so `/ci`, `/checkin`, and `/principals` can print
61
- * them directly rather than inventing their own wording.
58
+ * Context-free core of {@link getOperatorRpc}: resolve (or honestly refuse to
59
+ * resolve) an operator SDK client wired to this workspace's control-plane
60
+ * daemon from just the two things the resolution actually needs — the config
61
+ * manager and the home directory. Shared by the command layer (via
62
+ * getOperatorRpc) and the Fleet panel's gateway (fleet-gateway.ts) so both
63
+ * reach the same daemon over the same generic invoke path, never two divergent
64
+ * clients. Refusal reasons are surfaced verbatim so callers print them directly.
62
65
  */
63
- export function getOperatorRpc(context: CommandContext): OperatorRpc {
64
- const configManager = context.platform.configManager;
66
+ export function resolveOperatorRpc(deps: { readonly configManager: ConfigManager; readonly homeDirectory: string | (() => string) }): OperatorRpc {
67
+ const { configManager } = deps;
65
68
  if (!resolveDaemonEnabled(configManager)) {
66
69
  return { available: false, reason: 'the daemon is disabled (daemon.enabled=false) — no operator surface to reach. Enable it in /settings, then retry.' };
67
70
  }
68
- const baseUrl = resolveControlPlaneBaseUrl(context);
71
+ const baseUrl = resolveControlPlaneBaseUrl(configManager);
69
72
  if (!baseUrl) {
70
73
  return { available: false, reason: 'no control-plane base URL is configured (controlPlane.baseUrl / controlPlane.host+port) — cannot reach the operator surface.' };
71
74
  }
72
- const shellPaths = requireShellPaths(context);
73
- const daemonHomeDir = join(shellPaths.homeDirectory, '.goodvibes', 'daemon');
75
+ // Resolve the home directory only AFTER the static refusals — a caller whose
76
+ // shell paths are not wired (a disabled-daemon path) must still get the honest
77
+ // unavailable reason above, never a "shell paths not wired" throw.
78
+ const homeDirectory = typeof deps.homeDirectory === 'function' ? deps.homeDirectory() : deps.homeDirectory;
79
+ const daemonHomeDir = join(homeDirectory, '.goodvibes', 'daemon');
74
80
  const token = getOrCreateCompanionToken('tui', { daemonHomeDir }).token;
75
81
  const sdk = createGoodVibesSdk({ baseUrl, authToken: token });
76
82
  return { available: true, sdk };
77
83
  }
78
84
 
85
+ /**
86
+ * Resolve (or honestly refuse to resolve) an operator SDK client wired to
87
+ * this workspace's control-plane daemon. Reasons for refusal are surfaced
88
+ * verbatim to the caller so `/ci`, `/checkin`, and `/principals` can print
89
+ * them directly rather than inventing their own wording.
90
+ */
91
+ export function getOperatorRpc(context: CommandContext): OperatorRpc {
92
+ return resolveOperatorRpc({
93
+ configManager: context.platform.configManager,
94
+ // Lazy: only resolved once the daemon-enabled + base-URL refusals pass, so a
95
+ // disabled-daemon context with no shell paths still returns the honest reason.
96
+ homeDirectory: () => requireShellPaths(context).homeDirectory,
97
+ });
98
+ }
99
+
79
100
  /**
80
101
  * Render a thrown operator-rpc error honestly: a 404 means this daemon has
81
102
  * not wired a handler for the verb (a live local runtime that simply doesn't
@@ -63,7 +63,7 @@ const PERMISSION_KEYS: readonly { readonly key: string; readonly label: string;
63
63
  { key: 'permissions.mode', label: 'Permission mode', note: 'also the current session mode' },
64
64
  { key: 'permissions.backgroundAgents', label: 'Background-agent mode' },
65
65
  { key: 'behavior.autoApprove', label: 'Auto-approve everything' },
66
- { key: 'featureFlags.permissions-policy-engine', label: 'Runtime policy engine (flag)' },
66
+ { key: 'permissions.engine', label: 'Permission evaluator', note: 'baseline or policy-engine' },
67
67
  { key: 'permissions.tools.read', label: 'Tool rule: read' },
68
68
  { key: 'permissions.tools.write', label: 'Tool rule: write' },
69
69
  { key: 'permissions.tools.edit', label: 'Tool rule: edit' },
@@ -1,21 +1,112 @@
1
- import type { CommandRegistry } from '../command-registry.ts';
1
+ import type { CommandContext, CommandRegistry } from '../command-registry.ts';
2
+ import type { SelectionItem, SelectionAction } from '../selection-modal.ts';
2
3
  import { buildPermissionProvenance, renderPermissionProvenance } from './permissions-provenance.ts';
4
+ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
5
+
6
+ /** One remembered-rule row: what it does, and how/when it was remembered. */
7
+ function describeRuleRow(record: {
8
+ rule: { id: string; description?: string | undefined; effect: 'allow' | 'deny' };
9
+ tier: string;
10
+ tool: string;
11
+ createdAt: number;
12
+ }): { label: string; detail: string } {
13
+ const what = record.rule.description ?? record.rule.id;
14
+ return {
15
+ label: `${record.tool}: ${what}`,
16
+ detail: `${record.rule.effect} · remembered ${record.tier} · ${new Date(record.createdAt).toLocaleString()}`,
17
+ };
18
+ }
19
+
20
+ function showRememberedRules(ctx: CommandContext): void {
21
+ const store = ctx.clients?.userPermissionRuleStore;
22
+ if (!store) {
23
+ ctx.print('Remembered permission rules are not available on this surface.');
24
+ return;
25
+ }
26
+ const records = store.list();
27
+ if (records.length === 0) {
28
+ ctx.print('No remembered permission rules. Approving an ask with a remember tier stores one here.');
29
+ return;
30
+ }
31
+ if (ctx.openSelection) {
32
+ const revoke = new Map<string, SelectionAction>([['d', 'delete']]);
33
+ const items: SelectionItem[] = records.map((record) => {
34
+ const { label, detail } = describeRuleRow(record);
35
+ return { id: record.rule.id, label, detail, actions: '[d] revoke' };
36
+ });
37
+ ctx.openSelection('Remembered permission rules', items, { allowSearch: true, customActions: revoke }, (result) => {
38
+ if (!result || result.action !== 'delete') return;
39
+ void (async () => {
40
+ try {
41
+ const removed = await store.delete(result.item.id);
42
+ ctx.print(removed ? `Revoked remembered rule: ${result.item.label}` : `Rule already gone: ${result.item.label}`);
43
+ } catch (error) {
44
+ ctx.print(`Could not revoke the rule: ${summarizeError(error)}`);
45
+ }
46
+ })();
47
+ });
48
+ return;
49
+ }
50
+ // No selection surface: list them honestly with the revoke command to run.
51
+ ctx.print([
52
+ 'Remembered permission rules:',
53
+ ...records.map((record) => {
54
+ const { label, detail } = describeRuleRow(record);
55
+ return ` ${record.rule.id} ${label} (${detail})`;
56
+ }),
57
+ '',
58
+ 'Revoke one with: /permissions revoke <rule-id>',
59
+ ].join('\n'));
60
+ }
61
+
62
+ async function revokeRememberedRule(ctx: CommandContext, ruleId: string): Promise<void> {
63
+ const store = ctx.clients?.userPermissionRuleStore;
64
+ if (!store) {
65
+ ctx.print('Remembered permission rules are not available on this surface.');
66
+ return;
67
+ }
68
+ if (!ruleId) {
69
+ ctx.print('Usage: /permissions revoke <rule-id> (see /permissions rules for ids)');
70
+ return;
71
+ }
72
+ try {
73
+ const removed = await store.delete(ruleId);
74
+ ctx.print(removed ? `Revoked remembered rule: ${ruleId}` : `No remembered rule with id: ${ruleId}`);
75
+ } catch (error) {
76
+ ctx.print(`Could not revoke the rule: ${summarizeError(error)}`);
77
+ }
78
+ }
3
79
 
4
80
  /**
5
- * `/permissions` — the permission provenance panel. Prints every
6
- * permission-relevant setting in effect and where each value came from
7
- * (default / config file / runtime override / session mode), plus the current
8
- * session mode. Read-only: it changes no config. See permissions-provenance.ts
9
- * for how origin is resolved from the platform's own ConfigManager.
81
+ * `/permissions` — the permission settings surface. With no arguments it prints
82
+ * every permission-relevant setting in effect and where each value came from
83
+ * (provenance; read-only). `rules` lists the durable remembered approvals with a
84
+ * press-Enter revoke; `revoke <id>` removes one by id. Revokes hit the same
85
+ * in-process rule store the evaluator reads, so they take effect live.
10
86
  */
11
87
  export function registerPermissionsRuntimeCommands(registry: CommandRegistry): void {
12
88
  registry.register({
13
89
  name: 'permissions',
14
90
  aliases: ['perms'],
15
- description: 'Show permission settings in effect and where each value came from (provenance)',
16
- handler(_args, ctx) {
91
+ description: 'Show permission settings and provenance, and list or revoke remembered approval rules',
92
+ usage: '[rules | revoke <rule-id>]',
93
+ argsHint: '[rules|revoke]',
94
+ async handler(args, ctx) {
95
+ const sub = (args[0] ?? '').toLowerCase();
96
+ if (sub === 'rules' || sub === 'rule') {
97
+ showRememberedRules(ctx);
98
+ return;
99
+ }
100
+ if (sub === 'revoke' || sub === 'forget') {
101
+ await revokeRememberedRule(ctx, args[1] ?? '');
102
+ return;
103
+ }
17
104
  const provenance = buildPermissionProvenance(ctx.platform.configManager);
18
105
  ctx.print(renderPermissionProvenance(provenance));
106
+ const ruleCount = ctx.clients?.userPermissionRuleStore?.list().length ?? 0;
107
+ ctx.print(ruleCount > 0
108
+ ? `\n${ruleCount} remembered approval rule${ruleCount === 1 ? '' : 's'} in effect. Use /permissions rules to review or revoke them.`
109
+ : '\nNo remembered approval rules. Use /permissions rules once you have some.');
19
110
  },
20
111
  });
21
112
  }
@@ -13,7 +13,7 @@ import { togglePlanMode, permissionModeLabel, type PermissionModeValue } from '.
13
13
  * A lone one of these is refused rather than seeded as a goal, so a stray verb
14
14
  * can never overwrite the project goal with itself.
15
15
  *
16
- * DEBT-3: `dismiss` and `answer` are now REAL subcommands (handled above this
16
+ * `dismiss` and `answer` are now REAL subcommands (handled above this
17
17
  * guard), so they were removed from the refuse-list. `pause`/`stop`/`cancel`
18
18
  * remain here — they still have no backing verb and must not seed a goal.
19
19
  */
@@ -178,7 +178,7 @@ export function registerPlanningRuntimeCommands(registry: CommandRegistry): void
178
178
  return;
179
179
  }
180
180
 
181
- // DEBT-3: /plan dismiss — archive the current plan. Dismisses the active
181
+ // /plan dismiss — archive the current plan. Dismisses the active
182
182
  // execution plan (ExecutionPlanManager.dismiss, honest per-state) AND
183
183
  // deactivates the project-planning interview state shown in the modal so a
184
184
  // later /plan <goal> starts fresh. Mid-execution is refused outright.
@@ -223,7 +223,7 @@ export function registerPlanningRuntimeCommands(registry: CommandRegistry): void
223
223
  return;
224
224
  }
225
225
 
226
- // DEBT-3: /plan answer <n|question-id> <text> — record a real answer to an
226
+ // /plan answer <n|question-id> <text> — record a real answer to an
227
227
  // open planning question (moves open → answered, consumed on next refine).
228
228
  if (args[0] === 'answer') {
229
229
  if (!projectPlanningService || !projectId) {
@@ -98,7 +98,7 @@ export function registerPlatformSandboxRuntimeCommands(registry: CommandRegistry
98
98
  const shellPaths = requireShellPaths(ctx);
99
99
  const sub = args[0] ?? 'open';
100
100
  if (sub === 'open' || sub === 'panel') {
101
- ctx.openModal?.('sandbox-modal'); // W6.1: sandbox panel -> config modal
101
+ ctx.openModal?.('sandbox-modal'); // sandbox panel -> config modal
102
102
  return;
103
103
  }
104
104
  if (sub === 'review') {
@@ -0,0 +1,56 @@
1
+ import type { CommandRegistry } from '../command-registry.ts';
2
+ import { powerStatusLines } from '../../core/power-status.ts';
3
+
4
+ // ---------------------------------------------------------------------------
5
+ // /power — host sleep ownership (the ops/status idiom surface for power.*).
6
+ //
7
+ // Shows the honest account of sleep ownership — the "sleep disabled" chip
8
+ // meaning, each "held because X" work-inhibition reason, and the lid-split note
9
+ // rendered VERBATIM when the OS could block idle-sleep but not lid-close
10
+ // suspend — and toggles the daemon-held keep-awake switch. Owner ruling: the
11
+ // always-visible chip IS the safety mechanism, so there is no timer and no
12
+ // AC-only option — keep-awake is exactly one on/off toggle.
13
+ // ---------------------------------------------------------------------------
14
+
15
+ export function registerPowerRuntimeCommands(registry: CommandRegistry): void {
16
+ registry.register({
17
+ name: 'power',
18
+ description: 'Host sleep ownership — show status and toggle keep-awake',
19
+ usage: '[status | on | off | toggle]',
20
+ argsHint: '[status|on|off|toggle]',
21
+ async handler(args, ctx) {
22
+ const sub = (args[0] ?? 'status').toLowerCase();
23
+
24
+ if (sub === 'status' || args.length === 0) {
25
+ const state = ctx.getPowerState?.();
26
+ if (!state) {
27
+ ctx.print('Power status is unavailable in this session.');
28
+ return;
29
+ }
30
+ const lines = ['Host sleep ownership:', ...powerStatusLines(state).map((l) => ` ${l}`)];
31
+ lines.push(state.keepAwake
32
+ ? 'Keep-awake is ON. Turn it off with /power off.'
33
+ : 'Keep-awake is OFF. Turn it on with /power on.');
34
+ ctx.print(lines.join('\n'));
35
+ return;
36
+ }
37
+
38
+ if (sub === 'on' || sub === 'off' || sub === 'toggle') {
39
+ if (!ctx.setKeepAwake || !ctx.getPowerState) {
40
+ ctx.print('Keep-awake cannot be changed in this session.');
41
+ return;
42
+ }
43
+ const current = ctx.getPowerState().keepAwake;
44
+ const next = sub === 'on' ? true : sub === 'off' ? false : !current;
45
+ const state = await ctx.setKeepAwake(next);
46
+ const lines = [state.keepAwake ? 'Keep-awake ON — this host will not idle-sleep.' : 'Keep-awake OFF.'];
47
+ // Surface the honest lid-split note verbatim if the SDK served one.
48
+ if (state.note) lines.push(` ${state.note}`);
49
+ ctx.print(lines.join('\n'));
50
+ return;
51
+ }
52
+
53
+ ctx.print('Usage: /power [status | on | off | toggle]');
54
+ },
55
+ });
56
+ }
@@ -3,11 +3,13 @@ import type {
3
3
  ProviderAccountRecord,
4
4
  ProviderAccountSnapshot,
5
5
  } from '@/runtime/index.ts';
6
+ import type { SelectionItem } from '../selection-modal.ts';
6
7
  import {
7
- openCommandPanel,
8
8
  requireOperatorClient,
9
9
  } from './runtime-services.ts';
10
10
 
11
+ type RecommendedAction = ProviderAccountRecord['recommendedActions'][number];
12
+
11
13
  async function loadProviderAccountSnapshot(context: CommandContext): Promise<ProviderAccountSnapshot> {
12
14
  return await requireOperatorClient(context).providers.accountSnapshot();
13
15
  }
@@ -20,6 +22,71 @@ function findProviderAccountRecord(
20
22
  return snapshot.providers.find((entry) => entry.providerId === providerId);
21
23
  }
22
24
 
25
+ /**
26
+ * Present a record's recommended actions as executable rows. A recommended
27
+ * action that carries a structured command becomes a press-Enter row that RUNS
28
+ * that command through the command registry — never a line the user has to
29
+ * retype. Actions with no command are genuinely out-of-product steps, so they
30
+ * stay as plain (non-executing) rows. When there is no interactive selection
31
+ * surface, each action is printed honestly instead.
32
+ */
33
+ export function presentRecommendedActions(
34
+ ctx: CommandContext,
35
+ providerId: string,
36
+ actions: readonly RecommendedAction[],
37
+ ): void {
38
+ if (actions.length === 0) {
39
+ ctx.print('No active repair actions suggested.');
40
+ return;
41
+ }
42
+ if (ctx.openSelection) {
43
+ const items: SelectionItem[] = actions.map((action, index) => {
44
+ if (action.command) {
45
+ const argsPart = action.command.args.length > 0 ? ` ${action.command.args.join(' ')}` : '';
46
+ return {
47
+ id: String(index),
48
+ label: action.description,
49
+ detail: `runs /${action.command.name}${argsPart}`,
50
+ primaryAction: 'select',
51
+ };
52
+ }
53
+ return {
54
+ id: String(index),
55
+ label: action.description,
56
+ detail: 'manual step (nothing to run)',
57
+ primaryAction: 'select',
58
+ };
59
+ });
60
+ ctx.openSelection(
61
+ `Repair ${providerId}`,
62
+ items,
63
+ { allowSearch: false, primaryVerbLabel: 'Run' },
64
+ (result) => {
65
+ if (!result) return;
66
+ const action = actions[Number(result.item.id)];
67
+ if (!action) return;
68
+ if (action.command) {
69
+ const args = [...action.command.args];
70
+ void (ctx.executeCommand?.(action.command.name, args)
71
+ ?? Promise.resolve(ctx.print(`Cannot run /${action.command.name} — command execution is unavailable here.`)));
72
+ return;
73
+ }
74
+ ctx.print(action.description);
75
+ },
76
+ );
77
+ return;
78
+ }
79
+ // No selection surface (headless/command-only): list actions honestly.
80
+ ctx.print([
81
+ 'Recommended actions:',
82
+ ...actions.map((action) => (
83
+ action.command
84
+ ? ` • ${action.description} — /${action.command.name}${action.command.args.length > 0 ? ` ${action.command.args.join(' ')}` : ''}`
85
+ : ` • ${action.description} (manual step)`
86
+ )),
87
+ ].join('\n'));
88
+ }
89
+
23
90
  export function registerProviderAccountsRuntimeCommands(registry: CommandRegistry): void {
24
91
  registry.register({
25
92
  name: 'accounts',
@@ -29,7 +96,7 @@ export function registerProviderAccountsRuntimeCommands(registry: CommandRegistr
29
96
  async handler(args, ctx) {
30
97
  const sub = (args[0] ?? 'review').toLowerCase();
31
98
  if (sub === 'panel' || sub === 'open') {
32
- ctx.openModal?.('providers-modal'); // W6.1: accounts alias -> provider-health modal
99
+ ctx.openModal?.('providers-modal'); // accounts alias -> provider-health modal
33
100
  return;
34
101
  }
35
102
  const snapshot = await loadProviderAccountSnapshot(ctx);
@@ -63,10 +130,8 @@ export function registerProviderAccountsRuntimeCommands(registry: CommandRegistr
63
130
  ` preferred route: ${record.preferredRoute}`,
64
131
  ...(record.fallbackRisk ? [` fallback: ${record.fallbackRisk}`] : []),
65
132
  ...(record.issues.map((issue) => ` issue: ${issue}`)),
66
- ...(record.recommendedActions.length > 0
67
- ? [' next:', ...record.recommendedActions.map((action) => ` ${action}`)]
68
- : [' No active repair actions suggested.']),
69
133
  ].join('\n'));
134
+ presentRecommendedActions(ctx, record.providerId, record.recommendedActions);
70
135
  return;
71
136
  }
72
137
  if (sub === 'show') {
@@ -95,8 +160,8 @@ export function registerProviderAccountsRuntimeCommands(registry: CommandRegistr
95
160
  ...record.usageWindows.map((entry) => ` window: ${entry.label} — ${entry.detail}`),
96
161
  ...record.issues.map((issue) => ` issue: ${issue}`),
97
162
  ...record.notes.map((note) => ` note: ${note}`),
98
- ...record.recommendedActions.map((action) => ` next: ${action}`),
99
163
  ].join('\n'));
164
+ presentRecommendedActions(ctx, record.providerId, record.recommendedActions);
100
165
  return;
101
166
  }
102
167
  ctx.print([
@@ -15,14 +15,13 @@
15
15
  */
16
16
 
17
17
  import type { SlashCommand, CommandContext } from '../command-registry.ts';
18
- import type { ConfigKey } from '../../config/index.ts';
19
18
  import type { RouteExplanation } from '@pellux/goodvibes-sdk/platform/providers';
20
19
  import type { FallbackTestResult, FallbackTransition } from '@pellux/goodvibes-sdk/platform/providers';
21
20
  import type { ProviderApiModelRecord } from '@pellux/goodvibes-sdk/platform/providers';
22
21
  import { requireProviderApi } from './runtime-services.ts';
22
+ import { featureEnablementWrite } from '../../runtime/feature-settings.ts';
23
23
 
24
24
  const PROVIDER_OPTIMIZER_FLAG = 'provider-optimizer';
25
- const PROVIDER_OPTIMIZER_CONFIG_KEY = `featureFlags.${PROVIDER_OPTIMIZER_FLAG}` as ConfigKey;
26
25
 
27
26
  // ---------------------------------------------------------------------------
28
27
  // Formatting helpers
@@ -95,9 +94,12 @@ function handleOptimizerToggle(
95
94
  const wasEnabled = optimizer.enabled;
96
95
  optimizer.setEnabled(enable);
97
96
 
98
- // Persist to config so the setting survives restart.
99
- const flagValue = enable ? 'enabled' : 'disabled';
100
- context.platform.configManager.setDynamic(PROVIDER_OPTIMIZER_CONFIG_KEY, flagValue);
97
+ // Persist to config so the setting survives restart: the optimizer is
98
+ // governed by the provider.optimizerMode settings key (off = disabled;
99
+ // manual/auto/pinned = active), and the settings bridge forwards the same
100
+ // write to the runtime gate.
101
+ const write = featureEnablementWrite(PROVIDER_OPTIMIZER_FLAG, enable);
102
+ if (write) context.platform.configManager.setDynamic(write.key, write.value);
101
103
 
102
104
  if (enable && !wasEnabled) {
103
105
  context.print('[provider] Optimizer enabled.');
@@ -1,36 +1,31 @@
1
- import { join } from 'node:path';
2
1
  import type { CommandRegistry } from '../command-registry.ts';
3
- import { openModalCommand, requireShellPaths } from './runtime-services.ts';
4
- import { regenerateCompanionToken } from '@pellux/goodvibes-sdk/platform/pairing';
2
+ import { openModalCommand } from './runtime-services.ts';
5
3
 
6
4
  /**
7
5
  * Register the /qrcode command.
8
6
  *
9
- * `/qrcode` (no args) opens the companion-app pairing modal (QR code + URL/
10
- * token/username). `/qrcode regenerate` rotates the companion pairing token:
11
- * it re-keys the shared operator-token store, so the previously-issued token is
12
- * rejected on the next auth and a fresh QR must be scanned to reconnect.
7
+ * `/qrcode` (no args, aliases /qr /pair) opens the device-pairing modal: a QR of
8
+ * the `#pair=<token>` deep link plus the offer set. Each open mints its own
9
+ * named per-device token, so `/qrcode regenerate` no longer rotates a shared
10
+ * token — it simply re-opens the modal, which mints a fresh token and QR. The
11
+ * previously-shown token stays valid until you revoke it in
12
+ * /settings → security → devices.
13
13
  */
14
14
  export function registerQrcodeRuntimeCommands(registry: CommandRegistry): void {
15
15
  registry.register({
16
16
  name: 'qrcode',
17
17
  aliases: ['qr', 'pair'],
18
- description: 'Open the companion-app pairing modal (QR code), or regenerate the pairing token',
18
+ description: 'Open the device-pairing modal (QR deep link + offers); each open mints a fresh device token',
19
19
  usage: '[regenerate]',
20
20
  argsHint: '[regenerate]',
21
21
  handler(args, ctx) {
22
22
  if (args[0]?.toLowerCase() === 'regenerate') {
23
- const shellPaths = requireShellPaths(ctx);
24
- const daemonHomeDir = join(shellPaths.homeDirectory, '.goodvibes', 'daemon');
25
- try {
26
- const rotated = regenerateCompanionToken({ daemonHomeDir });
27
- ctx.print(
28
- 'Pairing token rotated. The previous token is now rejected — ' +
29
- `re-scan the QR (new peer ${rotated.peerId.slice(0, 8)}…) to reconnect the companion app.`,
30
- );
31
- } catch (error) {
32
- ctx.print(`Could not regenerate the pairing token: ${String(error)}`);
33
- }
23
+ // Each pairing-modal open mints its own token, so "regenerate" is just a
24
+ // fresh open: the re-pull mints a new named token + QR in place. The
25
+ // prior token is not rotated out from under a live device — revoke it
26
+ // explicitly in /settings → security → devices when you no longer need it.
27
+ ctx.print('Opening a fresh pairing QR — it mints a new device token. Revoke old ones in /settings → security → devices.');
28
+ openModalCommand(ctx, 'pairing-modal');
34
29
  return;
35
30
  }
36
31
  openModalCommand(ctx, 'pairing-modal');
@@ -0,0 +1,70 @@
1
+ import type { CommandRegistry } from '../command-registry.ts';
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // /queue — the mid-turn message queue as an editable list.
5
+ //
6
+ // Messages typed while a turn is streaming are QUEUED (they deliver after the
7
+ // turn). Until a message is delivered it stays editable and deletable via the
8
+ // SDK's editQueuedMessage / deleteQueuedMessage verbs; a delivered message has
9
+ // left the queue and is no longer listed — delivery is immutability. This
10
+ // command is the editable interface over the same list the composer renders.
11
+ // ---------------------------------------------------------------------------
12
+
13
+ export function registerQueueRuntimeCommands(registry: CommandRegistry): void {
14
+ registry.register({
15
+ name: 'queue',
16
+ aliases: ['q'],
17
+ description: 'Mid-turn message queue — list, edit, or delete still-undelivered messages',
18
+ usage: '[list | edit <n> <text> | delete <n>]',
19
+ argsHint: '[list|edit <n> <text>|delete <n>]',
20
+ handler(args, ctx) {
21
+ const list = ctx.listQueuedMessages?.() ?? [];
22
+ const sub = (args[0] ?? 'list').toLowerCase();
23
+
24
+ if (sub === 'list' || args.length === 0) {
25
+ if (list.length === 0) {
26
+ ctx.print('No queued messages. (Messages typed while a turn is streaming queue here until it finishes.)');
27
+ return;
28
+ }
29
+ const lines = [`Queued messages (${list.length}) — editable until delivered:`];
30
+ list.forEach((m, i) => lines.push(` ${i + 1}. ${m.text}`));
31
+ lines.push('Use /queue edit <n> <new text> or /queue delete <n>.');
32
+ ctx.print(lines.join('\n'));
33
+ return;
34
+ }
35
+
36
+ // Both edit and delete address a message by its 1-based list position.
37
+ const n = Number.parseInt(args[1] ?? '', 10);
38
+ if (!Number.isInteger(n) || n < 1 || n > list.length) {
39
+ ctx.print(list.length === 0
40
+ ? 'No queued messages to act on.'
41
+ : `No queued message #${args[1] ?? ''}. Valid range: 1..${list.length}. Run /queue to list them.`);
42
+ return;
43
+ }
44
+ const target = list[n - 1]!;
45
+
46
+ if (sub === 'delete') {
47
+ const ok = ctx.deleteQueuedMessage?.(target.id) ?? false;
48
+ ctx.print(ok
49
+ ? `Deleted queued message #${n}.`
50
+ : `Could not delete message #${n} — it was already delivered (delivered messages are immutable).`);
51
+ return;
52
+ }
53
+
54
+ if (sub === 'edit') {
55
+ const text = args.slice(2).join(' ');
56
+ if (!text.trim()) {
57
+ ctx.print('Usage: /queue edit <n> <new text>. Provide the replacement text.');
58
+ return;
59
+ }
60
+ const ok = ctx.editQueuedMessage?.(target.id, text) ?? false;
61
+ ctx.print(ok
62
+ ? `Edited queued message #${n}.`
63
+ : `Could not edit message #${n} — it was already delivered (delivered messages are immutable).`);
64
+ return;
65
+ }
66
+
67
+ ctx.print('Usage: /queue [list | edit <n> <text> | delete <n>]');
68
+ },
69
+ });
70
+ }