@pellux/goodvibes-tui 1.7.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/README.md +10 -9
- 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/composer-state.ts +11 -3
- package/src/core/conversation-line-cache.ts +27 -3
- package/src/core/conversation-rendering.ts +71 -14
- 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 +68 -1
- package/src/core/turn-cancellation.ts +7 -2
- package/src/core/turn-event-wiring.ts +10 -2
- 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/input/autocomplete.ts +27 -1
- package/src/input/command-registry.ts +46 -4
- package/src/input/commands/codebase-runtime.ts +46 -6
- package/src/input/commands/config.ts +43 -3
- package/src/input/commands/health-runtime.ts +9 -1
- package/src/input/commands/memory.ts +68 -35
- package/src/input/commands/operator-panel-runtime.ts +31 -7
- package/src/input/commands/planning-runtime.ts +95 -6
- package/src/input/commands/qrcode-runtime.ts +25 -5
- package/src/input/commands/remote-runtime-setup.ts +5 -3
- package/src/input/commands/session-content.ts +20 -9
- package/src/input/commands/settings-sync-runtime.ts +15 -3
- package/src/input/commands/shell-core.ts +10 -1
- package/src/input/commands/workstream-runtime.ts +168 -18
- package/src/input/config-modal-types.ts +15 -1
- package/src/input/config-modal.ts +227 -12
- package/src/input/feed-context-factory.ts +3 -0
- package/src/input/handler-command-route.ts +10 -3
- package/src/input/handler-content-actions.ts +17 -2
- package/src/input/handler-feed-routes.ts +43 -121
- package/src/input/handler-feed.ts +32 -4
- package/src/input/handler-modal-routes.ts +78 -13
- package/src/input/handler-modal-stack.ts +11 -2
- 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 +15 -8
- package/src/input/handler-shortcuts.ts +59 -9
- package/src/input/handler.ts +6 -1
- package/src/input/keybindings.ts +6 -5
- package/src/input/model-picker.ts +19 -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-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 +51 -2
- package/src/input/settings-modal-types.ts +4 -2
- package/src/main.ts +28 -27
- package/src/panels/builtin/shared.ts +9 -3
- 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 +89 -131
- package/src/panels/fleet-read-model.ts +43 -0
- package/src/panels/fleet-steer.ts +35 -2
- package/src/panels/fleet-stop.ts +29 -1
- package/src/panels/modals/keybindings-modal.ts +16 -1
- package/src/panels/modals/modal-theme.ts +35 -29
- package/src/panels/modals/pairing-modal.ts +25 -6
- package/src/panels/modals/planning-modal.ts +43 -21
- package/src/panels/modals/work-plan-modal.ts +28 -0
- package/src/panels/panel-manager.ts +15 -4
- package/src/panels/polish-core.ts +38 -25
- package/src/panels/project-planning-answer-actions.ts +8 -13
- package/src/panels/types.ts +24 -0
- package/src/permissions/prompt.ts +160 -13
- package/src/renderer/autocomplete-overlay.ts +28 -2
- package/src/renderer/compositor.ts +2 -3
- package/src/renderer/config-modal.ts +19 -5
- package/src/renderer/footer-tips.ts +5 -1
- package/src/renderer/fullscreen-primitives.ts +32 -22
- package/src/renderer/git-status.ts +3 -1
- 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 +18 -3
- package/src/renderer/overlay-box.ts +21 -17
- 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 +22 -3
- package/src/renderer/ui-factory.ts +154 -85
- package/src/renderer/ui-primitives.ts +30 -129
- package/src/runtime/bootstrap-command-context.ts +6 -0
- package/src/runtime/bootstrap-command-parts.ts +8 -4
- package/src/runtime/bootstrap-core.ts +33 -11
- package/src/runtime/bootstrap-hook-bridge.ts +7 -0
- package/src/runtime/bootstrap-shell.ts +19 -1
- package/src/runtime/bootstrap.ts +118 -5
- package/src/runtime/code-index-services.ts +25 -2
- 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 +10 -0
- package/src/runtime/resume-notice.ts +209 -0
- package/src/runtime/services.ts +12 -8
- 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 +19 -3
- package/src/runtime/workstream-services.ts +160 -28
- package/src/runtime/wrfc-persistence.ts +124 -17
- package/src/shell/blocking-input.ts +46 -3
- package/src/shell/recovery-input-helpers.ts +170 -1
- package/src/shell/ui-openers.ts +42 -9
- package/src/utils/terminal-width.ts +52 -0
- package/src/version.ts +1 -1
|
@@ -20,16 +20,19 @@ import { buildAnswerActions, isGenericRecommendation, type PlanningAnswerAction
|
|
|
20
20
|
// ---------------------------------------------------------------------------
|
|
21
21
|
// Project Planning → 'planning' config-modal surface (W6.1 group-B port). Shows
|
|
22
22
|
// readiness/questions/decisions/task-graph/handoff — read-only except choosing
|
|
23
|
-
//
|
|
24
|
-
// or refreshing.
|
|
23
|
+
// an answer to the current open question, approving execution, dismissing the
|
|
24
|
+
// plan, or refreshing.
|
|
25
25
|
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
26
|
+
// DEBT-3 — the seams are now real:
|
|
27
|
+
// - A CANNED answer to a real open question dispatches `/plan answer <id> <text>`
|
|
28
|
+
// (records it; the open-question gap clears on the next refine).
|
|
29
|
+
// - The CUSTOM free-form typed answer (and any answer to a synthetic readiness
|
|
30
|
+
// question with no open-question record) is submitted to chat via the generic
|
|
31
|
+
// `submitInput` seam — a real model turn. ORDERING GUARD: the modal closes
|
|
32
|
+
// BEFORE the turn starts (a turn under a live modal is the modal-liveness
|
|
33
|
+
// hazard). No more `/plan <text>` reseed approximation.
|
|
34
|
+
// - Dismiss is a first-class CONFIRMED action (`d`) dispatching `/plan dismiss`,
|
|
35
|
+
// plus the plain Esc close (planning unchanged).
|
|
33
36
|
// ---------------------------------------------------------------------------
|
|
34
37
|
|
|
35
38
|
export type PlanningModalService = Pick<ProjectPlanningService, 'status' | 'getState' | 'listDecisions' | 'getLanguage' | 'evaluate'>;
|
|
@@ -134,6 +137,7 @@ class PlanningModalSurface implements ConfigModalSurface {
|
|
|
134
137
|
readonly actions = [
|
|
135
138
|
{ key: 'enter', id: 'submit', label: 'submit', enabledFor: () => this.currentAnswerActions().actions.length > 0 },
|
|
136
139
|
{ key: 'a', id: 'approve', label: 'approve execution' },
|
|
140
|
+
{ key: 'd', id: 'dismiss', label: 'dismiss planning', confirm: true },
|
|
137
141
|
{ key: 'r', id: 'refresh', label: 'refresh' },
|
|
138
142
|
];
|
|
139
143
|
|
|
@@ -199,7 +203,7 @@ class PlanningModalSurface implements ConfigModalSurface {
|
|
|
199
203
|
for (const action of actions) {
|
|
200
204
|
rows.push({ id: action.id, label: `${action.label} - ${action.detail}`, ...(action.disabled ? { selectable: false } : {}) });
|
|
201
205
|
}
|
|
202
|
-
line({ content: '
|
|
206
|
+
line({ content: 'Enter records a canned answer against this question; the custom row submits your typed text to chat.', fg: undefined });
|
|
203
207
|
}
|
|
204
208
|
|
|
205
209
|
for (const l of buildGapsLines(evaluation)) line(l);
|
|
@@ -209,31 +213,49 @@ class PlanningModalSurface implements ConfigModalSurface {
|
|
|
209
213
|
|
|
210
214
|
return {
|
|
211
215
|
title: 'Planning',
|
|
212
|
-
tabs: [{ id: 'planning', label: 'Planning', header, rows
|
|
216
|
+
tabs: [{ id: 'planning', label: 'Planning', header, rows }],
|
|
213
217
|
};
|
|
214
218
|
}
|
|
215
219
|
|
|
216
220
|
onAction(id: string, ctx: ConfigModalActionContext): void {
|
|
217
221
|
if (id === 'refresh') { this.refresh(); ctx.setStatus('Reloading project planning state…'); return; }
|
|
218
222
|
if (id === 'approve') { void ctx.executeCommand?.('plan', ['approve']); ctx.setStatus('Dispatched /plan approve.'); return; }
|
|
223
|
+
if (id === 'dismiss') {
|
|
224
|
+
// First-class, confirmed (host two-press) mutating dismiss.
|
|
225
|
+
void ctx.executeCommand?.('plan', ['dismiss']);
|
|
226
|
+
ctx.setStatus('Dispatched /plan dismiss.');
|
|
227
|
+
ctx.close();
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
219
230
|
if (id !== 'submit') return;
|
|
220
231
|
const { question, actions } = this.currentAnswerActions();
|
|
221
232
|
if (!question || actions.length === 0) return;
|
|
222
233
|
const action = ctx.row ? actions.find((a) => a.id === ctx.row!.id) : undefined;
|
|
223
|
-
if (!action || action.disabled
|
|
234
|
+
if (!action || action.disabled) { ctx.print('Choose an answer option.'); return; }
|
|
224
235
|
if (action.kind === 'approve') { void ctx.executeCommand?.('plan', ['approve']); ctx.setStatus('Dispatched /plan approve.'); return; }
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
236
|
+
|
|
237
|
+
const answerText = action.answer.trim();
|
|
238
|
+
if (!answerText) { ctx.print('Choose a non-empty answer, or type an answer for the custom row.'); return; }
|
|
239
|
+
|
|
240
|
+
// A canned answer to a REAL open question records structurally via /plan answer.
|
|
241
|
+
const isOpenQuestion = this.snapshot?.state?.openQuestions.some(
|
|
242
|
+
(q) => q.id === question.id && (q.status ?? 'open') === 'open',
|
|
243
|
+
) ?? false;
|
|
244
|
+
if (action.id !== 'custom' && isOpenQuestion) {
|
|
245
|
+
void ctx.executeCommand?.('plan', ['answer', question.id, ...answerText.split(/\s+/)]);
|
|
246
|
+
ctx.setStatus('Dispatched /plan answer for the current question.');
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Free-form (custom typed) answer, or an answer to a synthetic readiness
|
|
251
|
+
// question with no open-question record → submit to chat as a real turn.
|
|
252
|
+
// ORDERING GUARD: close the modal BEFORE the turn starts (modal-liveness).
|
|
253
|
+
if (ctx.submitInput) {
|
|
232
254
|
ctx.close();
|
|
255
|
+
ctx.submitInput(answerText);
|
|
233
256
|
return;
|
|
234
257
|
}
|
|
235
|
-
|
|
236
|
-
ctx.setStatus(`Dispatched /plan ${action.answer.trim()} (reseeds the goal — see the in-modal note).`);
|
|
258
|
+
ctx.print('Submitting to chat is unavailable in this runtime; answer left unsent.');
|
|
237
259
|
}
|
|
238
260
|
}
|
|
239
261
|
|
|
@@ -64,6 +64,16 @@ class WorkPlanModalSurface implements ConfigModalSurface {
|
|
|
64
64
|
|
|
65
65
|
private readonly hasRow = (row: ConfigModalRow | null): boolean => row !== null;
|
|
66
66
|
|
|
67
|
+
/** 'i'/'w' jump to the selected item's linked agent/WRFC chain in Fleet
|
|
68
|
+
* (DEBT-5 item 4 — restores the retired WorkPlanPanel's per-item deep-link,
|
|
69
|
+
* lost when W6.1 migrated this surface to a modal without it). Gated on
|
|
70
|
+
* the link actually being present, same as the retired panel gated the key
|
|
71
|
+
* on `item?.linked?.agentId`/`wrfcId` before ever reaching the jump. */
|
|
72
|
+
private readonly hasAgentLink = (row: ConfigModalRow | null): boolean =>
|
|
73
|
+
!!(row && this.itemFrom(row.id)?.linked?.agentId);
|
|
74
|
+
private readonly hasWrfcLink = (row: ConfigModalRow | null): boolean =>
|
|
75
|
+
!!(row && this.itemFrom(row.id)?.linked?.wrfcId);
|
|
76
|
+
|
|
67
77
|
readonly actions = [
|
|
68
78
|
{ key: 'enter', id: 'cycleStatus', label: 'cycle status', enabledFor: this.hasRow },
|
|
69
79
|
{ key: '1', id: 'setPending', label: 'pending', enabledFor: this.hasRow },
|
|
@@ -75,6 +85,8 @@ class WorkPlanModalSurface implements ConfigModalSurface {
|
|
|
75
85
|
{ key: 'd', id: 'remove', label: 'delete', enabledFor: this.hasRow },
|
|
76
86
|
{ key: 'c', id: 'clearCompleted', label: 'clear done' },
|
|
77
87
|
{ key: 'a', id: 'add', label: 'add' },
|
|
88
|
+
{ key: 'i', id: 'jumpAgent', label: 'jump agent', enabledFor: this.hasAgentLink },
|
|
89
|
+
{ key: 'w', id: 'jumpWrfc', label: 'jump wrfc', enabledFor: this.hasWrfcLink },
|
|
78
90
|
{ key: 'r', id: 'refresh', label: 'refresh' },
|
|
79
91
|
];
|
|
80
92
|
|
|
@@ -122,6 +134,22 @@ class WorkPlanModalSurface implements ConfigModalSurface {
|
|
|
122
134
|
if (!item) return;
|
|
123
135
|
if (id === 'cycleStatus') { void ctx.executeCommand?.('work-plan', ['cycle', item.id]); ctx.setStatus(`Dispatched /work-plan cycle ${item.id}.`); return; }
|
|
124
136
|
if (id === 'remove') { void ctx.executeCommand?.('work-plan', ['remove', item.id]); ctx.setStatus(`Dispatched /work-plan remove ${item.id}.`); return; }
|
|
137
|
+
if (id === 'jumpAgent') {
|
|
138
|
+
const agentId = item.linked?.agentId;
|
|
139
|
+
if (!agentId) return;
|
|
140
|
+
// 'agent' matches the SDK's ProcessKind for a live agent node — see
|
|
141
|
+
// FleetPanel.receiveDeepLink's id+kind match.
|
|
142
|
+
void ctx.executeCommand?.('panel', ['open', 'fleet', '--target', `${agentId}:agent`]);
|
|
143
|
+
ctx.setStatus(`Opened Fleet on agent ${agentId}.`);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (id === 'jumpWrfc') {
|
|
147
|
+
const wrfcId = item.linked?.wrfcId;
|
|
148
|
+
if (!wrfcId) return;
|
|
149
|
+
void ctx.executeCommand?.('panel', ['open', 'fleet', '--target', `${wrfcId}:wrfc-chain`]);
|
|
150
|
+
ctx.setStatus(`Opened Fleet on WRFC chain ${wrfcId}.`);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
125
153
|
const status: WorkPlanItemStatus | null =
|
|
126
154
|
id === 'setPending' ? 'pending' : id === 'setInProgress' ? 'in_progress' : id === 'setBlocked' ? 'blocked'
|
|
127
155
|
: id === 'setDone' ? 'done' : id === 'setFailed' ? 'failed' : id === 'setCancelled' ? 'cancelled' : null;
|
|
@@ -2,7 +2,8 @@
|
|
|
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';
|
|
6
7
|
// Type-only, erased at runtime, routed through the `@/` alias (not a relative
|
|
7
8
|
// path) so it stays out of the relative-import graph the architecture
|
|
8
9
|
// cycle-checker walks — same discipline as the PanelManager import in types.ts.
|
|
@@ -228,7 +229,7 @@ export class PanelManager {
|
|
|
228
229
|
this._cachedWorkspaceTabs = null;
|
|
229
230
|
}
|
|
230
231
|
|
|
231
|
-
open(panelIdOrAlias: string, pane?: 'top' | 'bottom'): Panel {
|
|
232
|
+
open(panelIdOrAlias: string, pane?: 'top' | 'bottom', target?: PanelDeepLinkTarget): Panel {
|
|
232
233
|
const modalName = this.modalRedirects.get(panelIdOrAlias);
|
|
233
234
|
if (modalName !== undefined) {
|
|
234
235
|
this.openModalCallback?.(modalName);
|
|
@@ -244,12 +245,12 @@ export class PanelManager {
|
|
|
244
245
|
// pane (fixes `/panel open <id> top` and the panel-list T/B move keys).
|
|
245
246
|
if (pane && pane !== existingPane) {
|
|
246
247
|
this._moveBetweenPanes(existingPane, pane, panelId);
|
|
247
|
-
return this.getPanel(panelId)
|
|
248
|
+
return this._deliverDeepLink(this.getPanel(panelId)!, target);
|
|
248
249
|
}
|
|
249
250
|
this._activateByIdInPane(panelId, existingPane);
|
|
250
251
|
this._focusedPane = existingPane;
|
|
251
252
|
if (existingPane === 'bottom') this._bottomPaneVisible = true;
|
|
252
|
-
return this._getPane(existingPane).panels[this._getPane(existingPane).activeIndex]
|
|
253
|
+
return this._deliverDeepLink(this._getPane(existingPane).panels[this._getPane(existingPane).activeIndex]!, target);
|
|
253
254
|
}
|
|
254
255
|
|
|
255
256
|
const targetPane = pane ?? this._focusedPane;
|
|
@@ -271,6 +272,16 @@ export class PanelManager {
|
|
|
271
272
|
}
|
|
272
273
|
panel.onActivate();
|
|
273
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);
|
|
274
285
|
return panel;
|
|
275
286
|
}
|
|
276
287
|
|
|
@@ -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,18 +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
|
-
// Honest label: there is no /plan subcommand that pauses planning, so this
|
|
129
|
-
// row only closes the panel — it does NOT change any planning state.
|
|
130
|
-
label: 'Close (planning unchanged)',
|
|
131
|
-
detail: 'Close this panel. Pausing project planning is not available as a command yet, so planning state is left unchanged; /plan reopens it later.',
|
|
132
|
-
answer: 'Pause project planning for this workspace and continue without the planning panel.',
|
|
133
|
-
kind: 'dismiss',
|
|
134
|
-
});
|
|
135
130
|
return actions;
|
|
136
131
|
}
|
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;
|
|
@@ -139,6 +151,18 @@ export interface Panel {
|
|
|
139
151
|
* to intervene before the global shortcut acts.
|
|
140
152
|
*/
|
|
141
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;
|
|
142
166
|
}
|
|
143
167
|
|
|
144
168
|
export interface PanelRegistration extends Pick<Panel, 'id' | 'name' | 'icon' | 'category'> {
|
|
@@ -12,6 +12,72 @@ export type { PermissionPromptRequest, PermissionPromptDecision, PermissionReque
|
|
|
12
12
|
/** Visible hunk rows before a "+N more" trailer kicks in (Risk 3). */
|
|
13
13
|
const MAX_VISIBLE_HUNKS = 8;
|
|
14
14
|
|
|
15
|
+
/** Path rows shown before collapsing to a "N files: a, b, +K more" line (UX-B 2a). */
|
|
16
|
+
const MAX_PATH_ROWS = 3;
|
|
17
|
+
|
|
18
|
+
/** Trailing filename for compact display of a long/absolute path. */
|
|
19
|
+
function baseName(p: string): string {
|
|
20
|
+
const cleaned = p.replace(/[/\\]+$/, '');
|
|
21
|
+
const idx = Math.max(cleaned.lastIndexOf('/'), cleaned.lastIndexOf('\\'));
|
|
22
|
+
return idx >= 0 ? cleaned.slice(idx + 1) : cleaned;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Make an absolute path relative to the working directory when it sits under it. */
|
|
26
|
+
function relativize(p: string, cwd?: string): string {
|
|
27
|
+
if (!cwd || !p.startsWith('/')) return p;
|
|
28
|
+
if (p === cwd) return '.';
|
|
29
|
+
const prefix = cwd.endsWith('/') ? cwd : `${cwd}/`;
|
|
30
|
+
return p.startsWith(prefix) ? p.slice(prefix.length) : p;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Collect string elements or `el[field]` strings from an array arg. */
|
|
34
|
+
function collectField(arr: unknown, field: string): string[] {
|
|
35
|
+
if (!Array.isArray(arr)) return [];
|
|
36
|
+
const out: string[] = [];
|
|
37
|
+
for (const el of arr) {
|
|
38
|
+
if (typeof el === 'string') out.push(el);
|
|
39
|
+
else if (el && typeof el === 'object' && typeof (el as Record<string, unknown>)[field] === 'string') {
|
|
40
|
+
out.push((el as Record<string, unknown>)[field] as string);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The real display target(s) of a tool invocation — the paths/commands/urls a
|
|
48
|
+
* user needs to see, extracted from the actual arg shapes so a nested
|
|
49
|
+
* `{files:[{path}]}` no longer falls through to a raw JSON blob. (UX-B 2a.)
|
|
50
|
+
* Returns [] when nothing recognisable is present (caller falls back).
|
|
51
|
+
*/
|
|
52
|
+
function cardTargets(args: Record<string, unknown>): string[] {
|
|
53
|
+
const files = collectField(args.files, 'path');
|
|
54
|
+
if (files.length) return files;
|
|
55
|
+
if (typeof args.path === 'string') return [args.path];
|
|
56
|
+
if (typeof args.file === 'string') return [args.file];
|
|
57
|
+
const commands = collectField(args.commands, 'cmd');
|
|
58
|
+
if (commands.length) return commands;
|
|
59
|
+
if (typeof args.command === 'string') return [args.command];
|
|
60
|
+
if (typeof args.cmd === 'string') return [args.cmd];
|
|
61
|
+
if (typeof args.pattern === 'string') return [args.pattern];
|
|
62
|
+
const urls = collectField(args.urls, 'url');
|
|
63
|
+
if (urls.length) return urls;
|
|
64
|
+
const queries = collectField(args.queries, 'query');
|
|
65
|
+
if (queries.length) return queries;
|
|
66
|
+
if (typeof args.query === 'string') return [args.query];
|
|
67
|
+
if (typeof args.task === 'string') return [args.task];
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Short human verb for a permission category, for the condensed summary line. */
|
|
72
|
+
function categoryVerb(category: PermissionCategory): string {
|
|
73
|
+
switch (category) {
|
|
74
|
+
case 'write': return 'write';
|
|
75
|
+
case 'execute': return 'run';
|
|
76
|
+
case 'delegate': return 'delegate';
|
|
77
|
+
default: return 'read';
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
15
81
|
/**
|
|
16
82
|
* Single source of truth for how many Line rows the hunk-list section
|
|
17
83
|
* occupies: 1 header row + up to MAX_VISIBLE_HUNKS checkbox rows + 1 trailer
|
|
@@ -49,12 +115,62 @@ export class PermissionPromptUI {
|
|
|
49
115
|
};
|
|
50
116
|
}
|
|
51
117
|
|
|
52
|
-
|
|
118
|
+
/** The relativised display target(s) for the Path field, with a raw fallback. */
|
|
119
|
+
private static resolvedTargets(request: PermissionPromptRequest): string[] {
|
|
120
|
+
const cwd = request.workingDirectory;
|
|
121
|
+
const targets = cardTargets(request.args).map((t) => relativize(t, cwd));
|
|
122
|
+
return targets.length > 0 ? targets : [getDisplayArg(request.tool, request.args)];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Path-field row texts — one per target when few, a "N files:" summary when many. */
|
|
126
|
+
private static pathRowTexts(targets: string[], maxLen: number): string[] {
|
|
127
|
+
const clamp = (s: string): string => (s.length > maxLen ? `...${s.slice(-(maxLen - 3))}` : s);
|
|
128
|
+
if (targets.length <= 1) return [clamp(targets[0] ?? '(unknown)')];
|
|
129
|
+
if (targets.length <= MAX_PATH_ROWS) return targets.map(clamp);
|
|
130
|
+
const shown = targets.slice(0, 2).map(baseName).join(', ');
|
|
131
|
+
return [clamp(`${targets.length} files: ${shown}, +${targets.length - 2} more`)];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* A low-risk, project-local request is shown condensed (one summary line +
|
|
136
|
+
* choices) unless the user expanded it with `d`. High/critical risk, external
|
|
137
|
+
* scope, and hunk-selectable edits always show the full block. (UX-B 2b.)
|
|
138
|
+
*/
|
|
139
|
+
private static isCondensed(
|
|
140
|
+
request: PermissionPromptRequest,
|
|
141
|
+
hunkState: HunkSelectionState | undefined,
|
|
142
|
+
detailsExpanded: boolean,
|
|
143
|
+
): boolean {
|
|
144
|
+
if (hunkState || detailsExpanded) return false;
|
|
145
|
+
// Shell execution, delegation and network requests always show the full
|
|
146
|
+
// card — their action semantics (command, side effects, host, checklist)
|
|
147
|
+
// warrant scrutiny even when the risk model rates them low. Only mundane
|
|
148
|
+
// low-risk filesystem reads/writes condense. (UX-B 2b.)
|
|
149
|
+
if (request.category === 'execute' || request.category === 'delegate') return false;
|
|
150
|
+
const analysis = this.fallbackAnalysis(request);
|
|
151
|
+
if (analysis.riskLevel !== 'low') return false;
|
|
152
|
+
const scope = analysis.blastRadius;
|
|
153
|
+
return scope === undefined || scope === 'local' || scope === 'project';
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
static getPromptHeight(
|
|
157
|
+
request: PermissionPromptRequest,
|
|
158
|
+
hunkState?: HunkSelectionState,
|
|
159
|
+
detailsExpanded = false,
|
|
160
|
+
): number {
|
|
161
|
+
// Condensed low-risk card: top separator, title, summary, choices, bottom
|
|
162
|
+
// separator — see createPromptLines' condensed branch (must stay in sync).
|
|
163
|
+
if (this.isCondensed(request, hunkState, detailsExpanded)) return 5;
|
|
53
164
|
const analysis = this.fallbackAnalysis(request);
|
|
54
165
|
const reasonLines = Math.min(2, Math.max(1, analysis.reasons.length));
|
|
55
166
|
const extraLines = (analysis.host ? 1 : 0) + (analysis.surface ? 1 : 0) + (analysis.sideEffects && analysis.sideEffects.length > 0 ? 1 : 0);
|
|
56
167
|
const hunkLines = hunkState ? hunkListRowCount(hunkState) : 0;
|
|
57
|
-
|
|
168
|
+
// Base 12 counted a single arg line; the Path field now spans `pathRows`
|
|
169
|
+
// lines and the full card adds one raw-args row (2a reachability), so the
|
|
170
|
+
// arg allotment becomes pathRows + 1 → base 12 + pathRows (12 already
|
|
171
|
+
// included one of those). See createPromptLines' full branch.
|
|
172
|
+
const pathRows = this.pathRowTexts(this.resolvedTargets(request), 999).length;
|
|
173
|
+
return 12 + pathRows + reasonLines + extraLines + hunkLines;
|
|
58
174
|
}
|
|
59
175
|
|
|
60
176
|
/** Returns the key argument to display for a given tool invocation. */
|
|
@@ -84,12 +200,16 @@ export class PermissionPromptUI {
|
|
|
84
200
|
* createPromptLines - Renders the permission prompt as an array of Lines.
|
|
85
201
|
* Injected into the viewport by the render function when a request is pending.
|
|
86
202
|
*/
|
|
87
|
-
static createPromptLines(
|
|
203
|
+
static createPromptLines(
|
|
204
|
+
width: number,
|
|
205
|
+
request: PermissionRequest,
|
|
206
|
+
hunkState?: HunkSelectionState,
|
|
207
|
+
detailsExpanded = false,
|
|
208
|
+
): Line[] {
|
|
88
209
|
const lines: Line[] = [];
|
|
89
210
|
const { tool, args, category } = request;
|
|
90
211
|
const analysis = this.fallbackAnalysis(request);
|
|
91
212
|
const brief = buildPermissionApprovalBrief(request);
|
|
92
|
-
const displayArg = this.getDisplayArg(tool, args);
|
|
93
213
|
const { label, color } = this.getCategoryLabel(category);
|
|
94
214
|
|
|
95
215
|
const ACCENT = '135'; // purple
|
|
@@ -97,6 +217,24 @@ export class PermissionPromptUI {
|
|
|
97
217
|
const TEXT = '252';
|
|
98
218
|
const DIM = '244';
|
|
99
219
|
|
|
220
|
+
const maxArgLen = Math.max(10, width - 16);
|
|
221
|
+
const pathRows = this.pathRowTexts(this.resolvedTargets(request), maxArgLen);
|
|
222
|
+
|
|
223
|
+
// Condensed low-risk / project-local card: one summary line (verb → target
|
|
224
|
+
// (scope)) plus the choices. Full block is one `d` away. (UX-B 2b.)
|
|
225
|
+
if (this.isCondensed(request, hunkState, detailsExpanded)) {
|
|
226
|
+
lines.push(UIFactory.stringToLine('─'.repeat(width), width, { fg: ACCENT, dim: true }));
|
|
227
|
+
lines.push(UIFactory.stringToLine(` [${label}] ${brief.title} `.padEnd(width), width, { fg: WARN, bold: true }));
|
|
228
|
+
const scopeText = analysis.blastRadius ? ` (${analysis.blastRadius})` : '';
|
|
229
|
+
const summaryLine = ` ${categoryVerb(category)} → ${pathRows[0]}${scopeText}`;
|
|
230
|
+
lines.push(UIFactory.stringToLine(summaryLine.padEnd(width), width, { fg: TEXT }));
|
|
231
|
+
lines.push(UIFactory.stringToLine(
|
|
232
|
+
` [Y] Allow once [A] Allow always (session) [N] Deny [d] details`.padEnd(width),
|
|
233
|
+
width, { fg: ACCENT, bold: true }));
|
|
234
|
+
lines.push(UIFactory.stringToLine('─'.repeat(width), width, { fg: ACCENT, dim: true }));
|
|
235
|
+
return lines;
|
|
236
|
+
}
|
|
237
|
+
|
|
100
238
|
// Top separator
|
|
101
239
|
lines.push(UIFactory.stringToLine('─'.repeat(width), width, { fg: ACCENT, dim: true }));
|
|
102
240
|
|
|
@@ -109,13 +247,13 @@ export class PermissionPromptUI {
|
|
|
109
247
|
const toolLine = ` Tool : ${tool}`;
|
|
110
248
|
lines.push(UIFactory.stringToLine(toolLine.padEnd(width), width, { fg: TEXT }));
|
|
111
249
|
|
|
112
|
-
//
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
:
|
|
117
|
-
|
|
118
|
-
|
|
250
|
+
// Path/subject row(s): actual target path(s), one per line when few, a
|
|
251
|
+
// "N files: a, b, +K more" summary when many. Raw args move to the Args row
|
|
252
|
+
// below so this field never shows a truncated JSON blob. (UX-B 2a.)
|
|
253
|
+
pathRows.forEach((rowText, i) => {
|
|
254
|
+
const labelCol = i === 0 ? brief.subjectLabel.padEnd(9) : ' '.repeat(9);
|
|
255
|
+
lines.push(UIFactory.stringToLine(` ${labelCol}: ${rowText}`.padEnd(width), width, { fg: TEXT }));
|
|
256
|
+
});
|
|
119
257
|
|
|
120
258
|
// Working directory row
|
|
121
259
|
const cwd = request.workingDirectory ?? '(unknown)';
|
|
@@ -170,6 +308,13 @@ export class PermissionPromptUI {
|
|
|
170
308
|
const checklistLine = ` Checklist : ${truncatedChecklist}`;
|
|
171
309
|
lines.push(UIFactory.stringToLine(checklistLine.padEnd(width), width, { fg: DIM }));
|
|
172
310
|
|
|
311
|
+
// Raw args row — the full tool arguments live here in the details view so
|
|
312
|
+
// the Path field above can render clean path(s), never a JSON blob. (2a.)
|
|
313
|
+
const rawArgs = JSON.stringify(args);
|
|
314
|
+
const maxRawLen = Math.max(10, width - 16);
|
|
315
|
+
const truncatedRaw = rawArgs.length > maxRawLen ? `${rawArgs.slice(0, maxRawLen - 3)}...` : rawArgs;
|
|
316
|
+
lines.push(UIFactory.stringToLine(` Args : ${truncatedRaw}`.padEnd(width), width, { fg: DIM }));
|
|
317
|
+
|
|
173
318
|
// Blank spacer
|
|
174
319
|
lines.push(UIFactory.stringToLine(' '.repeat(width), width));
|
|
175
320
|
|
|
@@ -207,10 +352,12 @@ export class PermissionPromptUI {
|
|
|
207
352
|
lines.push(UIFactory.stringToLine(trailerLine.padEnd(width), width, { fg: DIM }));
|
|
208
353
|
}
|
|
209
354
|
|
|
210
|
-
// Choices row
|
|
355
|
+
// Choices row. A condensable card shown in full is the expanded form, so it
|
|
356
|
+
// offers `[d]` to collapse back; other full cards omit the details toggle.
|
|
357
|
+
const collapseHint = this.isCondensed(request, hunkState, false) ? ' [d] hide details' : '';
|
|
211
358
|
const choicesLine = hunkState
|
|
212
359
|
? ` [j/k] Navigate [Space] Toggle [A] All [Enter] Apply selected [N] Deny`
|
|
213
|
-
: ` [Y] Allow once [A] Allow always (session) [N] Deny`;
|
|
360
|
+
: ` [Y] Allow once [A] Allow always (session) [N] Deny${collapseHint}`;
|
|
214
361
|
lines.push(UIFactory.stringToLine(choicesLine.padEnd(width), width, { fg: ACCENT, bold: true }));
|
|
215
362
|
|
|
216
363
|
// Bottom separator
|