@pellux/goodvibes-tui 1.16.0 → 1.17.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 (220) hide show
  1. package/CHANGELOG.md +95 -9
  2. package/README.md +1 -1
  3. package/docs/foundation-artifacts/operator-contract.json +1091 -271
  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/parser.ts +5 -0
  12. package/src/cli/status.ts +13 -0
  13. package/src/cli/surface-command.ts +5 -5
  14. package/src/cli/tui-startup.ts +60 -97
  15. package/src/cli/types.ts +2 -0
  16. package/src/config/index.ts +1 -1
  17. package/src/config/tui-extension-settings.ts +33 -0
  18. package/src/core/composer-state.ts +1 -1
  19. package/src/core/conversation-line-cache.ts +3 -3
  20. package/src/core/conversation-rendering.ts +4 -4
  21. package/src/core/conversation.ts +2 -2
  22. package/src/core/long-task-notifier.ts +7 -7
  23. package/src/core/stream-event-wiring.ts +8 -6
  24. package/src/core/system-message-noise.ts +1 -1
  25. package/src/core/system-message-router.ts +6 -6
  26. package/src/core/turn-cancellation.ts +1 -1
  27. package/src/core/turn-event-wiring.ts +8 -8
  28. package/src/daemon/cli.ts +20 -5
  29. package/src/daemon/lifecycle.ts +41 -0
  30. package/src/daemon/service-commands.ts +5 -5
  31. package/src/export/cost-utils.ts +128 -6
  32. package/src/input/autocomplete.ts +2 -2
  33. package/src/input/command-registry.ts +34 -9
  34. package/src/input/commands/ci-runtime.ts +5 -1
  35. package/src/input/commands/codebase-runtime.ts +6 -6
  36. package/src/input/commands/cost-runtime.ts +1 -1
  37. package/src/input/commands/health-runtime.ts +2 -2
  38. package/src/input/commands/intelligence-runtime.ts +1 -1
  39. package/src/input/commands/local-auth-runtime.ts +1 -1
  40. package/src/input/commands/local-provider-runtime.ts +49 -10
  41. package/src/input/commands/local-runtime.ts +53 -5
  42. package/src/input/commands/memory.ts +2 -2
  43. package/src/input/commands/operator-panel-runtime.ts +5 -5
  44. package/src/input/commands/permissions-provenance.ts +1 -1
  45. package/src/input/commands/permissions-runtime.ts +99 -8
  46. package/src/input/commands/planning-runtime.ts +3 -3
  47. package/src/input/commands/platform-sandbox-runtime.ts +1 -1
  48. package/src/input/commands/provider-accounts-runtime.ts +71 -6
  49. package/src/input/commands/provider.ts +7 -5
  50. package/src/input/commands/recall-files-git.ts +13 -51
  51. package/src/input/commands/recall-files.ts +8 -11
  52. package/src/input/commands/recall-review.ts +2 -2
  53. package/src/input/commands/relay-runtime.ts +1 -1
  54. package/src/input/commands/remote-runtime.ts +1 -1
  55. package/src/input/commands/runtime-services.ts +4 -4
  56. package/src/input/commands/schedule-runtime.ts +14 -7
  57. package/src/input/commands/services-runtime.ts +1 -1
  58. package/src/input/commands/session-workflow.ts +2 -2
  59. package/src/input/commands/settings-sync-runtime.ts +2 -2
  60. package/src/input/commands/share-runtime.ts +1 -1
  61. package/src/input/commands/shell-core.ts +46 -68
  62. package/src/input/commands/subscription-runtime.ts +1 -1
  63. package/src/input/commands/update-runtime.ts +153 -96
  64. package/src/input/commands/work-plan-runtime.ts +1 -1
  65. package/src/input/commands/workstream-runtime.ts +2 -2
  66. package/src/input/commands.ts +0 -2
  67. package/src/input/config-modal-types.ts +3 -3
  68. package/src/input/config-modal.ts +10 -11
  69. package/src/input/feature-unit-layout.ts +93 -82
  70. package/src/input/feed-context-factory.ts +2 -2
  71. package/src/input/handler-command-route.ts +2 -2
  72. package/src/input/handler-content-actions.ts +1 -1
  73. package/src/input/handler-feed-routes.ts +7 -7
  74. package/src/input/handler-feed.ts +4 -4
  75. package/src/input/handler-interactions.ts +3 -2
  76. package/src/input/handler-modal-routes.ts +12 -4
  77. package/src/input/handler-modal-token-routes.ts +1 -1
  78. package/src/input/handler-onboarding-daemon-adopt.ts +2 -2
  79. package/src/input/handler-onboarding.ts +3 -2
  80. package/src/input/handler-picker-routes.ts +26 -13
  81. package/src/input/handler-shortcuts.ts +6 -6
  82. package/src/input/handler.ts +2 -2
  83. package/src/input/keybindings.ts +2 -2
  84. package/src/input/onboarding/onboarding-feature-units.ts +34 -27
  85. package/src/input/onboarding/onboarding-provider-key-fields.ts +112 -0
  86. package/src/input/onboarding/onboarding-wizard-apply.ts +22 -21
  87. package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +17 -0
  88. package/src/input/onboarding/onboarding-wizard-network-adopt.ts +2 -2
  89. package/src/input/onboarding/onboarding-wizard-steps.ts +30 -42
  90. package/src/input/panel-integration-actions.ts +2 -2
  91. package/src/input/panel-mouse-geometry.ts +1 -1
  92. package/src/input/panel-paste-flood-guard.ts +1 -1
  93. package/src/input/provider-key-intake.ts +167 -0
  94. package/src/input/sandbox-exec-config.ts +2 -2
  95. package/src/input/selection-modal.ts +1 -1
  96. package/src/input/settings-modal-activation.ts +5 -0
  97. package/src/input/settings-modal-adjustment.ts +4 -31
  98. package/src/input/settings-modal-data.ts +22 -21
  99. package/src/input/settings-modal-mutations.ts +12 -68
  100. package/src/input/settings-modal-types.ts +23 -16
  101. package/src/input/settings-modal.ts +74 -55
  102. package/src/main.ts +48 -48
  103. package/src/panels/agent-inspector-shared.ts +4 -4
  104. package/src/panels/base-panel.ts +2 -2
  105. package/src/panels/builtin/agent.ts +4 -4
  106. package/src/panels/builtin/development.ts +4 -4
  107. package/src/panels/builtin/knowledge.ts +1 -1
  108. package/src/panels/builtin/operations.ts +29 -10
  109. package/src/panels/builtin/session.ts +5 -5
  110. package/src/panels/builtin/shared.ts +4 -4
  111. package/src/panels/builtin-modals.ts +10 -10
  112. package/src/panels/builtin-panels.ts +1 -1
  113. package/src/panels/cost-tracker-panel.ts +124 -7
  114. package/src/panels/diff-panel.ts +5 -5
  115. package/src/panels/eval-registry.ts +2 -2
  116. package/src/panels/fleet-deep-link.ts +1 -1
  117. package/src/panels/fleet-panel-format.ts +44 -8
  118. package/src/panels/fleet-panel.ts +16 -16
  119. package/src/panels/fleet-read-model.ts +52 -13
  120. package/src/panels/fleet-steer.ts +2 -2
  121. package/src/panels/fleet-stop.ts +3 -3
  122. package/src/panels/fleet-tabs.ts +13 -14
  123. package/src/panels/fleet-transcript.ts +5 -5
  124. package/src/panels/git-panel.ts +2 -2
  125. package/src/panels/index.ts +4 -1
  126. package/src/panels/local-auth-panel.ts +1 -1
  127. package/src/panels/modals/hooks-modal.ts +1 -1
  128. package/src/panels/modals/keybindings-modal.ts +2 -2
  129. package/src/panels/modals/knowledge-modal.ts +1 -1
  130. package/src/panels/modals/marketplace-modal.ts +4 -4
  131. package/src/panels/modals/memory-modal.ts +4 -4
  132. package/src/panels/modals/modal-surface-helpers.ts +1 -1
  133. package/src/panels/modals/modal-theme.ts +1 -1
  134. package/src/panels/modals/pairing-modal.ts +2 -2
  135. package/src/panels/modals/planning-modal.ts +2 -2
  136. package/src/panels/modals/plugins-modal.ts +1 -1
  137. package/src/panels/modals/policy-modal.ts +1 -1
  138. package/src/panels/modals/security-modal.ts +1 -1
  139. package/src/panels/modals/skills-modal.ts +1 -1
  140. package/src/panels/modals/work-plan-modal.ts +3 -3
  141. package/src/panels/notifications-feed.ts +127 -0
  142. package/src/panels/notifications-panel.ts +160 -0
  143. package/src/panels/panel-manager.ts +9 -9
  144. package/src/panels/plugins-panel.ts +4 -4
  145. package/src/panels/project-planning-answer-actions.ts +1 -1
  146. package/src/panels/provider-health-routes.ts +2 -2
  147. package/src/panels/skills-panel.ts +4 -5
  148. package/src/panels/token-budget-panel.ts +11 -3
  149. package/src/panels/types.ts +5 -5
  150. package/src/permissions/broker-approval-card.ts +201 -0
  151. package/src/permissions/hunk-selection.ts +28 -4
  152. package/src/permissions/prompt.ts +244 -29
  153. package/src/renderer/autocomplete-overlay.ts +107 -54
  154. package/src/renderer/code-block.ts +2 -2
  155. package/src/renderer/compaction-history-modal.ts +1 -1
  156. package/src/renderer/compaction-preview.ts +1 -1
  157. package/src/renderer/compaction-quality.ts +1 -1
  158. package/src/renderer/compositor.ts +2 -2
  159. package/src/renderer/config-modal.ts +1 -1
  160. package/src/renderer/conversation-overlays.ts +1 -1
  161. package/src/renderer/footer-tips.ts +3 -3
  162. package/src/renderer/hint-grammar.ts +1 -1
  163. package/src/renderer/modal-factory.ts +1 -1
  164. package/src/renderer/model-picker-overlay.ts +1 -1
  165. package/src/renderer/model-workspace.ts +7 -2
  166. package/src/renderer/onboarding/onboarding-wizard.ts +22 -8
  167. package/src/renderer/panel-composite.ts +2 -2
  168. package/src/renderer/panel-workspace-bar.ts +1 -1
  169. package/src/renderer/process-indicator.ts +1 -1
  170. package/src/renderer/selection-modal-overlay.ts +102 -24
  171. package/src/renderer/session-picker-modal.ts +2 -2
  172. package/src/renderer/settings-modal-helpers.ts +14 -5
  173. package/src/renderer/settings-modal.ts +127 -39
  174. package/src/renderer/shell-surface.ts +3 -0
  175. package/src/renderer/terminal-bg-probe.ts +1 -1
  176. package/src/renderer/theme.ts +3 -3
  177. package/src/renderer/tool-call.ts +1 -1
  178. package/src/renderer/tool-result-summary.ts +1 -1
  179. package/src/renderer/turn-injection.ts +7 -6
  180. package/src/renderer/ui-factory.ts +6 -1
  181. package/src/runtime/bootstrap-command-context.ts +4 -1
  182. package/src/runtime/bootstrap-command-parts.ts +6 -4
  183. package/src/runtime/bootstrap-core.ts +35 -35
  184. package/src/runtime/bootstrap-hook-bridge.ts +1 -1
  185. package/src/runtime/bootstrap-shell.ts +6 -5
  186. package/src/runtime/bootstrap.ts +34 -32
  187. package/src/runtime/channel-composition.ts +46 -0
  188. package/src/runtime/code-index-services.ts +3 -4
  189. package/src/runtime/daemon-attach-notices.ts +120 -0
  190. package/src/runtime/durability-services.ts +55 -0
  191. package/src/runtime/embedded-service-factories.ts +40 -0
  192. package/src/runtime/feature-settings.ts +147 -0
  193. package/src/runtime/index.ts +4 -21
  194. package/src/runtime/legacy-daemon-migration.ts +29 -19
  195. package/src/runtime/memory-fold.ts +1 -1
  196. package/src/runtime/notification-dispatch.ts +110 -0
  197. package/src/runtime/onboarding/apply.ts +3 -29
  198. package/src/runtime/onboarding/fast-path.ts +39 -6
  199. package/src/runtime/onboarding/provider-key-capture.ts +126 -0
  200. package/src/runtime/onboarding/snapshot.ts +0 -1
  201. package/src/runtime/onboarding/types.ts +9 -3
  202. package/src/runtime/provider-fallback.ts +55 -0
  203. package/src/runtime/provider-optimizer-wiring.ts +55 -0
  204. package/src/runtime/release-artifacts.ts +19 -103
  205. package/src/runtime/resume-notice.ts +1 -1
  206. package/src/runtime/services.ts +81 -82
  207. package/src/runtime/session-continuity-hints.ts +38 -0
  208. package/src/runtime/surface-feature-flags.ts +40 -9
  209. package/src/runtime/terminal-output-guard.ts +2 -2
  210. package/src/runtime/trust/workspace-registration.ts +10 -2
  211. package/src/runtime/trust/workspace-trust.ts +58 -31
  212. package/src/runtime/ui-services.ts +3 -1
  213. package/src/runtime/update-check.ts +15 -69
  214. package/src/runtime/workstream-services.ts +2 -2
  215. package/src/shell/blocking-input.ts +102 -6
  216. package/src/shell/ui-openers.ts +38 -6
  217. package/src/utils/format-duration.ts +1 -1
  218. package/src/verification/verification-ledger.ts +21 -4
  219. package/src/version.ts +1 -1
  220. package/src/input/commands/flags-runtime.ts +0 -328
@@ -77,6 +77,8 @@ export interface UiPlatformServices {
77
77
  readonly sessionSpineActive?: boolean;
78
78
  readonly sessionSpineStatus?: 'unknown' | 'online' | 'offline';
79
79
  };
80
+ /** Undelivered daemon receipts (update/crash/migration) for the attach-time consuming read; empty when no daemon runs. */
81
+ collectDaemonReceipts(): readonly { readonly id: string; readonly text: string; readonly at: number }[];
80
82
  restart(): Promise<{
81
83
  readonly daemonRunning: boolean;
82
84
  readonly daemonPortInUse?: boolean;
@@ -113,7 +115,7 @@ export interface UiRuntimeSharedServices {
113
115
  readonly distributedRuntime: RuntimeServices['distributedRuntime'];
114
116
  readonly remoteRunnerRegistry: RuntimeServices['remoteRunnerRegistry'] & RemoteRunnerRegistry;
115
117
  readonly remoteSupervisor: RuntimeServices['remoteSupervisor'] & RemoteSupervisor;
116
- /** W2.2: the shared live process registry backing the Fleet panel. */
118
+ /** the shared live process registry backing the Fleet panel. */
117
119
  readonly processRegistry: RuntimeServices['processRegistry'];
118
120
  /** The shared runtime event bus — the Fleet panel subscribes to its 'communication' domain for the honest steer-consumed signal. */
119
121
  readonly runtimeBus: RuntimeServices['runtimeBus'];
@@ -1,79 +1,25 @@
1
1
  /**
2
2
  * Pure logic for `/update`: version comparison, the latest-release-tag
3
- * redirect lookup, and honest install-kind detection. No filesystem or
4
- * process side effects here — everything takes its inputs as arguments so
5
- * tests exercise it without touching the network or the host, matching the
6
- * repo rule that tests never compare against the live build VERSION and
7
- * never make live network calls.
3
+ * redirect lookup, and honest install-kind detection.
4
+ *
5
+ * Version comparison and the release-tag lookup are re-exported from the
6
+ * SDK's canonical update policy module (platform/runtime/self-update), which
7
+ * was hoisted from this file's semantics — one mechanism everywhere.
8
+ * Install-kind detection stays local: it encodes how THIS package is
9
+ * installed (compiled binary vs bun/npm package vs source run) and what
10
+ * command replaces a swap for each kind.
8
11
  *
9
12
  * The self-update download/verify/swap orchestration that USES these lives
10
13
  * in src/input/commands/update-runtime.ts; this module only decides "is
11
14
  * there a newer version" and "can this install be swapped in place".
12
15
  */
13
-
14
- export function normalizeVersion(version: string): string {
15
- return version.replace(/^v/i, '').trim();
16
- }
17
-
18
- /**
19
- * Compares two version strings component-wise as dotted non-negative
20
- * integers (a leading "v" is ignored, matching the "vX.Y.Z" release tag
21
- * format). Returns -1/0/1 for a<b / a==b / a>b. Non-numeric or missing
22
- * components are treated as 0, so "1.2" and "1.2.0" compare equal.
23
- */
24
- export function compareVersions(a: string, b: string): -1 | 0 | 1 {
25
- const partsA = normalizeVersion(a).split('.');
26
- const partsB = normalizeVersion(b).split('.');
27
- const length = Math.max(partsA.length, partsB.length);
28
- for (let i = 0; i < length; i++) {
29
- const x = Number.parseInt(partsA[i] ?? '0', 10) || 0;
30
- const y = Number.parseInt(partsB[i] ?? '0', 10) || 0;
31
- if (x < y) return -1;
32
- if (x > y) return 1;
33
- }
34
- return 0;
35
- }
36
-
37
- /**
38
- * Extracts the release tag from a GitHub "/releases/latest" redirect
39
- * Location header, e.g. "https://github.com/mgd34msu/goodvibes-tui/releases/tag/v1.13.2"
40
- * -> "v1.13.2". Mirrors scripts/install.sh's own redirect-URL tag parsing
41
- * (it takes everything after the final slash of the redirect target).
42
- */
43
- export function parseReleaseTagFromLocation(location: string | null | undefined): string | null {
44
- if (!location) return null;
45
- const segments = location.split('/').filter((segment) => segment.length > 0);
46
- const tag = segments[segments.length - 1];
47
- return tag && tag.length > 0 ? tag : null;
48
- }
49
-
50
- /** Minimal shape of the subset of the fetch API this module needs, so tests inject a stub instead of the real network. */
51
- export interface UpdateFetchLike {
52
- (url: string, init?: { method?: string; redirect?: 'manual' | 'follow' | 'error' }): Promise<{
53
- readonly ok: boolean;
54
- readonly status: number;
55
- readonly url: string;
56
- readonly headers: { get(name: string): string | null };
57
- text(): Promise<string>;
58
- arrayBuffer(): Promise<ArrayBuffer>;
59
- }>;
60
- }
61
-
62
- /**
63
- * Resolves the latest release tag the same way scripts/install.sh does:
64
- * a HEAD request with redirects NOT followed, reading the tag out of the
65
- * response's redirect Location header. Throws if no tag can be resolved —
66
- * callers must not silently fall back to "already current".
67
- */
68
- export async function resolveLatestReleaseTag(fetchImpl: UpdateFetchLike, releasesLatestUrl: string): Promise<string> {
69
- const response = await fetchImpl(releasesLatestUrl, { method: 'HEAD', redirect: 'manual' });
70
- const location = response.headers.get('location');
71
- const tag = parseReleaseTagFromLocation(location) ?? (response.url !== releasesLatestUrl ? parseReleaseTagFromLocation(response.url) : null);
72
- if (!tag) {
73
- throw new Error(`could not resolve the latest release tag from ${releasesLatestUrl} (no redirect Location header)`);
74
- }
75
- return tag;
76
- }
16
+ export {
17
+ compareVersions,
18
+ normalizeVersion,
19
+ parseReleaseTagFromLocation,
20
+ resolveLatestReleaseTag,
21
+ type UpdateFetchLike,
22
+ } from '@pellux/goodvibes-sdk/platform/runtime/self-update';
77
23
 
78
24
  /**
79
25
  * How this running process was installed, detected honestly from
@@ -2,7 +2,7 @@
2
2
  // workstream-services.ts — phase/work-item orchestration engine
3
3
  //
4
4
  // Constructs the TUI's OrchestrationEngine instance (@pellux/goodvibes-sdk/
5
- // platform/orchestration, landed on SDK main as wo701/W4.1) and a thin
5
+ // platform/orchestration, landed on SDK main as) and a thin
6
6
  // command-facing facade around it. Extracted into its own module rather than
7
7
  // built inline in services.ts: services.ts sits at the architecture check's
8
8
  // 800-line cap (scripts/check-architecture.ts), so any new service gets its
@@ -17,7 +17,7 @@
17
17
  // (input/commands/workstream-runtime.ts) needs a create -> propose -> approve
18
18
  // -> launch flow (Pillar-3 doctrine: render the plan in the transcript before
19
19
  // spending anything, mirroring /plan's approve step). REALITY-WINS DIVERGENCE
20
- // from the wo703 design brief: the brief recommended an ENGINE-owned draft; the
20
+ // from the design brief: the brief recommended an ENGINE-owned draft; the
21
21
  // real engine (verified against the linked SDK build) exposes no pre-creation
22
22
  // draft concept at all, so WorkstreamDraft below is TUI-owned state, held on
23
23
  // this module's facade instance (constructed once, threaded onto
@@ -1,5 +1,5 @@
1
1
  import type { ConversationManager } from '../core/conversation';
2
- import type { PermissionRequest } from '@pellux/goodvibes-sdk/platform/permissions';
2
+ import type { PermissionRequest, RememberTier } from '@pellux/goodvibes-sdk/platform/permissions';
3
3
  import type { SessionSnapshot } from '@/runtime/index.ts';
4
4
  import type { SystemMessageRouter } from '../core/system-message-router.ts';
5
5
  import type { ConversationMessageSnapshot } from '@pellux/goodvibes-sdk/platform/core';
@@ -7,11 +7,20 @@ import { replayJournalForSession } from '../core/session-recovery.ts';
7
7
  import { applyHunkKey, buildModifiedEditArgs, type HunkSelectionState } from '../permissions/hunk-selection.ts';
8
8
 
9
9
  export type PendingPermissionState = PermissionRequest & {
10
- resolve: (approved: boolean, remember?: boolean, modifiedArgs?: Record<string, unknown>) => void;
10
+ resolve: (approved: boolean, remember?: boolean, modifiedArgs?: Record<string, unknown>, extras?: { rememberTier?: RememberTier; reason?: string }) => void;
11
11
  /** Present only when isHunkSelectable(request) was true when the prompt was opened. */
12
12
  hunkState?: HunkSelectionState;
13
- /** True once the user pressed `d` to expand a condensed low-risk card. (UX-B 2b.) */
13
+ /** True once the user pressed `d` to expand a condensed low-risk card. (2b.) */
14
14
  detailsExpanded?: boolean;
15
+ /**
16
+ * Active typed-reply mode: 'deny-reason' (started by typing while the card
17
+ * is up — Enter denies with the typed reason as feedback) or 'exec-answer'
18
+ * (an exec-prompt ask; Enter approves with the typed answer feeding the
19
+ * running command's stdin). Undefined = plain card keys.
20
+ */
21
+ replyMode?: 'deny-reason' | 'exec-answer';
22
+ /** The reply draft while replyMode is active. */
23
+ replyBuffer?: string;
15
24
  /**
16
25
  * Epoch ms when the prompt first appeared. Keystrokes that arrive within
17
26
  * APPROVAL_INPUT_DEBOUNCE_MS of this are swallowed (not interpreted as an
@@ -135,6 +144,69 @@ export function handleBlockingShellInput(
135
144
  return { handled: true, pendingPermission: { ...req, hunkState: state }, recoveryPending };
136
145
  }
137
146
 
147
+ // Ctrl+C is ALWAYS the hard abort: deny and kill the turn, in every mode.
148
+ if (data === '\x03') {
149
+ req.resolve(false, false);
150
+ abortTurn();
151
+ render();
152
+ return { handled: true, pendingPermission: null, recoveryPending };
153
+ }
154
+
155
+ // Typed-reply mode: deny-with-reason (started by typing on any card) or
156
+ // exec-answer (an exec-prompt ask opens in this mode). Every printable
157
+ // key is text here — nothing is a card command.
158
+ if (req.replyMode) {
159
+ const buffer = req.replyBuffer ?? '';
160
+ if (data === '\r' || data === '\n') {
161
+ if (req.replyMode === 'exec-answer') {
162
+ // The typed answer feeds the running command's stdin via the
163
+ // decision's modifiedArgs (SDK exec-prompt wiring contract).
164
+ req.resolve(true, false, { answer: buffer });
165
+ } else {
166
+ // Deny is feedback: the reason rides the structured "user declined"
167
+ // tool result so the model can adapt — the turn is NOT aborted.
168
+ req.resolve(false, false, undefined, { reason: buffer });
169
+ }
170
+ render();
171
+ return { handled: true, pendingPermission: null, recoveryPending };
172
+ }
173
+ if (data === '\x1b') {
174
+ if (req.replyMode === 'exec-answer') {
175
+ // Esc on an exec-prompt: clear a draft first; with nothing typed it
176
+ // declines the prompt (the run gets the honest unanswered result).
177
+ if (buffer.length > 0) {
178
+ render();
179
+ return { handled: true, pendingPermission: { ...req, replyBuffer: '' }, recoveryPending };
180
+ }
181
+ req.resolve(false, false);
182
+ render();
183
+ return { handled: true, pendingPermission: null, recoveryPending };
184
+ }
185
+ // Esc leaves deny-reason mode back to the plain card.
186
+ render();
187
+ return { handled: true, pendingPermission: { ...req, replyMode: undefined, replyBuffer: undefined }, recoveryPending };
188
+ }
189
+ if (data === '\x7f' || data === '\b') {
190
+ render();
191
+ return { handled: true, pendingPermission: { ...req, replyBuffer: buffer.slice(0, -1) }, recoveryPending };
192
+ }
193
+ if (data.length >= 1 && !data.startsWith('\x1b') && data >= ' ') {
194
+ render();
195
+ return { handled: true, pendingPermission: { ...req, replyBuffer: buffer + data }, recoveryPending };
196
+ }
197
+ render();
198
+ return { handled: true, pendingPermission, recoveryPending };
199
+ }
200
+
201
+ // Scroll, mouse, PageUp/Down, arrow, and panel-navigation keys — plus a
202
+ // bare Esc — are not card answers. Pass them through to the normal input
203
+ // handler so the transcript stays scrollable and Esc only drops focus. The
204
+ // request stays pending (answer it with y/n or a remember tier when ready);
205
+ // Ctrl+C above is still the hard abort, and 'n' still denies.
206
+ if (data.startsWith('\x1b')) {
207
+ return { handled: false, pendingPermission, recoveryPending };
208
+ }
209
+
138
210
  const key = data.toLowerCase().trim();
139
211
 
140
212
  if (key === 'y') {
@@ -144,14 +216,31 @@ export function handleBlockingShellInput(
144
216
  }
145
217
 
146
218
  if (key === 'a') {
147
- req.resolve(true, true);
219
+ // Legacy always-this-session choice — the 'session' remember tier.
220
+ req.resolve(true, true, undefined, { rememberTier: 'session' });
148
221
  render();
149
222
  return { handled: true, pendingPermission: null, recoveryPending };
150
223
  }
151
224
 
152
- if (key === 'n' || data === '\x1b' || data === '\x03') {
225
+ // Numbered remember tiers ([1]..[N], most specific first) from the SDK's
226
+ // rememberOptions: approve AND remember at that tier. A generalizing tier
227
+ // writes a durable user-origin rule; 'session' only caches in memory.
228
+ if (/^[1-9]$/.test(key) && req.rememberOptions && req.rememberOptions.length > 0) {
229
+ const option = req.rememberOptions[Number(key) - 1];
230
+ if (option) {
231
+ req.resolve(true, option.tier === 'session', undefined, { rememberTier: option.tier });
232
+ render();
233
+ return { handled: true, pendingPermission: null, recoveryPending };
234
+ }
235
+ render();
236
+ return { handled: true, pendingPermission, recoveryPending };
237
+ }
238
+
239
+ if (key === 'n') {
240
+ // Plain deny — feedback to the model (the SDK renders an honest "user
241
+ // declined" result), never a turn abort. Ctrl+C above is the hard stop;
242
+ // Esc drops focus (handled above) rather than denying.
153
243
  req.resolve(false, false);
154
- abortTurn();
155
244
  render();
156
245
  return { handled: true, pendingPermission: null, recoveryPending };
157
246
  }
@@ -166,6 +255,13 @@ export function handleBlockingShellInput(
166
255
  };
167
256
  }
168
257
 
258
+ // Any other printable key starts deny-with-reason: what the user types
259
+ // becomes the denial feedback, seeded with this first character.
260
+ if (data.length === 1 && data >= ' ') {
261
+ render();
262
+ return { handled: true, pendingPermission: { ...req, replyMode: 'deny-reason', replyBuffer: data }, recoveryPending };
263
+ }
264
+
169
265
  render();
170
266
  return { handled: true, pendingPermission, recoveryPending };
171
267
  }
@@ -19,6 +19,8 @@ import { syncServiceSettingToPlatform } from './service-settings-sync.ts';
19
19
  import { setActiveThemeMode } from '../renderer/theme.ts';
20
20
  import { THEME_MODE_CONFIG_KEY, coerceThemeModeSetting } from '../renderer/theme-mode-config.ts';
21
21
  import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
22
+ import { buildFirstOpenItems, decodeFirstOpenChoice, selfRecordWorkspaceRegistration } from '../cli/tui-startup.ts';
23
+ import type { WorkspaceTrustLevel } from '../runtime/trust/workspace-trust.ts';
22
24
 
23
25
  type WireShellUiOpenersOptions = {
24
26
  commandContext: CommandContext;
@@ -33,13 +35,15 @@ type WireShellUiOpenersOptions = {
33
35
  subscriptionManager: SubscriptionManager;
34
36
  secretsManager?: Pick<SecretsManager, 'delete' | 'get' | 'set'>;
35
37
  serviceRegistry: Pick<ServiceInspectionQuery, 'getAll'>;
36
- /** B29: backs the model picker's 'embeddings' target and its own item-list mode. */
38
+ /** Backs the model picker's 'embeddings' target and its own item-list mode. */
37
39
  memoryEmbeddingRegistry: Pick<MemoryEmbeddingProviderRegistry, 'getDefaultProviderId' | 'setDefaultProvider' | 'status'>;
38
40
  workingDirectory: string;
39
41
  homeDirectory: string;
40
42
  getConfiguredProviderIds: () => string[];
41
43
  getPinned: () => Promise<string[]>;
42
44
  render: () => void;
45
+ /** Trust-at-consequence-time bridge — patched here with the real modal-driving implementation. */
46
+ trustPromptRef: { requestTrustDecision: () => Promise<WorkspaceTrustLevel> };
43
47
  };
44
48
 
45
49
  let commandCategoryMapCache: Map<string, string> | null = null;
@@ -123,8 +127,25 @@ export function wireShellUiOpeners(options: WireShellUiOpenersOptions): void {
123
127
  getConfiguredProviderIds,
124
128
  getPinned,
125
129
  render,
130
+ trustPromptRef,
126
131
  } = options;
127
132
 
133
+ // Trust-at-consequence-time: raised by trustGatedAsk on the first non-read
134
+ // request in an undecided workspace; the answer persists via setLevel().
135
+ // Registration self-records here too when trusted (never for restricted —
136
+ // see selfRecordWorkspaceRegistration's doc).
137
+ trustPromptRef.requestTrustDecision = () =>
138
+ new Promise((resolve) => {
139
+ const { title, items } = buildFirstOpenItems();
140
+ if (!commandContext.openSelection) { resolve('restricted'); return; } // no selection surface (should not happen live)
141
+ commandContext.openSelection(title, items, { allowSearch: false, primaryVerbLabel: 'Choose' }, (result) => {
142
+ const level = decodeFirstOpenChoice(result?.item.id ?? null);
143
+ resolve(level);
144
+ if (level === 'trusted') void selfRecordWorkspaceRegistration(commandContext.workspace?.workspaceRegistrationManager);
145
+ render();
146
+ });
147
+ });
148
+
128
149
  /**
129
150
  * Pre-resolve which provider IDs have secrets-manager keys (async batch, SDK tier pattern).
130
151
  * Returns a set of provider IDs (not env var names) that are secrets-backed.
@@ -248,6 +269,13 @@ export function wireShellUiOpeners(options: WireShellUiOpenersOptions): void {
248
269
  };
249
270
 
250
271
  commandContext.openModelPicker = () => {
272
+ // Picker-open re-check: re-verify each provider's live model list (TTL-
273
+ // respecting) so a freshly-opened picker reflects models the provider
274
+ // started or stopped serving. Fire-and-forget; a completed refresh re-renders
275
+ // so the list updates in place without blocking the open.
276
+ void providerRegistry.refreshLiveModelDiscovery?.().then((reports) => {
277
+ if (reports.some((report) => report.added.length > 0 || report.removed.length > 0)) render();
278
+ }).catch(() => {});
251
279
  void (async () => {
252
280
  // getSelectableModels() is catalog-driven and can list models whose
253
281
  // `provider` id (e.g. 'google', sourced from the pricing catalog) was never
@@ -358,7 +386,7 @@ export function wireShellUiOpeners(options: WireShellUiOpenersOptions): void {
358
386
  input.modalOpened('settings');
359
387
  input.settingsModal.open(configManager, featureFlags, subscriptionManager, serviceRegistry, mcpRegistry, secretsManager, {
360
388
  onSettingApplied: (change) => {
361
- // DEBT-2: forced dark/light applies immediately (rebuild palettes + full
389
+ // forced dark/light applies immediately (rebuild palettes + full
362
390
  // repaint); auto only re-probes at startup, so it takes effect next launch.
363
391
  if (String(change.key) === THEME_MODE_CONFIG_KEY) {
364
392
  const next = coerceThemeModeSetting(change.value);
@@ -379,7 +407,7 @@ export function wireShellUiOpeners(options: WireShellUiOpenersOptions): void {
379
407
  render();
380
408
  };
381
409
 
382
- // W6.1 (the purge): open a MIGRATE-TO-MODAL surface by name. Fed from both
410
+ // (the purge): open a MIGRATE-TO-MODAL surface by name. Fed from both
383
411
  // ctx.openModal(name) (migrated command front-doors) and PanelManager's
384
412
  // open()-time modal-redirect callback (a legacy panel id resolving to a modal
385
413
  // name). Surfaces are registered in builtin-modals.ts; a name with no
@@ -445,7 +473,7 @@ export function wireShellUiOpeners(options: WireShellUiOpenersOptions): void {
445
473
  return;
446
474
  }
447
475
  if (panelManager.getAllOpen().length === 0) {
448
- // W6.1 (the purge): 'panel-list' (a browse-all-panels picker PANEL)
476
+ // (the purge): 'panel-list' (a browse-all-panels picker PANEL)
449
477
  // was DELETE-disposition — a picker over a handful of panels is dead
450
478
  // weight now. Its replacement is this selection MODAL, built from the
451
479
  // live registry (PanelManager.getRegisteredTypes()) rather than a
@@ -475,6 +503,10 @@ export function wireShellUiOpeners(options: WireShellUiOpenersOptions): void {
475
503
  render();
476
504
  };
477
505
 
506
+ // Registry-driven surfaces built inside command handlers (/help) group by
507
+ // the same categories as the palette and the generated reference.
508
+ commandContext.getCommandCategories = () => getCommandCategoryMap();
509
+
478
510
  commandContext.openCommandPalette = () => {
479
511
  // The palette is built live from the command registry — never a hardcoded
480
512
  // list — so it can never drift from the real command set. Category labels
@@ -534,11 +566,11 @@ export function wireShellUiOpeners(options: WireShellUiOpenersOptions): void {
534
566
  };
535
567
 
536
568
  commandContext.showPanel = (panelId, pane, target, opts) => {
537
- // DEBT-5: forward the deep-link target so a jumped-to panel lands on the
569
+ // forward the deep-link target so a jumped-to panel lands on the
538
570
  // right row (fleet --target); undefined for ordinary opens.
539
571
  panelManager.open(panelId, pane, target);
540
572
  panelManager.show();
541
- // UX-C focus rule 1a: every registered caller of showPanel is a slash
573
+ // focus rule 1a: every registered caller of showPanel is a slash
542
574
  // command (/panel open, /routes, /approval, /tasks, /ops-control, ...) —
543
575
  // the command path leaves focus in the composer ("the user is
544
576
  // mid-command-flow") unless the caller explicitly asks to grab it.
@@ -6,7 +6,7 @@
6
6
  * formatElapsed floors to integer seconds and lacks the null/'?ms' guards
7
7
  * that the latency panels require.
8
8
  *
9
- * W6.1 (the purge): formatShortDuration (used only by incident-review-panel
9
+ * (the purge): formatShortDuration (used only by incident-review-panel
10
10
  * and eval-panel, both removed — RETIRE-INTO-FLEET and DELETE respectively)
11
11
  * was removed as a genuinely orphaned export — no remaining caller anywhere
12
12
  * in src/.
@@ -1,7 +1,8 @@
1
1
  import { readFileSync, readdirSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { CONFIG_SCHEMA } from '@pellux/goodvibes-sdk/platform/config';
4
- import { FEATURE_FLAG_MAP } from '@pellux/goodvibes-sdk/platform/runtime/state';
4
+ import { FEATURE_SETTINGS } from '@pellux/goodvibes-sdk/platform/runtime/state';
5
+ import { featureEnablementWrite } from '../runtime/feature-settings.ts';
5
6
  import { CommandRegistry } from '../input/command-registry.ts';
6
7
  import { registerBuiltinCommands } from '../input/commands.ts';
7
8
 
@@ -78,8 +79,24 @@ export const FEATURE_KNOB_LOCAL_SETTINGS = [
78
79
  'runtime.toolBudget.maxCostUsd',
79
80
  ] as const;
80
81
 
81
- /** Settings with real local behavior verification: the authored baseline plus the newly persistence-tested feature-knob keys. */
82
- const SETTINGS_BEHAVIOR_VERIFIED = SETTINGS_BEHAVIOR_BASELINE + FEATURE_KNOB_LOCAL_SETTINGS.length;
82
+ /**
83
+ * Every feature's enablement settings key whose on/off writes round-trip
84
+ * through a REAL on-disk ConfigManager in
85
+ * `src/test/verification/../input/settings-modal-flag-persistence.test.ts`
86
+ * ("every feature with an off position round-trips both directions"). These
87
+ * are first-class schema keys under the dissolved feature model, so they are
88
+ * genuinely behavior-verified locally. Keys already counted in the
89
+ * feature-knob list are excluded to keep the sum honest.
90
+ */
91
+ const ENABLEMENT_KEYS_BEHAVIOR_VERIFIED = new Set(
92
+ FEATURE_SETTINGS
93
+ .filter((feature) => featureEnablementWrite(feature.id, true) !== null && featureEnablementWrite(feature.id, false) !== null)
94
+ .map((feature) => feature.enablement.key)
95
+ .filter((key) => !(FEATURE_KNOB_LOCAL_SETTINGS as readonly string[]).includes(key)),
96
+ ).size;
97
+
98
+ /** Settings with real local behavior verification: the authored baseline plus the persistence-tested feature-knob and enablement keys. */
99
+ const SETTINGS_BEHAVIOR_VERIFIED = SETTINGS_BEHAVIOR_BASELINE + FEATURE_KNOB_LOCAL_SETTINGS.length + ENABLEMENT_KEYS_BEHAVIOR_VERIFIED;
83
100
 
84
101
  const EXTERNAL_SLASH_COMMANDS = new Set([
85
102
  'auth',
@@ -181,7 +198,7 @@ export function buildVerificationLedger(root: string): VerificationLedger {
181
198
  const slashCommands = slashCommandNames.length;
182
199
  const panels = countBuiltinPanels(root);
183
200
  const cliCommands = cliCommandNames.length;
184
- const featureFlags = FEATURE_FLAG_MAP.size;
201
+ const featureFlags = FEATURE_SETTINGS.length;
185
202
  const settings = CONFIG_SCHEMA.length;
186
203
  const externalSlashCommands = slashCommandNames.filter((command) => EXTERNAL_SLASH_COMMANDS.has(command)).length;
187
204
  const externalCliCommands = cliCommandNames.filter((command) => EXTERNAL_CLI_COMMANDS.has(command)).length;
package/src/version.ts CHANGED
@@ -6,7 +6,7 @@ import { join } from 'node:path';
6
6
  // The prebuild script updates the fallback value before compilation.
7
7
  // Uses import.meta.dir (Bun) to locate package.json relative to this file,
8
8
  // which is correct regardless of the process working directory.
9
- let _version = '1.16.0';
9
+ let _version = '1.17.0';
10
10
  try {
11
11
  const pkg = JSON.parse(readFileSync(join(import.meta.dir, '..', 'package.json'), 'utf-8'));
12
12
  _version = pkg.version ?? _version;