@pellux/goodvibes-tui 1.1.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 +85 -21
- package/README.md +20 -11
- package/docs/foundation-artifacts/operator-contract.json +1 -1
- package/package.json +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/conversation-rendering.ts +19 -0
- 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/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 +36 -0
- package/src/input/command-registry.ts +38 -1
- package/src/input/commands/checkpoint-runtime.ts +280 -0
- package/src/input/commands/codebase-runtime.ts +192 -0
- 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-workflow.ts +16 -1
- package/src/input/commands/settings-sync-runtime.ts +1 -1
- 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-feed-routes.ts +61 -23
- package/src/input/handler-feed.ts +47 -23
- package/src/input/handler-interactions.ts +1 -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 +2 -6
- package/src/input/handler-ui-state.ts +10 -22
- package/src/input/handler.ts +6 -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 +24 -27
- 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 +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 +36 -10
- package/src/panels/diff-panel.ts +12 -43
- 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/index.ts +7 -31
- 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/skills-panel.ts +7 -51
- package/src/panels/types.ts +13 -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/turn-injection.ts +114 -0
- package/src/runtime/bootstrap-command-context.ts +21 -1
- package/src/runtime/bootstrap-command-parts.ts +34 -7
- package/src/runtime/bootstrap-core.ts +14 -4
- package/src/runtime/bootstrap-shell.ts +29 -16
- 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 -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
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
// ---------------------------------------------------------------------------
|
|
4
4
|
|
|
5
5
|
import type { Panel, PanelRegistration, PanelCategory } from './types.ts';
|
|
6
|
+
// Type-only, erased at runtime, routed through the `@/` alias (not a relative
|
|
7
|
+
// path) so it stays out of the relative-import graph the architecture
|
|
8
|
+
// cycle-checker walks — same discipline as the PanelManager import in types.ts.
|
|
9
|
+
import type { ConfigModalSurface } from '@/input/config-modal-types.ts';
|
|
6
10
|
|
|
7
11
|
// ---------------------------------------------------------------------------
|
|
8
12
|
// Pane
|
|
@@ -31,6 +35,29 @@ export class PanelManager {
|
|
|
31
35
|
private retainedPanels = new Map<string, Panel>();
|
|
32
36
|
/** Old/absorbed panel id -> merged target id (WO-1xx console merges). */
|
|
33
37
|
private aliases = new Map<string, string>();
|
|
38
|
+
/**
|
|
39
|
+
* Retired panel id -> modal name (W6.1 purge, MIGRATE-TO-MODAL surfaces).
|
|
40
|
+
* Unlike `aliases` (panel -> panel), a hit here means no panel is ever
|
|
41
|
+
* constructed for this id — `open()` invokes `openModalCallback` instead
|
|
42
|
+
* and returns a sentinel. Registrations are added by WO-A/B; this map and
|
|
43
|
+
* the open()-time check are WO-C's mechanism only.
|
|
44
|
+
*/
|
|
45
|
+
private modalRedirects = new Map<string, string>();
|
|
46
|
+
/**
|
|
47
|
+
* Modal name -> the surface the config-modal host renders (W6.1). Built once
|
|
48
|
+
* in builtin-modals.ts (closing over read-models) and looked up by
|
|
49
|
+
* ui-openers' openModal callback. Distinct from `modalRedirects` (panel id ->
|
|
50
|
+
* modal name): a redirect resolves a legacy panel id to a name; this map
|
|
51
|
+
* resolves that name to the actual surface data/actions.
|
|
52
|
+
*/
|
|
53
|
+
private modalSurfaces = new Map<string, ConfigModalSurface>();
|
|
54
|
+
/**
|
|
55
|
+
* Late-bound: the modal stack is constructed after PanelManager (same
|
|
56
|
+
* ordering constraint as the openAgentDetail callback in
|
|
57
|
+
* builtin/shared.ts), so this is injected via a setter rather than the
|
|
58
|
+
* constructor.
|
|
59
|
+
*/
|
|
60
|
+
private openModalCallback?: (modalName: string) => void;
|
|
34
61
|
private _visible: boolean = false;
|
|
35
62
|
private _splitRatio: number = 0.6;
|
|
36
63
|
|
|
@@ -99,10 +126,76 @@ export class PanelManager {
|
|
|
99
126
|
this.aliases.set(aliasId, targetId);
|
|
100
127
|
}
|
|
101
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Register a redirect so `open(panelId)` opens the named modal instead of
|
|
131
|
+
* constructing a panel (MIGRATE-TO-MODAL surfaces). Checked in open()
|
|
132
|
+
* before alias/registry resolution.
|
|
133
|
+
*/
|
|
134
|
+
registerModalRedirect(panelId: string, modalName: string): void {
|
|
135
|
+
this.modalRedirects.set(panelId, modalName);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Inject the callback `open()` invokes when it hits a modal redirect.
|
|
140
|
+
* Late-bound via setter because the modal stack is constructed after
|
|
141
|
+
* PanelManager (mirrors the openAgentDetail callback pattern).
|
|
142
|
+
*/
|
|
143
|
+
setOpenModalCallback(callback: (modalName: string) => void): void {
|
|
144
|
+
this.openModalCallback = callback;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The modal name `panelIdOrAlias` redirects to, if any — lets callers
|
|
149
|
+
* (e.g. the `/panel` command) print "moved to the <name> modal" before or
|
|
150
|
+
* instead of calling open().
|
|
151
|
+
*/
|
|
152
|
+
getModalRedirect(panelIdOrAlias: string): string | undefined {
|
|
153
|
+
return this.modalRedirects.get(panelIdOrAlias);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Register the surface (data + actions) the config-modal host renders for a
|
|
158
|
+
* modal name. Called from builtin-modals.ts alongside registerModalRedirect.
|
|
159
|
+
*/
|
|
160
|
+
registerModalSurface(surface: ConfigModalSurface): void {
|
|
161
|
+
this.modalSurfaces.set(surface.name, surface);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Resolve a modal name to its registered surface, if any. */
|
|
165
|
+
getModalSurface(name: string): ConfigModalSurface | undefined {
|
|
166
|
+
return this.modalSurfaces.get(name);
|
|
167
|
+
}
|
|
168
|
+
|
|
102
169
|
private _resolveId(panelId: string): string {
|
|
103
170
|
return this.aliases.get(panelId) ?? panelId;
|
|
104
171
|
}
|
|
105
172
|
|
|
173
|
+
/**
|
|
174
|
+
* Placeholder returned by open() for a modal-redirected id. Never pushed
|
|
175
|
+
* into a pane, never retained, never rendered — it exists only so open()
|
|
176
|
+
* can keep its non-null `Panel` return type without constructing the real
|
|
177
|
+
* (deleted) panel view. `name` carries the modal name so a caller that
|
|
178
|
+
* inspects the returned panel's `name` (rather than calling
|
|
179
|
+
* getModalRedirect() beforehand) still gets an honest answer.
|
|
180
|
+
*/
|
|
181
|
+
private _modalRedirectSentinel(panelId: string, modalName: string): Panel {
|
|
182
|
+
return {
|
|
183
|
+
id: panelId,
|
|
184
|
+
name: modalName,
|
|
185
|
+
icon: '·',
|
|
186
|
+
category: 'session',
|
|
187
|
+
onActivate: () => {},
|
|
188
|
+
onDeactivate: () => {},
|
|
189
|
+
onDestroy: () => {},
|
|
190
|
+
render: () => [],
|
|
191
|
+
isTransient: true,
|
|
192
|
+
isPinned: false,
|
|
193
|
+
needsRender: false,
|
|
194
|
+
invalidate: () => {},
|
|
195
|
+
markRendered: () => {},
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
106
199
|
getRegisteredTypes(): PanelRegistration[] {
|
|
107
200
|
return [...this.registry];
|
|
108
201
|
}
|
|
@@ -136,6 +229,11 @@ export class PanelManager {
|
|
|
136
229
|
}
|
|
137
230
|
|
|
138
231
|
open(panelIdOrAlias: string, pane?: 'top' | 'bottom'): Panel {
|
|
232
|
+
const modalName = this.modalRedirects.get(panelIdOrAlias);
|
|
233
|
+
if (modalName !== undefined) {
|
|
234
|
+
this.openModalCallback?.(modalName);
|
|
235
|
+
return this._modalRedirectSentinel(panelIdOrAlias, modalName);
|
|
236
|
+
}
|
|
139
237
|
const panelId = this._resolveId(panelIdOrAlias);
|
|
140
238
|
this._recordRecent(panelId);
|
|
141
239
|
const existingPane = this._findPaneOf(panelId);
|
|
@@ -387,9 +485,12 @@ export class PanelManager {
|
|
|
387
485
|
this.bottomPane.panels.push(panel);
|
|
388
486
|
this.bottomPane.activeIndex = 0;
|
|
389
487
|
} else {
|
|
390
|
-
// Open a predictable default panel in the bottom pane
|
|
391
|
-
//
|
|
392
|
-
|
|
488
|
+
// Open a predictable default panel in the bottom pane. W6.1 purge:
|
|
489
|
+
// 'panel-list' was deleted (dead weight over a 5-panel registry —
|
|
490
|
+
// see the DELETE disposition), so the default is explicitly
|
|
491
|
+
// 'fleet' rather than falling back to registry[0] (whatever
|
|
492
|
+
// registers first, currently 'git' — see risk 5 in the brief).
|
|
493
|
+
const defaultPanel = this._getRegistration('fleet') ?? this.registry[0];
|
|
393
494
|
if (defaultPanel) {
|
|
394
495
|
this.open(defaultPanel.id, 'bottom');
|
|
395
496
|
}
|
|
@@ -493,9 +594,11 @@ export class PanelManager {
|
|
|
493
594
|
|
|
494
595
|
toggle(): void {
|
|
495
596
|
this._visible = !this._visible;
|
|
496
|
-
// Auto-open a default panel if toggling visible with nothing open
|
|
597
|
+
// Auto-open a default panel if toggling visible with nothing open.
|
|
598
|
+
// W6.1 purge: explicitly 'fleet' rather than registry[0] — see the
|
|
599
|
+
// matching comment in toggleBottomPane() above.
|
|
497
600
|
if (this._visible && this.topPane.panels.length === 0 && this.bottomPane.panels.length === 0) {
|
|
498
|
-
const defaultPanel = this._getRegistration('
|
|
601
|
+
const defaultPanel = this._getRegistration('fleet') ?? this.registry[0];
|
|
499
602
|
if (defaultPanel) this.open(defaultPanel.id);
|
|
500
603
|
}
|
|
501
604
|
}
|
|
@@ -125,8 +125,10 @@ export function buildAnswerActions(question: ProjectPlanningQuestion, draftAnswe
|
|
|
125
125
|
});
|
|
126
126
|
actions.push({
|
|
127
127
|
id: 'dismiss-planning',
|
|
128
|
-
label:
|
|
129
|
-
|
|
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.',
|
|
130
132
|
answer: 'Pause project planning for this workspace and continue without the planning panel.',
|
|
131
133
|
kind: 'dismiss',
|
|
132
134
|
});
|
|
@@ -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
|
@@ -126,6 +126,19 @@ export interface Panel {
|
|
|
126
126
|
* back to its built-in `instanceof` routing.
|
|
127
127
|
*/
|
|
128
128
|
handlePanelIntegrationAction?(key: string, ctx: PanelIntegrationContext): boolean;
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Optional: called by the global Ctrl+X (`panel-close`) shortcut BEFORE it
|
|
132
|
+
* closes this panel (handler-shortcuts.ts). Return `true` to consume
|
|
133
|
+
* Ctrl+X for an in-panel action instead — e.g. FleetPanel (Wave-3 session
|
|
134
|
+
* tabs) detaches its active tab and leaves the panel open. Return `false`
|
|
135
|
+
* (or omit this hook) to fall through to the ordinary close behavior.
|
|
136
|
+
* Ctrl+X never reaches a panel's own `handleInput` (ctrl/meta combos are
|
|
137
|
+
* intercepted earlier — see handlePanelFocusToken in
|
|
138
|
+
* src/input/handler-feed-routes.ts), so this is the one seam a panel has
|
|
139
|
+
* to intervene before the global shortcut acts.
|
|
140
|
+
*/
|
|
141
|
+
interceptPanelClose?(): boolean;
|
|
129
142
|
}
|
|
130
143
|
|
|
131
144
|
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
|
+
}
|
|
@@ -2,10 +2,29 @@ import { type Line } from '../types/grid.ts';
|
|
|
2
2
|
import { UIFactory } from '../renderer/ui-factory.ts';
|
|
3
3
|
import type { PermissionCategory, PermissionRequestAnalysis } from '@pellux/goodvibes-sdk/platform/permissions';
|
|
4
4
|
import { buildPermissionApprovalBrief, getDisplayArg } from '@pellux/goodvibes-sdk/platform/permissions';
|
|
5
|
+
import { DIFF_TONES, UI_TONES } from '../renderer/ui-primitives.ts';
|
|
6
|
+
import type { HunkSelectionState } from './hunk-selection.ts';
|
|
7
|
+
export { buildPendingPermissionExtras } from './hunk-selection.ts';
|
|
5
8
|
|
|
6
9
|
import type { PermissionPromptRequest, PermissionPromptDecision, PermissionRequestHandler, PermissionRequest } from '@pellux/goodvibes-sdk/platform/permissions';
|
|
7
10
|
export type { PermissionPromptRequest, PermissionPromptDecision, PermissionRequestHandler, PermissionRequest };
|
|
8
11
|
|
|
12
|
+
/** Visible hunk rows before a "+N more" trailer kicks in (Risk 3). */
|
|
13
|
+
const MAX_VISIBLE_HUNKS = 8;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Single source of truth for how many Line rows the hunk-list section
|
|
17
|
+
* occupies: 1 header row + up to MAX_VISIBLE_HUNKS checkbox rows + 1 trailer
|
|
18
|
+
* row (rendered as "+N more" when truncated, blank otherwise — always
|
|
19
|
+
* present so the row count never depends on which branch fires). Both
|
|
20
|
+
* getPromptHeight and createPromptLines call this SAME function, so they
|
|
21
|
+
* cannot drift apart (Risk 2 — main.ts's render loop reserves viewport
|
|
22
|
+
* space from getPromptHeight *before* the real render happens).
|
|
23
|
+
*/
|
|
24
|
+
function hunkListRowCount(hunkState: HunkSelectionState): number {
|
|
25
|
+
return Math.min(hunkState.hunks.length, MAX_VISIBLE_HUNKS) + 2;
|
|
26
|
+
}
|
|
27
|
+
|
|
9
28
|
/**
|
|
10
29
|
* PermissionPromptUI - Renders a permission prompt as Line[] fragments.
|
|
11
30
|
*
|
|
@@ -30,11 +49,12 @@ export class PermissionPromptUI {
|
|
|
30
49
|
};
|
|
31
50
|
}
|
|
32
51
|
|
|
33
|
-
static getPromptHeight(request: PermissionPromptRequest): number {
|
|
52
|
+
static getPromptHeight(request: PermissionPromptRequest, hunkState?: HunkSelectionState): number {
|
|
34
53
|
const analysis = this.fallbackAnalysis(request);
|
|
35
54
|
const reasonLines = Math.min(2, Math.max(1, analysis.reasons.length));
|
|
36
55
|
const extraLines = (analysis.host ? 1 : 0) + (analysis.surface ? 1 : 0) + (analysis.sideEffects && analysis.sideEffects.length > 0 ? 1 : 0);
|
|
37
|
-
|
|
56
|
+
const hunkLines = hunkState ? hunkListRowCount(hunkState) : 0;
|
|
57
|
+
return 12 + reasonLines + extraLines + hunkLines;
|
|
38
58
|
}
|
|
39
59
|
|
|
40
60
|
/** Returns the key argument to display for a given tool invocation. */
|
|
@@ -64,7 +84,7 @@ export class PermissionPromptUI {
|
|
|
64
84
|
* createPromptLines - Renders the permission prompt as an array of Lines.
|
|
65
85
|
* Injected into the viewport by the render function when a request is pending.
|
|
66
86
|
*/
|
|
67
|
-
static createPromptLines(width: number, request: PermissionRequest): Line[] {
|
|
87
|
+
static createPromptLines(width: number, request: PermissionRequest, hunkState?: HunkSelectionState): Line[] {
|
|
68
88
|
const lines: Line[] = [];
|
|
69
89
|
const { tool, args, category } = request;
|
|
70
90
|
const analysis = this.fallbackAnalysis(request);
|
|
@@ -153,8 +173,44 @@ export class PermissionPromptUI {
|
|
|
153
173
|
// Blank spacer
|
|
154
174
|
lines.push(UIFactory.stringToLine(' '.repeat(width), width));
|
|
155
175
|
|
|
176
|
+
if (hunkState) {
|
|
177
|
+
// Hunk list — see hunkListRowCount() for the row-count contract this
|
|
178
|
+
// block must match exactly (Risk 2: getPromptHeight/createPromptLines
|
|
179
|
+
// parity is what keeps main.ts's render loop from clipping the
|
|
180
|
+
// viewport).
|
|
181
|
+
const { hunks, cursor, selected } = hunkState;
|
|
182
|
+
const headerLine = ` Hunks (${selected.size}/${hunks.length} selected):`;
|
|
183
|
+
lines.push(UIFactory.stringToLine(headerLine.padEnd(width), width, { fg: TEXT, bold: true }));
|
|
184
|
+
|
|
185
|
+
const visible = hunks.slice(0, MAX_VISIBLE_HUNKS);
|
|
186
|
+
const maxPreviewLen = Math.max(6, Math.floor((width - 20) / 2));
|
|
187
|
+
for (let i = 0; i < visible.length; i++) {
|
|
188
|
+
const hunk = visible[i]!;
|
|
189
|
+
const box = selected.has(i) ? '[x]' : '[ ]';
|
|
190
|
+
const findPreview = hunk.find.replace(/\n/g, '⏎').slice(0, maxPreviewLen);
|
|
191
|
+
const replacePreview = hunk.replace.replace(/\n/g, '⏎').slice(0, maxPreviewLen);
|
|
192
|
+
const rowText = ` ${box} ${i + 1}. ${hunk.path} — "${findPreview}" -> "${replacePreview}"`;
|
|
193
|
+
const isCursor = i === cursor;
|
|
194
|
+
lines.push(UIFactory.stringToLine(
|
|
195
|
+
rowText.padEnd(width),
|
|
196
|
+
width,
|
|
197
|
+
{
|
|
198
|
+
fg: selected.has(i) ? DIFF_TONES.add : DIFF_TONES.del,
|
|
199
|
+
bg: isCursor ? UI_TONES.bg.selected : undefined,
|
|
200
|
+
bold: isCursor,
|
|
201
|
+
},
|
|
202
|
+
));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const hiddenCount = hunks.length - visible.length;
|
|
206
|
+
const trailerLine = hiddenCount > 0 ? ` +${hiddenCount} more` : '';
|
|
207
|
+
lines.push(UIFactory.stringToLine(trailerLine.padEnd(width), width, { fg: DIM }));
|
|
208
|
+
}
|
|
209
|
+
|
|
156
210
|
// Choices row
|
|
157
|
-
const choicesLine =
|
|
211
|
+
const choicesLine = hunkState
|
|
212
|
+
? ` [j/k] Navigate [Space] Toggle [A] All [Enter] Apply selected [N] Deny`
|
|
213
|
+
: ` [Y] Allow once [A] Allow always (session) [N] Deny`;
|
|
158
214
|
lines.push(UIFactory.stringToLine(choicesLine.padEnd(width), width, { fg: ACCENT, bold: true }));
|
|
159
215
|
|
|
160
216
|
// Bottom separator
|
|
@@ -10,11 +10,22 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { getCompactionEvents } from '@pellux/goodvibes-sdk/platform/core';
|
|
13
|
+
import { getCompactionQualityScore } from './compaction-quality.ts';
|
|
13
14
|
import type { CompactionEvent } from '@pellux/goodvibes-sdk/platform/core';
|
|
15
|
+
import type { CompactionQualityScore } from './compaction-quality.ts';
|
|
14
16
|
|
|
15
17
|
// ─── formatCompactionEvent ────────────────────────────────────────────────────
|
|
16
18
|
|
|
17
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Exported for direct unit testing (W5.4/B28) — the grade suffix is a pure
|
|
21
|
+
* function of the event and an optional score, independent of whichever
|
|
22
|
+
* runtime lookup (getCompactionQualityScore) supplies that score.
|
|
23
|
+
*/
|
|
24
|
+
export function formatCompactionEvent(
|
|
25
|
+
ev: CompactionEvent,
|
|
26
|
+
n: number,
|
|
27
|
+
qualityScore?: CompactionQualityScore | null,
|
|
28
|
+
): string {
|
|
18
29
|
const date = new Date(ev.timestamp);
|
|
19
30
|
const timeStr = date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
20
31
|
const savings = Math.max(0, ev.tokensBeforeEstimate - ev.tokensAfterEstimate);
|
|
@@ -22,11 +33,15 @@ function formatCompactionEvent(ev: CompactionEvent, n: number): string {
|
|
|
22
33
|
? Math.round((savings / ev.tokensBeforeEstimate) * 100)
|
|
23
34
|
: 0;
|
|
24
35
|
const trigger = ev.trigger === 'auto' ? 'auto' : 'manual';
|
|
36
|
+
// Honest omission: no score means none was computed for this event (e.g. it
|
|
37
|
+
// pre-dates this feature, or ran through the small-window path, which has
|
|
38
|
+
// no CompactionEvent to key a score by) — never fabricate a grade.
|
|
39
|
+
const qualityStr = qualityScore ? ` quality=${qualityScore.grade} (${qualityScore.score.toFixed(2)})` : '';
|
|
25
40
|
return (
|
|
26
41
|
`#${n} ${timeStr} [${trigger}] ` +
|
|
27
42
|
`${ev.messagesBeforeCompaction}→${ev.messagesAfterCompaction} msgs ` +
|
|
28
43
|
`~${fmtN(ev.tokensBeforeEstimate)}→~${fmtN(ev.tokensAfterEstimate)} tok ` +
|
|
29
|
-
`saved ${savingsPct}
|
|
44
|
+
`saved ${savingsPct}%${qualityStr}`
|
|
30
45
|
);
|
|
31
46
|
}
|
|
32
47
|
|
|
@@ -48,7 +63,8 @@ export function buildCompactionHistoryText(): string {
|
|
|
48
63
|
];
|
|
49
64
|
const ordered = [...events].reverse();
|
|
50
65
|
for (let i = 0; i < ordered.length; i++) {
|
|
51
|
-
|
|
66
|
+
const ev = ordered[i]!;
|
|
67
|
+
lines.push(' ' + formatCompactionEvent(ev, ordered.length - i, getCompactionQualityScore(ev.timestamp)));
|
|
52
68
|
}
|
|
53
69
|
lines.push(' (Restore not available — the SDK does not yet expose a snapshot restore API.)');
|
|
54
70
|
return lines.join('\n');
|
|
@@ -18,8 +18,10 @@
|
|
|
18
18
|
|
|
19
19
|
import { estimateConversationTokens } from '@pellux/goodvibes-sdk/platform/core';
|
|
20
20
|
import { computeContextUsage } from '../core/context-usage.ts';
|
|
21
|
+
import { formatQualityScoreLine } from './compaction-quality.ts';
|
|
21
22
|
import type { CompactionEvent } from '@pellux/goodvibes-sdk/platform/core';
|
|
22
23
|
import type { ProviderMessage } from '@pellux/goodvibes-sdk/platform/providers';
|
|
24
|
+
import type { CompactionQualityScore } from './compaction-quality.ts';
|
|
23
25
|
|
|
24
26
|
/**
|
|
25
27
|
* Default compaction totalCeiling from context-compaction DEFAULT_COMPACTION_CONFIG.
|
|
@@ -44,6 +46,12 @@ export interface CompactionAfterOptions {
|
|
|
44
46
|
readonly event: CompactionEvent;
|
|
45
47
|
/** Number of session memories that survived compaction. */
|
|
46
48
|
readonly pinnedMemoryCount: number;
|
|
49
|
+
/**
|
|
50
|
+
* Out-of-band quality-score grade for this run (W5.4/B28). Omitted or null
|
|
51
|
+
* when no score was computed (e.g. the small-window compaction path, which
|
|
52
|
+
* has no CompactionEvent to key a score by) — no line is rendered then.
|
|
53
|
+
*/
|
|
54
|
+
readonly qualityScore?: CompactionQualityScore | null | undefined;
|
|
47
55
|
}
|
|
48
56
|
|
|
49
57
|
/**
|
|
@@ -82,7 +90,7 @@ export function buildCompactionPreview(opts: CompactionPreviewOptions): string {
|
|
|
82
90
|
* after token counts. The trigger field controls wording.
|
|
83
91
|
*/
|
|
84
92
|
export function buildCompactionAfterNotice(opts: CompactionAfterOptions): string {
|
|
85
|
-
const { event, pinnedMemoryCount } = opts;
|
|
93
|
+
const { event, pinnedMemoryCount, qualityScore } = opts;
|
|
86
94
|
const {
|
|
87
95
|
messagesBeforeCompaction,
|
|
88
96
|
messagesAfterCompaction,
|
|
@@ -102,11 +110,14 @@ export function buildCompactionAfterNotice(opts: CompactionAfterOptions): string
|
|
|
102
110
|
|
|
103
111
|
const triggerStr = trigger === 'auto' ? 'Auto-compact complete' : 'Compact complete';
|
|
104
112
|
|
|
113
|
+
const qualityStr = qualityScore ? `\n ${formatQualityScoreLine(qualityScore)}` : '';
|
|
114
|
+
|
|
105
115
|
return (
|
|
106
116
|
`[Context] ${triggerStr}: ${messagesBeforeCompaction} → ${messagesAfterCompaction} messages,` +
|
|
107
117
|
` ~${fmtN(tokensBeforeEstimate)} → ~${fmtN(tokensAfterEstimate)} tokens` +
|
|
108
118
|
` (saved ~${fmtN(savings)}, ${savingsPct}%).` +
|
|
109
|
-
(pinStr ? ` ${pinStr.trim()}` : '')
|
|
119
|
+
(pinStr ? ` ${pinStr.trim()}` : '') +
|
|
120
|
+
qualityStr
|
|
110
121
|
);
|
|
111
122
|
}
|
|
112
123
|
|