@pellux/goodvibes-tui 1.13.1 → 1.14.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.
- package/CHANGELOG.md +96 -0
- package/README.md +6 -6
- package/bin/goodvibes +6 -0
- package/bin/goodvibes-daemon +6 -0
- package/bin/launcher-support.js +34 -1
- package/docs/foundation-artifacts/operator-contract.json +8076 -2047
- package/package.json +15 -11
- package/scripts/postinstall.js +73 -48
- package/src/cli/bundle-command.ts +5 -5
- package/src/cli/completion.ts +1 -1
- package/src/cli/completions/generate.ts +15 -3
- package/src/cli/doctor.ts +385 -0
- package/src/cli/entrypoint.ts +72 -1
- package/src/cli/help.ts +29 -9
- package/src/cli/hooks-command.ts +74 -0
- package/src/cli/hooks-report.ts +167 -0
- package/src/cli/index.ts +3 -0
- package/src/cli/management.ts +13 -1
- package/src/cli/parser.ts +10 -2
- package/src/cli/plugin-bundles-command.ts +228 -0
- package/src/cli/plugin-command.ts +179 -0
- package/src/cli/service-posture.ts +10 -0
- package/src/cli/status.ts +275 -2
- package/src/cli/tui-startup.ts +119 -1
- package/src/cli/types.ts +3 -1
- package/src/config/index.ts +16 -0
- package/src/config/tui-extension-settings.ts +169 -0
- package/src/core/approval-alert.ts +15 -2
- package/src/core/compaction-receipt.ts +127 -0
- package/src/core/model-routing-chip.ts +82 -0
- package/src/core/permission-mode.ts +89 -0
- package/src/core/rewind-receipt.ts +83 -0
- package/src/core/rewind-turn-anchors.ts +160 -0
- package/src/core/scriptable-statusline.ts +141 -0
- package/src/core/session-auto-titler.ts +110 -0
- package/src/core/stream-event-wiring.ts +20 -0
- package/src/core/system-message-router.ts +1 -1
- package/src/core/terminal-notifier.ts +102 -0
- package/src/core/turn-event-wiring.ts +80 -1
- package/src/input/command-reference.ts +96 -0
- package/src/input/command-registry.ts +25 -0
- package/src/input/commands/channel-pairing.ts +245 -0
- package/src/input/commands/channel-runtime.ts +152 -6
- package/src/input/commands/checkin-runtime.ts +89 -0
- package/src/input/commands/checkpoint-runtime.ts +7 -179
- package/src/input/commands/ci-runtime.ts +207 -0
- package/src/input/commands/cost-attribution-format.ts +52 -0
- package/src/input/commands/cost-runtime.ts +44 -4
- package/src/input/commands/editor-runtime.ts +22 -0
- package/src/input/commands/flags-runtime.ts +328 -0
- package/src/input/commands/health-metrics-format.ts +67 -0
- package/src/input/commands/health-runtime.ts +55 -1
- package/src/input/commands/memory.ts +23 -0
- package/src/input/commands/operator-rpc.ts +95 -0
- package/src/input/commands/palette-runtime.ts +22 -0
- package/src/input/commands/permissions-provenance.ts +264 -0
- package/src/input/commands/permissions-runtime.ts +21 -0
- package/src/input/commands/planning-runtime.ts +28 -1
- package/src/input/commands/platform-access-runtime.ts +2 -97
- package/src/input/commands/principals-runtime.ts +212 -0
- package/src/input/commands/product-runtime.ts +43 -3
- package/src/input/commands/recall-files-config.ts +54 -0
- package/src/input/commands/recall-files-git.ts +57 -0
- package/src/input/commands/recall-files.ts +185 -0
- package/src/input/commands/recall-query.ts +8 -3
- package/src/input/commands/recall-review.ts +2 -2
- package/src/input/commands/recall-shared.ts +13 -0
- package/src/input/commands/relay-runtime.ts +86 -0
- package/src/input/commands/review-runtime.ts +160 -0
- package/src/input/commands/rewind-runtime.ts +362 -0
- package/src/input/commands/schedule-nl.ts +147 -0
- package/src/input/commands/schedule-runtime.ts +29 -10
- package/src/input/commands/secret-runtime.ts +55 -0
- package/src/input/commands/session-content.ts +19 -8
- package/src/input/commands/session-workflow.ts +10 -0
- package/src/input/commands/update-runtime.ts +363 -0
- package/src/input/commands/workstream-attempt-dependents.ts +103 -0
- package/src/input/commands/workstream-attempts.ts +244 -0
- package/src/input/commands/workstream-runtime.ts +164 -26
- package/src/input/commands/worktree-runtime.ts +56 -2
- package/src/input/commands.ts +135 -80
- package/src/input/composer-capture.ts +67 -0
- package/src/input/composer-editor.ts +134 -0
- package/src/input/concealed-input.ts +94 -0
- package/src/input/exec-env-scrub-config.ts +84 -0
- package/src/input/feature-unit-layout.ts +142 -0
- package/src/input/feed-context-factory.ts +2 -0
- package/src/input/handler-feed-routes.ts +17 -0
- package/src/input/handler-feed.ts +3 -0
- package/src/input/handler-modal-routes.ts +5 -2
- package/src/input/handler-shortcuts.ts +37 -0
- package/src/input/handler.ts +24 -2
- package/src/input/keybindings.ts +11 -2
- package/src/input/onboarding/onboarding-feature-units.ts +536 -0
- package/src/input/onboarding/onboarding-wizard-apply.ts +40 -6
- package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +324 -629
- package/src/input/onboarding/onboarding-wizard-steps.ts +5 -5
- package/src/input/onboarding/onboarding-wizard-types.ts +6 -0
- package/src/input/relay-settings-descriptions.ts +35 -0
- package/src/input/sandbox-exec-config.ts +107 -0
- package/src/input/settings-modal-adjustment.ts +4 -7
- package/src/input/settings-modal-data.ts +178 -8
- package/src/input/settings-modal-mutations.ts +41 -13
- package/src/input/settings-modal-types.ts +42 -5
- package/src/input/settings-modal.ts +67 -15
- package/src/input/submission-router.ts +4 -1
- package/src/input/worktree-setup-config.ts +93 -0
- package/src/main.ts +43 -43
- package/src/panels/builtin/development.ts +14 -0
- package/src/panels/diff-review-model.ts +189 -0
- package/src/panels/diff-review-panel.ts +395 -0
- package/src/panels/fleet-panel-format.ts +114 -2
- package/src/panels/fleet-panel.ts +65 -78
- package/src/panels/fleet-read-model.ts +91 -11
- package/src/panels/fleet-stop.ts +19 -4
- package/src/panels/modals/memory-modal.ts +11 -2
- package/src/permissions/hunk-selection.ts +86 -5
- package/src/permissions/prompt.ts +108 -7
- package/src/permissions/sandbox-exec-gate.ts +162 -0
- package/src/renderer/settings-modal-helpers.ts +21 -1
- package/src/renderer/settings-modal.ts +70 -44
- package/src/renderer/shell-surface.ts +23 -1
- package/src/renderer/system-message.ts +4 -0
- package/src/renderer/tool-result-summary.ts +95 -5
- package/src/renderer/ui-factory.ts +22 -0
- package/src/runtime/bootstrap-command-context.ts +15 -1
- package/src/runtime/bootstrap-command-parts.ts +14 -2
- package/src/runtime/bootstrap-core.ts +38 -38
- package/src/runtime/bootstrap-notifier-sync.ts +41 -0
- package/src/runtime/bootstrap-shell.ts +8 -0
- package/src/runtime/bootstrap.ts +21 -23
- package/src/runtime/context-accounting-source.ts +103 -0
- package/src/runtime/conversation-rewind-port.ts +130 -0
- package/src/runtime/fleet-needs-input-push.ts +70 -0
- package/src/runtime/install-self-check-startup.ts +36 -0
- package/src/runtime/install-self-check.ts +149 -0
- package/src/runtime/onboarding/fast-path.ts +93 -0
- package/src/runtime/process-lifecycle.ts +32 -24
- package/src/runtime/relay-reachability-bridge.ts +53 -0
- package/src/runtime/release-artifacts.ts +79 -0
- package/src/runtime/services.ts +33 -33
- package/src/runtime/trust/workspace-registration.ts +184 -0
- package/src/runtime/trust/workspace-trust.ts +202 -0
- package/src/runtime/ui-services.ts +2 -1
- package/src/runtime/update-check.ts +113 -0
- package/src/runtime/workspace-checkpointing.ts +32 -0
- package/src/runtime/workstream-attempts-validation.ts +77 -0
- package/src/runtime/workstream-draft-edits.ts +95 -0
- package/src/runtime/workstream-draft-store.ts +101 -0
- package/src/runtime/workstream-draft-types.ts +42 -0
- package/src/runtime/workstream-services.ts +70 -39
- package/src/shell/blocking-input.ts +29 -0
- package/src/shell/ui-openers.ts +65 -0
- package/src/verification/verification-ledger.ts +56 -2
- package/src/version.ts +1 -1
- package/src/input/onboarding/onboarding-wizard-external-surface-extra-specs.ts +0 -130
- package/src/runtime/gateway-verbs.ts +0 -47
- 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');
|
package/src/input/handler.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
/**
|
package/src/input/keybindings.ts
CHANGED
|
@@ -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
|
-
|
|
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' (
|
|
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 }],
|