@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
package/src/input/handler.ts
CHANGED
|
@@ -81,6 +81,7 @@ import {
|
|
|
81
81
|
} from './handler-picker-routes.ts';
|
|
82
82
|
import { handleGlobalShortcutToken } from './handler-shortcuts.ts';
|
|
83
83
|
import { feedInputTokens } from './handler-feed.ts';
|
|
84
|
+
import { buildInitialFeedContext, syncFeedContextMutableFields } from './feed-context-factory.ts';
|
|
84
85
|
import { handlePanelIntegrationAction as runPanelIntegrationAction } from './panel-integration-actions.ts';
|
|
85
86
|
import type { Panel } from '../panels/types.ts';
|
|
86
87
|
import type { UiRuntimeServices } from '../runtime/ui-services.ts';
|
|
@@ -112,6 +113,8 @@ export class InputHandler {
|
|
|
112
113
|
private pasteRegistry = new Map<string, string>();
|
|
113
114
|
private nextPasteId = 1;
|
|
114
115
|
private lastCtrlCTime = 0;
|
|
116
|
+
/** Long-lived feed context — reused across every feed() call to avoid per-keystroke allocation. */
|
|
117
|
+
private feedContext!: import('./handler-feed.ts').InputFeedContext;
|
|
115
118
|
private commandRegistry: CommandRegistry | null = null;
|
|
116
119
|
private commandContext: CommandContext | undefined = undefined;
|
|
117
120
|
public autocomplete: AutocompleteEngine | null = null;
|
|
@@ -208,32 +211,108 @@ export class InputHandler {
|
|
|
208
211
|
this.bookmarkModal = new BookmarkModal(uiServices.shell.bookmarkManager);
|
|
209
212
|
this.sessionPickerModal = new SessionPickerModal(uiServices.sessions.sessionManager);
|
|
210
213
|
this.profilePickerModal = new ProfilePickerModal(uiServices.shell.profileManager);
|
|
214
|
+
this.initFeedContext();
|
|
211
215
|
}
|
|
212
216
|
|
|
213
217
|
/**
|
|
214
|
-
*
|
|
215
|
-
*
|
|
218
|
+
* initFeedContext — Build the long-lived InputFeedContext once via factory.
|
|
219
|
+
* See feed-context-factory.ts for full field documentation.
|
|
216
220
|
*/
|
|
217
|
-
|
|
218
|
-
this.
|
|
221
|
+
private initFeedContext(): void {
|
|
222
|
+
this.feedContext = buildInitialFeedContext(
|
|
223
|
+
{
|
|
224
|
+
prompt: this.prompt, cursorPos: this.cursorPos, commandMode: this.commandMode,
|
|
225
|
+
panelFocused: this.panelFocused, indicatorFocused: this.indicatorFocused,
|
|
226
|
+
helpOverlayActive: this.helpOverlayActive, helpScrollOffset: this.helpScrollOffset,
|
|
227
|
+
shortcutsOverlayActive: this.shortcutsOverlayActive, shortcutsScrollOffset: this.shortcutsScrollOffset,
|
|
228
|
+
nextPasteId: this.nextPasteId, nextImageId: this.nextImageId,
|
|
229
|
+
mouseDownRow: this.mouseDownRow, mouseDownCol: this.mouseDownCol,
|
|
230
|
+
contentWidth: this.contentWidth, selectionCallback: this.selectionCallback,
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
selection: this.selection,
|
|
234
|
+
pasteRegistry: this.pasteRegistry,
|
|
235
|
+
imageRegistry: this.imageRegistry,
|
|
236
|
+
selectionModal: this.selectionModal,
|
|
237
|
+
bookmarkModal: this.bookmarkModal,
|
|
238
|
+
settingsModal: this.settingsModal,
|
|
239
|
+
sessionPickerModal: this.sessionPickerModal,
|
|
240
|
+
profilePickerModal: this.profilePickerModal,
|
|
241
|
+
historySearch: this.historySearch,
|
|
242
|
+
commandRegistry: this.commandRegistry,
|
|
243
|
+
commandContext: this.commandContext,
|
|
244
|
+
autocomplete: this.autocomplete,
|
|
245
|
+
filePicker: this.filePicker,
|
|
246
|
+
modelPicker: this.modelPicker,
|
|
247
|
+
processModal: this.processModal,
|
|
248
|
+
liveTailModal: this.liveTailModal,
|
|
249
|
+
agentDetailModal: this.agentDetailModal,
|
|
250
|
+
contextInspectorModal: this.contextInspectorModal,
|
|
251
|
+
blockActionsMenu: this.blockActionsMenu,
|
|
252
|
+
searchManager: this.searchManager,
|
|
253
|
+
modalStack: this.modalStack,
|
|
254
|
+
inputHistory: this.inputHistory,
|
|
255
|
+
conversationManager: this.conversationManager,
|
|
256
|
+
panelManager: this.uiServices.shell.panelManager,
|
|
257
|
+
keybindingsManager: this.uiServices.shell.keybindingsManager,
|
|
258
|
+
getHistory: this.getHistory,
|
|
259
|
+
getViewportHeight: this.getViewportHeight,
|
|
260
|
+
getScrollTop: this.getScrollTop,
|
|
261
|
+
scroll: this.scroll,
|
|
262
|
+
exitApp: this.exitApp,
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
modalOpened: (name: string) => this.modalOpened(name),
|
|
266
|
+
handleEscape: () => { this.handleEscape(); this.syncFeedContextMutableFields(); },
|
|
267
|
+
handleCopy: () => this.handleCopy(),
|
|
268
|
+
handleCtrlC: () => { this.handleCtrlC(); this.syncFeedContextMutableFields(); },
|
|
269
|
+
handleBlockCopy: () => this.handleBlockCopy(),
|
|
270
|
+
handleBookmark: () => this.handleBookmark(),
|
|
271
|
+
handleBlockSave: () => this.handleBlockSave(),
|
|
272
|
+
handleDiffApply: () => this.handleDiffApply(),
|
|
273
|
+
handleUndo: () => { this.handleUndo(); this.syncFeedContextMutableFields(); },
|
|
274
|
+
handleRedo: () => { this.handleRedo(); this.syncFeedContextMutableFields(); },
|
|
275
|
+
handlePaste: () => { this.handlePaste(); this.syncFeedContextMutableFields(); },
|
|
276
|
+
saveUndoState: () => this.saveUndoState(),
|
|
277
|
+
ensureInputCursorVisible: (contentWidth?: number) => this.ensureInputCursorVisible(contentWidth),
|
|
278
|
+
registerPaste: (content: string) => this.registerPaste(content),
|
|
279
|
+
executeBlockAction: (id: string) => this.executeBlockAction(id),
|
|
280
|
+
cyclePanelTab: (direction: 'next' | 'prev') => this.cyclePanelTab(direction),
|
|
281
|
+
onPanelInputConsumed: (activePanel: Panel | null, key: string) => this.handlePanelIntegrationAction(activePanel, key),
|
|
282
|
+
getWrappedPromptInfo: (contentWidth: number) => this.getWrappedPromptInfo(contentWidth),
|
|
283
|
+
moveCursorVertical: (direction: -1 | 1) => this.moveCursorVertical(direction),
|
|
284
|
+
handlePathCompletion: () => this.handlePathCompletion(),
|
|
285
|
+
handleBlockToggle: () => this.handleBlockToggle(),
|
|
286
|
+
findMarkerAtPos: (pos: number) => this.findMarkerAtPos(pos),
|
|
287
|
+
cleanupMarkerRegistry: (text: string) => this.cleanupMarkerRegistry(text),
|
|
288
|
+
expandPrompt: (text: string) => this.expandPrompt(text),
|
|
289
|
+
},
|
|
290
|
+
);
|
|
219
291
|
}
|
|
220
292
|
|
|
221
|
-
/**
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
293
|
+
/** Sync mutable handler fields back into feedContext after in-feed mutations. */
|
|
294
|
+
private syncFeedContextMutableFields(): void {
|
|
295
|
+
const h = this;
|
|
296
|
+
syncFeedContextMutableFields({ prompt: h.prompt, cursorPos: h.cursorPos, commandMode: h.commandMode,
|
|
297
|
+
panelFocused: h.panelFocused, indicatorFocused: h.indicatorFocused, helpOverlayActive: h.helpOverlayActive,
|
|
298
|
+
helpScrollOffset: h.helpScrollOffset, shortcutsOverlayActive: h.shortcutsOverlayActive,
|
|
299
|
+
shortcutsScrollOffset: h.shortcutsScrollOffset, selectionCallback: h.selectionCallback,
|
|
300
|
+
nextPasteId: h.nextPasteId, nextImageId: h.nextImageId, mouseDownRow: h.mouseDownRow,
|
|
301
|
+
mouseDownCol: h.mouseDownCol, contentWidth: h.contentWidth }, this.feedContext);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** Wire in the InputHistory instance. Optional; disables history navigation if unset. */
|
|
305
|
+
public setHistory(history: InputHistory): void { this.inputHistory = history; }
|
|
306
|
+
|
|
307
|
+
/** Wire in the slash command registry and context. Must be called before commands work. */
|
|
225
308
|
public setCommandRegistry(registry: CommandRegistry, context: CommandContext): void {
|
|
226
309
|
this.commandRegistry = registry;
|
|
227
310
|
this.commandContext = context;
|
|
228
311
|
this.autocomplete = new AutocompleteEngine(registry);
|
|
229
312
|
}
|
|
230
313
|
|
|
231
|
-
/**
|
|
232
|
-
|
|
233
|
-
*/
|
|
234
|
-
public setConversationManager(cm: ConversationManager): void {
|
|
235
|
-
this.conversationManager = cm;
|
|
236
|
-
}
|
|
314
|
+
/** Wire in the conversation manager for block copy/apply/collapse. */
|
|
315
|
+
public setConversationManager(cm: ConversationManager): void { this.conversationManager = cm; }
|
|
237
316
|
|
|
238
317
|
/**
|
|
239
318
|
* openSelection - Open the generic selection modal with a callback.
|
|
@@ -464,6 +543,7 @@ export class InputHandler {
|
|
|
464
543
|
|
|
465
544
|
/**
|
|
466
545
|
* feed - Process raw stdin data through the tokenizer.
|
|
546
|
+
* Reuses the long-lived this.feedContext to avoid per-keystroke object allocation.
|
|
467
547
|
*/
|
|
468
548
|
public feed(data: string): void {
|
|
469
549
|
const immediateRequestRender = this.requestRender;
|
|
@@ -479,111 +559,31 @@ export class InputHandler {
|
|
|
479
559
|
|
|
480
560
|
this.requestRender = bufferedRequestRender;
|
|
481
561
|
try {
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
context =
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
helpScrollOffset: this.helpScrollOffset,
|
|
508
|
-
shortcutsOverlayActive: this.shortcutsOverlayActive,
|
|
509
|
-
shortcutsScrollOffset: this.shortcutsScrollOffset,
|
|
510
|
-
nextPasteId: this.nextPasteId,
|
|
511
|
-
nextImageId: this.nextImageId,
|
|
512
|
-
mouseDownRow: this.mouseDownRow,
|
|
513
|
-
mouseDownCol: this.mouseDownCol,
|
|
514
|
-
contentWidth: this.contentWidth,
|
|
515
|
-
pasteRegistry: this.pasteRegistry,
|
|
516
|
-
imageRegistry: this.imageRegistry,
|
|
517
|
-
selection: this.selection,
|
|
518
|
-
selectionModal: this.selectionModal,
|
|
519
|
-
selectionCallback: this.selectionCallback,
|
|
520
|
-
bookmarkModal: this.bookmarkModal,
|
|
521
|
-
settingsModal: this.settingsModal,
|
|
522
|
-
sessionPickerModal: this.sessionPickerModal,
|
|
523
|
-
profilePickerModal: this.profilePickerModal,
|
|
524
|
-
historySearch: this.historySearch,
|
|
525
|
-
commandRegistry: this.commandRegistry,
|
|
526
|
-
commandContext: this.commandContext,
|
|
527
|
-
autocomplete: this.autocomplete,
|
|
528
|
-
filePicker: this.filePicker,
|
|
529
|
-
modelPicker: this.modelPicker,
|
|
530
|
-
processModal: this.processModal,
|
|
531
|
-
liveTailModal: this.liveTailModal,
|
|
532
|
-
agentDetailModal: this.agentDetailModal,
|
|
533
|
-
contextInspectorModal: this.contextInspectorModal,
|
|
534
|
-
blockActionsMenu: this.blockActionsMenu,
|
|
535
|
-
searchManager: this.searchManager,
|
|
536
|
-
modalStack: this.modalStack,
|
|
537
|
-
inputHistory: this.inputHistory,
|
|
538
|
-
conversationManager: this.conversationManager,
|
|
539
|
-
getHistory: this.getHistory,
|
|
540
|
-
getViewportHeight: this.getViewportHeight,
|
|
541
|
-
getScrollTop: this.getScrollTop,
|
|
542
|
-
scroll: this.scroll,
|
|
543
|
-
requestRender: bufferedRequestRender,
|
|
544
|
-
modalOpened: (name: string) => this.modalOpened(name),
|
|
545
|
-
handleEscape: () => {
|
|
546
|
-
this.handleEscape();
|
|
547
|
-
syncFeedContextFromHandler();
|
|
548
|
-
},
|
|
549
|
-
handleCopy: () => this.handleCopy(),
|
|
550
|
-
handleCtrlC: () => {
|
|
551
|
-
this.handleCtrlC();
|
|
552
|
-
syncFeedContextFromHandler();
|
|
553
|
-
},
|
|
554
|
-
handleBlockCopy: () => this.handleBlockCopy(),
|
|
555
|
-
handleBookmark: () => this.handleBookmark(),
|
|
556
|
-
handleBlockSave: () => this.handleBlockSave(),
|
|
557
|
-
handleDiffApply: () => this.handleDiffApply(),
|
|
558
|
-
handleUndo: () => {
|
|
559
|
-
this.handleUndo();
|
|
560
|
-
syncFeedContextFromHandler();
|
|
561
|
-
},
|
|
562
|
-
handleRedo: () => {
|
|
563
|
-
this.handleRedo();
|
|
564
|
-
syncFeedContextFromHandler();
|
|
565
|
-
},
|
|
566
|
-
handlePaste: () => {
|
|
567
|
-
this.handlePaste();
|
|
568
|
-
syncFeedContextFromHandler();
|
|
569
|
-
},
|
|
570
|
-
saveUndoState: () => this.saveUndoState(),
|
|
571
|
-
ensureInputCursorVisible: (contentWidth?: number) => this.ensureInputCursorVisible(contentWidth),
|
|
572
|
-
registerPaste: (content: string) => this.registerPaste(content),
|
|
573
|
-
executeBlockAction: (id: string) => this.executeBlockAction(id),
|
|
574
|
-
cyclePanelTab: (direction: 'next' | 'prev') => this.cyclePanelTab(direction),
|
|
575
|
-
onPanelInputConsumed: (activePanel: Panel | null, key: string) => this.handlePanelIntegrationAction(activePanel, key),
|
|
576
|
-
panelManager: this.uiServices.shell.panelManager,
|
|
577
|
-
keybindingsManager: this.uiServices.shell.keybindingsManager,
|
|
578
|
-
getWrappedPromptInfo: (contentWidth: number) => this.getWrappedPromptInfo(contentWidth),
|
|
579
|
-
moveCursorVertical: (direction: -1 | 1) => this.moveCursorVertical(direction),
|
|
580
|
-
handlePathCompletion: () => this.handlePathCompletion(),
|
|
581
|
-
handleBlockToggle: () => this.handleBlockToggle(),
|
|
582
|
-
findMarkerAtPos: (pos: number) => this.findMarkerAtPos(pos),
|
|
583
|
-
cleanupMarkerRegistry: (text: string) => this.cleanupMarkerRegistry(text),
|
|
584
|
-
expandPrompt: (text: string) => this.expandPrompt(text),
|
|
585
|
-
exitApp: this.exitApp,
|
|
586
|
-
};
|
|
562
|
+
const context = this.feedContext;
|
|
563
|
+
// Sync mutable scalars from handler into the reused context.
|
|
564
|
+
context.prompt = this.prompt;
|
|
565
|
+
context.cursorPos = this.cursorPos;
|
|
566
|
+
context.commandMode = this.commandMode;
|
|
567
|
+
context.panelFocused = this.panelFocused;
|
|
568
|
+
context.indicatorFocused = this.indicatorFocused;
|
|
569
|
+
context.helpOverlayActive = this.helpOverlayActive;
|
|
570
|
+
context.helpScrollOffset = this.helpScrollOffset;
|
|
571
|
+
context.shortcutsOverlayActive = this.shortcutsOverlayActive;
|
|
572
|
+
context.shortcutsScrollOffset = this.shortcutsScrollOffset;
|
|
573
|
+
context.selectionCallback = this.selectionCallback;
|
|
574
|
+
context.nextPasteId = this.nextPasteId;
|
|
575
|
+
context.nextImageId = this.nextImageId;
|
|
576
|
+
context.mouseDownRow = this.mouseDownRow;
|
|
577
|
+
context.mouseDownCol = this.mouseDownCol;
|
|
578
|
+
context.contentWidth = this.contentWidth;
|
|
579
|
+
// Sync semi-stable refs that may be wired after construction.
|
|
580
|
+
context.commandRegistry = this.commandRegistry;
|
|
581
|
+
context.commandContext = this.commandContext;
|
|
582
|
+
context.autocomplete = this.autocomplete;
|
|
583
|
+
context.inputHistory = this.inputHistory;
|
|
584
|
+
context.conversationManager = this.conversationManager;
|
|
585
|
+
// Swap requestRender to buffered version for this feed.
|
|
586
|
+
context.requestRender = bufferedRequestRender;
|
|
587
587
|
this.syncFeedSelectionCallback = (callback) => {
|
|
588
588
|
context.selectionCallback = callback;
|
|
589
589
|
};
|
package/src/input/keybindings.ts
CHANGED
|
@@ -138,11 +138,14 @@ function resolveKeybindingsPath(options?: KeybindingsManagerOptions): string {
|
|
|
138
138
|
export class KeybindingsManager {
|
|
139
139
|
private bindings: Record<KeyAction, KeyCombo[]>;
|
|
140
140
|
private configPath: string;
|
|
141
|
+
/** Inverted lookup map: composite key → KeyAction. Built in buildLookupMap(). */
|
|
142
|
+
private lookupMap = new Map<string, KeyAction>();
|
|
141
143
|
|
|
142
144
|
constructor(options: KeybindingsManagerOptions) {
|
|
143
145
|
this.configPath = resolveKeybindingsPath(options);
|
|
144
146
|
// Start with deep copy of defaults
|
|
145
147
|
this.bindings = this.cloneDefaults();
|
|
148
|
+
this.buildLookupMap();
|
|
146
149
|
}
|
|
147
150
|
|
|
148
151
|
private cloneDefaults(): Record<KeyAction, KeyCombo[]> {
|
|
@@ -184,6 +187,33 @@ export class KeybindingsManager {
|
|
|
184
187
|
} catch (err) {
|
|
185
188
|
logger.debug('keybindings: failed to load config file', { path: this.configPath, err: summarizeError(err) });
|
|
186
189
|
}
|
|
190
|
+
this.buildLookupMap();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* buildLookupMap — Rebuild the inverted lookup map from the current bindings table.
|
|
195
|
+
* Called after constructor init and after loadFromDisk().
|
|
196
|
+
* Map key format: "logicalName:ctrl:shift:alt" (booleans as 0/1).
|
|
197
|
+
* Last writer wins for duplicate combos (deterministic: iterate actions in order).
|
|
198
|
+
*/
|
|
199
|
+
private buildLookupMap(): void {
|
|
200
|
+
this.lookupMap.clear();
|
|
201
|
+
for (const [action, combos] of Object.entries(this.bindings) as [KeyAction, KeyCombo[]][]) {
|
|
202
|
+
for (const combo of combos) {
|
|
203
|
+
const key = `${combo.key}:${combo.ctrl ? 1 : 0}:${combo.shift ? 1 : 0}:${combo.alt ? 1 : 0}`;
|
|
204
|
+
this.lookupMap.set(key, action);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* lookup — O(1) keybinding lookup by token.
|
|
211
|
+
* Returns the matching KeyAction, or null if no binding matches.
|
|
212
|
+
*/
|
|
213
|
+
lookup(token: { logicalName?: string; ctrl?: boolean; shift?: boolean; alt?: boolean }): KeyAction | null {
|
|
214
|
+
if (!token.logicalName) return null;
|
|
215
|
+
const key = `${token.logicalName}:${token.ctrl ? 1 : 0}:${token.shift ? 1 : 0}:${token.alt ? 1 : 0}`;
|
|
216
|
+
return this.lookupMap.get(key) ?? null;
|
|
187
217
|
}
|
|
188
218
|
|
|
189
219
|
private validateCombos(combos: unknown[]): combos is KeyCombo[] {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { CommandContext } from './command-registry.ts';
|
|
2
|
+
import { logger } from '@pellux/goodvibes-sdk/platform/utils/logger';
|
|
2
3
|
import type { Panel } from '../panels/types.ts';
|
|
3
4
|
import type { PanelManager } from '../panels/panel-manager.ts';
|
|
4
5
|
import { FileExplorerPanel } from '../panels/file-explorer-panel.ts';
|
|
@@ -69,7 +70,7 @@ export function handlePanelIntegrationAction(
|
|
|
69
70
|
const parts = command.replace(/^\//, '').split(/\s+/).filter(Boolean);
|
|
70
71
|
const [name, ...args] = parts;
|
|
71
72
|
if (!name) return false;
|
|
72
|
-
void commandContext.executeCommand(name, args).catch(() => {});
|
|
73
|
+
void commandContext.executeCommand(name, args).catch((err) => { logger.debug('approval panel command dispatch failed', { err }); });
|
|
73
74
|
return true;
|
|
74
75
|
}
|
|
75
76
|
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { ConfigSetting } from '@pellux/goodvibes-sdk/platform/config/schema';
|
|
2
|
+
import type { ProviderAuthFreshness, ProviderAuthRoute } from '@pellux/goodvibes-sdk/platform/runtime/provider-accounts/registry';
|
|
3
|
+
import type { FeatureFlag, FlagState } from '@pellux/goodvibes-sdk/platform/runtime/feature-flags/types';
|
|
4
|
+
|
|
5
|
+
export type SettingsCategory = 'display' | 'ui' | 'provider' | 'subscriptions' | 'behavior' | 'storage' | 'permissions' | 'mcp' | 'sandbox' | 'danger' | 'tools' | 'flags' | 'network';
|
|
6
|
+
|
|
7
|
+
export const SETTINGS_CATEGORIES: SettingsCategory[] = [
|
|
8
|
+
'display',
|
|
9
|
+
'ui',
|
|
10
|
+
'provider',
|
|
11
|
+
'subscriptions',
|
|
12
|
+
'behavior',
|
|
13
|
+
'storage',
|
|
14
|
+
'permissions',
|
|
15
|
+
'mcp',
|
|
16
|
+
'sandbox',
|
|
17
|
+
'danger',
|
|
18
|
+
'tools',
|
|
19
|
+
'flags',
|
|
20
|
+
'network',
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
export interface SettingEntry {
|
|
24
|
+
setting: ConfigSetting;
|
|
25
|
+
currentValue: unknown;
|
|
26
|
+
isDefault: boolean;
|
|
27
|
+
effectiveSource?: 'default' | 'local' | 'synced' | 'managed';
|
|
28
|
+
locked?: boolean;
|
|
29
|
+
conflict?: boolean;
|
|
30
|
+
sourceLabel?: string;
|
|
31
|
+
lockReason?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface FlagEntry {
|
|
35
|
+
flag: FeatureFlag;
|
|
36
|
+
state: FlagState;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface McpEntry {
|
|
40
|
+
name: string;
|
|
41
|
+
connected: boolean;
|
|
42
|
+
role: 'general' | 'docs' | 'filesystem' | 'git' | 'database' | 'browser' | 'automation' | 'ops' | 'remote';
|
|
43
|
+
trustMode: 'constrained' | 'ask-on-risk' | 'allow-all' | 'blocked';
|
|
44
|
+
allowedPaths: string[];
|
|
45
|
+
allowedHosts: string[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface SubscriptionEntry {
|
|
49
|
+
provider: string;
|
|
50
|
+
state: 'active' | 'pending' | 'available';
|
|
51
|
+
tokenType?: string;
|
|
52
|
+
expiresAt?: number;
|
|
53
|
+
oauthConfigured: boolean;
|
|
54
|
+
activeRoute?: ProviderAuthRoute;
|
|
55
|
+
preferredRoute?: ProviderAuthRoute;
|
|
56
|
+
authFreshness?: ProviderAuthFreshness;
|
|
57
|
+
routeReason?: string;
|
|
58
|
+
issues?: string[];
|
|
59
|
+
nextActions?: string[];
|
|
60
|
+
}
|
|
@@ -23,67 +23,23 @@ import type { FeatureFlag, FlagState } from '@pellux/goodvibes-sdk/platform/runt
|
|
|
23
23
|
import type { McpRegistry } from '@pellux/goodvibes-sdk/platform/mcp/registry';
|
|
24
24
|
import { logger } from '@pellux/goodvibes-sdk/platform/utils/logger';
|
|
25
25
|
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils/error-display';
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
'danger',
|
|
44
|
-
'tools',
|
|
45
|
-
'flags',
|
|
46
|
-
];
|
|
47
|
-
|
|
48
|
-
export interface SettingEntry {
|
|
49
|
-
setting: ConfigSetting;
|
|
50
|
-
currentValue: unknown;
|
|
51
|
-
isDefault: boolean;
|
|
52
|
-
effectiveSource?: 'default' | 'local' | 'synced' | 'managed';
|
|
53
|
-
locked?: boolean;
|
|
54
|
-
conflict?: boolean;
|
|
55
|
-
sourceLabel?: string;
|
|
56
|
-
lockReason?: string;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** A single feature flag entry for the flags tab. */
|
|
60
|
-
export interface FlagEntry {
|
|
61
|
-
flag: FeatureFlag;
|
|
62
|
-
state: FlagState;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export interface McpEntry {
|
|
66
|
-
name: string;
|
|
67
|
-
connected: boolean;
|
|
68
|
-
role: 'general' | 'docs' | 'filesystem' | 'git' | 'database' | 'browser' | 'automation' | 'ops' | 'remote';
|
|
69
|
-
trustMode: 'constrained' | 'ask-on-risk' | 'allow-all' | 'blocked';
|
|
70
|
-
allowedPaths: string[];
|
|
71
|
-
allowedHosts: string[];
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export interface SubscriptionEntry {
|
|
75
|
-
provider: string;
|
|
76
|
-
state: 'active' | 'pending' | 'available';
|
|
77
|
-
tokenType?: string;
|
|
78
|
-
expiresAt?: number;
|
|
79
|
-
oauthConfigured: boolean;
|
|
80
|
-
activeRoute?: ProviderAuthRoute;
|
|
81
|
-
preferredRoute?: ProviderAuthRoute;
|
|
82
|
-
authFreshness?: ProviderAuthFreshness;
|
|
83
|
-
routeReason?: string;
|
|
84
|
-
issues?: string[];
|
|
85
|
-
nextActions?: string[];
|
|
86
|
-
}
|
|
26
|
+
import {
|
|
27
|
+
SETTINGS_CATEGORIES,
|
|
28
|
+
type FlagEntry,
|
|
29
|
+
type McpEntry,
|
|
30
|
+
type SettingEntry,
|
|
31
|
+
type SettingsCategory,
|
|
32
|
+
type SubscriptionEntry,
|
|
33
|
+
} from './settings-modal-types.ts';
|
|
34
|
+
|
|
35
|
+
export {
|
|
36
|
+
SETTINGS_CATEGORIES,
|
|
37
|
+
type FlagEntry,
|
|
38
|
+
type McpEntry,
|
|
39
|
+
type SettingEntry,
|
|
40
|
+
type SettingsCategory,
|
|
41
|
+
type SubscriptionEntry,
|
|
42
|
+
} from './settings-modal-types.ts';
|
|
87
43
|
|
|
88
44
|
/**
|
|
89
45
|
* Map a config key to the model picker target it should open, or null if the
|
|
@@ -151,6 +107,13 @@ export class SettingsModal {
|
|
|
151
107
|
/** Provider subscription entries (populated when subscriptions tab is active). */
|
|
152
108
|
public subscriptionEntries: SubscriptionEntry[] = [];
|
|
153
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Set after a network-category save that touches controlPlane or httpListener
|
|
112
|
+
* config keys. Renderer reads this to display a transient restart notice.
|
|
113
|
+
* Cleared on next open() or close().
|
|
114
|
+
*/
|
|
115
|
+
public lastSaveTriggeredRestart: 'control-plane' | 'http-listener' | 'web' | null = null;
|
|
116
|
+
|
|
154
117
|
private configManager: ConfigManager | null = null;
|
|
155
118
|
private featureFlagManager: FeatureFlagManager | null = null;
|
|
156
119
|
private mcpRegistry: McpRegistry | null = null;
|
|
@@ -185,6 +148,7 @@ export class SettingsModal {
|
|
|
185
148
|
this.editBuffer = '';
|
|
186
149
|
this.mcpAllowAllConfirmationTarget = null;
|
|
187
150
|
this.subscriptionLogoutConfirmationTarget = null;
|
|
151
|
+
this.lastSaveTriggeredRestart = null;
|
|
188
152
|
this.active = true;
|
|
189
153
|
}
|
|
190
154
|
|
|
@@ -194,6 +158,7 @@ export class SettingsModal {
|
|
|
194
158
|
this.editBuffer = '';
|
|
195
159
|
this.mcpAllowAllConfirmationTarget = null;
|
|
196
160
|
this.subscriptionLogoutConfirmationTarget = null;
|
|
161
|
+
this.lastSaveTriggeredRestart = null;
|
|
197
162
|
this.serviceRegistry = null;
|
|
198
163
|
}
|
|
199
164
|
|
|
@@ -548,7 +513,15 @@ export class SettingsModal {
|
|
|
548
513
|
for (const setting of CONFIG_SCHEMA) {
|
|
549
514
|
const rawCat = setting.key.split('.')[0] as string;
|
|
550
515
|
// Route helper.* settings into the tools group for unified display
|
|
551
|
-
|
|
516
|
+
// Route controlPlane.* and httpListener.* into the network group
|
|
517
|
+
let cat: SettingsCategory;
|
|
518
|
+
if (rawCat === 'helper') {
|
|
519
|
+
cat = 'tools';
|
|
520
|
+
} else if (rawCat === 'controlPlane' || rawCat === 'httpListener' || rawCat === 'web') {
|
|
521
|
+
cat = 'network';
|
|
522
|
+
} else {
|
|
523
|
+
cat = rawCat as SettingsCategory;
|
|
524
|
+
}
|
|
552
525
|
if (!this.groups.has(cat)) continue;
|
|
553
526
|
const currentValue = configManager.get(setting.key as ConfigKey);
|
|
554
527
|
const resolved = getResolvedSettingLookup(configManager, setting.key as ConfigKey)?.entry;
|
|
@@ -720,17 +693,62 @@ export class SettingsModal {
|
|
|
720
693
|
/** Returns [] for the flags category (flags use flagEntries instead). */
|
|
721
694
|
private _currentItems(): SettingEntry[] {
|
|
722
695
|
if (this.currentCategory === 'flags' || this.currentCategory === 'mcp' || this.currentCategory === 'subscriptions') return [];
|
|
723
|
-
|
|
696
|
+
const items = this.groups.get(this.currentCategory) ?? [];
|
|
697
|
+
if (this.currentCategory === 'network') {
|
|
698
|
+
// Hide host fields when the corresponding hostMode is not 'custom'
|
|
699
|
+
return items.filter(entry => {
|
|
700
|
+
if (entry.setting.key === 'controlPlane.host') {
|
|
701
|
+
const hostMode = this.configManager?.get('controlPlane.hostMode');
|
|
702
|
+
return hostMode === 'custom';
|
|
703
|
+
}
|
|
704
|
+
if (entry.setting.key === 'httpListener.host') {
|
|
705
|
+
const hostMode = this.configManager?.get('httpListener.hostMode');
|
|
706
|
+
return hostMode === 'custom';
|
|
707
|
+
}
|
|
708
|
+
if (entry.setting.key === 'web.host') {
|
|
709
|
+
const hostMode = this.configManager?.get('web.hostMode');
|
|
710
|
+
return hostMode === 'custom';
|
|
711
|
+
}
|
|
712
|
+
return true;
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
return items;
|
|
724
716
|
}
|
|
725
717
|
|
|
726
718
|
private _setValue(key: ConfigKey, value: unknown): void {
|
|
727
719
|
if (!this.configManager) return;
|
|
720
|
+
// Diff previous value before writing — avoids false restart notices on no-op saves
|
|
721
|
+
const previousValue = this.configManager.get(key);
|
|
722
|
+
const isRestartKey = ['host', 'port', 'hostMode', 'enabled'].includes(key.split('.')[1] ?? '');
|
|
728
723
|
try {
|
|
729
724
|
this.configManager.setDynamic(key, value);
|
|
730
725
|
// Update the cached entry in-place — avoids full schema re-scan on each edit
|
|
731
726
|
const rawCat = key.split('.')[0] as string;
|
|
732
|
-
//
|
|
733
|
-
|
|
727
|
+
// Resolve the display category from the key prefix
|
|
728
|
+
let cat: SettingsCategory;
|
|
729
|
+
if (rawCat === 'helper') {
|
|
730
|
+
cat = 'tools';
|
|
731
|
+
} else if (rawCat === 'controlPlane') {
|
|
732
|
+
cat = 'network';
|
|
733
|
+
// SDK auto-restarts the daemon server on controlPlane binding changes
|
|
734
|
+
if (isRestartKey && previousValue !== value) {
|
|
735
|
+
this.lastSaveTriggeredRestart = 'control-plane';
|
|
736
|
+
}
|
|
737
|
+
} else if (rawCat === 'httpListener') {
|
|
738
|
+
cat = 'network';
|
|
739
|
+
// SDK auto-restarts the HTTP listener on binding changes
|
|
740
|
+
if (isRestartKey && previousValue !== value) {
|
|
741
|
+
this.lastSaveTriggeredRestart = 'http-listener';
|
|
742
|
+
}
|
|
743
|
+
} else if (rawCat === 'web') {
|
|
744
|
+
cat = 'network';
|
|
745
|
+
// SDK auto-restarts the web server on binding changes
|
|
746
|
+
if (isRestartKey && previousValue !== value) {
|
|
747
|
+
this.lastSaveTriggeredRestart = 'web';
|
|
748
|
+
}
|
|
749
|
+
} else {
|
|
750
|
+
cat = rawCat as SettingsCategory;
|
|
751
|
+
}
|
|
734
752
|
const entries = this.groups.get(cat);
|
|
735
753
|
if (entries) {
|
|
736
754
|
const entry = entries.find(e => e.setting.key === key);
|
|
@@ -97,7 +97,7 @@ export class AgentInspectorPanel extends BasePanel {
|
|
|
97
97
|
private cursorIndex = 0;
|
|
98
98
|
|
|
99
99
|
// Refresh timer (active only while panel is active)
|
|
100
|
-
private
|
|
100
|
+
private refreshTimerId: ReturnType<typeof setInterval> | null = null;
|
|
101
101
|
|
|
102
102
|
// Row cache — cleared on markDirty(), computed once per render cycle
|
|
103
103
|
private _cachedRows: DisplayRow[] | null = null;
|
|
@@ -131,7 +131,7 @@ export class AgentInspectorPanel extends BasePanel {
|
|
|
131
131
|
this.cursorIndex = 0;
|
|
132
132
|
this.timeline = [];
|
|
133
133
|
this.markDirty();
|
|
134
|
-
this._refreshTimeline().catch(() => {});
|
|
134
|
+
this._refreshTimeline().catch((err) => { logger.debug('agent inspector timeline refresh failed', { err }); });
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
// -------------------------------------------------------------------------
|
|
@@ -149,6 +149,7 @@ export class AgentInspectorPanel extends BasePanel {
|
|
|
149
149
|
|
|
150
150
|
override onDestroy(): void {
|
|
151
151
|
this._stopRefresh();
|
|
152
|
+
super.onDestroy();
|
|
152
153
|
}
|
|
153
154
|
|
|
154
155
|
// -------------------------------------------------------------------------
|
|
@@ -461,16 +462,16 @@ export class AgentInspectorPanel extends BasePanel {
|
|
|
461
462
|
}
|
|
462
463
|
|
|
463
464
|
private _startRefresh(): void {
|
|
464
|
-
if (this.
|
|
465
|
-
this.
|
|
466
|
-
this._refreshTimeline().catch(() => {});
|
|
467
|
-
}, REFRESH_MS);
|
|
465
|
+
if (this.refreshTimerId) return;
|
|
466
|
+
this.refreshTimerId = this.registerTimer(setInterval(() => {
|
|
467
|
+
this._refreshTimeline().catch((err) => { logger.debug('agent inspector timeline refresh tick failed', { err }); });
|
|
468
|
+
}, REFRESH_MS));
|
|
468
469
|
}
|
|
469
470
|
|
|
470
471
|
private _stopRefresh(): void {
|
|
471
|
-
if (this.
|
|
472
|
-
|
|
473
|
-
this.
|
|
472
|
+
if (this.refreshTimerId) {
|
|
473
|
+
this.clearTimer(this.refreshTimerId);
|
|
474
|
+
this.refreshTimerId = null;
|
|
474
475
|
}
|
|
475
476
|
}
|
|
476
477
|
|