@pellux/goodvibes-tui 1.0.0 → 1.7.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 +130 -127
- package/README.md +32 -14
- package/docs/foundation-artifacts/operator-contract.json +1 -1
- package/package.json +2 -2
- package/src/cli/ensure-goodvibes-gitignore.ts +32 -0
- package/src/cli/entrypoint.ts +2 -0
- package/src/core/alert-gating.ts +67 -0
- package/src/core/approval-alert.ts +72 -0
- package/src/core/budget-breach-notifier.ts +106 -0
- package/src/core/conversation-rendering.ts +19 -0
- package/src/core/conversation.ts +58 -0
- package/src/core/focus-tracker.ts +41 -0
- package/src/core/long-task-notifier.ts +33 -6
- package/src/core/stream-event-wiring.ts +104 -2
- package/src/core/stream-stall-watchdog.ts +41 -17
- package/src/core/system-message-router.ts +37 -51
- package/src/core/turn-cancellation.ts +20 -0
- package/src/core/turn-event-wiring.ts +64 -3
- package/src/export/cost-utils.ts +126 -8
- package/src/input/command-registry.ts +54 -1
- package/src/input/commands/checkpoint-runtime.ts +280 -0
- package/src/input/commands/codebase-runtime.ts +192 -0
- package/src/input/commands/diff-runtime.ts +61 -30
- package/src/input/commands/eval.ts +1 -1
- package/src/input/commands/health-runtime.ts +1 -1
- package/src/input/commands/image-runtime.ts +112 -0
- package/src/input/commands/intelligence-runtime.ts +11 -1
- package/src/input/commands/local-auth-runtime.ts +4 -1
- package/src/input/commands/local-runtime.ts +9 -3
- package/src/input/commands/marketplace-runtime.ts +2 -2
- package/src/input/commands/memory.ts +6 -1
- package/src/input/commands/operator-panel-runtime.ts +40 -24
- package/src/input/commands/planning-runtime.ts +26 -3
- package/src/input/commands/platform-sandbox-runtime.ts +1 -6
- package/src/input/commands/plugin-runtime.ts +2 -2
- package/src/input/commands/policy-dispatch.ts +21 -0
- package/src/input/commands/provider-accounts-runtime.ts +1 -1
- package/src/input/commands/qrcode-runtime.ts +3 -3
- package/src/input/commands/recall-review.ts +35 -0
- package/src/input/commands/remote-runtime.ts +3 -3
- package/src/input/commands/runtime-services.ts +54 -13
- package/src/input/commands/services-runtime.ts +1 -1
- package/src/input/commands/session-content.ts +12 -0
- package/src/input/commands/session-workflow.ts +19 -1
- package/src/input/commands/settings-sync-runtime.ts +1 -1
- package/src/input/commands/share-runtime.ts +9 -2
- package/src/input/commands/shell-core.ts +15 -7
- package/src/input/commands/skills-runtime.ts +2 -8
- package/src/input/commands/subscription-runtime.ts +2 -2
- package/src/input/commands/test-runtime.ts +277 -0
- package/src/input/commands/websearch-runtime.ts +101 -0
- package/src/input/commands/work-plan-runtime.ts +36 -10
- package/src/input/commands/workstream-runtime.ts +338 -0
- package/src/input/commands.ts +12 -0
- package/src/input/config-modal-types.ts +161 -0
- package/src/input/config-modal.ts +377 -0
- package/src/input/feed-context-factory.ts +7 -8
- package/src/input/handler-command-route.ts +27 -17
- package/src/input/handler-content-actions.ts +22 -8
- package/src/input/handler-feed-routes.ts +107 -7
- package/src/input/handler-feed.ts +49 -13
- package/src/input/handler-interactions.ts +3 -3
- package/src/input/handler-modal-routes.ts +65 -0
- package/src/input/handler-modal-stack.ts +3 -5
- package/src/input/handler-modal-token-routes.ts +16 -49
- package/src/input/handler-picker-routes.ts +11 -94
- package/src/input/handler-shortcuts.ts +24 -14
- package/src/input/handler-types.ts +3 -6
- package/src/input/handler-ui-state.ts +10 -22
- package/src/input/handler.ts +10 -28
- package/src/input/keybindings.ts +22 -8
- package/src/input/model-picker-types.ts +24 -6
- package/src/input/model-picker.ts +58 -0
- package/src/input/panel-integration-actions.ts +9 -170
- package/src/input/settings-modal-data.ts +95 -0
- package/src/input/settings-modal-mutations.ts +6 -4
- package/src/main.ts +41 -44
- package/src/panels/agent-inspector-shared.ts +35 -11
- package/src/panels/base-panel.ts +22 -1
- package/src/panels/builtin/agent.ts +21 -107
- package/src/panels/builtin/development.ts +16 -62
- package/src/panels/builtin/knowledge.ts +8 -32
- package/src/panels/builtin/operations.ts +84 -428
- package/src/panels/builtin/session.ts +21 -112
- package/src/panels/builtin/shared.ts +9 -43
- package/src/panels/builtin-modals.ts +218 -0
- package/src/panels/builtin-panels.ts +5 -0
- package/src/panels/cost-tracker-panel.ts +63 -14
- package/src/panels/diff-panel.ts +123 -60
- package/src/panels/eval-registry.ts +60 -0
- package/src/panels/fleet-panel.ts +800 -0
- package/src/panels/fleet-read-model.ts +477 -0
- package/src/panels/fleet-steer.ts +92 -0
- package/src/panels/fleet-stop.ts +125 -0
- package/src/panels/fleet-tabs.ts +230 -0
- package/src/panels/fleet-transcript.ts +356 -0
- package/src/panels/git-panel.ts +9 -9
- package/src/panels/index.ts +7 -31
- package/src/panels/local-auth-panel.ts +10 -0
- package/src/panels/modals/hooks-modal.ts +187 -0
- package/src/panels/modals/keybindings-modal.ts +151 -0
- package/src/panels/modals/knowledge-modal.ts +158 -0
- package/src/panels/modals/local-auth-modal.ts +132 -0
- package/src/panels/modals/marketplace-modal.ts +218 -0
- package/src/panels/modals/memory-modal.ts +180 -0
- package/src/panels/modals/modal-surface-helpers.ts +54 -0
- package/src/panels/modals/modal-theme.ts +36 -0
- package/src/panels/modals/pairing-modal.ts +144 -0
- package/src/panels/modals/planning-modal.ts +282 -0
- package/src/panels/modals/plugins-modal.ts +174 -0
- package/src/panels/modals/policy-modal.ts +256 -0
- package/src/panels/modals/provider-health-modal.ts +136 -0
- package/src/panels/modals/remote-modal.ts +115 -0
- package/src/panels/modals/sandbox-modal.ts +150 -0
- package/src/panels/modals/security-modal.ts +217 -0
- package/src/panels/modals/services-modal.ts +179 -0
- package/src/panels/modals/settings-sync-modal.ts +142 -0
- package/src/panels/modals/skills-modal.ts +189 -0
- package/src/panels/modals/subscription-modal.ts +172 -0
- package/src/panels/modals/work-plan-modal.ts +149 -0
- package/src/panels/panel-confirm-overlay.ts +72 -0
- package/src/panels/panel-manager.ts +108 -5
- package/src/panels/project-planning-answer-actions.ts +4 -2
- package/src/panels/scrollable-list-panel.ts +9 -0
- package/src/panels/skills-panel.ts +7 -51
- package/src/panels/token-budget-panel.ts +5 -3
- package/src/panels/types.ts +26 -0
- package/src/permissions/hunk-selection.ts +179 -0
- package/src/permissions/prompt.ts +60 -4
- package/src/renderer/compaction-history-modal.ts +19 -3
- package/src/renderer/compaction-preview.ts +13 -2
- package/src/renderer/compaction-quality.ts +100 -0
- package/src/renderer/config-modal.ts +81 -0
- package/src/renderer/conversation-overlays.ts +10 -17
- package/src/renderer/fleet-tab-strip.ts +56 -0
- package/src/renderer/footer-tips.ts +10 -2
- package/src/renderer/git-status.ts +40 -0
- package/src/renderer/help-overlay.ts +2 -2
- package/src/renderer/model-workspace.ts +44 -1
- package/src/renderer/panel-workspace-bar.ts +8 -2
- package/src/renderer/shell-surface.ts +8 -1
- package/src/renderer/turn-injection.ts +114 -0
- package/src/renderer/ui-factory.ts +91 -7
- package/src/runtime/bootstrap-command-context.ts +24 -1
- package/src/runtime/bootstrap-command-parts.ts +36 -7
- package/src/runtime/bootstrap-core.ts +19 -4
- package/src/runtime/bootstrap-hook-bridge.ts +5 -0
- package/src/runtime/bootstrap-shell.ts +41 -17
- package/src/runtime/bootstrap.ts +11 -17
- package/src/runtime/code-index-services.ts +112 -0
- package/src/runtime/orchestrator-core-services.ts +49 -0
- package/src/runtime/process-lifecycle.ts +3 -1
- package/src/runtime/services.ts +91 -43
- package/src/runtime/ui-services.ts +8 -0
- package/src/runtime/workstream-services.ts +195 -0
- package/src/shell/blocking-input.ts +22 -1
- package/src/shell/ui-openers.ts +129 -17
- package/src/utils/format-duration.ts +8 -18
- package/src/utils/splash-lines.ts +1 -1
- package/src/version.ts +1 -1
- package/src/panels/agent-inspector-panel.ts +0 -786
- package/src/panels/approval-panel.ts +0 -252
- package/src/panels/automation-control-panel.ts +0 -479
- package/src/panels/cockpit-panel.ts +0 -481
- package/src/panels/cockpit-read-model.ts +0 -233
- package/src/panels/communication-panel.ts +0 -256
- package/src/panels/control-plane-panel.ts +0 -470
- package/src/panels/debug-panel.ts +0 -609
- package/src/panels/docs-panel.ts +0 -375
- package/src/panels/eval-panel.ts +0 -627
- package/src/panels/file-explorer-panel.ts +0 -664
- package/src/panels/file-preview-panel.ts +0 -517
- package/src/panels/hooks-panel.ts +0 -401
- package/src/panels/incident-review-panel.ts +0 -406
- package/src/panels/intelligence-panel.ts +0 -383
- package/src/panels/knowledge-graph-panel.ts +0 -506
- package/src/panels/marketplace-panel.ts +0 -399
- package/src/panels/memory-panel.ts +0 -558
- package/src/panels/ops-control-panel.ts +0 -329
- package/src/panels/ops-strategy-panel.ts +0 -321
- package/src/panels/orchestration-panel.ts +0 -465
- package/src/panels/panel-list-panel.ts +0 -557
- package/src/panels/plan-dashboard-panel.ts +0 -707
- package/src/panels/policy-panel.ts +0 -517
- package/src/panels/project-planning-panel.ts +0 -721
- package/src/panels/provider-health-panel.ts +0 -678
- package/src/panels/provider-health-tracker.ts +0 -306
- package/src/panels/provider-health-views.ts +0 -560
- package/src/panels/qr-panel.ts +0 -280
- package/src/panels/remote-panel.ts +0 -534
- package/src/panels/routes-panel.ts +0 -241
- package/src/panels/sandbox-panel.ts +0 -456
- package/src/panels/security-panel.ts +0 -447
- package/src/panels/services-panel.ts +0 -329
- package/src/panels/session-browser-panel.ts +0 -487
- package/src/panels/settings-sync-panel.ts +0 -398
- package/src/panels/subscription-panel.ts +0 -342
- package/src/panels/symbol-outline-panel.ts +0 -619
- package/src/panels/system-messages-panel.ts +0 -364
- package/src/panels/tasks-panel.ts +0 -608
- package/src/panels/thinking-panel.ts +0 -333
- package/src/panels/tool-inspector-panel.ts +0 -537
- package/src/panels/work-plan-panel.ts +0 -530
- package/src/panels/worktree-panel.ts +0 -360
- package/src/panels/wrfc-panel.ts +0 -790
- package/src/renderer/agent-detail-modal.ts +0 -465
- package/src/renderer/live-tail-modal.ts +0 -156
- package/src/renderer/process-modal.ts +0 -656
- package/src/renderer/qr-renderer.ts +0 -120
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import type { ModalSectionStyle } from '../renderer/modal-factory.ts';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Config-modal host — the shared seam every W6.1 MIGRATE-TO-MODAL surface
|
|
5
|
+
* (provider-health, services, subscription, remote, local-auth, settings-sync,
|
|
6
|
+
* sandbox, and the WO-B ecosystem/governance set) is expressed through.
|
|
7
|
+
*
|
|
8
|
+
* A surface is PURE DATA + ACTIONS: it hands the host a `buildView()` that maps
|
|
9
|
+
* live read-models to a tabbed, list-structured view, plus a declarative action
|
|
10
|
+
* table. The host owns all key routing (tab switch, list nav, Esc, confirm) and
|
|
11
|
+
* the stable-layout liveness contract — the surface never touches the input
|
|
12
|
+
* system directly (the charter's "do not invent a parallel input system").
|
|
13
|
+
*
|
|
14
|
+
* Liveness doctrine (charter: "live data, stable layout"): `buildView()` is
|
|
15
|
+
* called every render tick so values (latency, status, counts) update in place,
|
|
16
|
+
* but the host renders against the row/tab STRUCTURE captured at the last
|
|
17
|
+
* interaction boundary (open, or a key press). Rows never reflow under the
|
|
18
|
+
* cursor mid-interaction; a structural change (a row appears/disappears) is
|
|
19
|
+
* deferred until the next key press. See ConfigModal for the mechanism.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** A single row within a tab. Identity is `id` (structural); `label` is live. */
|
|
23
|
+
export interface ConfigModalRow {
|
|
24
|
+
/**
|
|
25
|
+
* Stable identity across live-refresh ticks. The liveness contract keys
|
|
26
|
+
* structural stability off this: two builds with the same ordered id set are
|
|
27
|
+
* "structurally identical" and repaint values in place; a differing id set is
|
|
28
|
+
* a structural change, deferred to the next interaction boundary.
|
|
29
|
+
*/
|
|
30
|
+
readonly id: string;
|
|
31
|
+
/** Display label. MAY embed live value fields (latency, status dot, counts). */
|
|
32
|
+
readonly label: string;
|
|
33
|
+
/** Optional per-row style (e.g. bad/warn tone for an errored row). */
|
|
34
|
+
readonly style?: ModalSectionStyle;
|
|
35
|
+
/**
|
|
36
|
+
* Non-selectable informational row (sub-header, spacer text). Skipped by
|
|
37
|
+
* up/down navigation and never receives an action. Defaults to selectable.
|
|
38
|
+
*/
|
|
39
|
+
readonly selectable?: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** One tab (section) of a surface. Tabs are switched with left/right or Tab. */
|
|
43
|
+
export interface ConfigModalTab {
|
|
44
|
+
/** Stable id — preserves the active tab across refresh + re-open. */
|
|
45
|
+
readonly id: string;
|
|
46
|
+
/** Tab-strip label. */
|
|
47
|
+
readonly label: string;
|
|
48
|
+
/** Live status/posture lines rendered above the list (non-selectable). */
|
|
49
|
+
readonly header?: readonly string[];
|
|
50
|
+
/** Selectable/informational rows. */
|
|
51
|
+
readonly rows: readonly ConfigModalRow[];
|
|
52
|
+
/** Honest empty-state text shown when `rows` is empty. */
|
|
53
|
+
readonly emptyText?: string;
|
|
54
|
+
/** Footer hints specific to this tab, appended after the surface hints. */
|
|
55
|
+
readonly hints?: readonly string[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The whole surface view for one render tick. */
|
|
59
|
+
export interface ConfigModalView {
|
|
60
|
+
/** Live title (may change tick-to-tick; not structural). */
|
|
61
|
+
readonly title: string;
|
|
62
|
+
readonly tabs: readonly ConfigModalTab[];
|
|
63
|
+
/**
|
|
64
|
+
* Non-null when the read-model is unavailable/degraded — the host renders a
|
|
65
|
+
* banner and still shows whatever tabs/rows are present (honest degraded
|
|
66
|
+
* state rather than a blank or a throw).
|
|
67
|
+
*/
|
|
68
|
+
readonly degraded?: string;
|
|
69
|
+
/** Base footer hints (Esc close is always appended by the host). */
|
|
70
|
+
readonly hints?: readonly string[];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Context handed to a surface's action handler when an action key fires. */
|
|
74
|
+
export interface ConfigModalActionContext {
|
|
75
|
+
/** The selected row the action targets (null when the active tab has none). */
|
|
76
|
+
readonly row: ConfigModalRow | null;
|
|
77
|
+
/** The active tab's id. */
|
|
78
|
+
readonly tabId: string;
|
|
79
|
+
/** Print a line to the conversation transcript. */
|
|
80
|
+
readonly print: (message: string) => void;
|
|
81
|
+
/**
|
|
82
|
+
* Execute a slash command — the same seam panels used via
|
|
83
|
+
* `PanelIntegrationContext.executeCommand`. This is how a migrated action
|
|
84
|
+
* preserves parity: the mutation still runs through its existing, tested
|
|
85
|
+
* command (e.g. `/settings-sync resolve`, `/local-auth delete-user`).
|
|
86
|
+
*/
|
|
87
|
+
readonly executeCommand?: (name: string, args: string[]) => Promise<unknown>;
|
|
88
|
+
/** Open another named modal surface (cross-surface navigation, e.g. the
|
|
89
|
+
* services surface jumping to the subscription surface). Swaps the active
|
|
90
|
+
* surface in place — the same seam ctx.openModal uses. */
|
|
91
|
+
readonly openModal?: (name: string) => void;
|
|
92
|
+
/** Request a re-render. */
|
|
93
|
+
readonly requestRender: () => void;
|
|
94
|
+
/** Set the modal's transient status line (e.g. a result or error message). */
|
|
95
|
+
readonly setStatus: (message: string) => void;
|
|
96
|
+
/** Close the modal (focus returns to whatever opened it). */
|
|
97
|
+
readonly close: () => void;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** A declarative action bound to a key. */
|
|
101
|
+
export interface ConfigModalAction {
|
|
102
|
+
/**
|
|
103
|
+
* Trigger key: a single printable char ('r', 'd') or a named key ('enter').
|
|
104
|
+
* Must not collide with the host-reserved nav keys (up/down/left/right/tab/
|
|
105
|
+
* j/k/escape) — those are consumed by the host before actions are consulted.
|
|
106
|
+
*/
|
|
107
|
+
readonly key: string;
|
|
108
|
+
/** Action id passed to `onAction`. */
|
|
109
|
+
readonly id: string;
|
|
110
|
+
/** Short verb for the footer hint (e.g. "refresh", "delete user"). */
|
|
111
|
+
readonly label: string;
|
|
112
|
+
/**
|
|
113
|
+
* Destructive: require an explicit second press of the same key to fire
|
|
114
|
+
* (the host shows "press <key> again to <label>" between presses). Matches
|
|
115
|
+
* the session-picker delete-confirm precedent.
|
|
116
|
+
*/
|
|
117
|
+
readonly confirm?: boolean;
|
|
118
|
+
/**
|
|
119
|
+
* When set, the action is only offered while the selected row's id satisfies
|
|
120
|
+
* this predicate (e.g. sandbox "stop" only on a session row). Rows failing
|
|
121
|
+
* the predicate neither show the hint nor fire the action.
|
|
122
|
+
*/
|
|
123
|
+
readonly enabledFor?: (row: ConfigModalRow | null, tabId: string) => boolean;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* A named config-modal surface. Pure seam — the host knows nothing about the
|
|
128
|
+
* surface's data. Built once (closing over its read-models) and registered on
|
|
129
|
+
* PanelManager via `registerModalSurface`; opened by name from a panel-id
|
|
130
|
+
* redirect hit or `ctx.openModal(name)`.
|
|
131
|
+
*/
|
|
132
|
+
export interface ConfigModalSurface {
|
|
133
|
+
/** Modal name — the key `registerModalRedirect` / `ctx.openModal` use. */
|
|
134
|
+
readonly name: string;
|
|
135
|
+
/** Fallback title (buildView().title wins per tick). */
|
|
136
|
+
readonly title: string;
|
|
137
|
+
/**
|
|
138
|
+
* Build the current view from live read-models. Called on open, on every
|
|
139
|
+
* interaction boundary, AND on every render tick. MUST be pure and cheap —
|
|
140
|
+
* snapshot reads only, no async, no mutation.
|
|
141
|
+
*/
|
|
142
|
+
buildView(): ConfigModalView;
|
|
143
|
+
/** Declarative action table (optional — a read-only surface has none). */
|
|
144
|
+
readonly actions?: readonly ConfigModalAction[];
|
|
145
|
+
/** Handle an action fired by its key. */
|
|
146
|
+
onAction?(actionId: string, ctx: ConfigModalActionContext): void;
|
|
147
|
+
/**
|
|
148
|
+
* Optional lifecycle: called when the modal opens this surface and when it
|
|
149
|
+
* closes. A live surface uses `onOpen` to subscribe to its read-model/events
|
|
150
|
+
* (calling the provided `requestRender` on change so live values refresh
|
|
151
|
+
* between key presses) and `onClose` to unsubscribe. The host guarantees
|
|
152
|
+
* `onClose` runs exactly once per `onOpen`.
|
|
153
|
+
*/
|
|
154
|
+
onOpen?(requestRender: () => void): void;
|
|
155
|
+
onClose?(): void;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Read-only registry the host consults to resolve a modal name to a surface. */
|
|
159
|
+
export interface ConfigModalSurfaceRegistry {
|
|
160
|
+
getModalSurface(name: string): ConfigModalSurface | undefined;
|
|
161
|
+
}
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ConfigModalAction,
|
|
3
|
+
ConfigModalActionContext,
|
|
4
|
+
ConfigModalRow,
|
|
5
|
+
ConfigModalSurface,
|
|
6
|
+
ConfigModalTab,
|
|
7
|
+
ConfigModalView,
|
|
8
|
+
} from './config-modal-types.ts';
|
|
9
|
+
import type { ModalSectionStyle } from '../renderer/modal-factory.ts';
|
|
10
|
+
|
|
11
|
+
export type {
|
|
12
|
+
ConfigModalAction,
|
|
13
|
+
ConfigModalActionContext,
|
|
14
|
+
ConfigModalRow,
|
|
15
|
+
ConfigModalSurface,
|
|
16
|
+
ConfigModalTab,
|
|
17
|
+
ConfigModalView,
|
|
18
|
+
} from './config-modal-types.ts';
|
|
19
|
+
|
|
20
|
+
/** A tab as the renderer sees it (structure frozen, label live). */
|
|
21
|
+
export interface ConfigModalRenderTab {
|
|
22
|
+
readonly id: string;
|
|
23
|
+
readonly label: string;
|
|
24
|
+
readonly active: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** A row as the renderer sees it: frozen identity, live label/value overlay. */
|
|
28
|
+
export interface ConfigModalRenderRow {
|
|
29
|
+
readonly id: string;
|
|
30
|
+
readonly label: string;
|
|
31
|
+
readonly style?: ModalSectionStyle;
|
|
32
|
+
readonly selected: boolean;
|
|
33
|
+
readonly selectable: boolean;
|
|
34
|
+
/** True when this frozen row has no live counterpart this tick (value went
|
|
35
|
+
* stale — kept in place, dimmed, until the next interaction boundary). */
|
|
36
|
+
readonly stale: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Everything renderConfigModal needs — computed by overlaying live values onto
|
|
40
|
+
* the frozen structure so layout stays stable between key presses. */
|
|
41
|
+
export interface ConfigModalRenderModel {
|
|
42
|
+
readonly title: string;
|
|
43
|
+
readonly tabs: readonly ConfigModalRenderTab[];
|
|
44
|
+
readonly header: readonly string[];
|
|
45
|
+
readonly rows: readonly ConfigModalRenderRow[];
|
|
46
|
+
readonly emptyText?: string;
|
|
47
|
+
readonly degraded?: string;
|
|
48
|
+
readonly status?: string;
|
|
49
|
+
readonly hints: readonly string[];
|
|
50
|
+
readonly scroll: { readonly offset: number; readonly total: number; readonly visible: number };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const DEFAULT_VISIBLE_ROWS = 10;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* ConfigModal — the single, generic, named config-modal host. One instance
|
|
57
|
+
* lives on the InputHandler (like `settingsModal`); it renders whatever
|
|
58
|
+
* `ConfigModalSurface` is currently open. Key routing is a single
|
|
59
|
+
* `handleConfigModalToken` path (handler-modal-routes.ts) — no parallel input
|
|
60
|
+
* system. See config-modal-types.ts for the surface contract and the liveness
|
|
61
|
+
* doctrine this class enforces.
|
|
62
|
+
*/
|
|
63
|
+
export class ConfigModal {
|
|
64
|
+
public active = false;
|
|
65
|
+
|
|
66
|
+
private surface: ConfigModalSurface | null = null;
|
|
67
|
+
private requestRender: () => void = () => {};
|
|
68
|
+
|
|
69
|
+
/** Active tab / selected row tracked by STABLE id (survives value refresh). */
|
|
70
|
+
private activeTabId = '';
|
|
71
|
+
private selectedRowId = '';
|
|
72
|
+
|
|
73
|
+
/** Transient status line (action result, error, or confirm prompt). */
|
|
74
|
+
private statusMessage = '';
|
|
75
|
+
/** Pending destructive confirm: the action key awaiting a second press. */
|
|
76
|
+
private pendingConfirmKey: string | null = null;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Structure captured at the last interaction boundary (open / key press).
|
|
80
|
+
* Renders overlay live values onto THIS so rows never reflow mid-interaction.
|
|
81
|
+
*/
|
|
82
|
+
private frozenView: ConfigModalView | null = null;
|
|
83
|
+
|
|
84
|
+
private scrollOffset = 0;
|
|
85
|
+
private visibleRows = DEFAULT_VISIBLE_ROWS;
|
|
86
|
+
|
|
87
|
+
// ── Lifecycle ──────────────────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
open(surface: ConfigModalSurface, requestRender: () => void = () => {}): void {
|
|
90
|
+
// Re-opening a different surface closes the previous one cleanly.
|
|
91
|
+
if (this.surface && this.surface !== surface) this.surface.onClose?.();
|
|
92
|
+
this.surface = surface;
|
|
93
|
+
this.requestRender = requestRender;
|
|
94
|
+
this.active = true;
|
|
95
|
+
this.statusMessage = '';
|
|
96
|
+
this.pendingConfirmKey = null;
|
|
97
|
+
this.scrollOffset = 0;
|
|
98
|
+
const view = surface.buildView();
|
|
99
|
+
this.frozenView = view;
|
|
100
|
+
this.activeTabId = view.tabs[0]?.id ?? '';
|
|
101
|
+
this.selectedRowId = this._firstSelectableId(this._frozenTab());
|
|
102
|
+
surface.onOpen?.(requestRender);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
close(): void {
|
|
106
|
+
if (!this.active) return;
|
|
107
|
+
this.surface?.onClose?.();
|
|
108
|
+
this.active = false;
|
|
109
|
+
this.surface = null;
|
|
110
|
+
this.frozenView = null;
|
|
111
|
+
this.statusMessage = '';
|
|
112
|
+
this.pendingConfirmKey = null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Re-activate the current surface after a nested modal closes (Esc stack). */
|
|
116
|
+
reopen(): void {
|
|
117
|
+
if (!this.surface) return;
|
|
118
|
+
this.active = true;
|
|
119
|
+
this.syncStructure();
|
|
120
|
+
this.surface.onOpen?.(this.requestRender);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
getSurfaceName(): string | null {
|
|
124
|
+
return this.surface?.name ?? null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
setViewportRows(rows: number): void {
|
|
128
|
+
this.visibleRows = Math.max(3, rows);
|
|
129
|
+
this._clampScroll();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ── Interaction boundary ────────────────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Re-capture the structure from the current live view. Called on every key
|
|
136
|
+
* press (never during a pure render tick) — this is what makes structural
|
|
137
|
+
* changes appear only at an interaction boundary. Selection + active tab are
|
|
138
|
+
* preserved by id and clamped if their target vanished.
|
|
139
|
+
*/
|
|
140
|
+
syncStructure(): void {
|
|
141
|
+
if (!this.surface) return;
|
|
142
|
+
const view = this.surface.buildView();
|
|
143
|
+
this.frozenView = view;
|
|
144
|
+
if (!view.tabs.some((t) => t.id === this.activeTabId)) {
|
|
145
|
+
this.activeTabId = view.tabs[0]?.id ?? '';
|
|
146
|
+
}
|
|
147
|
+
const tab = this._frozenTab();
|
|
148
|
+
if (!this._selectableIds(tab).includes(this.selectedRowId)) {
|
|
149
|
+
this.selectedRowId = this._firstSelectableId(tab);
|
|
150
|
+
}
|
|
151
|
+
this._clampScroll();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ── Navigation (each is an interaction boundary) ────────────────────────────
|
|
155
|
+
|
|
156
|
+
moveDown(): void {
|
|
157
|
+
this._clearConfirm();
|
|
158
|
+
this.syncStructure();
|
|
159
|
+
const ids = this._selectableIds(this._frozenTab());
|
|
160
|
+
if (ids.length === 0) return;
|
|
161
|
+
const i = ids.indexOf(this.selectedRowId);
|
|
162
|
+
this.selectedRowId = ids[(i + 1) % ids.length]!;
|
|
163
|
+
this._clampScroll();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
moveUp(): void {
|
|
167
|
+
this._clearConfirm();
|
|
168
|
+
this.syncStructure();
|
|
169
|
+
const ids = this._selectableIds(this._frozenTab());
|
|
170
|
+
if (ids.length === 0) return;
|
|
171
|
+
const i = ids.indexOf(this.selectedRowId);
|
|
172
|
+
this.selectedRowId = ids[(i - 1 + ids.length) % ids.length]!;
|
|
173
|
+
this._clampScroll();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
nextTab(): void {
|
|
177
|
+
this._switchTab(1);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
prevTab(): void {
|
|
181
|
+
this._switchTab(-1);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private _switchTab(dir: 1 | -1): void {
|
|
185
|
+
this._clearConfirm();
|
|
186
|
+
this.syncStructure();
|
|
187
|
+
const tabs = this.frozenView?.tabs ?? [];
|
|
188
|
+
if (tabs.length <= 1) return;
|
|
189
|
+
const i = tabs.findIndex((t) => t.id === this.activeTabId);
|
|
190
|
+
const next = tabs[(i + dir + tabs.length) % tabs.length]!;
|
|
191
|
+
this.activeTabId = next.id;
|
|
192
|
+
this.selectedRowId = this._firstSelectableId(next);
|
|
193
|
+
this.scrollOffset = 0;
|
|
194
|
+
this.statusMessage = '';
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ── Actions ─────────────────────────────────────────────────────────────────
|
|
198
|
+
|
|
199
|
+
/** The action bound to `key` on the active tab/row, if enabled. */
|
|
200
|
+
resolveAction(key: string): ConfigModalAction | null {
|
|
201
|
+
const actions = this.surface?.actions ?? [];
|
|
202
|
+
const row = this.getSelectedRow();
|
|
203
|
+
for (const action of actions) {
|
|
204
|
+
if (action.key !== key) continue;
|
|
205
|
+
if (action.enabledFor && !action.enabledFor(row, this.activeTabId)) return null;
|
|
206
|
+
return action;
|
|
207
|
+
}
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Attempt to fire the action bound to `key`. Returns true if a key was an
|
|
213
|
+
* action (consumed). Handles the two-press confirm for destructive actions.
|
|
214
|
+
* This is an interaction boundary (syncs structure first).
|
|
215
|
+
*/
|
|
216
|
+
fireAction(key: string, ctx: Omit<ConfigModalActionContext, 'row' | 'tabId' | 'setStatus' | 'close' | 'requestRender'>): boolean {
|
|
217
|
+
this.syncStructure();
|
|
218
|
+
const action = this.resolveAction(key);
|
|
219
|
+
if (!action) return false;
|
|
220
|
+
if (action.confirm && this.pendingConfirmKey !== key) {
|
|
221
|
+
this.pendingConfirmKey = key;
|
|
222
|
+
this.statusMessage = `Press ${key} again to ${action.label}.`;
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
this._clearConfirm();
|
|
226
|
+
const fullCtx: ConfigModalActionContext = {
|
|
227
|
+
row: this.getSelectedRow(),
|
|
228
|
+
tabId: this.activeTabId,
|
|
229
|
+
print: ctx.print,
|
|
230
|
+
executeCommand: ctx.executeCommand,
|
|
231
|
+
openModal: ctx.openModal,
|
|
232
|
+
requestRender: this.requestRender,
|
|
233
|
+
setStatus: (m: string) => { this.statusMessage = m; },
|
|
234
|
+
close: () => this.close(),
|
|
235
|
+
};
|
|
236
|
+
this.surface?.onAction?.(action.id, fullCtx);
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** Clear a pending confirm when the user navigates or presses an unrelated key. */
|
|
241
|
+
clearConfirmOnMiss(): void {
|
|
242
|
+
this._clearConfirm();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ── Read accessors (for the renderer + tests) ───────────────────────────────
|
|
246
|
+
|
|
247
|
+
getSelectedRow(): ConfigModalRow | null {
|
|
248
|
+
const tab = this._liveTab(this.activeTabId) ?? this._frozenTab();
|
|
249
|
+
return tab?.rows.find((r) => r.id === this.selectedRowId) ?? null;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
getActiveTabId(): string {
|
|
253
|
+
return this.activeTabId;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
getSelectedRowId(): string {
|
|
257
|
+
return this.selectedRowId;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
getStatusMessage(): string {
|
|
261
|
+
return this.statusMessage;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Compute the render model: frozen structure + live value overlay. Pure —
|
|
266
|
+
* does NOT sync structure, so calling it repeatedly with only value mutations
|
|
267
|
+
* yields byte-identical layout (the liveness contract).
|
|
268
|
+
*/
|
|
269
|
+
getRenderModel(): ConfigModalRenderModel {
|
|
270
|
+
const live = this.surface?.buildView() ?? null;
|
|
271
|
+
const frozen = this.frozenView;
|
|
272
|
+
if (!frozen) {
|
|
273
|
+
return { title: '', tabs: [], header: [], rows: [], hints: [], scroll: { offset: 0, total: 0, visible: this.visibleRows } };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const tabs: ConfigModalRenderTab[] = frozen.tabs.map((ft) => {
|
|
277
|
+
const lt = live?.tabs.find((t) => t.id === ft.id);
|
|
278
|
+
return { id: ft.id, label: lt?.label ?? ft.label, active: ft.id === this.activeTabId };
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
const frozenTab = frozen.tabs.find((t) => t.id === this.activeTabId) ?? frozen.tabs[0];
|
|
282
|
+
const liveTab = live?.tabs.find((t) => t.id === this.activeTabId);
|
|
283
|
+
|
|
284
|
+
// Header: all-or-nothing live overlay. Same line count → use live values;
|
|
285
|
+
// a count change is a structural change, deferred (keep frozen header).
|
|
286
|
+
const frozenHeader = frozenTab?.header ?? [];
|
|
287
|
+
const liveHeader = liveTab?.header ?? [];
|
|
288
|
+
const header = liveHeader.length === frozenHeader.length ? liveHeader : frozenHeader;
|
|
289
|
+
|
|
290
|
+
// Rows: iterate the frozen id order; overlay the live row (value refresh) by
|
|
291
|
+
// id, or keep the frozen row marked stale when its live counterpart is gone.
|
|
292
|
+
const frozenRows = frozenTab?.rows ?? [];
|
|
293
|
+
const allRows: ConfigModalRenderRow[] = frozenRows.map((fr) => {
|
|
294
|
+
const lr = liveTab?.rows.find((r) => r.id === fr.id);
|
|
295
|
+
const src = lr ?? fr;
|
|
296
|
+
return {
|
|
297
|
+
id: fr.id,
|
|
298
|
+
label: src.label,
|
|
299
|
+
style: src.style,
|
|
300
|
+
selected: fr.id === this.selectedRowId,
|
|
301
|
+
selectable: fr.selectable !== false,
|
|
302
|
+
stale: lr === undefined,
|
|
303
|
+
};
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
const visible = this.visibleRows;
|
|
307
|
+
const windowed = allRows.slice(this.scrollOffset, this.scrollOffset + visible);
|
|
308
|
+
|
|
309
|
+
const hints = [
|
|
310
|
+
...(frozen.hints ?? []),
|
|
311
|
+
...(frozenTab?.hints ?? []),
|
|
312
|
+
...this._actionHints(),
|
|
313
|
+
'Esc close',
|
|
314
|
+
];
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
title: live?.title ?? frozen.title,
|
|
318
|
+
tabs,
|
|
319
|
+
header,
|
|
320
|
+
rows: windowed,
|
|
321
|
+
emptyText: frozenRows.length === 0 ? (frozenTab?.emptyText ?? 'Nothing to show.') : undefined,
|
|
322
|
+
degraded: live?.degraded ?? frozen.degraded,
|
|
323
|
+
status: this.statusMessage || undefined,
|
|
324
|
+
hints,
|
|
325
|
+
scroll: { offset: this.scrollOffset, total: allRows.length, visible },
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// ── internals ────────────────────────────────────────────────────────────────
|
|
330
|
+
|
|
331
|
+
private _actionHints(): string[] {
|
|
332
|
+
const actions = this.surface?.actions ?? [];
|
|
333
|
+
const row = this.getSelectedRow();
|
|
334
|
+
const out: string[] = [];
|
|
335
|
+
for (const a of actions) {
|
|
336
|
+
if (a.enabledFor && !a.enabledFor(row, this.activeTabId)) continue;
|
|
337
|
+
out.push(`${a.key} ${a.label}`);
|
|
338
|
+
}
|
|
339
|
+
return out;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
private _clearConfirm(): void {
|
|
343
|
+
if (this.pendingConfirmKey !== null) {
|
|
344
|
+
this.pendingConfirmKey = null;
|
|
345
|
+
this.statusMessage = '';
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
private _frozenTab(): ConfigModalTab | undefined {
|
|
350
|
+
return this.frozenView?.tabs.find((t) => t.id === this.activeTabId) ?? this.frozenView?.tabs[0];
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
private _liveTab(id: string): ConfigModalTab | undefined {
|
|
354
|
+
return this.surface?.buildView().tabs.find((t) => t.id === id);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
private _selectableIds(tab: ConfigModalTab | undefined): string[] {
|
|
358
|
+
return (tab?.rows ?? []).filter((r) => r.selectable !== false).map((r) => r.id);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
private _firstSelectableId(tab: ConfigModalTab | undefined): string {
|
|
362
|
+
return this._selectableIds(tab)[0] ?? '';
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
private _clampScroll(): void {
|
|
366
|
+
const tab = this._frozenTab();
|
|
367
|
+
const rows = tab?.rows ?? [];
|
|
368
|
+
const idx = rows.findIndex((r) => r.id === this.selectedRowId);
|
|
369
|
+
const visible = Math.max(3, this.visibleRows);
|
|
370
|
+
if (idx >= 0) {
|
|
371
|
+
if (idx < this.scrollOffset) this.scrollOffset = idx;
|
|
372
|
+
else if (idx >= this.scrollOffset + visible) this.scrollOffset = idx - visible + 1;
|
|
373
|
+
}
|
|
374
|
+
const maxOffset = Math.max(0, rows.length - visible);
|
|
375
|
+
this.scrollOffset = Math.max(0, Math.min(this.scrollOffset, maxOffset));
|
|
376
|
+
}
|
|
377
|
+
}
|
|
@@ -22,15 +22,13 @@ import type { SelectionResult } from './selection-modal.ts';
|
|
|
22
22
|
import type { SearchManager } from './search.ts';
|
|
23
23
|
import type { InputHistory, HistorySearch } from './input-history.ts';
|
|
24
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
25
|
import type { BlockActionsMenu } from '../renderer/block-actions.ts';
|
|
28
|
-
import type { AgentDetailModal } from '../renderer/agent-detail-modal.ts';
|
|
29
26
|
import type { ContextInspectorModal } from '../renderer/context-inspector.ts';
|
|
30
27
|
import type { BookmarkModal } from './bookmark-modal.ts';
|
|
31
28
|
import type { SettingsModal } from './settings-modal.ts';
|
|
32
29
|
import type { McpWorkspace } from './mcp-workspace.ts';
|
|
33
30
|
import type { SessionPickerModal } from './session-picker-modal.ts';
|
|
31
|
+
import type { ConfigModal } from './config-modal.ts';
|
|
34
32
|
import type { ProfilePickerModal } from './profile-picker-modal.ts';
|
|
35
33
|
import type { OnboardingWizardController } from './onboarding/onboarding-wizard.ts';
|
|
36
34
|
import type { WrappedPromptInfo } from './handler-prompt-buffer.ts';
|
|
@@ -40,6 +38,7 @@ import type { KeybindingsManager } from './keybindings.ts';
|
|
|
40
38
|
import type { ModelPickerTarget } from './model-picker.ts';
|
|
41
39
|
import type { KillRing } from './kill-ring.ts';
|
|
42
40
|
import type { PanelMouseLayout } from './handler-feed-routes.ts';
|
|
41
|
+
import type { FocusTracker } from '../core/focus-tracker.ts';
|
|
43
42
|
|
|
44
43
|
/**
|
|
45
44
|
* Initial mutable scalar values for InputFeedContext.
|
|
@@ -82,8 +81,7 @@ export interface FeedContextMutableInit {
|
|
|
82
81
|
* - `pasteRegistry`, `imageRegistry` — owned Maps, never replaced
|
|
83
82
|
* - `selectionModal`, `bookmarkModal`, `settingsModal`, `sessionPickerModal`,
|
|
84
83
|
* `profilePickerModal` — modal objects constructed once
|
|
85
|
-
* - `filePicker`, `modelPicker`, `
|
|
86
|
-
* `agentDetailModal`, `contextInspectorModal`, `blockActionsMenu`,
|
|
84
|
+
* - `filePicker`, `modelPicker`, `contextInspectorModal`, `blockActionsMenu`,
|
|
87
85
|
* `searchManager`, `historySearch`, `onboardingWizard` — service objects constructed once
|
|
88
86
|
* - `panelManager`, `keybindingsManager` — from uiServices, stable
|
|
89
87
|
* - `modalStack` — reference to the handler's shared array
|
|
@@ -91,6 +89,8 @@ export interface FeedContextMutableInit {
|
|
|
91
89
|
* - `commandRegistry`, `commandContext`, `autocomplete`, `inputHistory`,
|
|
92
90
|
* `conversationManager` — wired after construction; synced at feed() entry only
|
|
93
91
|
* (not per-action) since no in-feed action changes them
|
|
92
|
+
* - `focusTracker` (W2.3) — shared instance from uiServices.platform, mutated
|
|
93
|
+
* directly by feedInputTokens() on 'focus' tokens; never reallocated
|
|
94
94
|
*
|
|
95
95
|
* **Rationale:** per-feed mutation on a single object avoids per-keystroke GC pressure
|
|
96
96
|
* from ~80-field object allocation. Stable references are service handles that never
|
|
@@ -107,6 +107,7 @@ export interface FeedContextStableRefs {
|
|
|
107
107
|
mcpWorkspace: McpWorkspace;
|
|
108
108
|
sessionPickerModal: SessionPickerModal;
|
|
109
109
|
profilePickerModal: ProfilePickerModal;
|
|
110
|
+
configModal: ConfigModal;
|
|
110
111
|
historySearch: HistorySearch;
|
|
111
112
|
commandRegistry: CommandRegistry | null;
|
|
112
113
|
commandContext: CommandContext | undefined;
|
|
@@ -114,9 +115,6 @@ export interface FeedContextStableRefs {
|
|
|
114
115
|
filePicker: FilePickerModal;
|
|
115
116
|
modelPicker: ModelPickerModal;
|
|
116
117
|
onboardingWizard: OnboardingWizardController;
|
|
117
|
-
processModal: ProcessModal;
|
|
118
|
-
liveTailModal: LiveTailModal;
|
|
119
|
-
agentDetailModal: AgentDetailModal;
|
|
120
118
|
contextInspectorModal: ContextInspectorModal;
|
|
121
119
|
blockActionsMenu: BlockActionsMenu;
|
|
122
120
|
searchManager: SearchManager;
|
|
@@ -126,6 +124,7 @@ export interface FeedContextStableRefs {
|
|
|
126
124
|
panelManager: PanelManager;
|
|
127
125
|
keybindingsManager: KeybindingsManager;
|
|
128
126
|
killRing: KillRing;
|
|
127
|
+
focusTracker: FocusTracker;
|
|
129
128
|
getHistory: () => InfiniteBuffer;
|
|
130
129
|
getViewportHeight: () => number;
|
|
131
130
|
getScrollTop: () => number;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { loadSkillByTrigger } from '@pellux/goodvibes-sdk/platform/tools';
|
|
2
|
+
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
2
3
|
import type { CommandContext, CommandRegistry } from './command-registry.ts';
|
|
3
4
|
import type { AutocompleteEngine } from './autocomplete.ts';
|
|
4
5
|
import type { InputToken } from '@pellux/goodvibes-sdk/platform/core';
|
|
@@ -92,25 +93,34 @@ export function handleCommandModeToken(state: CommandModeRouteState, token: Inpu
|
|
|
92
93
|
const commandPromise = state.commandRegistry.get(name)
|
|
93
94
|
? state.commandRegistry.execute(name, args, ctx)
|
|
94
95
|
: (ctx.executeCommand?.(name, args) ?? Promise.resolve(false));
|
|
95
|
-
commandPromise
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
} else {
|
|
99
|
-
const shellPaths = state.commandContext?.workspace.shellPaths;
|
|
100
|
-
const skillContent = shellPaths
|
|
101
|
-
? loadSkillByTrigger('/' + name, {
|
|
102
|
-
workingDirectory: shellPaths.workingDirectory,
|
|
103
|
-
homeDirectory: shellPaths.homeDirectory,
|
|
104
|
-
})
|
|
105
|
-
: null;
|
|
106
|
-
if (skillContent) {
|
|
107
|
-
state.commandContext?.submitInput?.(skillContent);
|
|
108
|
-
} else {
|
|
109
|
-
state.conversationManager?.log(`Unknown command: /${name}. Type /help for available commands.`, { fg: '#ef4444' });
|
|
96
|
+
commandPromise
|
|
97
|
+
.then((handled) => {
|
|
98
|
+
if (handled) {
|
|
110
99
|
state.requestRender();
|
|
100
|
+
} else {
|
|
101
|
+
const shellPaths = state.commandContext?.workspace.shellPaths;
|
|
102
|
+
const skillContent = shellPaths
|
|
103
|
+
? loadSkillByTrigger('/' + name, {
|
|
104
|
+
workingDirectory: shellPaths.workingDirectory,
|
|
105
|
+
homeDirectory: shellPaths.homeDirectory,
|
|
106
|
+
})
|
|
107
|
+
: null;
|
|
108
|
+
if (skillContent) {
|
|
109
|
+
state.commandContext?.submitInput?.(skillContent);
|
|
110
|
+
} else {
|
|
111
|
+
state.conversationManager?.log(`Unknown command: /${name}. Type /help for available commands.`, { fg: '#ef4444' });
|
|
112
|
+
state.requestRender();
|
|
113
|
+
}
|
|
111
114
|
}
|
|
112
|
-
}
|
|
113
|
-
|
|
115
|
+
})
|
|
116
|
+
.catch((err: unknown) => {
|
|
117
|
+
// Defense in depth for EVERY command, not just the ones that
|
|
118
|
+
// already guard their own internals: a handler that throws or
|
|
119
|
+
// awaits a rejected promise must never become a silent unhandled
|
|
120
|
+
// rejection — it renders the same way an unknown command does.
|
|
121
|
+
state.conversationManager?.log(`Command /${name} failed: ${summarizeError(err)}`, { fg: '#ef4444' });
|
|
122
|
+
state.requestRender();
|
|
123
|
+
});
|
|
114
124
|
} else {
|
|
115
125
|
closeCommandMode();
|
|
116
126
|
}
|