@pellux/goodvibes-tui 1.1.0 → 1.9.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 (267) hide show
  1. package/CHANGELOG.md +85 -21
  2. package/README.md +28 -18
  3. package/docs/foundation-artifacts/operator-contract.json +4045 -1763
  4. package/package.json +2 -2
  5. package/src/audio/spoken-turn-controller.ts +12 -2
  6. package/src/audio/spoken-turn-wiring.ts +2 -1
  7. package/src/cli/help.ts +8 -1
  8. package/src/cli/service-posture.ts +2 -1
  9. package/src/cli/status.ts +2 -1
  10. package/src/cli/surface-command.ts +2 -2
  11. package/src/core/alert-gating.ts +67 -0
  12. package/src/core/approval-alert.ts +72 -0
  13. package/src/core/budget-breach-notifier.ts +106 -0
  14. package/src/core/composer-state.ts +11 -3
  15. package/src/core/conversation-line-cache.ts +27 -3
  16. package/src/core/conversation-rendering.ts +90 -14
  17. package/src/core/conversation.ts +18 -0
  18. package/src/core/focus-tracker.ts +41 -0
  19. package/src/core/long-task-notifier.ts +33 -6
  20. package/src/core/stream-event-wiring.ts +20 -1
  21. package/src/core/system-message-noise.ts +87 -0
  22. package/src/core/system-message-router.ts +104 -51
  23. package/src/core/turn-cancellation.ts +25 -0
  24. package/src/core/turn-event-wiring.ts +72 -3
  25. package/src/daemon/cli.ts +29 -2
  26. package/src/daemon/handlers/register.ts +8 -1
  27. package/src/daemon/service-commands.ts +329 -0
  28. package/src/export/cost-utils.ts +36 -0
  29. package/src/input/autocomplete.ts +27 -1
  30. package/src/input/command-registry.ts +84 -5
  31. package/src/input/commands/checkpoint-runtime.ts +280 -0
  32. package/src/input/commands/codebase-runtime.ts +232 -0
  33. package/src/input/commands/config.ts +43 -3
  34. package/src/input/commands/eval.ts +1 -1
  35. package/src/input/commands/health-runtime.ts +10 -2
  36. package/src/input/commands/image-runtime.ts +112 -0
  37. package/src/input/commands/intelligence-runtime.ts +11 -1
  38. package/src/input/commands/local-auth-runtime.ts +4 -1
  39. package/src/input/commands/local-runtime.ts +9 -3
  40. package/src/input/commands/marketplace-runtime.ts +2 -2
  41. package/src/input/commands/memory.ts +73 -35
  42. package/src/input/commands/operator-panel-runtime.ts +71 -31
  43. package/src/input/commands/planning-runtime.ts +116 -4
  44. package/src/input/commands/platform-sandbox-runtime.ts +1 -6
  45. package/src/input/commands/plugin-runtime.ts +2 -2
  46. package/src/input/commands/policy-dispatch.ts +21 -0
  47. package/src/input/commands/provider-accounts-runtime.ts +1 -1
  48. package/src/input/commands/qrcode-runtime.ts +26 -6
  49. package/src/input/commands/recall-review.ts +35 -0
  50. package/src/input/commands/remote-runtime-setup.ts +5 -3
  51. package/src/input/commands/remote-runtime.ts +3 -3
  52. package/src/input/commands/runtime-services.ts +54 -13
  53. package/src/input/commands/services-runtime.ts +1 -1
  54. package/src/input/commands/session-content.ts +20 -9
  55. package/src/input/commands/session-workflow.ts +16 -1
  56. package/src/input/commands/settings-sync-runtime.ts +16 -4
  57. package/src/input/commands/shell-core.ts +25 -8
  58. package/src/input/commands/skills-runtime.ts +2 -8
  59. package/src/input/commands/subscription-runtime.ts +2 -2
  60. package/src/input/commands/test-runtime.ts +277 -0
  61. package/src/input/commands/websearch-runtime.ts +101 -0
  62. package/src/input/commands/work-plan-runtime.ts +36 -10
  63. package/src/input/commands/workstream-runtime.ts +488 -0
  64. package/src/input/commands.ts +12 -0
  65. package/src/input/config-modal-types.ts +175 -0
  66. package/src/input/config-modal.ts +592 -0
  67. package/src/input/feed-context-factory.ts +10 -8
  68. package/src/input/handler-command-route.ts +37 -20
  69. package/src/input/handler-content-actions.ts +17 -2
  70. package/src/input/handler-feed-routes.ts +74 -114
  71. package/src/input/handler-feed.ts +79 -27
  72. package/src/input/handler-interactions.ts +1 -3
  73. package/src/input/handler-modal-routes.ts +136 -6
  74. package/src/input/handler-modal-stack.ts +14 -7
  75. package/src/input/handler-modal-token-routes.ts +16 -49
  76. package/src/input/handler-onboarding-daemon-adopt.ts +149 -0
  77. package/src/input/handler-onboarding.ts +71 -59
  78. package/src/input/handler-picker-routes.ts +26 -102
  79. package/src/input/handler-shortcuts.ts +74 -14
  80. package/src/input/handler-types.ts +2 -6
  81. package/src/input/handler-ui-state.ts +10 -22
  82. package/src/input/handler.ts +12 -29
  83. package/src/input/keybindings.ts +23 -8
  84. package/src/input/model-picker-types.ts +24 -6
  85. package/src/input/model-picker.ts +77 -2
  86. package/src/input/onboarding/onboarding-runtime-status.ts +10 -1
  87. package/src/input/onboarding/onboarding-wizard-apply.ts +8 -1
  88. package/src/input/onboarding/onboarding-wizard-constants.ts +4 -1
  89. package/src/input/onboarding/onboarding-wizard-network-adopt.ts +136 -0
  90. package/src/input/onboarding/onboarding-wizard-steps.ts +9 -6
  91. package/src/input/onboarding/onboarding-wizard-types.ts +3 -1
  92. package/src/input/panel-integration-actions.ts +9 -170
  93. package/src/input/panel-mouse-geometry.ts +97 -0
  94. package/src/input/panel-paste-flood-guard.ts +86 -0
  95. package/src/input/selection-modal.ts +11 -0
  96. package/src/input/session-picker-modal.ts +44 -4
  97. package/src/input/settings-modal-data.ts +146 -2
  98. package/src/input/settings-modal-mutations.ts +6 -4
  99. package/src/input/settings-modal-types.ts +4 -2
  100. package/src/main.ts +49 -51
  101. package/src/panels/agent-inspector-shared.ts +2 -1
  102. package/src/panels/base-panel.ts +22 -1
  103. package/src/panels/builtin/agent.ts +21 -107
  104. package/src/panels/builtin/development.ts +15 -61
  105. package/src/panels/builtin/knowledge.ts +8 -32
  106. package/src/panels/builtin/operations.ts +84 -428
  107. package/src/panels/builtin/session.ts +21 -112
  108. package/src/panels/builtin/shared.ts +18 -46
  109. package/src/panels/builtin-modals.ts +218 -0
  110. package/src/panels/builtin-panels.ts +5 -0
  111. package/src/panels/cost-tracker-panel.ts +36 -10
  112. package/src/panels/diff-panel.ts +12 -43
  113. package/src/panels/eval-registry.ts +60 -0
  114. package/src/panels/fleet-deep-link.ts +31 -0
  115. package/src/panels/fleet-panel-format.ts +62 -0
  116. package/src/panels/fleet-panel-worktree-detail.ts +48 -0
  117. package/src/panels/fleet-panel.ts +758 -0
  118. package/src/panels/fleet-read-model.ts +520 -0
  119. package/src/panels/fleet-steer.ts +125 -0
  120. package/src/panels/fleet-stop.ts +153 -0
  121. package/src/panels/fleet-tabs.ts +230 -0
  122. package/src/panels/fleet-transcript.ts +356 -0
  123. package/src/panels/index.ts +7 -31
  124. package/src/panels/modals/hooks-modal.ts +187 -0
  125. package/src/panels/modals/keybindings-modal.ts +166 -0
  126. package/src/panels/modals/knowledge-modal.ts +158 -0
  127. package/src/panels/modals/local-auth-modal.ts +132 -0
  128. package/src/panels/modals/marketplace-modal.ts +218 -0
  129. package/src/panels/modals/memory-modal.ts +180 -0
  130. package/src/panels/modals/modal-surface-helpers.ts +54 -0
  131. package/src/panels/modals/modal-theme.ts +42 -0
  132. package/src/panels/modals/pairing-modal.ts +163 -0
  133. package/src/panels/modals/planning-modal.ts +304 -0
  134. package/src/panels/modals/plugins-modal.ts +174 -0
  135. package/src/panels/modals/policy-modal.ts +256 -0
  136. package/src/panels/modals/provider-health-modal.ts +136 -0
  137. package/src/panels/modals/remote-modal.ts +115 -0
  138. package/src/panels/modals/sandbox-modal.ts +150 -0
  139. package/src/panels/modals/security-modal.ts +217 -0
  140. package/src/panels/modals/services-modal.ts +179 -0
  141. package/src/panels/modals/settings-sync-modal.ts +142 -0
  142. package/src/panels/modals/skills-modal.ts +189 -0
  143. package/src/panels/modals/subscription-modal.ts +172 -0
  144. package/src/panels/modals/work-plan-modal.ts +177 -0
  145. package/src/panels/panel-confirm-overlay.ts +72 -0
  146. package/src/panels/panel-manager.ts +123 -9
  147. package/src/panels/polish-core.ts +38 -25
  148. package/src/panels/project-planning-answer-actions.ts +8 -11
  149. package/src/panels/skills-panel.ts +7 -51
  150. package/src/panels/types.ts +37 -0
  151. package/src/permissions/hunk-selection.ts +179 -0
  152. package/src/permissions/prompt.ts +216 -13
  153. package/src/renderer/autocomplete-overlay.ts +28 -2
  154. package/src/renderer/compaction-history-modal.ts +19 -3
  155. package/src/renderer/compaction-preview.ts +13 -2
  156. package/src/renderer/compaction-quality.ts +100 -0
  157. package/src/renderer/compositor.ts +2 -3
  158. package/src/renderer/config-modal.ts +95 -0
  159. package/src/renderer/conversation-overlays.ts +10 -17
  160. package/src/renderer/fleet-tab-strip.ts +56 -0
  161. package/src/renderer/footer-tips.ts +15 -3
  162. package/src/renderer/fullscreen-primitives.ts +32 -22
  163. package/src/renderer/git-status.ts +43 -1
  164. package/src/renderer/help-overlay.ts +2 -2
  165. package/src/renderer/layout.ts +0 -4
  166. package/src/renderer/markdown.ts +7 -3
  167. package/src/renderer/modal-factory.ts +25 -20
  168. package/src/renderer/model-workspace.ts +62 -4
  169. package/src/renderer/overlay-box.ts +21 -17
  170. package/src/renderer/panel-workspace-bar.ts +8 -2
  171. package/src/renderer/process-indicator.ts +14 -3
  172. package/src/renderer/selection-modal-overlay.ts +6 -1
  173. package/src/renderer/session-picker-modal.ts +196 -3
  174. package/src/renderer/settings-modal-helpers.ts +2 -0
  175. package/src/renderer/settings-modal.ts +7 -0
  176. package/src/renderer/shell-surface.ts +8 -1
  177. package/src/renderer/status-glyphs.ts +14 -15
  178. package/src/renderer/system-message.ts +15 -3
  179. package/src/renderer/terminal-bg-probe.ts +339 -0
  180. package/src/renderer/terminal-escapes.ts +20 -0
  181. package/src/renderer/theme-mode-config.ts +67 -0
  182. package/src/renderer/theme.ts +91 -1
  183. package/src/renderer/thinking.ts +11 -3
  184. package/src/renderer/tool-call.ts +15 -9
  185. package/src/renderer/tool-result-summary.ts +148 -0
  186. package/src/renderer/turn-injection.ts +133 -0
  187. package/src/renderer/ui-factory.ts +154 -85
  188. package/src/renderer/ui-primitives.ts +30 -129
  189. package/src/runtime/bootstrap-command-context.ts +27 -1
  190. package/src/runtime/bootstrap-command-parts.ts +41 -10
  191. package/src/runtime/bootstrap-core.ts +47 -15
  192. package/src/runtime/bootstrap-hook-bridge.ts +7 -0
  193. package/src/runtime/bootstrap-shell.ts +48 -17
  194. package/src/runtime/bootstrap.ts +129 -22
  195. package/src/runtime/code-index-services.ts +135 -0
  196. package/src/runtime/legacy-daemon-migration.ts +516 -0
  197. package/src/runtime/memory-fold.ts +26 -0
  198. package/src/runtime/onboarding/derivation.ts +7 -2
  199. package/src/runtime/onboarding/snapshot.ts +27 -1
  200. package/src/runtime/onboarding/types.ts +31 -1
  201. package/src/runtime/operator-token-cleanup.ts +82 -1
  202. package/src/runtime/orchestrator-core-services.ts +59 -0
  203. package/src/runtime/process-lifecycle.ts +3 -1
  204. package/src/runtime/resume-notice.ts +209 -0
  205. package/src/runtime/services.ts +98 -46
  206. package/src/runtime/session-inbound-inputs.ts +252 -0
  207. package/src/runtime/session-spine-transport.ts +64 -0
  208. package/src/runtime/terminal-output-guard.ts +15 -8
  209. package/src/runtime/ui-services.ts +27 -3
  210. package/src/runtime/workstream-services.ts +327 -0
  211. package/src/runtime/wrfc-persistence.ts +124 -17
  212. package/src/shell/blocking-input.ts +68 -4
  213. package/src/shell/recovery-input-helpers.ts +170 -1
  214. package/src/shell/ui-openers.ts +171 -26
  215. package/src/utils/format-duration.ts +8 -18
  216. package/src/utils/splash-lines.ts +1 -1
  217. package/src/utils/terminal-width.ts +52 -0
  218. package/src/version.ts +1 -1
  219. package/src/panels/agent-inspector-panel.ts +0 -786
  220. package/src/panels/approval-panel.ts +0 -252
  221. package/src/panels/automation-control-panel.ts +0 -479
  222. package/src/panels/cockpit-panel.ts +0 -481
  223. package/src/panels/cockpit-read-model.ts +0 -238
  224. package/src/panels/communication-panel.ts +0 -256
  225. package/src/panels/control-plane-panel.ts +0 -470
  226. package/src/panels/debug-panel.ts +0 -615
  227. package/src/panels/docs-panel.ts +0 -384
  228. package/src/panels/eval-panel.ts +0 -627
  229. package/src/panels/file-explorer-panel.ts +0 -673
  230. package/src/panels/file-preview-panel.ts +0 -517
  231. package/src/panels/hooks-panel.ts +0 -401
  232. package/src/panels/incident-review-panel.ts +0 -406
  233. package/src/panels/intelligence-panel.ts +0 -383
  234. package/src/panels/knowledge-graph-panel.ts +0 -515
  235. package/src/panels/marketplace-panel.ts +0 -399
  236. package/src/panels/memory-panel.ts +0 -558
  237. package/src/panels/ops-control-panel.ts +0 -329
  238. package/src/panels/ops-strategy-panel.ts +0 -321
  239. package/src/panels/orchestration-panel.ts +0 -465
  240. package/src/panels/panel-list-panel.ts +0 -566
  241. package/src/panels/plan-dashboard-panel.ts +0 -707
  242. package/src/panels/policy-panel.ts +0 -517
  243. package/src/panels/project-planning-panel.ts +0 -731
  244. package/src/panels/provider-health-panel.ts +0 -678
  245. package/src/panels/provider-health-tracker.ts +0 -310
  246. package/src/panels/provider-health-views.ts +0 -567
  247. package/src/panels/qr-panel.ts +0 -280
  248. package/src/panels/remote-panel.ts +0 -534
  249. package/src/panels/routes-panel.ts +0 -241
  250. package/src/panels/sandbox-panel.ts +0 -456
  251. package/src/panels/security-panel.ts +0 -447
  252. package/src/panels/services-panel.ts +0 -329
  253. package/src/panels/session-browser-panel.ts +0 -496
  254. package/src/panels/settings-sync-panel.ts +0 -398
  255. package/src/panels/subscription-panel.ts +0 -342
  256. package/src/panels/symbol-outline-panel.ts +0 -619
  257. package/src/panels/system-messages-panel.ts +0 -364
  258. package/src/panels/tasks-panel.ts +0 -608
  259. package/src/panels/thinking-panel.ts +0 -333
  260. package/src/panels/tool-inspector-panel.ts +0 -537
  261. package/src/panels/work-plan-panel.ts +0 -540
  262. package/src/panels/worktree-panel.ts +0 -360
  263. package/src/panels/wrfc-panel.ts +0 -790
  264. package/src/renderer/agent-detail-modal.ts +0 -466
  265. package/src/renderer/live-tail-modal.ts +0 -156
  266. package/src/renderer/process-modal.ts +0 -671
  267. package/src/renderer/qr-renderer.ts +0 -120
@@ -0,0 +1,329 @@
1
+ /**
2
+ * D7a Layer 1 (TUI wiring) — `goodvibes-daemon install-service | uninstall-service
3
+ * | service-status`.
4
+ *
5
+ * The daemon is a SYSTEM SERVICE. These subcommands install it as a durable host
6
+ * service (systemd user unit on Linux, launchd agent on macOS, a Scheduled Task
7
+ * on Windows) so N surfaces share ONE daemon that survives reboots, instead of
8
+ * every surface spawning a session-scoped daemon.
9
+ *
10
+ * W3 drift note: the SDK's D7a-era `systemd-user-service.ts` (a Linux-only,
11
+ * bespoke systemd shim) was dead code and got deleted in SDK W3-S5. The SDK's
12
+ * REAL wired service machinery — reached in production by the daemon's own HTTP
13
+ * `/api/service/*` routes via facade-composition.ts — is
14
+ * `PlatformServiceManager` (`@pellux/goodvibes-sdk/platform/daemon`): a single
15
+ * systemd/launchd/windows-aware manager with install/uninstall/status/start/
16
+ * stop/restart and a `suggestedCommands` hint list. This module now rewires the
17
+ * three CLI subcommands onto that manager instead of the deleted shim.
18
+ *
19
+ * Two real behavioral differences from the old shim, called out honestly:
20
+ * - `install()` only WRITES the unit/plist/task; it does not enable/start it.
21
+ * So `install-service` here calls `install()` then `start()` to preserve the
22
+ * old "install implies enabled + running" behavior.
23
+ * - `uninstall()` only REMOVES the unit file; there is no manager-level
24
+ * `disable` verb (only start/stop/restart exist). So `uninstall-service`
25
+ * calls `stop()` then `uninstall()`, and honestly tells the caller that a
26
+ * stray "enabled" symlink may remain until `systemctl --user daemon-reload`
27
+ * (offered back as a suggested follow-up) or the next login cleans it up.
28
+ *
29
+ * W4-D1: a fourth subcommand, `migrate-service`, closes the Wave-3 "detect and
30
+ * disclose only" inheritance (W3 Finding 4, below) with a GUIDED, CONSENTED
31
+ * takeover of the legacy `goodvibes-daemon.service` unit. Design constraints,
32
+ * all load-bearing:
33
+ * - NEVER auto-migrate. Without explicit consent (`confirmMigration`, wired
34
+ * from the CLI's existing `-y`/`--yes` flag) this prints the exact plan
35
+ * and touches nothing.
36
+ * - NEW-UP-THEN-OLD-DOWN. The new `goodvibes.service` unit is installed,
37
+ * started, and verified healthy (a fresh `status().running` read, which
38
+ * honestly queries systemd via the injected actionRunner) BEFORE the
39
+ * legacy unit is stopped, disabled, or removed. A failed or unhealthy new
40
+ * unit rolls itself back (uninstalled) and never touches the legacy one
41
+ * — a botched takeover must never cost the user their working daemon.
42
+ * - ADOPT-OR-WARN, NEVER KILL. If the legacy unit file is simply absent but
43
+ * something is already listening on the configured host:port (Mike's real
44
+ * dev-host case: a manual `nohup`'d daemon with no unit at all), this is
45
+ * an unidentified process, not a managed unit — there is nothing to stop
46
+ * or disable, and this module will not attempt to kill it. It warns and
47
+ * leaves the decision to the operator.
48
+ * - Every action (legacy stop/disable, unit-file removal, daemon-reload)
49
+ * goes through the SAME injectable `actionRunner`/`legacyUnitFileRemove`
50
+ * seams tests already use — this module never has a code path that bypasses
51
+ * them, so the migration is exercised deterministically via fakes and never
52
+ * touches a real running service in tests.
53
+ */
54
+
55
+ import { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
56
+ import { PlatformServiceManager, type ManagedServiceStatus } from '@pellux/goodvibes-sdk/platform/daemon';
57
+ import {
58
+ buildManagedDaemonServiceManager,
59
+ detectLegacyUnit,
60
+ legacyUnitNote,
61
+ resolveManagedUnitName,
62
+ runLegacyDaemonMigration,
63
+ LEGACY_SERVICE_UNIT_NAME,
64
+ MANAGED_SERVICE_NAME as SERVICE_NAME,
65
+ MANAGED_SERVICE_DESCRIPTION as SERVICE_DESCRIPTION,
66
+ type LegacyUnitInfo,
67
+ type ManagedServiceActionRunner,
68
+ } from '../runtime/legacy-daemon-migration.ts';
69
+
70
+ // `resolveInstalledDaemonBinary` lives in the runtime module (shared with the
71
+ // onboarding guided UX) — re-exported here so this module stays the CLI's
72
+ // stable public surface (and so existing test imports keep working).
73
+ export {
74
+ resolveInstalledDaemonBinary,
75
+ type ResolveDaemonBinaryOptions,
76
+ } from '../runtime/legacy-daemon-migration.ts';
77
+ export type { ManagedServiceActionRunner } from '../runtime/legacy-daemon-migration.ts';
78
+
79
+ // ---------------------------------------------------------------------------
80
+ // W3 Finding 4: legacy-unit detection. W4-D1: guided migration.
81
+ //
82
+ // The prior D7a-era command installed the daemon's systemd unit under the
83
+ // literal name `goodvibes-daemon.service`. This module (rewired onto
84
+ // PlatformServiceManager, see the file banner above) manages a DIFFERENT
85
+ // unit name (`goodvibes`, SERVICE_NAME). A host that still has the legacy
86
+ // unit installed — this dev machine included — would otherwise see
87
+ // service-status honestly report installed:false/running:false for the
88
+ // tracked name while the legacy unit keeps running untouched underneath it,
89
+ // uninstall-service would silently leave the legacy unit orphaned with no
90
+ // mention, and install-service could start a SECOND daemon competing for the
91
+ // same port.
92
+ //
93
+ // The detection (`detectLegacyUnit`/`legacyUnitNote`) and the guided
94
+ // migration engine (`runLegacyDaemonMigration`) both live in
95
+ // `../runtime/legacy-daemon-migration.ts` rather than here, so the TUI's
96
+ // onboarding UX (`src/input/handler-onboarding-daemon-adopt.ts`) can reuse
97
+ // them directly — the architecture gate forbids `src/input/**` from
98
+ // importing `src/daemon/**` (input must stay entrypoint-agnostic), so the
99
+ // shared engine lives in the entrypoint-agnostic `runtime` layer instead and
100
+ // this CLI module is just one of its two consumers.
101
+ //
102
+ // This detection is entirely independent of PlatformServiceManager's own
103
+ // status() — it does not rely on (or get invalidated by) the parallel SDK
104
+ // fix that makes status().running itself query systemd honestly for the
105
+ // TRACKED unit name.
106
+ // ---------------------------------------------------------------------------
107
+
108
+ export type DaemonServiceSubcommand = 'install-service' | 'uninstall-service' | 'service-status' | 'migrate-service';
109
+
110
+ export function isDaemonServiceSubcommand(value: string | undefined): value is DaemonServiceSubcommand {
111
+ return value === 'install-service' || value === 'uninstall-service' || value === 'service-status' || value === 'migrate-service';
112
+ }
113
+
114
+ export interface DaemonServiceCliInput {
115
+ readonly subcommand: DaemonServiceSubcommand;
116
+ readonly binaryPath: string;
117
+ readonly homeDir: string;
118
+ readonly host: string;
119
+ readonly port: number;
120
+ /** Defaults to `homeDir` — overridable so tests can scope both to one tempdir. */
121
+ readonly workingDirectory?: string | undefined;
122
+ /** Injected in tests; a real `ConfigManager` rooted at `homeDir` otherwise. */
123
+ readonly configManager?: ConfigManager | undefined;
124
+ /** Injectable systemctl/launchctl/schtasks runner so tests never touch the host. */
125
+ readonly actionRunner?: ManagedServiceActionRunner | undefined;
126
+ /** Injectable existsSync for the legacy-unit file check (W3 Finding 4) so tests never touch the host filesystem. */
127
+ readonly legacyUnitFileExists?: ((path: string) => boolean) | undefined;
128
+ /**
129
+ * `migrate-service` only: explicit consent to actually execute the
130
+ * migration (wired from the CLI's `-y`/`--yes` flag). Without it, the
131
+ * subcommand prints the exact plan and changes nothing — never auto-migrate.
132
+ */
133
+ readonly confirmMigration?: boolean | undefined;
134
+ /**
135
+ * `migrate-service` only: injectable port-liveness check for the
136
+ * legacy-absent branch (never a raw network call in tests). Defaults to a
137
+ * real, read-only TCP connect attempt.
138
+ */
139
+ readonly portProbe?: ((host: string, port: number) => boolean | Promise<boolean>) | undefined;
140
+ /**
141
+ * `migrate-service` only: injectable removal of the legacy unit file, so
142
+ * tests never call a raw fs op against a real path. Defaults to a real
143
+ * `rmSync`.
144
+ */
145
+ readonly legacyUnitFileRemove?: ((path: string) => void) | undefined;
146
+ }
147
+
148
+ export interface DaemonServiceCliResult {
149
+ readonly ok: boolean;
150
+ readonly exitCode: number;
151
+ /** Honest, human-readable stdout lines describing exactly what happened. */
152
+ readonly lines: readonly string[];
153
+ readonly status: ManagedServiceStatus;
154
+ }
155
+
156
+ /**
157
+ * The manager's definition (`ExecStart` command/args, name, description) is
158
+ * built once, in `../runtime/legacy-daemon-migration.ts`, and shared with the
159
+ * onboarding guided UX — see that module's doc comment for why.
160
+ */
161
+ function buildManager(input: DaemonServiceCliInput): PlatformServiceManager {
162
+ return buildManagedDaemonServiceManager({
163
+ binaryPath: input.binaryPath,
164
+ homeDir: input.homeDir,
165
+ host: input.host,
166
+ port: input.port,
167
+ workingDirectory: input.workingDirectory,
168
+ configManager: input.configManager,
169
+ actionRunner: input.actionRunner,
170
+ });
171
+ }
172
+
173
+ function statusLines(status: ManagedServiceStatus): string[] {
174
+ const lines = [
175
+ `platform: ${status.platform}`,
176
+ `installed: ${status.installed}`,
177
+ `running: ${status.running}`,
178
+ `unit path: ${status.path}`,
179
+ ];
180
+ if (status.pid !== undefined) lines.push(`pid: ${status.pid}`);
181
+ if (status.platform !== 'manual' && status.installed && !status.running) {
182
+ // W3 Finding 4: this used to assert "'running' here only reflects
183
+ // processes this tool started directly" — true for the pid-file-only
184
+ // check the (currently linked) SDK still uses, but the parallel SDK
185
+ // batch is making status().running query systemd honestly via
186
+ // `is-active`, which would make that specific claim stale. Drop the
187
+ // claim about HOW running was computed and just offer the escape
188
+ // hatch — true and useful under either SDK version.
189
+ lines.push(
190
+ `note: if this looks wrong, verify directly: ${status.suggestedCommands[status.suggestedCommands.length - 1] ?? 'the platform service-status command'}`,
191
+ );
192
+ }
193
+ return lines;
194
+ }
195
+
196
+ /**
197
+ * Build the install-branch's result lines. Exported for direct unit
198
+ * coverage: the "suggested follow-ups" block is gated on the actual
199
+ * not-started case (W3 Finding 4 friction fix) rather than printed
200
+ * unconditionally, and driving that through the full
201
+ * PlatformServiceManager/systemd integration can't reliably produce
202
+ * `running: true` under the (currently linked) SDK build, whose systemd
203
+ * status check is still pid-file-only (see the honesty note above).
204
+ */
205
+ export function buildInstallResultLines(status: ManagedServiceStatus): string[] {
206
+ const lines = [`installed the ${status.platform} service at ${status.path}`];
207
+ if (status.running) {
208
+ lines.push('service is enabled and running');
209
+ } else {
210
+ lines.push('suggested follow-ups if it did not start automatically:');
211
+ for (const cmd of status.suggestedCommands) lines.push(` ${cmd}`);
212
+ }
213
+ return lines;
214
+ }
215
+
216
+ function ok(action: 'install' | 'uninstall' | 'status', status: ManagedServiceStatus, extra: string[] = []): DaemonServiceCliResult {
217
+ const lines: string[] = [];
218
+ if (action === 'install') {
219
+ lines.push(...buildInstallResultLines(status));
220
+ } else if (action === 'uninstall') {
221
+ lines.push(`removed the ${status.platform} service at ${status.path}`);
222
+ if (status.platform === 'systemd') {
223
+ lines.push(
224
+ "note: this removes the unit file but does not run `disable` — run " +
225
+ "`systemctl --user daemon-reload` to clear any stale enablement symlink.",
226
+ );
227
+ }
228
+ } else {
229
+ lines.push(...statusLines(status));
230
+ }
231
+ lines.push(...extra);
232
+ return { ok: true, exitCode: 0, lines, status };
233
+ }
234
+
235
+ function failed(action: 'install' | 'uninstall' | 'status', status: ManagedServiceStatus): DaemonServiceCliResult {
236
+ return {
237
+ ok: false,
238
+ exitCode: 1,
239
+ lines: [`service ${action} failed: ${status.actionError ?? 'unknown error'}`],
240
+ status,
241
+ };
242
+ }
243
+
244
+ /**
245
+ * `migrate-service`: the guided, consented takeover of the legacy
246
+ * `goodvibes-daemon.service` unit (W4-D1). Thin wrapper over
247
+ * `runLegacyDaemonMigration` (`../runtime/legacy-daemon-migration.ts`) — see
248
+ * that module for the design constraints (never auto-migrate,
249
+ * new-up-then-old-down, adopt-or-warn/never kill an unrecognized process,
250
+ * every action through an injectable seam) and the onboarding UX consumer.
251
+ */
252
+ function runMigrateService(
253
+ input: DaemonServiceCliInput,
254
+ manager: PlatformServiceManager,
255
+ legacy: LegacyUnitInfo,
256
+ ): Promise<DaemonServiceCliResult> {
257
+ return runLegacyDaemonMigration(
258
+ {
259
+ host: input.host,
260
+ port: input.port,
261
+ trackedServiceName: SERVICE_NAME,
262
+ confirmMigration: input.confirmMigration,
263
+ portProbe: input.portProbe,
264
+ legacyUnitFileRemove: input.legacyUnitFileRemove,
265
+ actionRunner: input.actionRunner,
266
+ },
267
+ manager,
268
+ legacy,
269
+ );
270
+ }
271
+
272
+ /** Dispatch a daemon service subcommand to the SDK's `PlatformServiceManager`. */
273
+ export async function runDaemonServiceCli(input: DaemonServiceCliInput): Promise<DaemonServiceCliResult> {
274
+ const manager = buildManager(input);
275
+ const legacy = detectLegacyUnit(input);
276
+ switch (input.subcommand) {
277
+ case 'migrate-service':
278
+ return runMigrateService(input, manager, legacy);
279
+ case 'install-service': {
280
+ // W3 Finding 4: refuse rather than risk starting a second daemon
281
+ // alongside an already-installed legacy unit. Refuses whenever the
282
+ // legacy unit is present at all (not just when currently active) —
283
+ // an installed-but-inactive legacy unit can still be enabled and
284
+ // start competing for the same port later, and "never silently
285
+ // start a second daemon" is the bar here, not "never right now."
286
+ if (legacy.present) {
287
+ const status = manager.status();
288
+ const resolvedName = resolveManagedUnitName(status);
289
+ return {
290
+ ok: false,
291
+ exitCode: 1,
292
+ lines: [
293
+ `service install refused: a service is already installed under the legacy name ${LEGACY_SERVICE_UNIT_NAME}.service.`,
294
+ legacyUnitNote(legacy, resolvedName),
295
+ `Installing this tool's ${resolvedName}.service alongside it risks two daemons competing for the same port.`,
296
+ ],
297
+ status,
298
+ };
299
+ }
300
+ const installed = manager.install();
301
+ if (installed.actionError) return failed('install', installed);
302
+ const started = manager.start();
303
+ // start()'s actionError (e.g. a platform this manager can't dispatch
304
+ // actions for) doesn't undo the write — report install as ok, but surface
305
+ // the follow-up problem honestly instead of claiming it is running.
306
+ return started.actionError
307
+ ? ok('install', { ...started, running: false }, [`could not start it automatically: ${started.actionError}`])
308
+ : ok('install', started);
309
+ }
310
+ case 'uninstall-service': {
311
+ const stopped = manager.stop();
312
+ const uninstalled = manager.uninstall();
313
+ if (uninstalled.actionError) return failed('uninstall', uninstalled);
314
+ const extra: string[] = [];
315
+ if (stopped.actionError) extra.push(`(it may not have been running: ${stopped.actionError})`);
316
+ // W3 Finding 4: this command only ever touches the TRACKED unit
317
+ // (whatever name/path actually resolved, per F2 — not necessarily the
318
+ // SERVICE_NAME constant) above — say so explicitly when a legacy unit
319
+ // also exists, so its continued presence is never a silent surprise.
320
+ if (legacy.present) extra.push(legacyUnitNote(legacy, resolveManagedUnitName(uninstalled)));
321
+ return ok('uninstall', uninstalled, extra);
322
+ }
323
+ case 'service-status': {
324
+ const status = manager.status();
325
+ if (status.actionError) return failed('status', status);
326
+ return ok('status', status, legacy.present ? [legacyUnitNote(legacy, resolveManagedUnitName(status))] : []);
327
+ }
328
+ }
329
+ }
@@ -151,3 +151,39 @@ export function calcSessionCost(
151
151
  const billableInput = inputTokens + cacheRead + cacheWrite;
152
152
  return (billableInput * pricing.input + outputTokens * pricing.output) / 1_000_000;
153
153
  }
154
+
155
+ /**
156
+ * computeBudgetBreach — pure predicate shared by CostTrackerPanel's render-time
157
+ * "OVER BUDGET" flag and the background budget-breach notifier
158
+ * (core/budget-breach-notifier.ts), so both agree on exactly one definition
159
+ * of "over budget". `budgetThreshold <= 0` means no budget is configured
160
+ * (disabled), which can never be breached.
161
+ */
162
+ export function computeBudgetBreach(sessionCost: number, budgetThreshold: number): boolean {
163
+ return budgetThreshold > 0 && sessionCost > budgetThreshold;
164
+ }
165
+
166
+ /** Read/write access to the `behavior.budgetAlertUsd` TUI-local config key. Plain
167
+ * callbacks (rather than a ConfigManager reference) so callers can pass a
168
+ * generic-key-cast wrapper without this module depending on the SDK config types. */
169
+ export interface BudgetAlertConfigAccess {
170
+ readonly get: (key: string) => unknown;
171
+ readonly set: (key: string, value: unknown) => void;
172
+ }
173
+
174
+ /** Default for the `behavior.budgetAlertUsd` synthetic setting: 0 = no budget configured. */
175
+ export const BUDGET_ALERT_USD_DEFAULT = 0;
176
+
177
+ /**
178
+ * readBudgetAlertUsd — read the session cost-budget alert threshold (USD) from
179
+ * config. This is the single source of truth CostTrackerPanel and the
180
+ * background budget-breach notifier (core/budget-breach-notifier.ts) both
181
+ * read, so a threshold set via the panel's 'b' key is immediately visible to
182
+ * the notifier and vice versa. Falls back to BUDGET_ALERT_USD_DEFAULT (off)
183
+ * when the key is absent or invalid.
184
+ */
185
+ export function readBudgetAlertUsd(configGet: (key: string) => unknown): number {
186
+ const raw = configGet('behavior.budgetAlertUsd');
187
+ const parsed = typeof raw === 'number' ? raw : Number(raw);
188
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : BUDGET_ALERT_USD_DEFAULT;
189
+ }
@@ -1,4 +1,4 @@
1
- import type { SlashCommand } from './command-registry.ts';
1
+ import { COMMON_COMMAND_NAMES, type SlashCommand } from './command-registry.ts';
2
2
  import type { CommandRegistry } from './command-registry.ts';
3
3
 
4
4
  /**
@@ -18,6 +18,14 @@ export interface AutocompleteState {
18
18
  query: string;
19
19
  results: AutocompleteResult[];
20
20
  selectedIndex: number;
21
+ /**
22
+ * UX-C (item 4): how many leading entries of `results` belong to the
23
+ * curated "common" tier — only meaningful when `query === ''` (0
24
+ * otherwise, since a typed filter searches everything and the common/rest
25
+ * split no longer applies). The renderer uses this to draw a separator
26
+ * before the alphabetical rest.
27
+ */
28
+ commonCount: number;
21
29
  }
22
30
 
23
31
  /**
@@ -35,6 +43,7 @@ export class AutocompleteEngine {
35
43
  query: '',
36
44
  results: [],
37
45
  selectedIndex: 0,
46
+ commonCount: 0,
38
47
  };
39
48
 
40
49
  constructor(private registry: CommandRegistry) {}
@@ -47,6 +56,22 @@ export class AutocompleteEngine {
47
56
  this.state.query = query;
48
57
  this.state.results = this.registry.fuzzyMatch(query);
49
58
  this.state.active = true;
59
+ // UX-C: fuzzyMatch('') sorts the curated common tier first (score 2),
60
+ // then the alphabetical rest (score 1) — both tiers alphabetical within
61
+ // themselves. Count the leading common run to tell the renderer where to
62
+ // draw the separator. A non-empty query means every command was scored on
63
+ // its actual match quality, not tier, so the common/rest split does not
64
+ // apply — the resulting 0 is not a real answer to "how many are common"
65
+ // and is unused because renderAutocompleteOverlay itself gates on the
66
+ // query string too, but zeroing here keeps the field internally honest.
67
+ let commonCount = 0;
68
+ if (query === '') {
69
+ for (const result of this.state.results) {
70
+ if (!COMMON_COMMAND_NAMES.has(result.command.name)) break;
71
+ commonCount++;
72
+ }
73
+ }
74
+ this.state.commonCount = commonCount;
50
75
  // Clamp selection to new result count
51
76
  if (this.state.selectedIndex >= this.state.results.length) {
52
77
  this.state.selectedIndex = Math.max(0, this.state.results.length - 1);
@@ -79,6 +104,7 @@ export class AutocompleteEngine {
79
104
  query: '',
80
105
  results: [],
81
106
  selectedIndex: 0,
107
+ commonCount: 0,
82
108
  };
83
109
  }
84
110
 
@@ -7,7 +7,8 @@ import type { ToolRegistry } from '@pellux/goodvibes-sdk/platform/tools';
7
7
  import type { PermissionRequestHandler } from '@pellux/goodvibes-sdk/platform/permissions';
8
8
  import type { SelectionItem, SelectionResult, SelectionAction } from './selection-modal.ts';
9
9
  import type { FileUndoManager } from '@pellux/goodvibes-sdk/platform/state';
10
- import type { PanelManager } from '../panels/panel-manager.ts';
10
+ import type { WorkspaceCheckpointManager } from '@pellux/goodvibes-sdk/platform/workspace';
11
+ import type { PanelManager, PanelDeepLinkTarget } from '../panels/panel-manager.ts';
11
12
  import type { KeybindingsManager } from './keybindings.ts';
12
13
  import type { OnboardingWizardMode } from './onboarding/onboarding-wizard.ts';
13
14
  import type { OpenOnboardingWizardOptions } from './handler-ui-state.ts';
@@ -47,6 +48,13 @@ export interface CommandRuntimeState {
47
48
  systemPrompt: string;
48
49
  reasoningEffort: string;
49
50
  sessionId: string;
51
+ /**
52
+ * Cumulative count of direct terminal writes the output guard intercepted
53
+ * this session — surfaced by /debug. Optional so the SDK's MutableRuntimeState
54
+ * (which has no such field) stays assignable; the guard sets it lazily on the
55
+ * shared runtime object. (UX-B item 1a.)
56
+ */
57
+ terminalWritesIntercepted?: number;
50
58
  }
51
59
 
52
60
  /**
@@ -68,6 +76,9 @@ export interface CommandUiActions {
68
76
  };
69
77
  executeCommand?: (name: string, args: string[]) => Promise<boolean>;
70
78
  cancelGeneration?: () => void;
79
+ /** True while an LLM turn is actively streaming. Used to give Escape
80
+ * cancel-turn precedence over a focused panel's own escape handling. */
81
+ isGenerating?: () => boolean;
71
82
  completeModelSelection?: (selection: {
72
83
  model: { id: string; provider: string; displayName: string; registryKey: string };
73
84
  effort: string;
@@ -75,6 +86,14 @@ export interface CommandUiActions {
75
86
  /** Which config target to write the selected model to. Defaults to 'main'. */
76
87
  target?: import('./model-picker.ts').ModelPickerTarget;
77
88
  }) => void;
89
+ /**
90
+ * Commit an embedding-provider selection from the model picker's
91
+ * 'embeddingProvider' mode (the 'embeddings' target). Deliberately separate
92
+ * from completeModelSelection — embedding providers are not
93
+ * ModelDefinition-shaped, so routing them through the same commit path
94
+ * would require fabricating a fake model object.
95
+ */
96
+ completeEmbeddingProviderSelection?: (providerId: string) => void;
78
97
  clearScreen?: () => void;
79
98
  activatePlan?: (planId: string, task: string) => void;
80
99
  requestPermission?: PermissionRequestHandler;
@@ -103,16 +122,32 @@ export interface CommandShellUiOpeners {
103
122
  openSelection?: (
104
123
  title: string,
105
124
  items: SelectionItem[],
106
- opts: { preSelectId?: string; allowSearch?: boolean; customActions?: Map<string, SelectionAction> } | undefined,
125
+ opts: { preSelectId?: string; allowSearch?: boolean; customActions?: Map<string, SelectionAction>; primaryVerbLabel?: string } | undefined,
107
126
  callback: (result: SelectionResult | null) => void,
108
127
  ) => void;
109
128
  openSettingsModal?: (target?: string) => void;
129
+ /**
130
+ * Open a MIGRATE-TO-MODAL surface by name (W6.1 purge skeleton — WO-A/B
131
+ * command runtimes call this instead of openCommandPanel once their panel
132
+ * is converted to a ModalFactory config). Threaded from ui-openers.ts the
133
+ * same way openSettingsModal is.
134
+ */
135
+ openModal?: (name: string) => void;
110
136
  openSessionPicker?: () => void;
111
137
  openProfilePicker?: () => void;
112
138
  openShortcutsOverlay?: () => void;
113
139
  getScrollTop?: () => number;
114
140
  openPanelPicker?: () => void;
115
- showPanel?: (panelId: string, pane?: 'top' | 'bottom') => void;
141
+ /**
142
+ * Open (and optionally focus) a panel. UX-C focus rule: the command path is
143
+ * "the user is mid-command-flow" — opening a panel this way leaves keyboard
144
+ * focus in the composer by default. Pass `{ focus: true }` for a caller that
145
+ * genuinely wants to grab focus (chords use panelManager.focusPanels()
146
+ * directly instead of this method, so no current call site needs it — but
147
+ * the intent is explicit rather than implicit here). `target` (DEBT-5) is a
148
+ * fleet deep-link jump target forwarded to PanelManager.open.
149
+ */
150
+ showPanel?: (panelId: string, pane?: 'top' | 'bottom', target?: PanelDeepLinkTarget, opts?: { focus?: boolean }) => void;
116
151
  focusPanels?: () => void;
117
152
  focusPrompt?: () => void;
118
153
  openOpsPanel?: () => void;
@@ -157,6 +192,22 @@ export interface CommandSessionServices {
157
192
  * fresh Orchestrator's zeroed default (W0.9).
158
193
  */
159
194
  readonly hydrateSessionUsage?: () => void;
195
+ /** Wave 4 (wo703): the orchestration engine's command-facing facade — see runtime/workstream-services.ts. */
196
+ readonly workstreamEngine?: import('../runtime/workstream-services.ts').WorkstreamCommandService;
197
+ /** Wave 5 (wo804): the repo source-tree code index — see runtime/code-index-services.ts. */
198
+ readonly codeIndexStore?: import('@pellux/goodvibes-sdk/platform/state').CodeIndexStore;
199
+ /** Wave-5 Stage B: tool-site reindex scheduler — `/codebase status` reports its last activity. */
200
+ readonly codeIndexReindexScheduler?: import('@pellux/goodvibes-sdk/platform/state').CodeIndexReindexScheduler;
201
+ /** Wave-5 Stage B: whether the (default-off) `agent-passive-code-injection` flag is on — for `/codebase status`. */
202
+ readonly isPassiveCodeInjectionFlagEnabled?: () => boolean;
203
+ /**
204
+ * Wave 5 (wo805): the MAIN interactive session's per-turn passive-injection
205
+ * honesty ring — `Orchestrator.getTurnInjections()`, the main-session
206
+ * counterpart to `AgentRecord.turnInjections` (wo801). `/recall injections`
207
+ * with no agent id reads this. Optional so command contexts built without an
208
+ * orchestrator (headless, tests) fall back to the honest empty state.
209
+ */
210
+ readonly getMainSessionTurnInjections?: () => readonly import('../renderer/turn-injection.ts').TurnInjectionEntry[];
160
211
  }
161
212
 
162
213
  export interface CommandProviderServices {
@@ -173,6 +224,7 @@ export interface CommandProviderServices {
173
224
  export interface CommandWorkspaceUiServices {
174
225
  keybindingsManager?: KeybindingsManager;
175
226
  fileUndoManager?: FileUndoManager;
227
+ workspaceCheckpointManager?: WorkspaceCheckpointManager;
176
228
  panelManager?: PanelManager;
177
229
  profileManager?: import('@pellux/goodvibes-sdk/platform/profiles').ProfileManager;
178
230
  bookmarkManager?: import('@pellux/goodvibes-sdk/platform/bookmarks').BookmarkManager;
@@ -190,6 +242,11 @@ export interface CommandPlatformConfigServices {
190
242
  readonly configManager: ConfigManager;
191
243
  readonly voiceProviderRegistry?: VoiceProviderRegistry;
192
244
  readonly voiceService?: VoiceService;
245
+ /** Direct-command consumer (`/search`) alongside the existing agent-tool consumer. */
246
+ readonly webSearchService?: import('@pellux/goodvibes-sdk/platform/web-search').WebSearchService;
247
+ /** Direct-command consumer (`/image`) — first production caller of `.generate()`. */
248
+ readonly mediaProviders?: import('@pellux/goodvibes-sdk/platform/media').MediaProviderRegistry;
249
+ readonly artifactStore?: import('@pellux/goodvibes-sdk/platform/artifacts').ArtifactStore;
193
250
  }
194
251
 
195
252
  export interface CommandPlatformServices
@@ -202,7 +259,7 @@ export interface CommandOpsServices
202
259
  export interface CommandExtensionRegistryServices {
203
260
  readonly toolRegistry: ToolRegistry;
204
261
  readonly mcpRegistry: McpRegistry;
205
- readonly evalRegistry?: import('../panels/eval-panel.ts').EvalRegistry;
262
+ readonly evalRegistry?: import('../panels/eval-registry.ts').EvalRegistry;
206
263
  }
207
264
 
208
265
  export interface CommandExtensionServices
@@ -235,6 +292,23 @@ export interface CommandContext
235
292
  };
236
293
  }
237
294
 
295
+ /**
296
+ * UX-C palette curation (item 4): the "common" first tier the slash-command
297
+ * autocomplete dropdown shows before the alphabetical rest when it opens with
298
+ * no filter typed yet (bare '/') — the "132-command palette unranked"
299
+ * evaluator finding. Typed filtering (any non-empty query) is completely
300
+ * unaffected — fuzzyMatch still searches every registered command exactly as
301
+ * before; this only reorders the empty-query case. Curated for breadth across
302
+ * the product's main workflows rather than raw usage frequency: help/config
303
+ * (orientation), panel/model (workspace + provider), recall/codebase/search
304
+ * (knowledge), workstream/checkpoint (control-plane), imagine (generation),
305
+ * sessions (continuity), quit (exit).
306
+ */
307
+ export const COMMON_COMMAND_NAMES: ReadonlySet<string> = new Set([
308
+ 'help', 'config', 'panel', 'model', 'recall', 'codebase',
309
+ 'workstream', 'checkpoint', 'search', 'imagine', 'sessions', 'quit',
310
+ ]);
311
+
238
312
  /**
239
313
  * SlashCommand - A single slash command definition.
240
314
  */
@@ -337,7 +411,12 @@ export class CommandRegistry {
337
411
  }
338
412
 
339
413
  if (bestScore > 0 || q === '') {
340
- results.push({ command: cmd, score: q === '' ? 1 : bestScore });
414
+ // UX-C: with no query yet, rank the curated common tier (score 2)
415
+ // ahead of everything else (score 1) — the tie-break below then sorts
416
+ // each tier alphabetically, so the result is "common tier, then the
417
+ // alphabetical rest" rather than one flat alphabetical list.
418
+ const score = q === '' ? (COMMON_COMMAND_NAMES.has(cmd.name) ? 2 : 1) : bestScore;
419
+ results.push({ command: cmd, score });
341
420
  }
342
421
  }
343
422