@pellux/goodvibes-tui 0.28.0 → 0.29.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 +101 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/input/commands/cost-runtime.ts +49 -0
- package/src/input/commands/operator-runtime.ts +5 -1
- package/src/input/commands.ts +2 -0
- package/src/input/handler-feed-routes.ts +12 -35
- package/src/input/handler-feed.ts +4 -8
- package/src/input/handler-shortcuts.ts +51 -0
- package/src/input/handler.ts +43 -6
- package/src/input/keybindings.ts +48 -8
- package/src/input/panel-integration-actions.ts +107 -1
- package/src/main.ts +5 -1
- package/src/panels/agent-inspector-panel.ts +125 -36
- package/src/panels/agent-inspector-shared.ts +144 -0
- package/src/panels/approval-panel.ts +67 -16
- package/src/panels/automation-control-panel.ts +368 -124
- package/src/panels/builtin/agent.ts +28 -37
- package/src/panels/builtin/development.ts +40 -32
- package/src/panels/builtin/knowledge.ts +15 -6
- package/src/panels/builtin/operations.ts +178 -117
- package/src/panels/builtin/session.ts +35 -9
- package/src/panels/builtin/shared.ts +98 -6
- package/src/panels/cockpit-panel.ts +232 -73
- package/src/panels/communication-panel.ts +58 -20
- package/src/panels/confirm-state.ts +8 -1
- package/src/panels/control-plane-panel.ts +351 -118
- package/src/panels/cost-tracker-panel.ts +165 -7
- package/src/panels/debug-panel.ts +312 -159
- package/src/panels/diff-panel.ts +203 -57
- package/src/panels/docs-panel.ts +152 -66
- package/src/panels/eval-panel.ts +220 -42
- package/src/panels/file-explorer-panel.ts +202 -122
- package/src/panels/file-preview-panel.ts +132 -57
- package/src/panels/git-panel.ts +309 -128
- package/src/panels/hooks-panel.ts +150 -22
- package/src/panels/incident-review-panel.ts +223 -35
- package/src/panels/index.ts +0 -4
- package/src/panels/intelligence-panel.ts +212 -86
- package/src/panels/knowledge-graph-panel.ts +461 -101
- package/src/panels/local-auth-panel.ts +240 -28
- package/src/panels/marketplace-panel.ts +193 -26
- package/src/panels/memory-panel.ts +78 -77
- package/src/panels/ops-control-panel.ts +146 -29
- package/src/panels/ops-strategy-panel.ts +72 -4
- package/src/panels/orchestration-panel.ts +231 -69
- package/src/panels/panel-list-panel.ts +154 -131
- package/src/panels/panel-manager.ts +97 -9
- package/src/panels/plan-dashboard-panel.ts +333 -17
- package/src/panels/plugins-panel.ts +184 -29
- package/src/panels/policy-panel.ts +210 -38
- package/src/panels/polish-core.ts +7 -2
- package/src/panels/polish.ts +23 -4
- package/src/panels/project-planning-answer-actions.ts +134 -0
- package/src/panels/project-planning-panel.ts +62 -113
- package/src/panels/provider-health-panel.ts +434 -518
- package/src/panels/provider-health-routes.ts +203 -0
- package/src/panels/provider-health-tracker.ts +194 -6
- package/src/panels/provider-health-views.ts +560 -0
- package/src/panels/qr-panel.ts +116 -30
- package/src/panels/remote-panel.ts +114 -36
- package/src/panels/routes-panel.ts +63 -22
- package/src/panels/sandbox-panel.ts +174 -42
- package/src/panels/scrollable-list-panel.ts +19 -135
- package/src/panels/security-panel.ts +133 -33
- package/src/panels/services-panel.ts +116 -64
- package/src/panels/session-browser-panel.ts +73 -2
- package/src/panels/session-maintenance.ts +4 -122
- package/src/panels/settings-sync-panel.ts +335 -72
- package/src/panels/skills-panel.ts +157 -89
- package/src/panels/subscription-panel.ts +86 -33
- package/src/panels/symbol-outline-panel.ts +248 -108
- package/src/panels/system-messages-panel.ts +114 -13
- package/src/panels/tasks-panel.ts +326 -139
- package/src/panels/thinking-panel.ts +43 -10
- package/src/panels/token-budget-panel.ts +194 -18
- package/src/panels/tool-inspector-panel.ts +144 -34
- package/src/panels/types.ts +40 -4
- package/src/panels/work-plan-panel.ts +280 -17
- package/src/panels/worktree-panel.ts +175 -42
- package/src/panels/wrfc-panel.ts +116 -24
- package/src/renderer/conversation-overlays.ts +25 -11
- package/src/renderer/footer-tips.ts +41 -0
- package/src/renderer/fullscreen-primitives.ts +22 -16
- package/src/renderer/help-overlay.ts +91 -14
- package/src/renderer/hint-grammar.ts +52 -0
- package/src/renderer/layout.ts +7 -7
- package/src/renderer/modal-factory.ts +23 -15
- package/src/renderer/model-picker-overlay.ts +21 -7
- package/src/renderer/overlay-box.ts +16 -10
- package/src/renderer/process-indicator.ts +3 -1
- package/src/renderer/progress.ts +5 -4
- package/src/renderer/search-overlay.ts +27 -6
- package/src/renderer/session-picker-modal.ts +6 -4
- package/src/renderer/settings-modal.ts +70 -10
- package/src/renderer/shell-surface.ts +41 -15
- package/src/renderer/status-glyphs.ts +11 -9
- package/src/renderer/theme.ts +60 -3
- package/src/renderer/ui-factory.ts +41 -30
- package/src/renderer/ui-primitives.ts +23 -2
- package/src/runtime/bootstrap-shell.ts +35 -18
- package/src/runtime/diagnostics/panels/index.ts +0 -3
- package/src/shell/ui-openers.ts +14 -22
- package/src/utils/format-duration.ts +2 -2
- package/src/utils/splash-lines.ts +44 -3
- package/src/version.ts +1 -1
- package/src/work-plans/work-plan-store.ts +13 -0
- package/src/panels/agent-logs-panel.ts +0 -635
- package/src/panels/agent-logs-shared.ts +0 -129
- package/src/panels/context-visualizer-panel.ts +0 -238
- package/src/panels/forensics-panel.ts +0 -378
- package/src/panels/provider-account-snapshot.ts +0 -259
- package/src/panels/provider-accounts-panel.ts +0 -255
- package/src/panels/provider-stats-panel.ts +0 -391
- package/src/panels/schedule-panel.ts +0 -365
- package/src/panels/watchers-panel.ts +0 -213
- package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
- package/src/runtime/diagnostics/panels/policy.ts +0 -177
|
@@ -28,17 +28,33 @@ const C = extendPalette(DEFAULT_PANEL_PALETTE, {
|
|
|
28
28
|
});
|
|
29
29
|
|
|
30
30
|
interface ReasoningBlock {
|
|
31
|
-
|
|
31
|
+
// Real turn id from the TURN_*/STREAM_* event envelope — joins this block
|
|
32
|
+
// to the same turn's rows in TasksPanel and the tool inspector, which key
|
|
33
|
+
// off the identical turnId string.
|
|
34
|
+
turnId: string;
|
|
35
|
+
// Distributed-tracing correlation id (EventEnvelope.traceId), when the
|
|
36
|
+
// originating event carried one — same field RuntimeTask.correlationId
|
|
37
|
+
// joins against.
|
|
38
|
+
correlationId?: string;
|
|
32
39
|
content: string;
|
|
33
40
|
active: boolean; // true = currently streaming
|
|
34
41
|
collapsed: boolean;
|
|
42
|
+
startedAt: number;
|
|
43
|
+
updatedAt: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function fmtClock(ts: number): string {
|
|
47
|
+
const d = new Date(ts);
|
|
48
|
+
const h = String(d.getHours()).padStart(2, '0');
|
|
49
|
+
const m = String(d.getMinutes()).padStart(2, '0');
|
|
50
|
+
const s = String(d.getSeconds()).padStart(2, '0');
|
|
51
|
+
return `${h}:${m}:${s}`;
|
|
35
52
|
}
|
|
36
53
|
|
|
37
54
|
type FlatRow = { kind: 'header'; blockIndex: number; text: string } | { kind: 'content'; text: string };
|
|
38
55
|
|
|
39
56
|
export class ThinkingPanel extends BasePanel {
|
|
40
57
|
private blocks: ReasoningBlock[] = [];
|
|
41
|
-
private nextTurnId = 1;
|
|
42
58
|
private unsubs: Array<() => void> = [];
|
|
43
59
|
private cursorIndex = 0;
|
|
44
60
|
private scrollOffset = 0;
|
|
@@ -149,7 +165,7 @@ export class ThinkingPanel extends BasePanel {
|
|
|
149
165
|
? [
|
|
150
166
|
buildPanelLine(width, [
|
|
151
167
|
[' Turn ', DEFAULT_PANEL_PALETTE.label],
|
|
152
|
-
[
|
|
168
|
+
[selectedBlock.turnId, DEFAULT_PANEL_PALETTE.value],
|
|
153
169
|
[' State ', DEFAULT_PANEL_PALETTE.label],
|
|
154
170
|
[selectedBlock.active ? 'streaming' : 'complete', selectedBlock.active ? DEFAULT_PANEL_PALETTE.warn : DEFAULT_PANEL_PALETTE.good],
|
|
155
171
|
[' View ', DEFAULT_PANEL_PALETTE.label],
|
|
@@ -157,6 +173,14 @@ export class ThinkingPanel extends BasePanel {
|
|
|
157
173
|
[' Chars ', DEFAULT_PANEL_PALETTE.label],
|
|
158
174
|
[String(selectedBlock.content.length), DEFAULT_PANEL_PALETTE.dim],
|
|
159
175
|
]),
|
|
176
|
+
buildPanelLine(width, [
|
|
177
|
+
[' Correlation ', DEFAULT_PANEL_PALETTE.label],
|
|
178
|
+
[selectedBlock.correlationId ?? 'n/a', DEFAULT_PANEL_PALETTE.dim],
|
|
179
|
+
[' Started ', DEFAULT_PANEL_PALETTE.label],
|
|
180
|
+
[fmtClock(selectedBlock.startedAt), DEFAULT_PANEL_PALETTE.dim],
|
|
181
|
+
[' Updated ', DEFAULT_PANEL_PALETTE.label],
|
|
182
|
+
[fmtClock(selectedBlock.updatedAt), DEFAULT_PANEL_PALETTE.dim],
|
|
183
|
+
]),
|
|
160
184
|
]
|
|
161
185
|
: [buildPanelLine(width, [[' No block selected', DEFAULT_PANEL_PALETTE.dim]])],
|
|
162
186
|
};
|
|
@@ -210,7 +234,7 @@ export class ThinkingPanel extends BasePanel {
|
|
|
210
234
|
const rows: FlatRow[] = [];
|
|
211
235
|
for (let i = 0; i < this.blocks.length; i++) {
|
|
212
236
|
const block = this.blocks[i]!;
|
|
213
|
-
const turnLabel = `Turn ${block.turnId}${block.active ? ' (streaming)' : ''}`;
|
|
237
|
+
const turnLabel = `Turn ${block.turnId} ${fmtClock(block.startedAt)}${block.active ? ' (streaming)' : ''}`;
|
|
214
238
|
rows.push({ kind: 'header', blockIndex: i, text: turnLabel });
|
|
215
239
|
if (!block.collapsed) {
|
|
216
240
|
const wrapped = wrapText(block.content || '(empty)', Math.max(1, width - 2));
|
|
@@ -250,12 +274,18 @@ export class ThinkingPanel extends BasePanel {
|
|
|
250
274
|
|
|
251
275
|
let currentBlock: ReasoningBlock | null = null;
|
|
252
276
|
|
|
253
|
-
|
|
277
|
+
// onEnvelope (not on) so real turnId/traceId/timestamp — the same fields
|
|
278
|
+
// TasksPanel and the tool inspector join their own rows on — stamp each
|
|
279
|
+
// block instead of a locally-incrementing counter.
|
|
280
|
+
this.unsubs.push(this.turnEvents.onEnvelope('STREAM_START', (envelope) => {
|
|
254
281
|
const block: ReasoningBlock = {
|
|
255
|
-
turnId:
|
|
282
|
+
turnId: envelope.turnId ?? envelope.payload.turnId,
|
|
283
|
+
correlationId: envelope.traceId,
|
|
256
284
|
content: '',
|
|
257
285
|
active: true,
|
|
258
286
|
collapsed: false,
|
|
287
|
+
startedAt: envelope.ts,
|
|
288
|
+
updatedAt: envelope.ts,
|
|
259
289
|
};
|
|
260
290
|
// Cap block count to prevent unbounded growth
|
|
261
291
|
if (this.blocks.length >= ThinkingPanel.MAX_BLOCKS) {
|
|
@@ -267,26 +297,29 @@ export class ThinkingPanel extends BasePanel {
|
|
|
267
297
|
this.markDirty();
|
|
268
298
|
}));
|
|
269
299
|
|
|
270
|
-
this.unsubs.push(this.turnEvents.
|
|
271
|
-
const reasoning =
|
|
300
|
+
this.unsubs.push(this.turnEvents.onEnvelope('STREAM_DELTA', (envelope) => {
|
|
301
|
+
const reasoning = envelope.payload.reasoning;
|
|
272
302
|
if (reasoning && currentBlock) {
|
|
273
303
|
currentBlock.content += reasoning;
|
|
304
|
+
currentBlock.updatedAt = envelope.ts;
|
|
274
305
|
this.autoScroll = true;
|
|
275
306
|
this.markDirty();
|
|
276
307
|
}
|
|
277
308
|
}));
|
|
278
309
|
|
|
279
|
-
this.unsubs.push(this.turnEvents.
|
|
310
|
+
this.unsubs.push(this.turnEvents.onEnvelope('STREAM_END', (envelope) => {
|
|
280
311
|
if (currentBlock) {
|
|
281
312
|
currentBlock.active = false;
|
|
313
|
+
currentBlock.updatedAt = envelope.ts;
|
|
282
314
|
currentBlock = null;
|
|
283
315
|
this.markDirty();
|
|
284
316
|
}
|
|
285
317
|
}));
|
|
286
318
|
|
|
287
|
-
this.unsubs.push(this.turnEvents.
|
|
319
|
+
this.unsubs.push(this.turnEvents.onEnvelope('TURN_COMPLETED', (envelope) => {
|
|
288
320
|
if (currentBlock) {
|
|
289
321
|
currentBlock.active = false;
|
|
322
|
+
currentBlock.updatedAt = envelope.ts;
|
|
290
323
|
currentBlock = null;
|
|
291
324
|
this.markDirty();
|
|
292
325
|
}
|
|
@@ -3,13 +3,19 @@ import { BasePanel } from './base-panel.ts';
|
|
|
3
3
|
import { createEmptyLine, createStyledCell, type Line } from '../types/grid.ts';
|
|
4
4
|
import type { Orchestrator } from '../core/orchestrator';
|
|
5
5
|
import { evaluateSessionMaintenance } from '@/runtime/index.ts';
|
|
6
|
+
import type { TurnEvent } from '@/runtime/index.ts';
|
|
7
|
+
import type { UiEventFeed } from '../runtime/ui-events.ts';
|
|
6
8
|
import type { UiReadModel, UiSessionSnapshot } from '../runtime/ui-read-models.ts';
|
|
7
9
|
import type { SessionMemoryQuery } from '../runtime/ui-service-queries.ts';
|
|
10
|
+
import { calcSessionCost } from '../export/cost-utils.ts';
|
|
11
|
+
import { type ConfirmState, handleConfirmInput, renderConfirmLines } from './confirm-state.ts';
|
|
12
|
+
import type { PanelIntegrationContext } from './types.ts';
|
|
8
13
|
import {
|
|
9
14
|
buildEmptyState,
|
|
10
15
|
buildGuidanceLine,
|
|
11
16
|
buildKeyboardHints,
|
|
12
17
|
buildMeterLine,
|
|
18
|
+
buildStatusPill,
|
|
13
19
|
buildStyledPanelLine,
|
|
14
20
|
buildTable,
|
|
15
21
|
buildPanelWorkspace,
|
|
@@ -54,6 +60,9 @@ const WARN_RED = 0.90;
|
|
|
54
60
|
// Maximum turns to keep in per-turn history
|
|
55
61
|
const MAX_TURN_HISTORY = 10;
|
|
56
62
|
|
|
63
|
+
// Rows scrolled per pageup/pagedown key press through the Recent Turns history.
|
|
64
|
+
const PAGE_SCROLL_ROWS = 5;
|
|
65
|
+
|
|
57
66
|
// ---------------------------------------------------------------------------
|
|
58
67
|
// Helpers
|
|
59
68
|
// ---------------------------------------------------------------------------
|
|
@@ -74,10 +83,15 @@ function fmtTok(n: number): string {
|
|
|
74
83
|
*
|
|
75
84
|
* Displays:
|
|
76
85
|
* - Stacked bar: input / output / cache-read / cache-write
|
|
77
|
-
* - Context window fill percentage with progress bar
|
|
78
|
-
* - Per-turn token usage (last 10 turns)
|
|
79
|
-
* - Cumulative session totals
|
|
86
|
+
* - Context window fill percentage with progress bar + pressure pill
|
|
87
|
+
* - Per-turn token usage (last 10 turns), scrollable
|
|
88
|
+
* - Cumulative session totals + inline cost estimate
|
|
80
89
|
* - Warning threshold indicators (yellow at 70%, red at 90%)
|
|
90
|
+
*
|
|
91
|
+
* Absorbs the former ContextVisualizerPanel ('context'): the pressure pill,
|
|
92
|
+
* TURN_SUBMITTED/TURN_COMPLETED-driven refresh, and the compact-now action
|
|
93
|
+
* all live here now. `PanelManager` aliases the retired `context` id to
|
|
94
|
+
* `tokens` so old `/panel open context` calls keep working.
|
|
81
95
|
*/
|
|
82
96
|
export class TokenBudgetPanel extends BasePanel {
|
|
83
97
|
/** Snapshot of cumulative usage from the Orchestrator after each turn. */
|
|
@@ -95,19 +109,38 @@ export class TokenBudgetPanel extends BasePanel {
|
|
|
95
109
|
private orchestrator: Orchestrator | null = null;
|
|
96
110
|
private getContextWindow: (() => number) | null = null;
|
|
97
111
|
private sessionReadModel: UiReadModel<UiSessionSnapshot> | null = null;
|
|
112
|
+
/** Resolver for the active model id, for inline cost display. */
|
|
113
|
+
private getCurrentModelId: (() => string) | null = null;
|
|
98
114
|
private readonly sessionMemoryStore: SessionMemoryQuery;
|
|
99
115
|
private readonly configManager: Pick<ConfigManager, 'get'>;
|
|
100
116
|
private readonly requestRender: () => void;
|
|
101
117
|
|
|
118
|
+
/** Turn-event bus subscriptions — absorbed from ContextVisualizerPanel. */
|
|
119
|
+
private unsubs: Array<() => void> = [];
|
|
120
|
+
|
|
121
|
+
/** Manual scroll cursor into turnHistory; -1 = auto-follow the latest turn. */
|
|
122
|
+
private turnScrollIndex = -1;
|
|
123
|
+
|
|
124
|
+
/** Pending confirm dialog (currently only used for the compact-now action). */
|
|
125
|
+
private confirm: ConfirmState<'compact'> | null = null;
|
|
126
|
+
/**
|
|
127
|
+
* Set when a confirm resolves to 'confirmed'; consumed by
|
|
128
|
+
* handlePanelIntegrationAction on the very next dispatch of that same key
|
|
129
|
+
* so it can reach the ctx.executeCommand bridge (handleInput has no ctx).
|
|
130
|
+
*/
|
|
131
|
+
private compactConfirmedPending = false;
|
|
132
|
+
|
|
102
133
|
constructor(
|
|
103
134
|
sessionMemoryStore: SessionMemoryQuery,
|
|
104
135
|
configManager: Pick<ConfigManager, 'get'>,
|
|
105
136
|
requestRender: () => void = () => {},
|
|
137
|
+
turnEvents?: UiEventFeed<TurnEvent>,
|
|
106
138
|
) {
|
|
107
|
-
super('tokens', 'Tokens', '
|
|
139
|
+
super('tokens', 'Tokens', '▢', 'providers');
|
|
108
140
|
this.sessionMemoryStore = sessionMemoryStore;
|
|
109
141
|
this.configManager = configManager;
|
|
110
142
|
this.requestRender = requestRender;
|
|
143
|
+
if (turnEvents) this._attachBus(turnEvents);
|
|
111
144
|
}
|
|
112
145
|
|
|
113
146
|
// ---------------------------------------------------------------------------
|
|
@@ -117,17 +150,21 @@ export class TokenBudgetPanel extends BasePanel {
|
|
|
117
150
|
/**
|
|
118
151
|
* Wire the panel to live data sources. Call once after construction.
|
|
119
152
|
*
|
|
120
|
-
* @param orchestrator
|
|
121
|
-
* @param getCtxWindow
|
|
153
|
+
* @param orchestrator The main Orchestrator instance (for usage + lastInputTokens).
|
|
154
|
+
* @param getCtxWindow Callback returning the current model's contextWindow value.
|
|
155
|
+
* @param sessionReadModel Session read model (for the Maintenance status line).
|
|
156
|
+
* @param getCurrentModelId Callback returning the active model id, for the inline cost line.
|
|
122
157
|
*/
|
|
123
158
|
wire(
|
|
124
159
|
orchestrator: Orchestrator,
|
|
125
160
|
getCtxWindow: () => number,
|
|
126
161
|
sessionReadModel?: UiReadModel<UiSessionSnapshot>,
|
|
162
|
+
getCurrentModelId?: () => string,
|
|
127
163
|
): void {
|
|
128
164
|
this.orchestrator = orchestrator;
|
|
129
165
|
this.getContextWindow = getCtxWindow;
|
|
130
166
|
this.sessionReadModel = sessionReadModel ?? null;
|
|
167
|
+
this.getCurrentModelId = getCurrentModelId ?? null;
|
|
131
168
|
}
|
|
132
169
|
|
|
133
170
|
/**
|
|
@@ -152,10 +189,27 @@ export class TokenBudgetPanel extends BasePanel {
|
|
|
152
189
|
if (this.turnHistory.length > MAX_TURN_HISTORY) {
|
|
153
190
|
this.turnHistory.shift();
|
|
154
191
|
}
|
|
192
|
+
// A freshly completed turn always pulls the view back to the latest entry.
|
|
193
|
+
this.turnScrollIndex = -1;
|
|
155
194
|
|
|
156
195
|
this._refreshAndRender();
|
|
157
196
|
}
|
|
158
197
|
|
|
198
|
+
// ---------------------------------------------------------------------------
|
|
199
|
+
// Turn-event bus (absorbed from ContextVisualizerPanel)
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
|
|
202
|
+
private _attachBus(turnEvents: UiEventFeed<TurnEvent>): void {
|
|
203
|
+
if (this.unsubs.length > 0) return;
|
|
204
|
+
this.unsubs.push(turnEvents.on('TURN_COMPLETED', () => this.recordTurn()));
|
|
205
|
+
this.unsubs.push(turnEvents.on('TURN_SUBMITTED', () => this._refreshAndRender()));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private _detachBus(): void {
|
|
209
|
+
for (const unsub of this.unsubs) unsub();
|
|
210
|
+
this.unsubs = [];
|
|
211
|
+
}
|
|
212
|
+
|
|
159
213
|
// ---------------------------------------------------------------------------
|
|
160
214
|
// Lifecycle
|
|
161
215
|
// ---------------------------------------------------------------------------
|
|
@@ -170,6 +224,7 @@ export class TokenBudgetPanel extends BasePanel {
|
|
|
170
224
|
|
|
171
225
|
override onDeactivate(): void {
|
|
172
226
|
super.onDeactivate();
|
|
227
|
+
this.confirm = null;
|
|
173
228
|
// Stop the off-screen poll so a backgrounded Tokens tab does not keep
|
|
174
229
|
// refreshing (and, once requestRender is wired, force a repaint) every 2 s.
|
|
175
230
|
if (this.refreshTimer !== null) {
|
|
@@ -183,6 +238,78 @@ export class TokenBudgetPanel extends BasePanel {
|
|
|
183
238
|
clearInterval(this.refreshTimer);
|
|
184
239
|
this.refreshTimer = null;
|
|
185
240
|
}
|
|
241
|
+
this._detachBus();
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ---------------------------------------------------------------------------
|
|
245
|
+
// Input
|
|
246
|
+
// ---------------------------------------------------------------------------
|
|
247
|
+
|
|
248
|
+
private _pressureRatio(): number {
|
|
249
|
+
return this.contextWindow > 0 ? this.lastInputTokens / this.contextWindow : 0;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private _pressureElevated(): boolean {
|
|
253
|
+
return this.contextWindow > 0 && this._pressureRatio() >= WARN_YELLOW;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
private _scrollTurns(delta: number): void {
|
|
257
|
+
const current = this.turnScrollIndex === -1 ? this.turnHistory.length : this.turnScrollIndex;
|
|
258
|
+
const next = current + delta;
|
|
259
|
+
this.turnScrollIndex = Math.max(0, Math.min(this.turnHistory.length, next));
|
|
260
|
+
this.markDirty();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
handleInput(key: string): boolean {
|
|
264
|
+
// Confirmation dialog — use the shared handleConfirmInput for y/n/Esc UX
|
|
265
|
+
const confirmResult = handleConfirmInput(this.confirm, key);
|
|
266
|
+
if (confirmResult === 'confirmed') {
|
|
267
|
+
this.confirm = null;
|
|
268
|
+
this.compactConfirmedPending = true;
|
|
269
|
+
this.markDirty();
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
272
|
+
if (confirmResult === 'cancelled') {
|
|
273
|
+
this.confirm = null;
|
|
274
|
+
this.markDirty();
|
|
275
|
+
return true;
|
|
276
|
+
}
|
|
277
|
+
if (confirmResult === 'absorbed') return true;
|
|
278
|
+
|
|
279
|
+
if (key === 'C' && this._pressureElevated()) {
|
|
280
|
+
this.confirm = { subject: 'compact', label: 'Compact context now', verb: 'Compact' };
|
|
281
|
+
this.markDirty();
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (this.turnHistory.length > 0) {
|
|
286
|
+
switch (key) {
|
|
287
|
+
case 'up': this._scrollTurns(-1); return true;
|
|
288
|
+
case 'down': this._scrollTurns(1); return true;
|
|
289
|
+
case 'pageup': this._scrollTurns(-PAGE_SCROLL_ROWS); return true;
|
|
290
|
+
case 'pagedown': this._scrollTurns(PAGE_SCROLL_ROWS); return true;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (key === 'r') {
|
|
295
|
+
this._refreshAndRender();
|
|
296
|
+
return true;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Executes the compact-now action once the confirm dialog resolves. This
|
|
304
|
+
* lives in the integration hook (rather than inline in handleInput)
|
|
305
|
+
* because only this hook carries the `ctx.executeCommand` bridge into the
|
|
306
|
+
* real `/compact` command.
|
|
307
|
+
*/
|
|
308
|
+
handlePanelIntegrationAction(_key: string, ctx: PanelIntegrationContext): boolean {
|
|
309
|
+
if (!this.compactConfirmedPending) return false;
|
|
310
|
+
this.compactConfirmedPending = false;
|
|
311
|
+
void ctx.executeCommand?.('compact', []);
|
|
312
|
+
return true;
|
|
186
313
|
}
|
|
187
314
|
|
|
188
315
|
// ---------------------------------------------------------------------------
|
|
@@ -219,6 +346,17 @@ export class TokenBudgetPanel extends BasePanel {
|
|
|
219
346
|
|
|
220
347
|
override render(width: number, height: number): Line[] {
|
|
221
348
|
return this.trackedRender(() => {
|
|
349
|
+
if (this.confirm) {
|
|
350
|
+
return buildPanelWorkspace(width, height, {
|
|
351
|
+
title: ' Token Budget',
|
|
352
|
+
sections: [{
|
|
353
|
+
title: 'Confirmation',
|
|
354
|
+
lines: renderConfirmLines(width, this.confirm),
|
|
355
|
+
}],
|
|
356
|
+
palette: DEFAULT_PANEL_PALETTE,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
|
|
222
360
|
const sections: PanelWorkspaceSection[] = [];
|
|
223
361
|
|
|
224
362
|
if (this.contextWindow > 0) {
|
|
@@ -236,22 +374,33 @@ export class TokenBudgetPanel extends BasePanel {
|
|
|
236
374
|
],
|
|
237
375
|
});
|
|
238
376
|
|
|
377
|
+
const elevated = this._pressureElevated();
|
|
378
|
+
const footerHints = elevated
|
|
379
|
+
? [
|
|
380
|
+
{ keys: 'C', label: 'compact now' },
|
|
381
|
+
{ keys: 'up/down', label: 'scroll turns' },
|
|
382
|
+
{ keys: 'r', label: 'refresh' },
|
|
383
|
+
]
|
|
384
|
+
: [
|
|
385
|
+
{ keys: 'up/down', label: 'scroll turns' },
|
|
386
|
+
{ keys: 'r', label: 'refresh' },
|
|
387
|
+
{ keys: '/compact', label: 'compress context' },
|
|
388
|
+
];
|
|
389
|
+
|
|
239
390
|
if (this.turnHistory.length > 0) {
|
|
240
391
|
const priorSections = [...sections];
|
|
392
|
+
const scrollIndex = this.turnScrollIndex === -1 ? this.turnHistory.length : this.turnScrollIndex;
|
|
241
393
|
const turnsSection = resolveScrollablePanelSection(width, height, {
|
|
242
394
|
intro: 'Live context pressure, session token composition, cache usage, and recent turn deltas.',
|
|
243
395
|
footerLines: [
|
|
244
|
-
buildKeyboardHints(width,
|
|
245
|
-
{ keys: '/compact', label: 'compress context' },
|
|
246
|
-
{ keys: '/clear', label: 'reset session' },
|
|
247
|
-
], DEFAULT_PANEL_PALETTE),
|
|
396
|
+
buildKeyboardHints(width, footerHints, DEFAULT_PANEL_PALETTE),
|
|
248
397
|
],
|
|
249
398
|
palette: DEFAULT_PANEL_PALETTE,
|
|
250
399
|
beforeSections: priorSections,
|
|
251
400
|
section: {
|
|
252
401
|
title: 'Recent Turns',
|
|
253
402
|
scrollableLines: this.renderTurnHistory(width, this.turnHistory.length + 1),
|
|
254
|
-
scrollOffset:
|
|
403
|
+
scrollOffset: scrollIndex,
|
|
255
404
|
minRows: 6,
|
|
256
405
|
},
|
|
257
406
|
afterSections: [{
|
|
@@ -283,10 +432,7 @@ export class TokenBudgetPanel extends BasePanel {
|
|
|
283
432
|
intro: 'Live context pressure, session token composition, cache usage, and recent turn deltas.',
|
|
284
433
|
sections,
|
|
285
434
|
footerLines: [
|
|
286
|
-
buildKeyboardHints(width,
|
|
287
|
-
{ keys: '/compact', label: 'compress context' },
|
|
288
|
-
{ keys: '/clear', label: 'reset session' },
|
|
289
|
-
], DEFAULT_PANEL_PALETTE),
|
|
435
|
+
buildKeyboardHints(width, footerHints, DEFAULT_PANEL_PALETTE),
|
|
290
436
|
],
|
|
291
437
|
palette: DEFAULT_PANEL_PALETTE,
|
|
292
438
|
});
|
|
@@ -313,7 +459,17 @@ export class TokenBudgetPanel extends BasePanel {
|
|
|
313
459
|
}
|
|
314
460
|
|
|
315
461
|
if (status.guidanceMode !== 'off' && status.nextSteps.length > 0) {
|
|
316
|
-
|
|
462
|
+
const nextStep = status.nextSteps[0]!;
|
|
463
|
+
// WO-160: when the recommended next step is /compact and C is armed
|
|
464
|
+
// (see handleInput's elevated-only C branch and the footer hint),
|
|
465
|
+
// advertise the key instead of the command — pressing C already
|
|
466
|
+
// dispatches it for real, so printing '/compact' here was a redundant
|
|
467
|
+
// action substitute.
|
|
468
|
+
if (nextStep === '/compact' && this._pressureElevated()) {
|
|
469
|
+
lines.push(buildGuidanceLine(width, 'C', 'compact context now — the recommended next maintenance action', DEFAULT_PANEL_PALETTE));
|
|
470
|
+
} else {
|
|
471
|
+
lines.push(buildGuidanceLine(width, nextStep, 'open the next maintenance action directly', DEFAULT_PANEL_PALETTE));
|
|
472
|
+
}
|
|
317
473
|
}
|
|
318
474
|
|
|
319
475
|
return lines;
|
|
@@ -388,7 +544,7 @@ export class TokenBudgetPanel extends BasePanel {
|
|
|
388
544
|
return lines;
|
|
389
545
|
}
|
|
390
546
|
|
|
391
|
-
/** Renders a full-width progress bar for context window fill. */
|
|
547
|
+
/** Renders a full-width progress bar for context window fill, plus a pressure pill (absorbed from ContextVisualizerPanel). */
|
|
392
548
|
private renderContextBar(width: number): Line[] {
|
|
393
549
|
const lines: Line[] = [];
|
|
394
550
|
const pct = this.contextWindow > 0
|
|
@@ -419,10 +575,20 @@ export class TokenBudgetPanel extends BasePanel {
|
|
|
419
575
|
{ filled: barColor, empty: C.barBg, label: C.label },
|
|
420
576
|
{ prefix: label, suffix },
|
|
421
577
|
));
|
|
578
|
+
|
|
579
|
+
// Pressure pill — absorbed from ContextVisualizerPanel's headline indicator.
|
|
580
|
+
const overLimit = this.contextWindow > 0 && this.lastInputTokens > this.contextWindow;
|
|
581
|
+
const pressureState = overLimit || pct >= WARN_RED ? 'bad' : pct >= WARN_YELLOW ? 'warn' : 'good';
|
|
582
|
+
const pressureLabel = overLimit ? 'over limit' : pct >= WARN_RED ? 'critical' : pct >= WARN_YELLOW ? 'elevated' : 'healthy';
|
|
583
|
+
lines.push(buildStyledPanelLine(width, [
|
|
584
|
+
{ text: ' ', fg: C.dim },
|
|
585
|
+
...buildStatusPill(pressureState, pressureLabel),
|
|
586
|
+
]));
|
|
587
|
+
|
|
422
588
|
return lines;
|
|
423
589
|
}
|
|
424
590
|
|
|
425
|
-
/** Session cumulative totals with color-coded labels. */
|
|
591
|
+
/** Session cumulative totals with color-coded labels, plus an inline cost estimate. */
|
|
426
592
|
private renderTotals(width: number): Line[] {
|
|
427
593
|
const lines: Line[] = [];
|
|
428
594
|
lines.push(this.paintTextLine(' Session Totals:', width, C.label));
|
|
@@ -446,6 +612,16 @@ export class TokenBudgetPanel extends BasePanel {
|
|
|
446
612
|
]));
|
|
447
613
|
}
|
|
448
614
|
|
|
615
|
+
// Inline cost estimate — absorbed pricing wiring (cost-utils.ts), shown
|
|
616
|
+
// whenever the active model id is known.
|
|
617
|
+
if (this.getCurrentModelId) {
|
|
618
|
+
const cost = calcSessionCost(u.input, u.output, u.cacheRead, u.cacheWrite, this.getCurrentModelId());
|
|
619
|
+
lines.push(buildStyledPanelLine(width, [
|
|
620
|
+
{ text: ' Cost: ', fg: C.label },
|
|
621
|
+
{ text: `$${cost.toFixed(4)}`, fg: C.value, bold: true },
|
|
622
|
+
]));
|
|
623
|
+
}
|
|
624
|
+
|
|
449
625
|
return lines;
|
|
450
626
|
}
|
|
451
627
|
|