@pellux/goodvibes-tui 1.13.1 → 1.15.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 (158) hide show
  1. package/CHANGELOG.md +104 -0
  2. package/README.md +6 -6
  3. package/bin/goodvibes +6 -0
  4. package/bin/goodvibes-daemon +6 -0
  5. package/bin/launcher-support.js +34 -1
  6. package/docs/foundation-artifacts/operator-contract.json +8076 -2047
  7. package/package.json +15 -11
  8. package/scripts/postinstall.js +73 -48
  9. package/src/cli/bundle-command.ts +5 -5
  10. package/src/cli/completion.ts +1 -1
  11. package/src/cli/completions/generate.ts +15 -3
  12. package/src/cli/doctor.ts +385 -0
  13. package/src/cli/entrypoint.ts +72 -1
  14. package/src/cli/help.ts +29 -9
  15. package/src/cli/hooks-command.ts +74 -0
  16. package/src/cli/hooks-report.ts +167 -0
  17. package/src/cli/index.ts +3 -0
  18. package/src/cli/management.ts +13 -1
  19. package/src/cli/parser.ts +10 -2
  20. package/src/cli/plugin-bundles-command.ts +228 -0
  21. package/src/cli/plugin-command.ts +179 -0
  22. package/src/cli/service-posture.ts +10 -0
  23. package/src/cli/status.ts +275 -2
  24. package/src/cli/tui-startup.ts +119 -1
  25. package/src/cli/types.ts +3 -1
  26. package/src/config/index.ts +16 -0
  27. package/src/config/tui-extension-settings.ts +169 -0
  28. package/src/core/approval-alert.ts +15 -2
  29. package/src/core/compaction-receipt.ts +127 -0
  30. package/src/core/model-routing-chip.ts +82 -0
  31. package/src/core/permission-mode.ts +89 -0
  32. package/src/core/rewind-receipt.ts +83 -0
  33. package/src/core/rewind-turn-anchors.ts +160 -0
  34. package/src/core/scriptable-statusline.ts +141 -0
  35. package/src/core/session-auto-titler.ts +110 -0
  36. package/src/core/stream-event-wiring.ts +20 -0
  37. package/src/core/system-message-router.ts +1 -1
  38. package/src/core/terminal-notifier.ts +102 -0
  39. package/src/core/turn-event-wiring.ts +80 -1
  40. package/src/input/command-reference.ts +96 -0
  41. package/src/input/command-registry.ts +25 -0
  42. package/src/input/commands/channel-pairing.ts +245 -0
  43. package/src/input/commands/channel-runtime.ts +152 -6
  44. package/src/input/commands/checkin-runtime.ts +89 -0
  45. package/src/input/commands/checkpoint-runtime.ts +7 -179
  46. package/src/input/commands/ci-runtime.ts +207 -0
  47. package/src/input/commands/cost-attribution-format.ts +52 -0
  48. package/src/input/commands/cost-runtime.ts +44 -4
  49. package/src/input/commands/editor-runtime.ts +22 -0
  50. package/src/input/commands/flags-runtime.ts +328 -0
  51. package/src/input/commands/health-metrics-format.ts +67 -0
  52. package/src/input/commands/health-runtime.ts +55 -1
  53. package/src/input/commands/memory.ts +23 -0
  54. package/src/input/commands/operator-rpc.ts +95 -0
  55. package/src/input/commands/palette-runtime.ts +22 -0
  56. package/src/input/commands/permissions-provenance.ts +264 -0
  57. package/src/input/commands/permissions-runtime.ts +21 -0
  58. package/src/input/commands/planning-runtime.ts +28 -1
  59. package/src/input/commands/platform-access-runtime.ts +2 -97
  60. package/src/input/commands/principals-runtime.ts +212 -0
  61. package/src/input/commands/product-runtime.ts +43 -3
  62. package/src/input/commands/recall-files-config.ts +54 -0
  63. package/src/input/commands/recall-files-git.ts +57 -0
  64. package/src/input/commands/recall-files.ts +185 -0
  65. package/src/input/commands/recall-query.ts +8 -3
  66. package/src/input/commands/recall-review.ts +2 -2
  67. package/src/input/commands/recall-shared.ts +13 -0
  68. package/src/input/commands/relay-runtime.ts +86 -0
  69. package/src/input/commands/review-runtime.ts +160 -0
  70. package/src/input/commands/rewind-runtime.ts +362 -0
  71. package/src/input/commands/schedule-nl.ts +147 -0
  72. package/src/input/commands/schedule-runtime.ts +29 -10
  73. package/src/input/commands/secret-runtime.ts +55 -0
  74. package/src/input/commands/session-content.ts +19 -8
  75. package/src/input/commands/session-workflow.ts +10 -0
  76. package/src/input/commands/update-runtime.ts +412 -0
  77. package/src/input/commands/workstream-attempt-dependents.ts +103 -0
  78. package/src/input/commands/workstream-attempts.ts +244 -0
  79. package/src/input/commands/workstream-runtime.ts +164 -26
  80. package/src/input/commands/worktree-runtime.ts +56 -2
  81. package/src/input/commands.ts +135 -80
  82. package/src/input/composer-capture.ts +67 -0
  83. package/src/input/composer-editor.ts +134 -0
  84. package/src/input/concealed-input.ts +94 -0
  85. package/src/input/exec-env-scrub-config.ts +84 -0
  86. package/src/input/feature-unit-layout.ts +142 -0
  87. package/src/input/feed-context-factory.ts +2 -0
  88. package/src/input/handler-feed-routes.ts +17 -0
  89. package/src/input/handler-feed.ts +3 -0
  90. package/src/input/handler-modal-routes.ts +5 -2
  91. package/src/input/handler-shortcuts.ts +37 -0
  92. package/src/input/handler.ts +24 -2
  93. package/src/input/keybindings.ts +11 -2
  94. package/src/input/onboarding/onboarding-feature-units.ts +536 -0
  95. package/src/input/onboarding/onboarding-wizard-apply.ts +40 -6
  96. package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +324 -629
  97. package/src/input/onboarding/onboarding-wizard-steps.ts +5 -5
  98. package/src/input/onboarding/onboarding-wizard-types.ts +6 -0
  99. package/src/input/relay-settings-descriptions.ts +35 -0
  100. package/src/input/sandbox-exec-config.ts +107 -0
  101. package/src/input/settings-modal-adjustment.ts +4 -7
  102. package/src/input/settings-modal-data.ts +178 -8
  103. package/src/input/settings-modal-mutations.ts +41 -13
  104. package/src/input/settings-modal-types.ts +42 -5
  105. package/src/input/settings-modal.ts +67 -15
  106. package/src/input/submission-router.ts +4 -1
  107. package/src/input/worktree-setup-config.ts +93 -0
  108. package/src/main.ts +43 -43
  109. package/src/panels/builtin/development.ts +14 -0
  110. package/src/panels/diff-review-model.ts +189 -0
  111. package/src/panels/diff-review-panel.ts +395 -0
  112. package/src/panels/fleet-panel-format.ts +114 -2
  113. package/src/panels/fleet-panel.ts +65 -78
  114. package/src/panels/fleet-read-model.ts +91 -11
  115. package/src/panels/fleet-stop.ts +19 -4
  116. package/src/panels/modals/memory-modal.ts +11 -2
  117. package/src/permissions/hunk-selection.ts +86 -5
  118. package/src/permissions/prompt.ts +108 -7
  119. package/src/permissions/sandbox-exec-gate.ts +162 -0
  120. package/src/renderer/settings-modal-helpers.ts +21 -1
  121. package/src/renderer/settings-modal.ts +70 -44
  122. package/src/renderer/shell-surface.ts +23 -1
  123. package/src/renderer/system-message.ts +4 -0
  124. package/src/renderer/tool-result-summary.ts +95 -5
  125. package/src/renderer/ui-factory.ts +22 -0
  126. package/src/runtime/bootstrap-command-context.ts +15 -1
  127. package/src/runtime/bootstrap-command-parts.ts +14 -2
  128. package/src/runtime/bootstrap-core.ts +38 -38
  129. package/src/runtime/bootstrap-notifier-sync.ts +41 -0
  130. package/src/runtime/bootstrap-shell.ts +8 -0
  131. package/src/runtime/bootstrap.ts +21 -23
  132. package/src/runtime/context-accounting-source.ts +103 -0
  133. package/src/runtime/conversation-rewind-port.ts +130 -0
  134. package/src/runtime/fleet-needs-input-push.ts +70 -0
  135. package/src/runtime/install-self-check-startup.ts +36 -0
  136. package/src/runtime/install-self-check.ts +149 -0
  137. package/src/runtime/onboarding/fast-path.ts +93 -0
  138. package/src/runtime/process-lifecycle.ts +32 -24
  139. package/src/runtime/relay-reachability-bridge.ts +53 -0
  140. package/src/runtime/release-artifacts.ts +109 -0
  141. package/src/runtime/services.ts +33 -33
  142. package/src/runtime/trust/workspace-registration.ts +184 -0
  143. package/src/runtime/trust/workspace-trust.ts +202 -0
  144. package/src/runtime/ui-services.ts +2 -1
  145. package/src/runtime/update-check.ts +113 -0
  146. package/src/runtime/workspace-checkpointing.ts +32 -0
  147. package/src/runtime/workstream-attempts-validation.ts +77 -0
  148. package/src/runtime/workstream-draft-edits.ts +95 -0
  149. package/src/runtime/workstream-draft-store.ts +101 -0
  150. package/src/runtime/workstream-draft-types.ts +42 -0
  151. package/src/runtime/workstream-services.ts +70 -39
  152. package/src/shell/blocking-input.ts +29 -0
  153. package/src/shell/ui-openers.ts +65 -0
  154. package/src/verification/verification-ledger.ts +56 -2
  155. package/src/version.ts +1 -1
  156. package/src/input/onboarding/onboarding-wizard-external-surface-extra-specs.ts +0 -130
  157. package/src/runtime/gateway-verbs.ts +0 -47
  158. package/src/runtime/render-scheduler.ts +0 -90
@@ -0,0 +1,142 @@
1
+ /**
2
+ * feature-unit-layout — turns the flat "44 flags in one bucket + their config
3
+ * keys scattered across namespace categories" layout into feature UNITS: each
4
+ * feature flag renders as a toggle header immediately followed by the config
5
+ * keys that tune it, hosted in the topical category its config lives under
6
+ * (sandbox with Runtime & Data, surfaces with Surfaces & Cloud, and so on).
7
+ *
8
+ * Grouping metadata is sourced entirely from the SDK's FEATURE_FLAG_CONFIG map
9
+ * (the single source of truth, completeness test-guarded SDK-side) so the three
10
+ * surfaces that present features — the settings modal, the onboarding wizard,
11
+ * and /flags — agree on which config keys belong to which flag.
12
+ *
13
+ * A flag whose FEATURE_FLAG_CONFIG entry has no config keys is hosted in the
14
+ * Advanced Features bucket as a simple toggle (genuinely internal / startup-gated
15
+ * plumbing flags with nothing to tune).
16
+ */
17
+
18
+ import { FEATURE_FLAG_CONFIG, FEATURE_FLAGS } from '@pellux/goodvibes-sdk/platform/runtime/state';
19
+ import type { ConfigKey, ConfigSetting } from '@pellux/goodvibes-sdk/platform/config';
20
+ import type { FeatureFlag } from '@/runtime/index.ts';
21
+ import type { FlagEntry, SettingEntry, SettingsCategory } from './settings-modal-types.ts';
22
+
23
+ /**
24
+ * The category that hosts flags with no tunable config keys. Repurposed from the
25
+ * old flat "Feature Flags" tab — it now holds only the internal/plumbing flags
26
+ * that have nothing to configure, as simple toggles.
27
+ */
28
+ export const ADVANCED_FEATURES_CATEGORY: SettingsCategory = 'flags';
29
+
30
+ /**
31
+ * Host category for a flag's feature unit: the single top-level namespace its
32
+ * config keys live under (verified single-namespace SDK-side), or the Advanced
33
+ * Features bucket when the flag has no config keys.
34
+ */
35
+ export function getFeatureUnitHostCategory(flagId: string): SettingsCategory {
36
+ const assoc = FEATURE_FLAG_CONFIG[flagId];
37
+ const namespace = assoc?.configCategories[0];
38
+ return (namespace ?? ADVANCED_FEATURES_CATEGORY) as SettingsCategory;
39
+ }
40
+
41
+ /**
42
+ * Map every config key claimed by any flag to the flag that OWNS it — the first
43
+ * flag (in FEATURE_FLAGS declaration order) whose FEATURE_FLAG_CONFIG entry lists
44
+ * it. A key claimed by two flags (e.g. behavior.compactionStrategy, shared by
45
+ * session-compaction and compaction-distiller-strategy) is listed once, under its
46
+ * owner, so no config key double-lists across feature units.
47
+ */
48
+ export function buildConfigKeyOwnership(): Map<string, string> {
49
+ const owner = new Map<string, string>();
50
+ for (const flag of FEATURE_FLAGS) {
51
+ const assoc = FEATURE_FLAG_CONFIG[flag.id];
52
+ if (!assoc) continue;
53
+ for (const key of assoc.configKeys) {
54
+ if (!owner.has(key)) owner.set(key, flag.id);
55
+ }
56
+ }
57
+ return owner;
58
+ }
59
+
60
+ /**
61
+ * Build the synthetic SettingEntry that is a feature unit's toggle header. Its
62
+ * key is `featureFlags.<id>` (the same override path persistFlagState writes), so
63
+ * it round-trips through the config layer; type boolean so the existing
64
+ * activate/adjust toggle paths drive it. currentValue mirrors the live runtime
65
+ * state; isDefault is true when the flag sits at its declared default state (so
66
+ * the "modified" marker shows only for an actual override). The attached
67
+ * `flag` marks the row as a feature-unit header for rendering and interaction.
68
+ */
69
+ export function buildFlagHeaderEntry(entry: FlagEntry): SettingEntry {
70
+ const { flag, state } = entry;
71
+ const setting: ConfigSetting = {
72
+ key: `featureFlags.${flag.id}` as ConfigKey,
73
+ type: 'boolean',
74
+ default: flag.defaultState === 'enabled',
75
+ description: flag.description,
76
+ };
77
+ return {
78
+ setting,
79
+ currentValue: state === 'enabled',
80
+ isDefault: state === flag.defaultState,
81
+ flag: entry,
82
+ };
83
+ }
84
+
85
+ /** Shallow-clone a config SettingEntry so a host-category copy can carry ownerFlagId without mutating a cross-listed original (e.g. the network combined view shares controlPlane/web entry objects). */
86
+ function cloneOwnedEntry(entry: SettingEntry, ownerFlagId: string): SettingEntry {
87
+ return { ...entry, ownerFlagId };
88
+ }
89
+
90
+ /**
91
+ * Rewrite `groups` in place so each host category leads with its feature units
92
+ * (flag toggle header + its owned config sub-options, in FEATURE_FLAGS order),
93
+ * followed by the category's remaining un-owned config rows. The Advanced
94
+ * Features category is filled with the no-config flags as bare toggles.
95
+ *
96
+ * No-op when there are no flag entries (the flagless test path), leaving the
97
+ * plain namespace-bucketed layout intact.
98
+ */
99
+ export function applyFeatureUnitLayout(
100
+ groups: Map<SettingsCategory, SettingEntry[]>,
101
+ flagEntries: readonly FlagEntry[],
102
+ ): void {
103
+ if (flagEntries.length === 0) return;
104
+ const owner = buildConfigKeyOwnership();
105
+ const flagState = new Map<string, FlagEntry>(flagEntries.map((entry) => [entry.flag.id, entry]));
106
+
107
+ // Group the flags this runtime actually registered by their host category,
108
+ // preserving FEATURE_FLAGS declaration order within each category.
109
+ const flagsByCategory = new Map<SettingsCategory, FeatureFlag[]>();
110
+ for (const flag of FEATURE_FLAGS) {
111
+ if (!flagState.has(flag.id)) continue; // not registered in this runtime
112
+ const category = getFeatureUnitHostCategory(flag.id);
113
+ if (!flagsByCategory.has(category)) flagsByCategory.set(category, []);
114
+ flagsByCategory.get(category)!.push(flag);
115
+ }
116
+
117
+ for (const [category, flags] of flagsByCategory) {
118
+ const existing = groups.get(category) ?? [];
119
+ // Split existing config rows into those owned by a feature unit hosted here
120
+ // and the remaining orphan rows. An owned row's owner is guaranteed to be a
121
+ // flag hosted in THIS category (owned keys share the flag's namespace).
122
+ const ownedByFlag = new Map<string, SettingEntry[]>();
123
+ const orphans: SettingEntry[] = [];
124
+ for (const entry of existing) {
125
+ const ownerId = owner.get(entry.setting.key);
126
+ if (ownerId !== undefined && flagState.has(ownerId)) {
127
+ if (!ownedByFlag.has(ownerId)) ownedByFlag.set(ownerId, []);
128
+ ownedByFlag.get(ownerId)!.push(cloneOwnedEntry(entry, ownerId));
129
+ } else {
130
+ orphans.push(entry);
131
+ }
132
+ }
133
+
134
+ const rebuilt: SettingEntry[] = [];
135
+ for (const flag of flags) {
136
+ rebuilt.push(buildFlagHeaderEntry(flagState.get(flag.id)!));
137
+ for (const sub of ownedByFlag.get(flag.id) ?? []) rebuilt.push(sub);
138
+ }
139
+ rebuilt.push(...orphans);
140
+ groups.set(category, rebuilt);
141
+ }
142
+ }
@@ -139,6 +139,8 @@ export interface FeedContextStableRefs {
139
139
  export interface FeedContextClosures {
140
140
  modalOpened: (name: string) => void;
141
141
  handleEscape: () => void;
142
+ /** Deliver a concealed submission; returns true when concealed mode consumed it. */
143
+ submitConcealedInput: (value: string) => boolean;
142
144
  handleCopy: () => void;
143
145
  handleCtrlC: () => void;
144
146
  handleBlockCopy: () => void;
@@ -327,6 +327,13 @@ export type KeyRouteState = {
327
327
  indicatorFocused: boolean;
328
328
  conversationManager: ConversationManager | null;
329
329
  commandContext: CommandContext | undefined;
330
+ /**
331
+ * Deliver a concealed submission. When it returns true, concealed mode was
332
+ * active and consumed the value — the plaintext went straight to the
333
+ * requester, bypassing input history and the transcript. Optional so bare
334
+ * test callers omit it.
335
+ */
336
+ submitConcealedInput?: (value: string) => boolean;
330
337
  /**
331
338
  * Optional: only used by the enter-key desync safety net below (a stray
332
339
  * slash-prefixed submission with commandMode somehow still false). When
@@ -411,6 +418,16 @@ export function handlePromptKeyToken(state: KeyRouteState, token: InputToken): {
411
418
  return { handled: true, prompt, cursorPos, inputScrollTop, commandMode, indicatorFocused };
412
419
  }
413
420
 
421
+ // Concealed input: the typed line is a secret. Deliver the plaintext (the
422
+ // live feed snapshot, not the possibly-stale handler buffer) to the
423
+ // concealed consumer, never to input history or the transcript, then clear.
424
+ if (state.submitConcealedInput?.(prompt)) {
425
+ prompt = '';
426
+ cursorPos = 0;
427
+ state.requestRender();
428
+ return { handled: true, prompt, cursorPos, inputScrollTop, commandMode, indicatorFocused };
429
+ }
430
+
414
431
  const text = prompt.trim();
415
432
  if (!text && !commandMode) {
416
433
  const lineIndex = 0;
@@ -140,6 +140,8 @@ export interface InputFeedContext {
140
140
  requestRender: () => void;
141
141
  readonly modalOpened: (name: string) => void;
142
142
  readonly handleEscape: () => void;
143
+ /** Deliver a concealed submission; returns true when concealed mode consumed it. */
144
+ readonly submitConcealedInput: (value: string) => boolean;
143
145
  readonly handleCopy: () => void;
144
146
  readonly handleCtrlC: () => void;
145
147
  readonly handleBlockCopy: () => void;
@@ -472,6 +474,7 @@ export function feedInputTokens(context: InputFeedContext, tokens: readonly Inpu
472
474
  conversationManager: context.conversationManager,
473
475
  commandContext: context.commandContext,
474
476
  commandRegistry: context.commandRegistry,
477
+ submitConcealedInput: context.submitConcealedInput,
475
478
  autocomplete: context.autocomplete,
476
479
  blockActionsMenu: { open: (block: BlockMeta) => context.blockActionsMenu.open(block) },
477
480
  openFleetPanel,
@@ -384,8 +384,10 @@ export function handleSettingsModalToken(state: SettingsRouteState, token: Input
384
384
  state.settingsModal.activateSelected();
385
385
  consumeSettingsPickerRequest(state);
386
386
  } else if (focusPane === 'categories') state.settingsModal.focusSettings?.();
387
- else if (state.settingsModal.currentCategory === 'flags') state.settingsModal.toggleSelectedFlag();
388
387
  else {
388
+ // Feature-unit toggle headers are boolean settings rows now, so
389
+ // activateSelected toggles them (via the flag-toggle chokepoint) the same
390
+ // way it edits/toggles any other setting — no flags-category special case.
389
391
  state.settingsModal.activateSelected();
390
392
  consumeSettingsPickerRequest(state);
391
393
  }
@@ -447,8 +449,9 @@ export function handleSettingsModalToken(state: SettingsRouteState, token: Input
447
449
  } else if (token.value === ' ' && !state.settingsModal.editingMode) {
448
450
  const focusPane = state.settingsModal.focusPane ?? 'settings';
449
451
  if (focusPane === 'categories') state.settingsModal.focusSettings?.();
450
- else if (state.settingsModal.currentCategory === 'flags') state.settingsModal.toggleSelectedFlag();
451
452
  else {
453
+ // See the Enter/Space handler above: feature-unit headers toggle through
454
+ // activateSelected now, so no flags-category special case is needed.
452
455
  state.settingsModal.activateSelected();
453
456
  consumeSettingsPickerRequest(state);
454
457
  }
@@ -8,6 +8,7 @@ import type { PanelManager } from '../panels/panel-manager.ts';
8
8
  import type { KeybindingsManager } from './keybindings.ts';
9
9
  import type { KillRing } from './kill-ring.ts';
10
10
  import { wordBoundaryBack, wordBoundaryForward } from './kill-ring.ts';
11
+ import { nextPermissionMode, permissionModeLabel, type PermissionModeValue } from '../core/permission-mode.ts';
11
12
 
12
13
  type WrappedPromptInfo = {
13
14
  wrappedLines: string[];
@@ -92,6 +93,19 @@ export function handleGlobalShortcutToken(
92
93
  return true;
93
94
  }
94
95
 
96
+ // Shift+Tab cycles the session permission mode (normal → accept-edits → plan
97
+ // → auto → normal), the settled convention. Handled here — hardcoded, like
98
+ // escape/f2 — because it is not in the keybinding table and bare Tab is
99
+ // already heavily overloaded. Arrives as the legacy xterm backtab literal
100
+ // ('\x1b[Z') OR, under a kitty/CSI-u terminal, as tab-with-shift. Only when
101
+ // the composer owns focus (a focused panel keeps its own Shift+Tab, e.g. the
102
+ // diff panel's previous-file); modal/picker routes run earlier and swallow
103
+ // the token before it reaches here, so overlays keep their reverse-tab too.
104
+ if (!state.panelFocused && (token.logicalName === '\x1b[Z' || (token.logicalName === 'tab' && token.shift))) {
105
+ cycleSessionPermissionMode(state);
106
+ return true;
107
+ }
108
+
95
109
  // O(1) lookup via inverted map.
96
110
  const kb = state.keybindingsManager;
97
111
  const action = kb.lookup(token);
@@ -143,6 +157,11 @@ export function handleGlobalShortcutToken(
143
157
  return true;
144
158
  }
145
159
 
160
+ case 'command-palette':
161
+ state.commandContext?.openCommandPalette?.();
162
+ state.requestRender();
163
+ return true;
164
+
146
165
  case 'panel-picker':
147
166
  state.commandContext?.openPanelPicker?.();
148
167
  state.panelFocused = state.panelManager.isVisible() && state.panelManager.getAllOpen().length > 0;
@@ -394,6 +413,24 @@ export function handleGlobalShortcutToken(
394
413
  * the mocked PanelManager test doubles in global-shortcuts.test.ts actually
395
414
  * implement.
396
415
  */
416
+ /**
417
+ * Cycle the session permission mode (Shift+Tab). The change goes through the
418
+ * SDK config surface — configManager.set('permissions.mode', ...) — which is
419
+ * what the SDK PermissionManager reads and what the orchestrator consults for
420
+ * its standing plan-mode instruction, so every attached surface converges on
421
+ * the same mode. The footer pill re-renders off the config-key subscription
422
+ * wired in turn-event-wiring; the printed line narrates the change inline.
423
+ */
424
+ function cycleSessionPermissionMode(state: GlobalShortcutRouteState): void {
425
+ const configManager = state.commandContext?.platform?.configManager;
426
+ if (!configManager) return;
427
+ const current = configManager.get('permissions.mode') as PermissionModeValue | undefined;
428
+ const next = nextPermissionMode(current);
429
+ configManager.set('permissions.mode', next);
430
+ state.commandContext?.print(`[Permissions] Mode: ${permissionModeLabel(next)}`);
431
+ state.requestRender();
432
+ }
433
+
397
434
  function toggleFleetPanel(state: GlobalShortcutRouteState): void {
398
435
  const pm = state.panelManager;
399
436
  const fleetOpen = pm.getAllOpen().some((p) => p.id === 'fleet');
@@ -81,6 +81,7 @@ import {
81
81
  type WrappedPromptInfo,
82
82
  } from './handler-prompt-buffer.ts';
83
83
  import { KillRing } from './kill-ring.ts';
84
+ import { maskConcealedText, beginConcealedInputFor, submitConcealedInputFor, cancelConcealedInputFor, type ConcealedInputRequest } from './concealed-input.ts';
84
85
  import { clearModalStack, handleEscape, modalOpened } from './handler-modal-stack.ts';
85
86
  import { handleModalTokenRoutes } from './handler-modal-token-routes.ts';
86
87
  import {
@@ -128,6 +129,13 @@ export class InputHandler implements InputHandlerLike {
128
129
  public prompt = '';
129
130
  public cursorPos = 0;
130
131
  public showExitNotice = false;
132
+ /**
133
+ * Active concealed-input request, or null. When set, the composer masks its
134
+ * echo (see getWrappedPromptInfo) and diverts submission to the request's
135
+ * onSubmit (see submitConcealedInput) instead of the normal chat/command path,
136
+ * so the plaintext never reaches input history or the transcript.
137
+ */
138
+ public concealedInput: ConcealedInputRequest | null = null;
131
139
  /** Max visible rows for the input area. Content beyond this scrolls internally. */
132
140
  public static readonly MAX_INPUT_ROWS = 8;
133
141
  /** Internal scroll offset for the input area when content exceeds MAX_INPUT_ROWS. */
@@ -331,7 +339,8 @@ export class InputHandler implements InputHandlerLike {
331
339
  },
332
340
  {
333
341
  modalOpened: (name: string) => this.modalOpened(name),
334
- handleEscape: () => { this.handleEscape(); this.syncFeedContextMutableFields(); },
342
+ handleEscape: () => { if (!this.cancelConcealedInput()) this.handleEscape(); this.syncFeedContextMutableFields(); },
343
+ submitConcealedInput: (value: string) => this.submitConcealedInput(value),
335
344
  handleCopy: () => this.handleCopy(),
336
345
  handleCtrlC: () => { this.handleCtrlC(); this.syncFeedContextMutableFields(); },
337
346
  handleBlockCopy: () => this.handleBlockCopy(),
@@ -637,8 +646,12 @@ export class InputHandler implements InputHandlerLike {
637
646
  * and the visible slice respecting inputScrollTop.
638
647
  */
639
648
  public getWrappedPromptInfo(contentWidth: number): WrappedPromptInfo {
649
+ // In concealed mode, wrap the MASKED buffer. maskConcealedText preserves
650
+ // length and newlines, so wrapping and cursor coordinates are identical to
651
+ // the plaintext while no plaintext character reaches the screen buffer.
652
+ const displayPrompt = this.concealedInput ? maskConcealedText(this.prompt) : this.prompt;
640
653
  return getWrappedPromptInfo(
641
- this.prompt,
654
+ displayPrompt,
642
655
  this.cursorPos,
643
656
  this.inputScrollTop,
644
657
  contentWidth,
@@ -646,6 +659,15 @@ export class InputHandler implements InputHandlerLike {
646
659
  );
647
660
  }
648
661
 
662
+ /** Begin one line of concealed (masked) composer input. See concealed-input.ts. */
663
+ public beginConcealedInput(request: ConcealedInputRequest): void { beginConcealedInputFor(this, request); }
664
+ /** True while a concealed-input request is active. */
665
+ public isConcealedInput(): boolean { return this.concealedInput !== null; }
666
+ /** Deliver a concealed submission; returns true when concealed mode consumed it. */
667
+ public submitConcealedInput(value: string): boolean { return submitConcealedInputFor(this, value); }
668
+ /** Cancel an active concealed-input request without submitting (Escape). */
669
+ public cancelConcealedInput(): boolean { return cancelConcealedInputFor(this); }
670
+
649
671
  // ── Undo / Redo methods ─────────────────────────────────────────────────
650
672
 
651
673
  /**
@@ -33,6 +33,7 @@ export type KeyAction =
33
33
  | 'copy-selection'
34
34
  | 'clear-cancel'
35
35
  | 'screen-clear'
36
+ | 'command-palette'
36
37
  | 'panel-picker'
37
38
  | 'panel-close'
38
39
  | 'panel-close-all'
@@ -74,6 +75,7 @@ export const ACTION_DESCRIPTIONS: Record<KeyAction, string> = {
74
75
  'copy-selection': 'Copy selected text to clipboard',
75
76
  'clear-cancel': 'Clear input / cancel generation / exit (double)',
76
77
  'screen-clear': 'Repaint the screen',
78
+ 'command-palette': 'Open the command palette (search & run any command)',
77
79
  'panel-picker': 'Open, focus, or hide the panel workspace',
78
80
  'panel-close': 'Close the currently active panel',
79
81
  'panel-close-all': 'Close all open panels',
@@ -116,6 +118,10 @@ export const DEFAULT_KEYBINDINGS: Record<KeyAction, KeyCombo[]> = {
116
118
  'copy-selection': [{ key: 'c', ctrl: true, shift: true }],
117
119
  'clear-cancel': [{ key: 'c', ctrl: true }],
118
120
  'screen-clear': [{ key: 'l', ctrl: true }],
121
+ // Ctrl+K opens the command palette (the standard palette chord across editors).
122
+ // The readline kill-to-end-of-line that historically owned Ctrl+K is repointed
123
+ // to Alt+K below so the capability is kept, not lost.
124
+ 'command-palette': [{ key: 'k', ctrl: true }],
119
125
  'panel-picker': [{ key: 'p', ctrl: true }],
120
126
  'panel-close': [{ key: 'x', ctrl: true }],
121
127
  'panel-close-all': [{ key: 'x', ctrl: true, shift: true }],
@@ -157,7 +163,10 @@ export const DEFAULT_KEYBINDINGS: Record<KeyAction, KeyCombo[]> = {
157
163
  'delete-word': [{ key: 'w', ctrl: true }],
158
164
  'apply-diff-line-start': [{ key: 'a', ctrl: true }],
159
165
  'next-error-line-end': [{ key: 'e', ctrl: true }],
160
- 'kill-line': [{ key: 'k', ctrl: true }],
166
+ // Alt+K: kill to end of line. Ctrl+K was reassigned to the command palette
167
+ // (the standard palette chord); Alt+K keeps kill-to-end available and is
168
+ // otherwise unused in the default table.
169
+ 'kill-line': [{ key: 'k', alt: true }],
161
170
  // Alt+U: clear entire prompt. Ctrl+U is owned by kill-to-start (readline
162
171
  // convention). Alt+U is unused by any other default and is representable by
163
172
  // the tokenizer's { key, alt } combo form.
@@ -169,7 +178,7 @@ export const DEFAULT_KEYBINDINGS: Record<KeyAction, KeyCombo[]> = {
169
178
  'word-back': [{ key: 'b', alt: true }],
170
179
  'word-forward': [{ key: 'f', alt: true }],
171
180
  // Kill-ring operations.
172
- // Note: 'kill-line' (Ctrl+K) kills to end; 'kill-to-start' (Ctrl+U) kills to start.
181
+ // Note: 'kill-line' (Alt+K) kills to end; 'kill-to-start' (Ctrl+U) kills to start.
173
182
  // 'clear-prompt' (Alt+U) clears the entire buffer regardless of cursor position.
174
183
  // kill-to-start owns Ctrl+U (readline convention); clear-prompt uses Alt+U.
175
184
  'kill-to-start': [{ key: 'u', ctrl: true }],