@pellux/goodvibes-tui 1.1.0 → 1.9.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 +85 -21
- package/README.md +28 -18
- package/docs/foundation-artifacts/operator-contract.json +4045 -1763
- package/package.json +2 -2
- package/src/audio/spoken-turn-controller.ts +12 -2
- package/src/audio/spoken-turn-wiring.ts +2 -1
- package/src/cli/help.ts +8 -1
- package/src/cli/service-posture.ts +2 -1
- package/src/cli/status.ts +2 -1
- package/src/cli/surface-command.ts +2 -2
- 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/composer-state.ts +11 -3
- package/src/core/conversation-line-cache.ts +27 -3
- package/src/core/conversation-rendering.ts +90 -14
- package/src/core/conversation.ts +18 -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 +20 -1
- package/src/core/system-message-noise.ts +87 -0
- package/src/core/system-message-router.ts +104 -51
- package/src/core/turn-cancellation.ts +25 -0
- package/src/core/turn-event-wiring.ts +72 -3
- package/src/daemon/cli.ts +29 -2
- package/src/daemon/handlers/register.ts +8 -1
- package/src/daemon/service-commands.ts +329 -0
- package/src/export/cost-utils.ts +36 -0
- package/src/input/autocomplete.ts +27 -1
- package/src/input/command-registry.ts +84 -5
- package/src/input/commands/checkpoint-runtime.ts +280 -0
- package/src/input/commands/codebase-runtime.ts +232 -0
- package/src/input/commands/config.ts +43 -3
- package/src/input/commands/eval.ts +1 -1
- package/src/input/commands/health-runtime.ts +10 -2
- 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 +73 -35
- package/src/input/commands/operator-panel-runtime.ts +71 -31
- package/src/input/commands/planning-runtime.ts +116 -4
- 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 +26 -6
- package/src/input/commands/recall-review.ts +35 -0
- package/src/input/commands/remote-runtime-setup.ts +5 -3
- 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 +20 -9
- package/src/input/commands/session-workflow.ts +16 -1
- package/src/input/commands/settings-sync-runtime.ts +16 -4
- package/src/input/commands/shell-core.ts +25 -8
- 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 +488 -0
- package/src/input/commands.ts +12 -0
- package/src/input/config-modal-types.ts +175 -0
- package/src/input/config-modal.ts +592 -0
- package/src/input/feed-context-factory.ts +10 -8
- package/src/input/handler-command-route.ts +37 -20
- package/src/input/handler-content-actions.ts +17 -2
- package/src/input/handler-feed-routes.ts +74 -114
- package/src/input/handler-feed.ts +79 -27
- package/src/input/handler-interactions.ts +1 -3
- package/src/input/handler-modal-routes.ts +136 -6
- package/src/input/handler-modal-stack.ts +14 -7
- package/src/input/handler-modal-token-routes.ts +16 -49
- package/src/input/handler-onboarding-daemon-adopt.ts +149 -0
- package/src/input/handler-onboarding.ts +71 -59
- package/src/input/handler-picker-routes.ts +26 -102
- package/src/input/handler-shortcuts.ts +74 -14
- package/src/input/handler-types.ts +2 -6
- package/src/input/handler-ui-state.ts +10 -22
- package/src/input/handler.ts +12 -29
- package/src/input/keybindings.ts +23 -8
- package/src/input/model-picker-types.ts +24 -6
- package/src/input/model-picker.ts +77 -2
- package/src/input/onboarding/onboarding-runtime-status.ts +10 -1
- package/src/input/onboarding/onboarding-wizard-apply.ts +8 -1
- package/src/input/onboarding/onboarding-wizard-constants.ts +4 -1
- package/src/input/onboarding/onboarding-wizard-network-adopt.ts +136 -0
- package/src/input/onboarding/onboarding-wizard-steps.ts +9 -6
- package/src/input/onboarding/onboarding-wizard-types.ts +3 -1
- package/src/input/panel-integration-actions.ts +9 -170
- package/src/input/panel-mouse-geometry.ts +97 -0
- package/src/input/panel-paste-flood-guard.ts +86 -0
- package/src/input/selection-modal.ts +11 -0
- package/src/input/session-picker-modal.ts +44 -4
- package/src/input/settings-modal-data.ts +146 -2
- package/src/input/settings-modal-mutations.ts +6 -4
- package/src/input/settings-modal-types.ts +4 -2
- package/src/main.ts +49 -51
- package/src/panels/agent-inspector-shared.ts +2 -1
- package/src/panels/base-panel.ts +22 -1
- package/src/panels/builtin/agent.ts +21 -107
- package/src/panels/builtin/development.ts +15 -61
- 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 +18 -46
- package/src/panels/builtin-modals.ts +218 -0
- package/src/panels/builtin-panels.ts +5 -0
- package/src/panels/cost-tracker-panel.ts +36 -10
- package/src/panels/diff-panel.ts +12 -43
- package/src/panels/eval-registry.ts +60 -0
- package/src/panels/fleet-deep-link.ts +31 -0
- package/src/panels/fleet-panel-format.ts +62 -0
- package/src/panels/fleet-panel-worktree-detail.ts +48 -0
- package/src/panels/fleet-panel.ts +758 -0
- package/src/panels/fleet-read-model.ts +520 -0
- package/src/panels/fleet-steer.ts +125 -0
- package/src/panels/fleet-stop.ts +153 -0
- package/src/panels/fleet-tabs.ts +230 -0
- package/src/panels/fleet-transcript.ts +356 -0
- package/src/panels/index.ts +7 -31
- package/src/panels/modals/hooks-modal.ts +187 -0
- package/src/panels/modals/keybindings-modal.ts +166 -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 +42 -0
- package/src/panels/modals/pairing-modal.ts +163 -0
- package/src/panels/modals/planning-modal.ts +304 -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 +177 -0
- package/src/panels/panel-confirm-overlay.ts +72 -0
- package/src/panels/panel-manager.ts +123 -9
- package/src/panels/polish-core.ts +38 -25
- package/src/panels/project-planning-answer-actions.ts +8 -11
- package/src/panels/skills-panel.ts +7 -51
- package/src/panels/types.ts +37 -0
- package/src/permissions/hunk-selection.ts +179 -0
- package/src/permissions/prompt.ts +216 -13
- package/src/renderer/autocomplete-overlay.ts +28 -2
- 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/compositor.ts +2 -3
- package/src/renderer/config-modal.ts +95 -0
- package/src/renderer/conversation-overlays.ts +10 -17
- package/src/renderer/fleet-tab-strip.ts +56 -0
- package/src/renderer/footer-tips.ts +15 -3
- package/src/renderer/fullscreen-primitives.ts +32 -22
- package/src/renderer/git-status.ts +43 -1
- package/src/renderer/help-overlay.ts +2 -2
- package/src/renderer/layout.ts +0 -4
- package/src/renderer/markdown.ts +7 -3
- package/src/renderer/modal-factory.ts +25 -20
- package/src/renderer/model-workspace.ts +62 -4
- package/src/renderer/overlay-box.ts +21 -17
- package/src/renderer/panel-workspace-bar.ts +8 -2
- package/src/renderer/process-indicator.ts +14 -3
- package/src/renderer/selection-modal-overlay.ts +6 -1
- package/src/renderer/session-picker-modal.ts +196 -3
- package/src/renderer/settings-modal-helpers.ts +2 -0
- package/src/renderer/settings-modal.ts +7 -0
- package/src/renderer/shell-surface.ts +8 -1
- package/src/renderer/status-glyphs.ts +14 -15
- package/src/renderer/system-message.ts +15 -3
- package/src/renderer/terminal-bg-probe.ts +339 -0
- package/src/renderer/terminal-escapes.ts +20 -0
- package/src/renderer/theme-mode-config.ts +67 -0
- package/src/renderer/theme.ts +91 -1
- package/src/renderer/thinking.ts +11 -3
- package/src/renderer/tool-call.ts +15 -9
- package/src/renderer/tool-result-summary.ts +148 -0
- package/src/renderer/turn-injection.ts +133 -0
- package/src/renderer/ui-factory.ts +154 -85
- package/src/renderer/ui-primitives.ts +30 -129
- package/src/runtime/bootstrap-command-context.ts +27 -1
- package/src/runtime/bootstrap-command-parts.ts +41 -10
- package/src/runtime/bootstrap-core.ts +47 -15
- package/src/runtime/bootstrap-hook-bridge.ts +7 -0
- package/src/runtime/bootstrap-shell.ts +48 -17
- package/src/runtime/bootstrap.ts +129 -22
- package/src/runtime/code-index-services.ts +135 -0
- package/src/runtime/legacy-daemon-migration.ts +516 -0
- package/src/runtime/memory-fold.ts +26 -0
- package/src/runtime/onboarding/derivation.ts +7 -2
- package/src/runtime/onboarding/snapshot.ts +27 -1
- package/src/runtime/onboarding/types.ts +31 -1
- package/src/runtime/operator-token-cleanup.ts +82 -1
- package/src/runtime/orchestrator-core-services.ts +59 -0
- package/src/runtime/process-lifecycle.ts +3 -1
- package/src/runtime/resume-notice.ts +209 -0
- package/src/runtime/services.ts +98 -46
- package/src/runtime/session-inbound-inputs.ts +252 -0
- package/src/runtime/session-spine-transport.ts +64 -0
- package/src/runtime/terminal-output-guard.ts +15 -8
- package/src/runtime/ui-services.ts +27 -3
- package/src/runtime/workstream-services.ts +327 -0
- package/src/runtime/wrfc-persistence.ts +124 -17
- package/src/shell/blocking-input.ts +68 -4
- package/src/shell/recovery-input-helpers.ts +170 -1
- package/src/shell/ui-openers.ts +171 -26
- package/src/utils/format-duration.ts +8 -18
- package/src/utils/splash-lines.ts +1 -1
- package/src/utils/terminal-width.ts +52 -0
- 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 -238
- package/src/panels/communication-panel.ts +0 -256
- package/src/panels/control-plane-panel.ts +0 -470
- package/src/panels/debug-panel.ts +0 -615
- package/src/panels/docs-panel.ts +0 -384
- package/src/panels/eval-panel.ts +0 -627
- package/src/panels/file-explorer-panel.ts +0 -673
- 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 -515
- 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 -566
- 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 -731
- package/src/panels/provider-health-panel.ts +0 -678
- package/src/panels/provider-health-tracker.ts +0 -310
- package/src/panels/provider-health-views.ts +0 -567
- 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 -496
- 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 -540
- package/src/panels/worktree-panel.ts +0 -360
- package/src/panels/wrfc-panel.ts +0 -790
- package/src/renderer/agent-detail-modal.ts +0 -466
- package/src/renderer/live-tail-modal.ts +0 -156
- package/src/renderer/process-modal.ts +0 -671
- package/src/renderer/qr-renderer.ts +0 -120
|
@@ -2,7 +2,12 @@
|
|
|
2
2
|
// PanelManager — central manager for panel lifecycle, navigation, and split
|
|
3
3
|
// ---------------------------------------------------------------------------
|
|
4
4
|
|
|
5
|
-
import type { Panel, PanelRegistration, PanelCategory } from './types.ts';
|
|
5
|
+
import type { Panel, PanelRegistration, PanelCategory, PanelDeepLinkTarget } from './types.ts';
|
|
6
|
+
export type { PanelDeepLinkTarget } from './types.ts';
|
|
7
|
+
// Type-only, erased at runtime, routed through the `@/` alias (not a relative
|
|
8
|
+
// path) so it stays out of the relative-import graph the architecture
|
|
9
|
+
// cycle-checker walks — same discipline as the PanelManager import in types.ts.
|
|
10
|
+
import type { ConfigModalSurface } from '@/input/config-modal-types.ts';
|
|
6
11
|
|
|
7
12
|
// ---------------------------------------------------------------------------
|
|
8
13
|
// Pane
|
|
@@ -31,6 +36,29 @@ export class PanelManager {
|
|
|
31
36
|
private retainedPanels = new Map<string, Panel>();
|
|
32
37
|
/** Old/absorbed panel id -> merged target id (WO-1xx console merges). */
|
|
33
38
|
private aliases = new Map<string, string>();
|
|
39
|
+
/**
|
|
40
|
+
* Retired panel id -> modal name (W6.1 purge, MIGRATE-TO-MODAL surfaces).
|
|
41
|
+
* Unlike `aliases` (panel -> panel), a hit here means no panel is ever
|
|
42
|
+
* constructed for this id — `open()` invokes `openModalCallback` instead
|
|
43
|
+
* and returns a sentinel. Registrations are added by WO-A/B; this map and
|
|
44
|
+
* the open()-time check are WO-C's mechanism only.
|
|
45
|
+
*/
|
|
46
|
+
private modalRedirects = new Map<string, string>();
|
|
47
|
+
/**
|
|
48
|
+
* Modal name -> the surface the config-modal host renders (W6.1). Built once
|
|
49
|
+
* in builtin-modals.ts (closing over read-models) and looked up by
|
|
50
|
+
* ui-openers' openModal callback. Distinct from `modalRedirects` (panel id ->
|
|
51
|
+
* modal name): a redirect resolves a legacy panel id to a name; this map
|
|
52
|
+
* resolves that name to the actual surface data/actions.
|
|
53
|
+
*/
|
|
54
|
+
private modalSurfaces = new Map<string, ConfigModalSurface>();
|
|
55
|
+
/**
|
|
56
|
+
* Late-bound: the modal stack is constructed after PanelManager (same
|
|
57
|
+
* ordering constraint as the openAgentDetail callback in
|
|
58
|
+
* builtin/shared.ts), so this is injected via a setter rather than the
|
|
59
|
+
* constructor.
|
|
60
|
+
*/
|
|
61
|
+
private openModalCallback?: (modalName: string) => void;
|
|
34
62
|
private _visible: boolean = false;
|
|
35
63
|
private _splitRatio: number = 0.6;
|
|
36
64
|
|
|
@@ -99,10 +127,76 @@ export class PanelManager {
|
|
|
99
127
|
this.aliases.set(aliasId, targetId);
|
|
100
128
|
}
|
|
101
129
|
|
|
130
|
+
/**
|
|
131
|
+
* Register a redirect so `open(panelId)` opens the named modal instead of
|
|
132
|
+
* constructing a panel (MIGRATE-TO-MODAL surfaces). Checked in open()
|
|
133
|
+
* before alias/registry resolution.
|
|
134
|
+
*/
|
|
135
|
+
registerModalRedirect(panelId: string, modalName: string): void {
|
|
136
|
+
this.modalRedirects.set(panelId, modalName);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Inject the callback `open()` invokes when it hits a modal redirect.
|
|
141
|
+
* Late-bound via setter because the modal stack is constructed after
|
|
142
|
+
* PanelManager (mirrors the openAgentDetail callback pattern).
|
|
143
|
+
*/
|
|
144
|
+
setOpenModalCallback(callback: (modalName: string) => void): void {
|
|
145
|
+
this.openModalCallback = callback;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* The modal name `panelIdOrAlias` redirects to, if any — lets callers
|
|
150
|
+
* (e.g. the `/panel` command) print "moved to the <name> modal" before or
|
|
151
|
+
* instead of calling open().
|
|
152
|
+
*/
|
|
153
|
+
getModalRedirect(panelIdOrAlias: string): string | undefined {
|
|
154
|
+
return this.modalRedirects.get(panelIdOrAlias);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Register the surface (data + actions) the config-modal host renders for a
|
|
159
|
+
* modal name. Called from builtin-modals.ts alongside registerModalRedirect.
|
|
160
|
+
*/
|
|
161
|
+
registerModalSurface(surface: ConfigModalSurface): void {
|
|
162
|
+
this.modalSurfaces.set(surface.name, surface);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Resolve a modal name to its registered surface, if any. */
|
|
166
|
+
getModalSurface(name: string): ConfigModalSurface | undefined {
|
|
167
|
+
return this.modalSurfaces.get(name);
|
|
168
|
+
}
|
|
169
|
+
|
|
102
170
|
private _resolveId(panelId: string): string {
|
|
103
171
|
return this.aliases.get(panelId) ?? panelId;
|
|
104
172
|
}
|
|
105
173
|
|
|
174
|
+
/**
|
|
175
|
+
* Placeholder returned by open() for a modal-redirected id. Never pushed
|
|
176
|
+
* into a pane, never retained, never rendered — it exists only so open()
|
|
177
|
+
* can keep its non-null `Panel` return type without constructing the real
|
|
178
|
+
* (deleted) panel view. `name` carries the modal name so a caller that
|
|
179
|
+
* inspects the returned panel's `name` (rather than calling
|
|
180
|
+
* getModalRedirect() beforehand) still gets an honest answer.
|
|
181
|
+
*/
|
|
182
|
+
private _modalRedirectSentinel(panelId: string, modalName: string): Panel {
|
|
183
|
+
return {
|
|
184
|
+
id: panelId,
|
|
185
|
+
name: modalName,
|
|
186
|
+
icon: '·',
|
|
187
|
+
category: 'session',
|
|
188
|
+
onActivate: () => {},
|
|
189
|
+
onDeactivate: () => {},
|
|
190
|
+
onDestroy: () => {},
|
|
191
|
+
render: () => [],
|
|
192
|
+
isTransient: true,
|
|
193
|
+
isPinned: false,
|
|
194
|
+
needsRender: false,
|
|
195
|
+
invalidate: () => {},
|
|
196
|
+
markRendered: () => {},
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
106
200
|
getRegisteredTypes(): PanelRegistration[] {
|
|
107
201
|
return [...this.registry];
|
|
108
202
|
}
|
|
@@ -135,7 +229,12 @@ export class PanelManager {
|
|
|
135
229
|
this._cachedWorkspaceTabs = null;
|
|
136
230
|
}
|
|
137
231
|
|
|
138
|
-
open(panelIdOrAlias: string, pane?: 'top' | 'bottom'): Panel {
|
|
232
|
+
open(panelIdOrAlias: string, pane?: 'top' | 'bottom', target?: PanelDeepLinkTarget): Panel {
|
|
233
|
+
const modalName = this.modalRedirects.get(panelIdOrAlias);
|
|
234
|
+
if (modalName !== undefined) {
|
|
235
|
+
this.openModalCallback?.(modalName);
|
|
236
|
+
return this._modalRedirectSentinel(panelIdOrAlias, modalName);
|
|
237
|
+
}
|
|
139
238
|
const panelId = this._resolveId(panelIdOrAlias);
|
|
140
239
|
this._recordRecent(panelId);
|
|
141
240
|
const existingPane = this._findPaneOf(panelId);
|
|
@@ -146,12 +245,12 @@ export class PanelManager {
|
|
|
146
245
|
// pane (fixes `/panel open <id> top` and the panel-list T/B move keys).
|
|
147
246
|
if (pane && pane !== existingPane) {
|
|
148
247
|
this._moveBetweenPanes(existingPane, pane, panelId);
|
|
149
|
-
return this.getPanel(panelId)
|
|
248
|
+
return this._deliverDeepLink(this.getPanel(panelId)!, target);
|
|
150
249
|
}
|
|
151
250
|
this._activateByIdInPane(panelId, existingPane);
|
|
152
251
|
this._focusedPane = existingPane;
|
|
153
252
|
if (existingPane === 'bottom') this._bottomPaneVisible = true;
|
|
154
|
-
return this._getPane(existingPane).panels[this._getPane(existingPane).activeIndex]
|
|
253
|
+
return this._deliverDeepLink(this._getPane(existingPane).panels[this._getPane(existingPane).activeIndex]!, target);
|
|
155
254
|
}
|
|
156
255
|
|
|
157
256
|
const targetPane = pane ?? this._focusedPane;
|
|
@@ -173,6 +272,16 @@ export class PanelManager {
|
|
|
173
272
|
}
|
|
174
273
|
panel.onActivate();
|
|
175
274
|
this._invalidateWorkspaceTabs();
|
|
275
|
+
return this._deliverDeepLink(panel, target);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Hand an optional deep-link target (DEBT-5 item 4) to a panel that just
|
|
280
|
+
* resolved from open(). Panels without a "node" concept simply don't
|
|
281
|
+
* implement `receiveDeepLink` — the call is a no-op via optional chaining.
|
|
282
|
+
*/
|
|
283
|
+
private _deliverDeepLink(panel: Panel, target: PanelDeepLinkTarget | undefined): Panel {
|
|
284
|
+
if (target) panel.receiveDeepLink?.(target);
|
|
176
285
|
return panel;
|
|
177
286
|
}
|
|
178
287
|
|
|
@@ -387,9 +496,12 @@ export class PanelManager {
|
|
|
387
496
|
this.bottomPane.panels.push(panel);
|
|
388
497
|
this.bottomPane.activeIndex = 0;
|
|
389
498
|
} else {
|
|
390
|
-
// Open a predictable default panel in the bottom pane
|
|
391
|
-
//
|
|
392
|
-
|
|
499
|
+
// Open a predictable default panel in the bottom pane. W6.1 purge:
|
|
500
|
+
// 'panel-list' was deleted (dead weight over a 5-panel registry —
|
|
501
|
+
// see the DELETE disposition), so the default is explicitly
|
|
502
|
+
// 'fleet' rather than falling back to registry[0] (whatever
|
|
503
|
+
// registers first, currently 'git' — see risk 5 in the brief).
|
|
504
|
+
const defaultPanel = this._getRegistration('fleet') ?? this.registry[0];
|
|
393
505
|
if (defaultPanel) {
|
|
394
506
|
this.open(defaultPanel.id, 'bottom');
|
|
395
507
|
}
|
|
@@ -493,9 +605,11 @@ export class PanelManager {
|
|
|
493
605
|
|
|
494
606
|
toggle(): void {
|
|
495
607
|
this._visible = !this._visible;
|
|
496
|
-
// Auto-open a default panel if toggling visible with nothing open
|
|
608
|
+
// Auto-open a default panel if toggling visible with nothing open.
|
|
609
|
+
// W6.1 purge: explicitly 'fleet' rather than registry[0] — see the
|
|
610
|
+
// matching comment in toggleBottomPane() above.
|
|
497
611
|
if (this._visible && this.topPane.panels.length === 0 && this.bottomPane.panels.length === 0) {
|
|
498
|
-
const defaultPanel = this._getRegistration('
|
|
612
|
+
const defaultPanel = this._getRegistration('fleet') ?? this.registry[0];
|
|
499
613
|
if (defaultPanel) this.open(defaultPanel.id);
|
|
500
614
|
}
|
|
501
615
|
}
|
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
import type { Line } from '../types/grid.ts';
|
|
2
2
|
import { createEmptyLine, createStyledCell } from '../types/grid.ts';
|
|
3
3
|
import { getDisplayWidth } from '../utils/terminal-width.ts';
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
// DEFAULT_PANEL_PALETTE is built from the mode-resolved chrome tones
|
|
7
|
-
// (resolveUiTones) rather than the static UI_TONES constant — WO-001 single
|
|
8
|
-
// read path. Mode is fixed to 'dark' until the terminal-bg-probe lands.
|
|
9
|
-
const UI_TONES = resolveUiTones('dark');
|
|
4
|
+
import { activeUiTones, registerThemeRefresh } from '../renderer/theme.ts';
|
|
10
5
|
|
|
11
6
|
// ---------------------------------------------------------------------------
|
|
12
7
|
// Panel palette + core line primitives.
|
|
@@ -36,24 +31,36 @@ export interface PanelPalette {
|
|
|
36
31
|
readonly selectBg?: string;
|
|
37
32
|
}
|
|
38
33
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
34
|
+
// Built from the mode-resolved chrome tones (activeUiTones). Because 100+ call
|
|
35
|
+
// sites read this object by reference, mode changes rebuild it IN PLACE via the
|
|
36
|
+
// registered refresher below rather than re-resolving per call — see theme.ts's
|
|
37
|
+
// active-mode runtime note. This refresher is registered before any panel's
|
|
38
|
+
// extendPalette() runs (panels import polish → polish-core first), so on a mode
|
|
39
|
+
// flip the base is rebuilt before the extended palettes re-merge from it.
|
|
40
|
+
function buildPanelPalette(): Required<PanelPalette> {
|
|
41
|
+
const t = activeUiTones();
|
|
42
|
+
return {
|
|
43
|
+
header: t.fg.primary,
|
|
44
|
+
headerBg: t.bg.title,
|
|
45
|
+
label: t.fg.muted,
|
|
46
|
+
value: t.fg.primary,
|
|
47
|
+
dim: t.fg.dim,
|
|
48
|
+
info: t.state.info,
|
|
49
|
+
good: t.state.good,
|
|
50
|
+
warn: t.state.warn,
|
|
51
|
+
bad: t.state.bad,
|
|
52
|
+
empty: t.fg.empty,
|
|
53
|
+
surfaceBg: t.bg.surface,
|
|
54
|
+
sectionBg: t.bg.section,
|
|
55
|
+
summaryBg: t.bg.summary,
|
|
56
|
+
inputBg: t.bg.input,
|
|
57
|
+
accent: t.fg.secondary,
|
|
58
|
+
selectBg: t.bg.selected,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const DEFAULT_PANEL_PALETTE: Readonly<Required<PanelPalette>> = buildPanelPalette();
|
|
63
|
+
registerThemeRefresh(() => Object.assign(DEFAULT_PANEL_PALETTE as Required<PanelPalette>, buildPanelPalette()));
|
|
57
64
|
|
|
58
65
|
/**
|
|
59
66
|
* Extend the base panel palette with domain-specific colors.
|
|
@@ -73,7 +80,13 @@ export function extendPalette<T extends Record<string, string>>(
|
|
|
73
80
|
base: typeof DEFAULT_PANEL_PALETTE,
|
|
74
81
|
extras: T,
|
|
75
82
|
): typeof DEFAULT_PANEL_PALETTE & T {
|
|
76
|
-
|
|
83
|
+
const merged = { ...base, ...extras } as typeof DEFAULT_PANEL_PALETTE & T;
|
|
84
|
+
// Self-register an in-place rebuild so every extendPalette-derived panel
|
|
85
|
+
// palette (cost/token/git/skills/diff/wrfc) tracks the active mode with zero
|
|
86
|
+
// per-panel churn. Runs AFTER the base refresher (registered at module eval,
|
|
87
|
+
// before any panel calls this), so `base` already carries the new-mode values.
|
|
88
|
+
registerThemeRefresh(() => Object.assign(merged as Record<string, string>, base, extras));
|
|
89
|
+
return merged;
|
|
77
90
|
}
|
|
78
91
|
|
|
79
92
|
export function buildPanelLine(
|
|
@@ -13,7 +13,7 @@ export interface PlanningAnswerAction {
|
|
|
13
13
|
readonly label: string;
|
|
14
14
|
readonly detail: string;
|
|
15
15
|
readonly answer: string;
|
|
16
|
-
readonly kind?: 'answer' | 'approve'
|
|
16
|
+
readonly kind?: 'answer' | 'approve';
|
|
17
17
|
readonly disabled?: boolean;
|
|
18
18
|
}
|
|
19
19
|
|
|
@@ -35,8 +35,12 @@ function compactAnswerDetail(text: string): string {
|
|
|
35
35
|
* canned suggestions first (de-duplicated by normalized answer text — a
|
|
36
36
|
* question that matches more than one keyword category, e.g. scope +
|
|
37
37
|
* recommendedAnswer echoing one of the scope answers verbatim, must not show
|
|
38
|
-
* the same suggested answer twice), then the fixed ask-narrower/custom
|
|
39
|
-
*
|
|
38
|
+
* the same suggested answer twice), then the fixed ask-narrower/custom actions.
|
|
39
|
+
*
|
|
40
|
+
* DEBT-3: dismissing planning is no longer a pseudo answer-row — it is a
|
|
41
|
+
* first-class confirmed modal action (the `d` key), so no dismiss row is
|
|
42
|
+
* produced here. A canned answer records against the current open question via
|
|
43
|
+
* /plan answer; the custom row's free-form text is submitted to chat.
|
|
40
44
|
*/
|
|
41
45
|
export function buildAnswerActions(question: ProjectPlanningQuestion, draftAnswer: string): PlanningAnswerAction[] {
|
|
42
46
|
const canned: PlanningAnswerAction[] = [];
|
|
@@ -119,16 +123,9 @@ export function buildAnswerActions(question: ProjectPlanningQuestion, draftAnswe
|
|
|
119
123
|
actions.push({
|
|
120
124
|
id: 'custom',
|
|
121
125
|
label: 'Submit typed answer',
|
|
122
|
-
detail: draftAnswer ? compactAnswerDetail(draftAnswer) : 'Type an answer first; this row
|
|
126
|
+
detail: draftAnswer ? compactAnswerDetail(draftAnswer) : 'Type an answer first; this row submits it to chat.',
|
|
123
127
|
answer: draftAnswer.trim(),
|
|
124
128
|
disabled: !draftAnswer.trim(),
|
|
125
129
|
});
|
|
126
|
-
actions.push({
|
|
127
|
-
id: 'dismiss-planning',
|
|
128
|
-
label: 'Close planning and continue without it',
|
|
129
|
-
detail: 'Pause project planning for this workspace. Normal chat continues; /plan can reopen it later.',
|
|
130
|
-
answer: 'Pause project planning for this workspace and continue without the planning panel.',
|
|
131
|
-
kind: 'dismiss',
|
|
132
|
-
});
|
|
133
130
|
return actions;
|
|
134
131
|
}
|
|
@@ -5,8 +5,6 @@ import { createEmptyLine } from '../types/grid.ts';
|
|
|
5
5
|
import { type ConfirmState, handleConfirmInput, renderConfirmLines } from './confirm-state.ts';
|
|
6
6
|
import { getDisplayWidth, truncateDisplay } from '../utils/terminal-width.ts';
|
|
7
7
|
import { ScrollableListPanel } from './scrollable-list-panel.ts';
|
|
8
|
-
import { FilePreviewPanel } from './file-preview-panel.ts';
|
|
9
|
-
import type { PanelIntegrationContext } from './types.ts';
|
|
10
8
|
import type { ComponentHealthMonitor } from '../runtime/perf/panel-health-monitor.ts';
|
|
11
9
|
import { listInstalledEcosystemEntries, type EcosystemCatalogPathOptions, type ShellPathService } from '@/runtime/index.ts';
|
|
12
10
|
import {
|
|
@@ -257,11 +255,6 @@ export class SkillsPanel extends ScrollableListPanel<SkillRecord> {
|
|
|
257
255
|
// I1: confirm state for destructive delete
|
|
258
256
|
private confirm: ConfirmState | null = null;
|
|
259
257
|
private readyPromise: Promise<void> | null = null;
|
|
260
|
-
// Staged pending action consumed by handlePanelIntegrationAction (same
|
|
261
|
-
// pattern as diff-panel.ts's pendingOpenPreview): Enter marks the intent
|
|
262
|
-
// here, the actual PanelManager/preview wiring happens once the
|
|
263
|
-
// integration context is available.
|
|
264
|
-
private pendingOpenPreview = false;
|
|
265
258
|
|
|
266
259
|
public constructor(options: SkillsPanelOptions) {
|
|
267
260
|
super('skills', 'Skills', '▩', 'automation-control', options.componentHealthMonitor);
|
|
@@ -374,50 +367,13 @@ export class SkillsPanel extends ScrollableListPanel<SkillRecord> {
|
|
|
374
367
|
|
|
375
368
|
public override onDestroy(): void {}
|
|
376
369
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
this.pendingOpenPreview = true;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
/**
|
|
388
|
-
* Cross-panel integration hook — Enter opens the selected skill's markdown
|
|
389
|
-
* source in the preview panel via the same open/focus bridge DiffPanel
|
|
390
|
-
* uses (src/input/panel-integration-actions.ts), without this panel
|
|
391
|
-
* needing to know about PanelManager pane/focus mechanics beyond what ctx
|
|
392
|
-
* exposes.
|
|
393
|
-
*/
|
|
394
|
-
public handlePanelIntegrationAction(_key: string, ctx: PanelIntegrationContext): boolean {
|
|
395
|
-
if (!this.pendingOpenPreview) return false;
|
|
396
|
-
this.pendingOpenPreview = false;
|
|
397
|
-
const skill = this.getSelectedItem();
|
|
398
|
-
if (!skill) return false;
|
|
399
|
-
|
|
400
|
-
const pm = ctx.panelManager;
|
|
401
|
-
let previewPanel = pm.getPanel('preview');
|
|
402
|
-
if (previewPanel instanceof FilePreviewPanel) {
|
|
403
|
-
const pane = pm.getPaneOf('preview');
|
|
404
|
-
pm.activateById('preview');
|
|
405
|
-
if (pane) pm.focusPane(pane);
|
|
406
|
-
} else {
|
|
407
|
-
const targetPane: 'top' | 'bottom' = pm.isBottomPaneVisible()
|
|
408
|
-
? (pm.getFocusedPane() === 'top' ? 'bottom' : 'top')
|
|
409
|
-
: 'bottom';
|
|
410
|
-
const opened = pm.open('preview', targetPane);
|
|
411
|
-
pm.show();
|
|
412
|
-
pm.focusPane(targetPane);
|
|
413
|
-
previewPanel = opened instanceof FilePreviewPanel ? opened : null;
|
|
414
|
-
}
|
|
415
|
-
if (previewPanel instanceof FilePreviewPanel) {
|
|
416
|
-
previewPanel.openFile(skill.path);
|
|
417
|
-
return true;
|
|
418
|
-
}
|
|
419
|
-
return false;
|
|
420
|
-
}
|
|
370
|
+
// W6.1 (the purge): Enter used to open the skill's markdown source in the
|
|
371
|
+
// preview panel via a staged pendingOpenPreview flag + handlePanelIntegrationAction
|
|
372
|
+
// (same pattern diff-panel.ts used). 'preview' is DELETE-disposition with no
|
|
373
|
+
// successor surface (no file-picker-overlay preview to repoint to either —
|
|
374
|
+
// verified), so that cross-panel jump was removed rather than repointed;
|
|
375
|
+
// Enter is currently a no-op on this list (browse-only until WO-B migrates
|
|
376
|
+
// Skills to a modal).
|
|
421
377
|
|
|
422
378
|
public handleInput(key: string): boolean {
|
|
423
379
|
// I1: y/n confirmation dialog for delete
|
package/src/panels/types.ts
CHANGED
|
@@ -16,6 +16,18 @@ export interface PanelIntegrationContext {
|
|
|
16
16
|
readonly executeCommand?: (name: string, args: string[]) => Promise<unknown>;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* A cross-panel deep-link target (DEBT-5 item 4): a specific node a panel
|
|
21
|
+
* should select + reveal when opened via `PanelManager.open(id, pane, target)`,
|
|
22
|
+
* e.g. a Fleet tree node keyed by its process id and (optionally) its
|
|
23
|
+
* ProcessKind for disambiguation. Panel-agnostic on purpose — only panels that
|
|
24
|
+
* have a "node" concept implement `Panel.receiveDeepLink`.
|
|
25
|
+
*/
|
|
26
|
+
export interface PanelDeepLinkTarget {
|
|
27
|
+
readonly id: string;
|
|
28
|
+
readonly kind?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
19
31
|
/**
|
|
20
32
|
* Named logical key identifiers emitted by the input tokenizer.
|
|
21
33
|
* These are the ONLY key names that will appear in `handleInput` calls;
|
|
@@ -126,6 +138,31 @@ export interface Panel {
|
|
|
126
138
|
* back to its built-in `instanceof` routing.
|
|
127
139
|
*/
|
|
128
140
|
handlePanelIntegrationAction?(key: string, ctx: PanelIntegrationContext): boolean;
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Optional: called by the global Ctrl+X (`panel-close`) shortcut BEFORE it
|
|
144
|
+
* closes this panel (handler-shortcuts.ts). Return `true` to consume
|
|
145
|
+
* Ctrl+X for an in-panel action instead — e.g. FleetPanel (Wave-3 session
|
|
146
|
+
* tabs) detaches its active tab and leaves the panel open. Return `false`
|
|
147
|
+
* (or omit this hook) to fall through to the ordinary close behavior.
|
|
148
|
+
* Ctrl+X never reaches a panel's own `handleInput` (ctrl/meta combos are
|
|
149
|
+
* intercepted earlier — see handlePanelFocusToken in
|
|
150
|
+
* src/input/handler-feed-routes.ts), so this is the one seam a panel has
|
|
151
|
+
* to intervene before the global shortcut acts.
|
|
152
|
+
*/
|
|
153
|
+
interceptPanelClose?(): boolean;
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Optional: consume a deep-link target handed in by `PanelManager.open(id,
|
|
157
|
+
* pane, target)` (DEBT-5 item 4) — select + reveal the matching node on the
|
|
158
|
+
* panel's next snapshot. A panel without a "node" concept simply doesn't
|
|
159
|
+
* implement this (the call is a no-op via optional chaining in
|
|
160
|
+
* PanelManager). A panel that DOES implement it owns its own honest
|
|
161
|
+
* "not found" surface (e.g. FleetPanel.setError) when the target no longer
|
|
162
|
+
* resolves — PanelManager has no transcript/print seam to report on its
|
|
163
|
+
* behalf.
|
|
164
|
+
*/
|
|
165
|
+
receiveDeepLink?(target: PanelDeepLinkTarget): void;
|
|
129
166
|
}
|
|
130
167
|
|
|
131
168
|
export interface PanelRegistration extends Pick<Panel, 'id' | 'name' | 'icon' | 'category'> {
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import type { PermissionPromptRequest } from '@pellux/goodvibes-sdk/platform/permissions';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Per-hunk accept/reject at the approval gate (W1.3 v1).
|
|
5
|
+
*
|
|
6
|
+
* Scoped to the `edit` tool only: EditInput.edits is already an array of
|
|
7
|
+
* independent, atomic find/replace units (each a natural "hunk"), so no
|
|
8
|
+
* diff/hunk-apply primitive is needed here. The `write` (whole-file
|
|
9
|
+
* overwrite) tool stays whole-payload accept/reject — true whole-file hunk
|
|
10
|
+
* splitting would require a genuinely new SDK diff/patch-apply primitive,
|
|
11
|
+
* flagged as an explicit v2 follow-up, not built here.
|
|
12
|
+
*
|
|
13
|
+
* NOTE ON SDK TYPES: the installed @pellux/goodvibes-sdk (0.36.0) does not
|
|
14
|
+
* yet export `EditItem`/`EditInput` from its `platform/tools` subpath (that
|
|
15
|
+
* export is added in the in-flight SDK change for this work order, landing
|
|
16
|
+
* in 0.37). Until then this module reads `request.args.edits` defensively
|
|
17
|
+
* off a narrow local structural type rather than importing a not-yet-published
|
|
18
|
+
* symbol, per the cross-repo compile contract for this wave.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** Structural shape of one EditItem, read defensively — see module doc. */
|
|
22
|
+
export interface EditItemLike {
|
|
23
|
+
readonly path: string;
|
|
24
|
+
readonly find: string;
|
|
25
|
+
readonly replace: string;
|
|
26
|
+
readonly id?: string | undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface HunkSelectionState {
|
|
30
|
+
readonly hunks: readonly EditItemLike[];
|
|
31
|
+
readonly cursor: number;
|
|
32
|
+
readonly selected: ReadonlySet<number>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type HunkCommit = 'apply' | 'cancel' | null;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Structural widening of the installed SDK's `PermissionPromptDecision`
|
|
39
|
+
* (0.36.0, no `modifiedArgs` field yet) so main.ts can resolve a decision
|
|
40
|
+
* carrying `modifiedArgs` without a nominal import of a not-yet-published
|
|
41
|
+
* SDK symbol. Assign to a variable of this type before calling the SDK's
|
|
42
|
+
* typed `resolve(decision: PermissionPromptDecision)` — passing a variable
|
|
43
|
+
* (not an object literal) sidesteps TypeScript's excess-property check, and
|
|
44
|
+
* the extra field is invisible to 0.36.0 callers, structurally compatible
|
|
45
|
+
* once 0.37 adds it for real.
|
|
46
|
+
*/
|
|
47
|
+
export interface PermissionPromptDecisionWithModifiedArgs {
|
|
48
|
+
approved: boolean;
|
|
49
|
+
remember?: boolean;
|
|
50
|
+
modifiedArgs?: Record<string, unknown>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface HunkKeyResult {
|
|
54
|
+
readonly state: HunkSelectionState;
|
|
55
|
+
readonly commit: HunkCommit;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Runtime guard: is `value` shaped like an EditItem? */
|
|
59
|
+
function isEditItemLike(value: unknown): value is EditItemLike {
|
|
60
|
+
if (typeof value !== 'object' || value === null) return false;
|
|
61
|
+
const candidate = value as Record<string, unknown>;
|
|
62
|
+
return (
|
|
63
|
+
typeof candidate['path'] === 'string' &&
|
|
64
|
+
typeof candidate['find'] === 'string' &&
|
|
65
|
+
typeof candidate['replace'] === 'string' &&
|
|
66
|
+
(candidate['id'] === undefined || typeof candidate['id'] === 'string')
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Extracts a validated EditItemLike[] from a permission request's args, or null if not edit-shaped. */
|
|
71
|
+
function readEditItems(args: Record<string, unknown>): EditItemLike[] | null {
|
|
72
|
+
const edits = args['edits'];
|
|
73
|
+
if (!Array.isArray(edits) || edits.length === 0) return null;
|
|
74
|
+
const items: EditItemLike[] = [];
|
|
75
|
+
for (const entry of edits) {
|
|
76
|
+
if (!isEditItemLike(entry)) return null;
|
|
77
|
+
items.push(entry);
|
|
78
|
+
}
|
|
79
|
+
return items;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* isHunkSelectable — true iff this is a multi-edit `edit` tool call. Single-edit
|
|
84
|
+
* `edit` calls, `write` calls, and everything else keep the exact current
|
|
85
|
+
* y/a/n flow untouched (zero behavior change for the common case).
|
|
86
|
+
*/
|
|
87
|
+
export function isHunkSelectable(request: PermissionPromptRequest): boolean {
|
|
88
|
+
if (request.tool !== 'edit') return false;
|
|
89
|
+
const items = readEditItems(request.args);
|
|
90
|
+
return items !== null && items.length > 1;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Builds initial hunk-selection state — all hunks selected by default (matches today's "y = approve everything" expectation as the zero-navigation path). */
|
|
94
|
+
export function buildHunkSelectionState(request: PermissionPromptRequest): HunkSelectionState {
|
|
95
|
+
const hunks = readEditItems(request.args) ?? [];
|
|
96
|
+
return {
|
|
97
|
+
hunks,
|
|
98
|
+
cursor: 0,
|
|
99
|
+
selected: new Set(hunks.map((_, i) => i)),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* applyHunkKey — pure reducer over raw stdin `data` for an active hunk-selection
|
|
105
|
+
* prompt. Mirrors the outer y/a/n prompt's raw-data key routing (not the
|
|
106
|
+
* normalized panel key vocabulary) since Space must be distinguishable from
|
|
107
|
+
* the trimmed/lowercased `key` the outer switch uses.
|
|
108
|
+
*/
|
|
109
|
+
export function applyHunkKey(state: HunkSelectionState, data: string): HunkKeyResult {
|
|
110
|
+
const lower = data.toLowerCase().trim();
|
|
111
|
+
|
|
112
|
+
if (data === 'j' || data === '\x1b[B') {
|
|
113
|
+
return { state: { ...state, cursor: Math.min(state.hunks.length - 1, state.cursor + 1) }, commit: null };
|
|
114
|
+
}
|
|
115
|
+
if (data === 'k' || data === '\x1b[A') {
|
|
116
|
+
return { state: { ...state, cursor: Math.max(0, state.cursor - 1) }, commit: null };
|
|
117
|
+
}
|
|
118
|
+
if (data === ' ') {
|
|
119
|
+
const next = new Set(state.selected);
|
|
120
|
+
if (next.has(state.cursor)) {
|
|
121
|
+
next.delete(state.cursor);
|
|
122
|
+
} else {
|
|
123
|
+
next.add(state.cursor);
|
|
124
|
+
}
|
|
125
|
+
return { state: { ...state, selected: next }, commit: null };
|
|
126
|
+
}
|
|
127
|
+
if (lower === 'a') {
|
|
128
|
+
return { state: { ...state, selected: new Set(state.hunks.map((_, i) => i)) }, commit: null };
|
|
129
|
+
}
|
|
130
|
+
if (data === '\r' || data === '\n') {
|
|
131
|
+
if (state.selected.size === 0) {
|
|
132
|
+
// No-op: nothing selected, prompt stays open rather than applying an empty edit.
|
|
133
|
+
return { state, commit: null };
|
|
134
|
+
}
|
|
135
|
+
return { state, commit: 'apply' };
|
|
136
|
+
}
|
|
137
|
+
if (lower === 'y') {
|
|
138
|
+
// Bypass-navigation shortcut: preserves muscle memory for non-hunk prompts.
|
|
139
|
+
return { state: { ...state, selected: new Set(state.hunks.map((_, i) => i)) }, commit: 'apply' };
|
|
140
|
+
}
|
|
141
|
+
if (lower === 'n' || data === '\x1b' || data === '\x03') {
|
|
142
|
+
return { state, commit: 'cancel' };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return { state, commit: null };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* buildModifiedEditArgs — returns request.args with `edits` filtered to exactly
|
|
150
|
+
* the selected hunks (in original order), preserving every other EditInput
|
|
151
|
+
* field (match/transaction/output/dry_run/validate) untouched.
|
|
152
|
+
*/
|
|
153
|
+
export function buildModifiedEditArgs(
|
|
154
|
+
request: PermissionPromptRequest,
|
|
155
|
+
state: HunkSelectionState,
|
|
156
|
+
): Record<string, unknown> {
|
|
157
|
+
const filtered = state.hunks.filter((_, i) => state.selected.has(i));
|
|
158
|
+
return { ...request.args, edits: filtered };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* buildPendingPermissionExtras — the `hunkState` + wrapped `resolve` fields
|
|
163
|
+
* main.ts spreads onto its PendingPermissionState alongside `...request`.
|
|
164
|
+
* Bundled into one helper (rather than inlined in main.ts) to keep main.ts's
|
|
165
|
+
* net line count at zero for this change — main.ts sits at the 800-line
|
|
166
|
+
* architecture cap and must never grow it.
|
|
167
|
+
*/
|
|
168
|
+
export function buildPendingPermissionExtras(
|
|
169
|
+
request: PermissionPromptRequest,
|
|
170
|
+
resolvePromise: (decision: PermissionPromptDecisionWithModifiedArgs) => void,
|
|
171
|
+
): {
|
|
172
|
+
hunkState: HunkSelectionState | undefined;
|
|
173
|
+
resolve: (approved: boolean, remember?: boolean, modifiedArgs?: Record<string, unknown>) => void;
|
|
174
|
+
} {
|
|
175
|
+
return {
|
|
176
|
+
hunkState: isHunkSelectable(request) ? buildHunkSelectionState(request) : undefined,
|
|
177
|
+
resolve: (approved, remember = false, modifiedArgs) => resolvePromise({ approved, remember, modifiedArgs }),
|
|
178
|
+
};
|
|
179
|
+
}
|