@pellux/goodvibes-tui 0.18.20 → 0.19.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 +154 -0
- package/README.md +1 -1
- package/docs/foundation-artifacts/operator-contract.json +1 -1
- package/package.json +7 -3
- package/src/core/conversation-rendering.ts +22 -6
- package/src/core/orchestrator.ts +1 -1
- package/src/input/commands/diff-runtime.ts +6 -5
- package/src/input/commands/guidance-runtime.ts +1 -1
- package/src/input/commands/health-runtime.ts +2 -2
- package/src/input/commands/local-setup-review.ts +1 -1
- package/src/input/commands/session-content.ts +1 -1
- package/src/input/commands/session.ts +0 -1
- package/src/input/commands/shell-core.ts +3 -2
- package/src/input/commands/skills-runtime.ts +2 -2
- package/src/input/commands/subscription-runtime.ts +4 -4
- package/src/input/feed-context-factory.ts +236 -0
- package/src/input/handler-feed.ts +44 -6
- package/src/input/handler-shortcuts.ts +138 -125
- package/src/input/handler.ts +119 -119
- package/src/input/keybindings.ts +30 -0
- package/src/input/panel-integration-actions.ts +2 -1
- package/src/input/settings-modal-types.ts +60 -0
- package/src/input/settings-modal.ts +83 -65
- package/src/panels/agent-inspector-panel.ts +10 -9
- package/src/panels/agent-logs-panel.ts +26 -6
- package/src/panels/approval-panel.ts +55 -82
- package/src/panels/automation-control-panel.ts +120 -161
- package/src/panels/base-panel.ts +108 -3
- package/src/panels/communication-panel.ts +69 -107
- package/src/panels/context-visualizer-panel.ts +2 -0
- package/src/panels/control-plane-panel.ts +117 -172
- package/src/panels/diff-panel.ts +2 -0
- package/src/panels/file-explorer-panel.ts +51 -31
- package/src/panels/file-preview-panel.ts +57 -35
- package/src/panels/git-panel.ts +12 -13
- package/src/panels/hooks-panel.ts +103 -138
- package/src/panels/incident-review-panel.ts +59 -109
- package/src/panels/knowledge-panel.ts +75 -107
- package/src/panels/local-auth-panel.ts +77 -93
- package/src/panels/marketplace-panel.ts +51 -69
- package/src/panels/mcp-panel.ts +110 -155
- package/src/panels/memory-panel.ts +90 -158
- package/src/panels/ops-control-panel.ts +51 -85
- package/src/panels/orchestration-panel.ts +70 -51
- package/src/panels/panel-list-panel.ts +5 -4
- package/src/panels/panel-manager.ts +25 -2
- package/src/panels/plan-dashboard-panel.ts +2 -0
- package/src/panels/plugins-panel.ts +37 -60
- package/src/panels/polish.ts +51 -2
- package/src/panels/provider-accounts-panel.ts +1 -0
- package/src/panels/provider-health-panel.ts +6 -8
- package/src/panels/routes-panel.ts +91 -141
- package/src/panels/schedule-panel.ts +7 -6
- package/src/panels/scrollable-list-panel.ts +64 -16
- package/src/panels/security-panel.ts +118 -152
- package/src/panels/services-panel.ts +63 -105
- package/src/panels/session-browser-panel.ts +19 -18
- package/src/panels/settings-sync-panel.ts +79 -123
- package/src/panels/skills-panel.ts +114 -230
- package/src/panels/subscription-panel.ts +64 -86
- package/src/panels/system-messages-panel.ts +147 -141
- package/src/panels/tasks-panel.ts +130 -179
- package/src/panels/token-budget-panel.ts +2 -0
- package/src/panels/watchers-panel.ts +89 -137
- package/src/panels/worktree-panel.ts +1 -0
- package/src/panels/wrfc-panel.ts +2 -0
- package/src/renderer/agent-detail-modal.ts +2 -2
- package/src/renderer/ansi-sanitize.ts +76 -0
- package/src/renderer/buffer.ts +23 -1
- package/src/renderer/diff.ts +8 -0
- package/src/renderer/help-overlay.ts +48 -28
- package/src/renderer/markdown.ts +3 -145
- package/src/renderer/settings-modal-helpers.ts +27 -0
- package/src/renderer/settings-modal.ts +18 -1
- package/src/renderer/status-glyphs.ts +21 -0
- package/src/renderer/status-token.ts +4 -8
- package/src/renderer/tool-call.ts +4 -3
- package/src/runtime/bootstrap-core.ts +1 -1
- package/src/runtime/bootstrap-hook-bridge.ts +1 -1
- package/src/runtime/bootstrap.ts +7 -8
- package/src/runtime/diagnostics/panels/policy.ts +2 -1
- package/src/shell/ui-openers.ts +1 -1
- package/src/version.ts +1 -1
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* feed-context-factory.ts — Construction and mutable-field sync for InputFeedContext.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from handler.ts (Wave 4α review, 0.18.23) to keep handler.ts under the
|
|
5
|
+
* 800-line architecture cap.
|
|
6
|
+
*
|
|
7
|
+
* Two exported functions:
|
|
8
|
+
* - `buildInitialFeedContext` — assembles the long-lived InputFeedContext from
|
|
9
|
+
* pre-built field groups. Called once at InputHandler construction time.
|
|
10
|
+
* - `syncFeedContextMutableFields` — copies mutable scalar fields into the
|
|
11
|
+
* already-allocated context before each feed() dispatch.
|
|
12
|
+
*/
|
|
13
|
+
import type { InputFeedContext } from './handler-feed.ts';
|
|
14
|
+
import type { SelectionManager } from './selection.ts';
|
|
15
|
+
import type { InfiniteBuffer } from '../core/history.ts';
|
|
16
|
+
import type { CommandRegistry, CommandContext } from './command-registry.ts';
|
|
17
|
+
import type { AutocompleteEngine } from './autocomplete.ts';
|
|
18
|
+
import type { FilePickerModal } from './file-picker.ts';
|
|
19
|
+
import type { ModelPickerModal } from './model-picker.ts';
|
|
20
|
+
import type { SelectionModal } from './selection-modal.ts';
|
|
21
|
+
import type { SelectionResult } from './selection-modal.ts';
|
|
22
|
+
import type { SearchManager } from './search.ts';
|
|
23
|
+
import type { InputHistory, HistorySearch } from './input-history.ts';
|
|
24
|
+
import type { ConversationManager } from '../core/conversation';
|
|
25
|
+
import type { ProcessModal } from '../renderer/process-modal.ts';
|
|
26
|
+
import type { LiveTailModal } from '../renderer/live-tail-modal.ts';
|
|
27
|
+
import type { BlockActionsMenu } from '../renderer/block-actions.ts';
|
|
28
|
+
import type { AgentDetailModal } from '../renderer/agent-detail-modal.ts';
|
|
29
|
+
import type { ContextInspectorModal } from '../renderer/context-inspector.ts';
|
|
30
|
+
import type { BookmarkModal } from './bookmark-modal.ts';
|
|
31
|
+
import type { SettingsModal } from './settings-modal.ts';
|
|
32
|
+
import type { SessionPickerModal } from './session-picker-modal.ts';
|
|
33
|
+
import type { ProfilePickerModal } from './profile-picker-modal.ts';
|
|
34
|
+
import type { WrappedPromptInfo } from './handler-prompt-buffer.ts';
|
|
35
|
+
import type { Panel } from '../panels/types.ts';
|
|
36
|
+
import type { PanelManager } from '../panels/panel-manager.ts';
|
|
37
|
+
import type { KeybindingsManager } from './keybindings.ts';
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Initial mutable scalar values for InputFeedContext.
|
|
41
|
+
*
|
|
42
|
+
* **Mutable fields** (synced per-feed via syncFeedContextMutableFields or inside
|
|
43
|
+
* action closures that call syncFeedContextMutableFields):
|
|
44
|
+
* - `prompt`, `cursorPos` — current text buffer state
|
|
45
|
+
* - `commandMode`, `panelFocused`, `indicatorFocused` — focus-mode flags
|
|
46
|
+
* - `helpOverlayActive`, `helpScrollOffset` — help overlay state
|
|
47
|
+
* - `shortcutsOverlayActive`, `shortcutsScrollOffset` — shortcuts overlay state
|
|
48
|
+
* - `nextPasteId`, `nextImageId` — monotonically increasing ID counters
|
|
49
|
+
* - `mouseDownRow`, `mouseDownCol` — drag-tracking coordinates
|
|
50
|
+
* - `contentWidth` — reflow width, updated by setContentWidth()
|
|
51
|
+
* - `selectionCallback` — current selection modal callback (nullable)
|
|
52
|
+
*/
|
|
53
|
+
export interface FeedContextMutableInit {
|
|
54
|
+
prompt: string;
|
|
55
|
+
cursorPos: number;
|
|
56
|
+
commandMode: boolean;
|
|
57
|
+
panelFocused: boolean;
|
|
58
|
+
indicatorFocused: boolean;
|
|
59
|
+
helpOverlayActive: boolean;
|
|
60
|
+
helpScrollOffset: number;
|
|
61
|
+
shortcutsOverlayActive: boolean;
|
|
62
|
+
shortcutsScrollOffset: number;
|
|
63
|
+
nextPasteId: number;
|
|
64
|
+
nextImageId: number;
|
|
65
|
+
mouseDownRow: number;
|
|
66
|
+
mouseDownCol: number;
|
|
67
|
+
contentWidth: number;
|
|
68
|
+
selectionCallback: ((result: SelectionResult | null) => void) | null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Stable (readonly) service references for InputFeedContext.
|
|
73
|
+
*
|
|
74
|
+
* **Stable references** (set once at construction, never reallocated):
|
|
75
|
+
* - `pasteRegistry`, `imageRegistry` — owned Maps, never replaced
|
|
76
|
+
* - `selectionModal`, `bookmarkModal`, `settingsModal`, `sessionPickerModal`,
|
|
77
|
+
* `profilePickerModal` — modal objects constructed once
|
|
78
|
+
* - `filePicker`, `modelPicker`, `processModal`, `liveTailModal`,
|
|
79
|
+
* `agentDetailModal`, `contextInspectorModal`, `blockActionsMenu`,
|
|
80
|
+
* `searchManager`, `historySearch` — service objects constructed once
|
|
81
|
+
* - `panelManager`, `keybindingsManager` — from uiServices, stable
|
|
82
|
+
* - `modalStack` — reference to the handler's shared array
|
|
83
|
+
* - `getHistory`, `getViewportHeight`, `getScrollTop`, `scroll`, `exitApp` — callbacks
|
|
84
|
+
* - `commandRegistry`, `commandContext`, `autocomplete`, `inputHistory`,
|
|
85
|
+
* `conversationManager` — wired after construction; synced at feed() entry only
|
|
86
|
+
* (not per-action) since no in-feed action changes them
|
|
87
|
+
*
|
|
88
|
+
* **Rationale:** per-feed mutation on a single object avoids per-keystroke GC pressure
|
|
89
|
+
* from ~80-field object allocation. Stable references are service handles that never
|
|
90
|
+
* need to change after construction.
|
|
91
|
+
*/
|
|
92
|
+
export interface FeedContextStableRefs {
|
|
93
|
+
selection: SelectionManager;
|
|
94
|
+
pasteRegistry: Map<string, string>;
|
|
95
|
+
imageRegistry: Map<string, { data: string; mediaType: string }>;
|
|
96
|
+
selectionModal: SelectionModal;
|
|
97
|
+
bookmarkModal: BookmarkModal;
|
|
98
|
+
settingsModal: SettingsModal;
|
|
99
|
+
sessionPickerModal: SessionPickerModal;
|
|
100
|
+
profilePickerModal: ProfilePickerModal;
|
|
101
|
+
historySearch: HistorySearch;
|
|
102
|
+
commandRegistry: CommandRegistry | null;
|
|
103
|
+
commandContext: CommandContext | undefined;
|
|
104
|
+
autocomplete: AutocompleteEngine | null;
|
|
105
|
+
filePicker: FilePickerModal;
|
|
106
|
+
modelPicker: ModelPickerModal;
|
|
107
|
+
processModal: ProcessModal;
|
|
108
|
+
liveTailModal: LiveTailModal;
|
|
109
|
+
agentDetailModal: AgentDetailModal;
|
|
110
|
+
contextInspectorModal: ContextInspectorModal;
|
|
111
|
+
blockActionsMenu: BlockActionsMenu;
|
|
112
|
+
searchManager: SearchManager;
|
|
113
|
+
modalStack: string[];
|
|
114
|
+
inputHistory: InputHistory | null;
|
|
115
|
+
conversationManager: ConversationManager | null;
|
|
116
|
+
panelManager: PanelManager;
|
|
117
|
+
keybindingsManager: KeybindingsManager;
|
|
118
|
+
getHistory: () => InfiniteBuffer;
|
|
119
|
+
getViewportHeight: () => number;
|
|
120
|
+
getScrollTop: () => number;
|
|
121
|
+
scroll: (delta: number) => void;
|
|
122
|
+
exitApp: () => void;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Bound method closures for InputFeedContext. Built in handler.ts where private members are accessible. */
|
|
126
|
+
export interface FeedContextClosures {
|
|
127
|
+
modalOpened: (name: string) => void;
|
|
128
|
+
handleEscape: () => void;
|
|
129
|
+
handleCopy: () => void;
|
|
130
|
+
handleCtrlC: () => void;
|
|
131
|
+
handleBlockCopy: () => void;
|
|
132
|
+
handleBookmark: () => void;
|
|
133
|
+
handleBlockSave: () => void;
|
|
134
|
+
handleDiffApply: () => boolean;
|
|
135
|
+
handleUndo: () => void;
|
|
136
|
+
handleRedo: () => void;
|
|
137
|
+
handlePaste: () => void;
|
|
138
|
+
saveUndoState: () => void;
|
|
139
|
+
ensureInputCursorVisible: (contentWidth?: number) => void;
|
|
140
|
+
registerPaste: (content: string) => string;
|
|
141
|
+
executeBlockAction: (id: string) => void;
|
|
142
|
+
cyclePanelTab: (direction: 'next' | 'prev') => void;
|
|
143
|
+
onPanelInputConsumed: (activePanel: Panel | null, key: string) => void;
|
|
144
|
+
getWrappedPromptInfo: (contentWidth: number) => WrappedPromptInfo;
|
|
145
|
+
moveCursorVertical: (direction: -1 | 1) => boolean;
|
|
146
|
+
handlePathCompletion: () => boolean;
|
|
147
|
+
handleBlockToggle: () => void;
|
|
148
|
+
findMarkerAtPos: (pos: number) => { start: number; end: number } | null;
|
|
149
|
+
cleanupMarkerRegistry: (text: string) => void;
|
|
150
|
+
expandPrompt: (text: string) => string | import('@pellux/goodvibes-sdk/platform/providers/interface').ContentPart[];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* buildInitialFeedContext — Allocate the single InputFeedContext that lives for the
|
|
155
|
+
* lifetime of the InputHandler.
|
|
156
|
+
*
|
|
157
|
+
* Accepts pre-built field groups from handler.ts where private access is available.
|
|
158
|
+
* The resulting context object is mutated in place on every feed() call via
|
|
159
|
+
* syncFeedContextMutableFields — no re-allocation occurs per keystroke.
|
|
160
|
+
*
|
|
161
|
+
* @param mutable Initial mutable scalar values (synced per-feed).
|
|
162
|
+
* @param stable Stable service references (set once, never replaced).
|
|
163
|
+
* @param closures Pre-built bound method closures (built in handler.ts).
|
|
164
|
+
*/
|
|
165
|
+
export function buildInitialFeedContext(
|
|
166
|
+
mutable: FeedContextMutableInit,
|
|
167
|
+
stable: FeedContextStableRefs,
|
|
168
|
+
closures: FeedContextClosures,
|
|
169
|
+
): InputFeedContext {
|
|
170
|
+
const noop = (): void => {};
|
|
171
|
+
return {
|
|
172
|
+
// --- mutable scalars ---
|
|
173
|
+
...mutable,
|
|
174
|
+
// --- requestRender: placeholder, swapped per-feed to buffered version ---
|
|
175
|
+
requestRender: noop,
|
|
176
|
+
// --- stable refs ---
|
|
177
|
+
...stable,
|
|
178
|
+
// --- closures ---
|
|
179
|
+
...closures,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* syncFeedContextMutableFields — Copy mutable scalar fields into the reused
|
|
185
|
+
* InputFeedContext object.
|
|
186
|
+
*
|
|
187
|
+
* **Fields synced (updated on every call):**
|
|
188
|
+
* - `prompt` — current prompt text buffer
|
|
189
|
+
* - `cursorPos` — caret position within prompt
|
|
190
|
+
* - `commandMode` — whether command-mode prefix is active
|
|
191
|
+
* - `panelFocused` — whether the active panel owns keyboard focus
|
|
192
|
+
* - `indicatorFocused` — whether the status indicator owns focus
|
|
193
|
+
* - `helpOverlayActive` / `helpScrollOffset` — help overlay visibility and scroll
|
|
194
|
+
* - `shortcutsOverlayActive` / `shortcutsScrollOffset` — shortcuts overlay state
|
|
195
|
+
* - `selectionCallback` — the current in-flight selection modal callback
|
|
196
|
+
* - `nextPasteId` / `nextImageId` — monotonically increasing allocation counters
|
|
197
|
+
* - `mouseDownRow` / `mouseDownCol` — drag-start coordinates
|
|
198
|
+
*
|
|
199
|
+
* **Fields intentionally excluded (synced only at feed() entry, not here):**
|
|
200
|
+
* - `contentWidth` — semi-stable; only changes on terminal resize via setContentWidth().
|
|
201
|
+
* Synced at feed() entry because it is only relevant for layout, not action-reaction
|
|
202
|
+
* sequences within a single feed.
|
|
203
|
+
* - `commandRegistry`, `commandContext` — wired after construction via
|
|
204
|
+
* setCommandRegistry(); synced at feed() entry since no in-feed action changes them.
|
|
205
|
+
* - `autocomplete` — wired after construction; synced at feed() entry.
|
|
206
|
+
* - `inputHistory`, `conversationManager` — late-wired service handles; stable within
|
|
207
|
+
* a feed. Synced at feed() entry only.
|
|
208
|
+
* - `requestRender` — swapped to a buffered version at feed() entry and restored in
|
|
209
|
+
* the finally block; not managed by this function.
|
|
210
|
+
*
|
|
211
|
+
* Called from within action closures (`handleEscape`, `handleCtrlC`, `handleUndo`,
|
|
212
|
+
* `handleRedo`, `handlePaste`) that mutate handler state mid-feed, so subsequent
|
|
213
|
+
* route handlers see updated values without re-reading handler fields.
|
|
214
|
+
*
|
|
215
|
+
* @param fields Current mutable field values from the handler.
|
|
216
|
+
* @param ctx The InputFeedContext to update in place.
|
|
217
|
+
*/
|
|
218
|
+
export function syncFeedContextMutableFields(
|
|
219
|
+
fields: FeedContextMutableInit,
|
|
220
|
+
ctx: InputFeedContext,
|
|
221
|
+
): void {
|
|
222
|
+
ctx.prompt = fields.prompt;
|
|
223
|
+
ctx.cursorPos = fields.cursorPos;
|
|
224
|
+
ctx.commandMode = fields.commandMode;
|
|
225
|
+
ctx.panelFocused = fields.panelFocused;
|
|
226
|
+
ctx.indicatorFocused = fields.indicatorFocused;
|
|
227
|
+
ctx.helpOverlayActive = fields.helpOverlayActive;
|
|
228
|
+
ctx.helpScrollOffset = fields.helpScrollOffset;
|
|
229
|
+
ctx.shortcutsOverlayActive = fields.shortcutsOverlayActive;
|
|
230
|
+
ctx.shortcutsScrollOffset = fields.shortcutsScrollOffset;
|
|
231
|
+
ctx.selectionCallback = fields.selectionCallback;
|
|
232
|
+
ctx.nextPasteId = fields.nextPasteId;
|
|
233
|
+
ctx.nextImageId = fields.nextImageId;
|
|
234
|
+
ctx.mouseDownRow = fields.mouseDownRow;
|
|
235
|
+
ctx.mouseDownCol = fields.mouseDownCol;
|
|
236
|
+
}
|
|
@@ -40,6 +40,44 @@ import { SelectionManager } from './selection.ts';
|
|
|
40
40
|
import type { PanelManager } from '../panels/panel-manager.ts';
|
|
41
41
|
import type { KeybindingsManager } from './keybindings.ts';
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* InputFeedContext — The single long-lived context object passed to feedInputTokens
|
|
45
|
+
* on every keystroke. Allocated once at InputHandler construction; mutated in place
|
|
46
|
+
* per-feed to avoid per-keystroke GC pressure from ~80-field object allocation.
|
|
47
|
+
*
|
|
48
|
+
* **Mutable per-feed** (synced from handler at the top of every feed() call, and
|
|
49
|
+
* updated inside action closures via syncFeedContextMutableFields):
|
|
50
|
+
* - `prompt`, `cursorPos` — current text buffer state
|
|
51
|
+
* - `commandMode`, `panelFocused`, `indicatorFocused` — focus-mode flags
|
|
52
|
+
* - `helpOverlayActive`, `helpScrollOffset` — help overlay visibility and scroll
|
|
53
|
+
* - `shortcutsOverlayActive`, `shortcutsScrollOffset` — shortcuts overlay state
|
|
54
|
+
* - `nextPasteId`, `nextImageId` — monotonically increasing ID counters
|
|
55
|
+
* - `mouseDownRow`, `mouseDownCol` — drag-tracking coordinates
|
|
56
|
+
* - `contentWidth` — reflow width (semi-stable; synced at feed() entry only)
|
|
57
|
+
* - `selectionCallback` — current in-flight selection modal callback (nullable)
|
|
58
|
+
* - `requestRender` — swapped per-feed to a buffered version, restored after
|
|
59
|
+
*
|
|
60
|
+
* **Stable service handles** (set once at construction, never reallocated):
|
|
61
|
+
* - `commandRegistry`, `commandContext` — wired via setCommandRegistry() after
|
|
62
|
+
* construction; synced at feed() entry (not per-action) since no action changes them
|
|
63
|
+
* - `autocomplete` — wired after construction; synced at feed() entry
|
|
64
|
+
* - `inputHistory`, `conversationManager` — late-wired service handles; synced at
|
|
65
|
+
* feed() entry only since no in-feed action rewires them
|
|
66
|
+
* - `pasteRegistry`, `imageRegistry` — owned Maps, never replaced
|
|
67
|
+
* - `selectionModal`, `bookmarkModal`, `settingsModal`, `sessionPickerModal`,
|
|
68
|
+
* `profilePickerModal` — modal objects constructed once in InputHandler constructor
|
|
69
|
+
* - `filePicker`, `modelPicker`, `processModal`, `liveTailModal`, `agentDetailModal`,
|
|
70
|
+
* `contextInspectorModal`, `blockActionsMenu`, `searchManager`, `historySearch` —
|
|
71
|
+
* service objects constructed once
|
|
72
|
+
* - `panelManager`, `keybindingsManager` — from uiServices, stable for app lifetime
|
|
73
|
+
* - `modalStack` — reference to the handler's shared array (mutated in place)
|
|
74
|
+
* - `getHistory`, `getViewportHeight`, `getScrollTop`, `scroll`, `exitApp` — stable
|
|
75
|
+
* callbacks bound in the InputHandler constructor
|
|
76
|
+
* - All method closures (`modalOpened`, `handleEscape`, etc.) — bound once at init
|
|
77
|
+
*
|
|
78
|
+
* **Rationale:** per-feed mutation avoids per-keystroke allocation cost; stable
|
|
79
|
+
* references are service handles whose identity never changes after construction.
|
|
80
|
+
*/
|
|
43
81
|
export interface InputFeedContext {
|
|
44
82
|
prompt: string;
|
|
45
83
|
cursorPos: number;
|
|
@@ -65,9 +103,9 @@ export interface InputFeedContext {
|
|
|
65
103
|
readonly sessionPickerModal: SessionPickerModal;
|
|
66
104
|
readonly profilePickerModal: ProfilePickerModal;
|
|
67
105
|
readonly historySearch: HistorySearch;
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
106
|
+
commandRegistry: CommandRegistry | null;
|
|
107
|
+
commandContext: CommandContext | undefined;
|
|
108
|
+
autocomplete: AutocompleteEngine | null;
|
|
71
109
|
readonly filePicker: FilePickerModal;
|
|
72
110
|
readonly modelPicker: ModelPickerModal;
|
|
73
111
|
readonly processModal: ProcessModal;
|
|
@@ -79,13 +117,13 @@ export interface InputFeedContext {
|
|
|
79
117
|
readonly panelManager: PanelManager;
|
|
80
118
|
readonly keybindingsManager: KeybindingsManager;
|
|
81
119
|
readonly modalStack: string[];
|
|
82
|
-
|
|
83
|
-
|
|
120
|
+
inputHistory: InputHistory | null;
|
|
121
|
+
conversationManager: ConversationManager | null;
|
|
84
122
|
readonly getHistory: () => InfiniteBuffer;
|
|
85
123
|
readonly getViewportHeight: () => number;
|
|
86
124
|
readonly getScrollTop: () => number;
|
|
87
125
|
readonly scroll: (delta: number) => void;
|
|
88
|
-
|
|
126
|
+
requestRender: () => void;
|
|
89
127
|
readonly modalOpened: (name: string) => void;
|
|
90
128
|
readonly handleEscape: () => void;
|
|
91
129
|
readonly handleCopy: () => void;
|
|
@@ -52,144 +52,157 @@ export function handleGlobalShortcutToken(
|
|
|
52
52
|
): boolean {
|
|
53
53
|
if (token.type !== 'key') return false;
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
if (
|
|
57
|
-
state.
|
|
55
|
+
// Fast-path: bare pageup/pagedown have no keybinding entry.
|
|
56
|
+
if (token.logicalName === 'pageup') {
|
|
57
|
+
state.scroll(-Math.max(1, viewportHeight - 2));
|
|
58
58
|
return true;
|
|
59
59
|
}
|
|
60
|
-
if (
|
|
61
|
-
state.
|
|
60
|
+
if (token.logicalName === 'pagedown') {
|
|
61
|
+
state.scroll(Math.max(1, viewportHeight - 2));
|
|
62
62
|
return true;
|
|
63
63
|
}
|
|
64
|
+
// Bare escape is also not in the keybinding table.
|
|
64
65
|
if (token.logicalName === 'escape' && !state.panelFocused) {
|
|
65
66
|
state.handleEscape();
|
|
66
67
|
return true;
|
|
67
68
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
69
|
+
|
|
70
|
+
// O(1) lookup via inverted map.
|
|
71
|
+
const kb = state.keybindingsManager;
|
|
72
|
+
const action = kb.lookup(token);
|
|
73
|
+
|
|
74
|
+
switch (action) {
|
|
75
|
+
case 'copy-selection':
|
|
76
|
+
state.handleCopy();
|
|
77
|
+
return true;
|
|
78
|
+
|
|
79
|
+
case 'clear-cancel':
|
|
80
|
+
state.handleCtrlC();
|
|
81
|
+
return true;
|
|
82
|
+
|
|
83
|
+
case 'screen-clear':
|
|
84
|
+
state.commandContext?.clearScreen?.();
|
|
85
|
+
return true;
|
|
86
|
+
|
|
87
|
+
case 'panel-close-all': {
|
|
88
|
+
const pm = state.panelManager;
|
|
89
|
+
for (const p of pm.getAllOpen()) pm.close(p.id);
|
|
90
|
+
pm.hide();
|
|
84
91
|
state.requestRender();
|
|
92
|
+
return true;
|
|
85
93
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
if (kb.matches('panel-tab-next', token)) {
|
|
94
|
-
state.cyclePanelTab('next');
|
|
95
|
-
return true;
|
|
96
|
-
}
|
|
97
|
-
if (kb.matches('panel-tab-prev', token)) {
|
|
98
|
-
state.cyclePanelTab('prev');
|
|
99
|
-
return true;
|
|
100
|
-
}
|
|
101
|
-
if (kb.matches('history-search', token)) {
|
|
102
|
-
state.historySearch.open(state.prompt);
|
|
103
|
-
state.requestRender();
|
|
104
|
-
return true;
|
|
105
|
-
}
|
|
106
|
-
if (kb.matches('search', token)) {
|
|
107
|
-
if (state.searchManager.active) state.searchManager.close();
|
|
108
|
-
else state.searchManager.open();
|
|
109
|
-
state.requestRender();
|
|
110
|
-
return true;
|
|
111
|
-
}
|
|
112
|
-
if (kb.matches('block-copy', token) && !state.commandMode) {
|
|
113
|
-
state.handleBlockCopy();
|
|
114
|
-
return true;
|
|
115
|
-
}
|
|
116
|
-
if (kb.matches('bookmark', token) && !state.commandMode) {
|
|
117
|
-
state.handleBookmark();
|
|
118
|
-
return true;
|
|
119
|
-
}
|
|
120
|
-
if (kb.matches('block-save', token) && !state.commandMode) {
|
|
121
|
-
state.handleBlockSave();
|
|
122
|
-
return true;
|
|
123
|
-
}
|
|
124
|
-
if (kb.matches('delete-word', token)) {
|
|
125
|
-
state.saveUndoState();
|
|
126
|
-
let pos = state.cursorPos;
|
|
127
|
-
while (pos > 0 && state.prompt[pos - 1] === ' ') pos--;
|
|
128
|
-
while (pos > 0 && state.prompt[pos - 1] !== ' ') pos--;
|
|
129
|
-
state.prompt = state.prompt.slice(0, pos) + state.prompt.slice(state.cursorPos);
|
|
130
|
-
state.cursorPos = pos;
|
|
131
|
-
state.ensureInputCursorVisible();
|
|
132
|
-
return true;
|
|
133
|
-
}
|
|
134
|
-
if (kb.matches('apply-diff-line-start', token)) {
|
|
135
|
-
if (!state.commandMode && state.handleDiffApply()) return true;
|
|
136
|
-
const info = state.getWrappedPromptInfo(state.contentWidth);
|
|
137
|
-
state.cursorPos = info.wrappedLines.length > 1 ? info.segments[info.cursorWrappedLine].rawStart : 0;
|
|
138
|
-
state.ensureInputCursorVisible();
|
|
139
|
-
return true;
|
|
140
|
-
}
|
|
141
|
-
if (kb.matches('next-error-line-end', token)) {
|
|
142
|
-
if (state.prompt === '' && !state.commandMode) {
|
|
143
|
-
const nextLine = state.conversationManager?.nextErrorLine(state.getScrollTop()) ?? -1;
|
|
144
|
-
if (nextLine >= 0) {
|
|
145
|
-
state.scroll(nextLine - state.getScrollTop());
|
|
94
|
+
|
|
95
|
+
case 'panel-close': {
|
|
96
|
+
const pm = state.panelManager;
|
|
97
|
+
const active = pm.getActivePanel();
|
|
98
|
+
if (active) {
|
|
99
|
+
pm.close(active.id);
|
|
146
100
|
state.requestRender();
|
|
147
|
-
return true;
|
|
148
101
|
}
|
|
102
|
+
return true;
|
|
149
103
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
104
|
+
|
|
105
|
+
case 'panel-picker':
|
|
106
|
+
state.commandContext?.openPanelPicker?.();
|
|
107
|
+
state.requestRender();
|
|
108
|
+
return true;
|
|
109
|
+
|
|
110
|
+
case 'panel-tab-next':
|
|
111
|
+
state.cyclePanelTab('next');
|
|
112
|
+
return true;
|
|
113
|
+
|
|
114
|
+
case 'panel-tab-prev':
|
|
115
|
+
state.cyclePanelTab('prev');
|
|
116
|
+
return true;
|
|
117
|
+
|
|
118
|
+
case 'history-search':
|
|
119
|
+
state.historySearch.open(state.prompt);
|
|
120
|
+
state.requestRender();
|
|
121
|
+
return true;
|
|
122
|
+
|
|
123
|
+
case 'search':
|
|
124
|
+
if (state.searchManager.active) state.searchManager.close();
|
|
125
|
+
else state.searchManager.open();
|
|
126
|
+
state.requestRender();
|
|
127
|
+
return true;
|
|
128
|
+
|
|
129
|
+
case 'block-copy':
|
|
130
|
+
if (!state.commandMode) { state.handleBlockCopy(); return true; }
|
|
131
|
+
return false;
|
|
132
|
+
|
|
133
|
+
case 'bookmark':
|
|
134
|
+
if (!state.commandMode) { state.handleBookmark(); return true; }
|
|
135
|
+
return false;
|
|
136
|
+
|
|
137
|
+
case 'block-save':
|
|
138
|
+
if (!state.commandMode) { state.handleBlockSave(); return true; }
|
|
139
|
+
return false;
|
|
140
|
+
|
|
141
|
+
case 'delete-word': {
|
|
142
|
+
state.saveUndoState();
|
|
143
|
+
let pos = state.cursorPos;
|
|
144
|
+
while (pos > 0 && state.prompt[pos - 1] === ' ') pos--;
|
|
145
|
+
while (pos > 0 && state.prompt[pos - 1] !== ' ') pos--;
|
|
146
|
+
state.prompt = state.prompt.slice(0, pos) + state.prompt.slice(state.cursorPos);
|
|
147
|
+
state.cursorPos = pos;
|
|
148
|
+
state.ensureInputCursorVisible();
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
case 'apply-diff-line-start': {
|
|
153
|
+
if (!state.commandMode && state.handleDiffApply()) return true;
|
|
154
|
+
const info = state.getWrappedPromptInfo(state.contentWidth);
|
|
155
|
+
state.cursorPos = info.wrappedLines.length > 1 ? info.segments[info.cursorWrappedLine].rawStart : 0;
|
|
156
|
+
state.ensureInputCursorVisible();
|
|
157
|
+
return true;
|
|
170
158
|
}
|
|
171
|
-
return true;
|
|
172
|
-
}
|
|
173
|
-
if (kb.matches('undo', token)) {
|
|
174
|
-
state.handleUndo();
|
|
175
|
-
return true;
|
|
176
|
-
}
|
|
177
|
-
if (kb.matches('redo', token)) {
|
|
178
|
-
state.handleRedo();
|
|
179
|
-
return true;
|
|
180
|
-
}
|
|
181
|
-
if (kb.matches('paste', token)) {
|
|
182
|
-
state.handlePaste();
|
|
183
|
-
return true;
|
|
184
|
-
}
|
|
185
|
-
if (token.logicalName === 'pageup') {
|
|
186
|
-
state.scroll(-Math.max(1, viewportHeight - 2));
|
|
187
|
-
return true;
|
|
188
|
-
}
|
|
189
|
-
if (token.logicalName === 'pagedown') {
|
|
190
|
-
state.scroll(Math.max(1, viewportHeight - 2));
|
|
191
|
-
return true;
|
|
192
|
-
}
|
|
193
159
|
|
|
194
|
-
|
|
160
|
+
case 'next-error-line-end': {
|
|
161
|
+
if (state.prompt === '' && !state.commandMode) {
|
|
162
|
+
const nextLine = state.conversationManager?.nextErrorLine(state.getScrollTop()) ?? -1;
|
|
163
|
+
if (nextLine >= 0) {
|
|
164
|
+
state.scroll(nextLine - state.getScrollTop());
|
|
165
|
+
state.requestRender();
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const info = state.getWrappedPromptInfo(state.contentWidth);
|
|
170
|
+
state.cursorPos = info.wrappedLines.length > 1
|
|
171
|
+
? info.segments[info.cursorWrappedLine].rawStart + info.segments[info.cursorWrappedLine].length
|
|
172
|
+
: state.prompt.length;
|
|
173
|
+
state.ensureInputCursorVisible();
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
case 'kill-line':
|
|
178
|
+
state.saveUndoState();
|
|
179
|
+
state.prompt = state.prompt.slice(0, state.cursorPos);
|
|
180
|
+
state.ensureInputCursorVisible();
|
|
181
|
+
return true;
|
|
182
|
+
|
|
183
|
+
case 'clear-prompt':
|
|
184
|
+
state.saveUndoState();
|
|
185
|
+
state.prompt = '';
|
|
186
|
+
state.cursorPos = 0;
|
|
187
|
+
if (state.commandMode) {
|
|
188
|
+
state.commandMode = false;
|
|
189
|
+
state.autocomplete?.reset();
|
|
190
|
+
}
|
|
191
|
+
return true;
|
|
192
|
+
|
|
193
|
+
case 'undo':
|
|
194
|
+
state.handleUndo();
|
|
195
|
+
return true;
|
|
196
|
+
|
|
197
|
+
case 'redo':
|
|
198
|
+
state.handleRedo();
|
|
199
|
+
return true;
|
|
200
|
+
|
|
201
|
+
case 'paste':
|
|
202
|
+
state.handlePaste();
|
|
203
|
+
return true;
|
|
204
|
+
|
|
205
|
+
default:
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
195
208
|
}
|