@pellux/goodvibes-tui 0.27.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 +124 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/core/context-usage.ts +53 -0
- package/src/core/conversation.ts +4 -6
- package/src/core/session-recovery.ts +2 -0
- package/src/core/turn-event-wiring.ts +1 -0
- package/src/input/commands/cost-runtime.ts +49 -0
- package/src/input/commands/eval.ts +4 -3
- package/src/input/commands/operator-runtime.ts +5 -1
- package/src/input/commands.ts +2 -0
- package/src/input/handler-feed-routes.ts +60 -26
- package/src/input/handler-feed.ts +17 -8
- package/src/input/handler-picker-routes.ts +18 -13
- package/src/input/handler-shortcuts.ts +51 -0
- package/src/input/handler-ui-state.ts +4 -0
- package/src/input/handler.ts +43 -6
- package/src/input/keybindings.ts +53 -8
- package/src/input/model-picker.ts +5 -0
- package/src/input/panel-integration-actions.ts +117 -1
- package/src/main.ts +5 -2
- package/src/panels/agent-inspector-panel.ts +139 -45
- package/src/panels/agent-inspector-shared.ts +147 -3
- package/src/panels/approval-panel.ts +204 -102
- package/src/panels/automation-control-panel.ts +370 -103
- package/src/panels/base-panel.ts +26 -2
- 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 +253 -75
- package/src/panels/cockpit-read-model.ts +2 -1
- package/src/panels/communication-panel.ts +159 -56
- package/src/panels/confirm-state.ts +8 -1
- package/src/panels/control-plane-panel.ts +354 -95
- package/src/panels/cost-tracker-panel.ts +243 -57
- package/src/panels/debug-panel.ts +347 -159
- package/src/panels/diff-panel.ts +257 -82
- package/src/panels/docs-panel.ts +166 -64
- package/src/panels/eval-panel.ts +336 -109
- package/src/panels/expandable-list-panel.ts +189 -0
- package/src/panels/file-explorer-panel.ts +238 -159
- package/src/panels/file-preview-panel.ts +141 -59
- package/src/panels/git-panel.ts +360 -191
- package/src/panels/hooks-panel.ts +181 -19
- package/src/panels/incident-review-panel.ts +252 -43
- package/src/panels/index.ts +0 -4
- package/src/panels/intelligence-panel.ts +310 -103
- package/src/panels/knowledge-graph-panel.ts +480 -58
- package/src/panels/local-auth-panel.ts +251 -33
- package/src/panels/marketplace-panel.ts +219 -33
- package/src/panels/memory-panel.ts +83 -81
- package/src/panels/ops-control-panel.ts +211 -32
- package/src/panels/ops-strategy-panel.ts +131 -45
- package/src/panels/orchestration-panel.ts +259 -67
- package/src/panels/panel-list-panel.ts +184 -136
- package/src/panels/panel-manager.ts +120 -13
- package/src/panels/plan-dashboard-panel.ts +507 -74
- package/src/panels/plugins-panel.ts +227 -34
- package/src/panels/policy-panel.ts +264 -55
- package/src/panels/polish-core.ts +162 -0
- package/src/panels/polish-tables.ts +258 -0
- package/src/panels/polish.ts +67 -149
- package/src/panels/project-planning-answer-actions.ts +134 -0
- package/src/panels/project-planning-panel.ts +84 -113
- package/src/panels/provider-health-panel.ts +438 -480
- 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 +136 -38
- package/src/panels/remote-panel.ts +123 -38
- package/src/panels/routes-panel.ts +87 -24
- package/src/panels/sandbox-panel.ts +232 -65
- package/src/panels/scrollable-list-panel.ts +143 -145
- package/src/panels/security-panel.ts +204 -52
- package/src/panels/services-panel.ts +174 -76
- package/src/panels/session-browser-panel.ts +101 -6
- package/src/panels/session-maintenance.ts +4 -122
- package/src/panels/settings-sync-panel.ts +346 -68
- package/src/panels/skills-panel.ts +157 -89
- package/src/panels/subscription-panel.ts +105 -34
- package/src/panels/symbol-outline-panel.ts +287 -154
- package/src/panels/system-messages-panel.ts +172 -38
- package/src/panels/tasks-panel.ts +348 -134
- package/src/panels/thinking-panel.ts +66 -32
- package/src/panels/token-budget-panel.ts +233 -46
- package/src/panels/tool-inspector-panel.ts +170 -54
- package/src/panels/types.ts +65 -4
- package/src/panels/work-plan-panel.ts +393 -39
- package/src/panels/worktree-panel.ts +239 -61
- package/src/panels/wrfc-panel-format.ts +133 -0
- package/src/panels/wrfc-panel.ts +146 -150
- package/src/renderer/compaction-preview.ts +2 -1
- package/src/renderer/compositor.ts +46 -43
- 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 +30 -11
- package/src/renderer/model-workspace.ts +2 -3
- package/src/renderer/overlay-box.ts +16 -10
- package/src/renderer/panel-composite.ts +7 -20
- package/src/renderer/panel-workspace-bar.ts +14 -8
- package/src/renderer/process-indicator.ts +3 -1
- package/src/renderer/progress.ts +8 -6
- 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/tab-strip.ts +148 -34
- package/src/renderer/theme.ts +60 -3
- package/src/renderer/ui-factory.ts +45 -36
- 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/runtime/ui/model-picker/health-enrichment.ts +3 -0
- package/src/runtime/ui/model-picker/types.ts +4 -0
- package/src/shell/ui-openers.ts +14 -22
- package/src/utils/format-duration.ts +55 -0
- package/src/utils/format-number.ts +71 -0
- 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 -666
- package/src/panels/agent-logs-shared.ts +0 -129
- package/src/panels/context-visualizer-panel.ts +0 -214
- package/src/panels/forensics-panel.ts +0 -364
- package/src/panels/panel-picker.ts +0 -106
- package/src/panels/provider-account-snapshot.ts +0 -259
- package/src/panels/provider-accounts-panel.ts +0 -218
- package/src/panels/provider-stats-panel.ts +0 -366
- package/src/panels/schedule-panel.ts +0 -342
- package/src/panels/watchers-panel.ts +0 -193
- package/src/renderer/panel-picker-overlay.ts +0 -202
- package/src/renderer/panel-tab-bar.ts +0 -69
- package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
- package/src/runtime/diagnostics/panels/policy.ts +0 -177
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
DEFAULT_PANEL_PALETTE,
|
|
17
17
|
type PanelWorkspaceSection,
|
|
18
18
|
} from './polish.ts';
|
|
19
|
-
import { truncateDisplay } from '../utils/terminal-width.ts';
|
|
19
|
+
import { truncateDisplay, wrapText } from '../utils/terminal-width.ts';
|
|
20
20
|
|
|
21
21
|
const C = extendPalette(DEFAULT_PANEL_PALETTE, {
|
|
22
22
|
// Domain-specific reasoning tones (purples) and the active-stream cyan have no
|
|
@@ -28,34 +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;
|
|
35
44
|
}
|
|
36
45
|
|
|
37
|
-
function
|
|
38
|
-
const
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
} else {
|
|
44
|
-
let start = 0;
|
|
45
|
-
while (start < line.length) {
|
|
46
|
-
result.push(line.slice(start, start + width));
|
|
47
|
-
start += width;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
return result;
|
|
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}`;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
type FlatRow = { kind: 'header'; blockIndex: number; text: string } | { kind: 'content'; text: string };
|
|
55
55
|
|
|
56
56
|
export class ThinkingPanel extends BasePanel {
|
|
57
57
|
private blocks: ReasoningBlock[] = [];
|
|
58
|
-
private nextTurnId = 1;
|
|
59
58
|
private unsubs: Array<() => void> = [];
|
|
60
59
|
private cursorIndex = 0;
|
|
61
60
|
private scrollOffset = 0;
|
|
@@ -121,8 +120,8 @@ export class ThinkingPanel extends BasePanel {
|
|
|
121
120
|
lines: buildEmptyState(
|
|
122
121
|
width,
|
|
123
122
|
' No reasoning content yet',
|
|
124
|
-
'When the model emits thinking or reasoning deltas, they accumulate here in expandable blocks.',
|
|
125
|
-
[],
|
|
123
|
+
'When the model emits thinking or reasoning deltas during a turn, they accumulate here in expandable per-turn blocks.',
|
|
124
|
+
[{ command: '/chat <prompt>', summary: 'start a turn with a reasoning-capable model to stream thinking here' }],
|
|
126
125
|
DEFAULT_PANEL_PALETTE,
|
|
127
126
|
),
|
|
128
127
|
},
|
|
@@ -152,12 +151,38 @@ export class ThinkingPanel extends BasePanel {
|
|
|
152
151
|
],
|
|
153
152
|
};
|
|
154
153
|
|
|
155
|
-
|
|
154
|
+
// Resolve the reasoning block that owns the cursor row (walk back to its
|
|
155
|
+
// header) so the detail block describes the actual turn, not just row kind.
|
|
156
|
+
let selectedBlockIndex = -1;
|
|
157
|
+
for (let i = Math.min(this.cursorIndex, flat.length - 1); i >= 0; i--) {
|
|
158
|
+
const row = flat[i];
|
|
159
|
+
if (row?.kind === 'header') { selectedBlockIndex = row.blockIndex; break; }
|
|
160
|
+
}
|
|
161
|
+
const selectedBlock = selectedBlockIndex >= 0 ? this.blocks[selectedBlockIndex] : undefined;
|
|
156
162
|
const selectedSection: PanelWorkspaceSection = {
|
|
157
163
|
title: 'Selected',
|
|
158
|
-
lines:
|
|
159
|
-
|
|
160
|
-
|
|
164
|
+
lines: selectedBlock
|
|
165
|
+
? [
|
|
166
|
+
buildPanelLine(width, [
|
|
167
|
+
[' Turn ', DEFAULT_PANEL_PALETTE.label],
|
|
168
|
+
[selectedBlock.turnId, DEFAULT_PANEL_PALETTE.value],
|
|
169
|
+
[' State ', DEFAULT_PANEL_PALETTE.label],
|
|
170
|
+
[selectedBlock.active ? 'streaming' : 'complete', selectedBlock.active ? DEFAULT_PANEL_PALETTE.warn : DEFAULT_PANEL_PALETTE.good],
|
|
171
|
+
[' View ', DEFAULT_PANEL_PALETTE.label],
|
|
172
|
+
[selectedBlock.collapsed ? 'collapsed' : 'expanded', DEFAULT_PANEL_PALETTE.info],
|
|
173
|
+
[' Chars ', DEFAULT_PANEL_PALETTE.label],
|
|
174
|
+
[String(selectedBlock.content.length), DEFAULT_PANEL_PALETTE.dim],
|
|
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
|
+
]),
|
|
184
|
+
]
|
|
185
|
+
: [buildPanelLine(width, [[' No block selected', DEFAULT_PANEL_PALETTE.dim]])],
|
|
161
186
|
};
|
|
162
187
|
|
|
163
188
|
const reasoningSection = resolveScrollablePanelSection(width, height, {
|
|
@@ -209,10 +234,10 @@ export class ThinkingPanel extends BasePanel {
|
|
|
209
234
|
const rows: FlatRow[] = [];
|
|
210
235
|
for (let i = 0; i < this.blocks.length; i++) {
|
|
211
236
|
const block = this.blocks[i]!;
|
|
212
|
-
const turnLabel = `Turn ${block.turnId}${block.active ? ' (streaming)' : ''}`;
|
|
237
|
+
const turnLabel = `Turn ${block.turnId} ${fmtClock(block.startedAt)}${block.active ? ' (streaming)' : ''}`;
|
|
213
238
|
rows.push({ kind: 'header', blockIndex: i, text: turnLabel });
|
|
214
239
|
if (!block.collapsed) {
|
|
215
|
-
const wrapped =
|
|
240
|
+
const wrapped = wrapText(block.content || '(empty)', Math.max(1, width - 2));
|
|
216
241
|
for (const line of wrapped) {
|
|
217
242
|
rows.push({ kind: 'content', text: line });
|
|
218
243
|
}
|
|
@@ -249,12 +274,18 @@ export class ThinkingPanel extends BasePanel {
|
|
|
249
274
|
|
|
250
275
|
let currentBlock: ReasoningBlock | null = null;
|
|
251
276
|
|
|
252
|
-
|
|
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) => {
|
|
253
281
|
const block: ReasoningBlock = {
|
|
254
|
-
turnId:
|
|
282
|
+
turnId: envelope.turnId ?? envelope.payload.turnId,
|
|
283
|
+
correlationId: envelope.traceId,
|
|
255
284
|
content: '',
|
|
256
285
|
active: true,
|
|
257
286
|
collapsed: false,
|
|
287
|
+
startedAt: envelope.ts,
|
|
288
|
+
updatedAt: envelope.ts,
|
|
258
289
|
};
|
|
259
290
|
// Cap block count to prevent unbounded growth
|
|
260
291
|
if (this.blocks.length >= ThinkingPanel.MAX_BLOCKS) {
|
|
@@ -266,26 +297,29 @@ export class ThinkingPanel extends BasePanel {
|
|
|
266
297
|
this.markDirty();
|
|
267
298
|
}));
|
|
268
299
|
|
|
269
|
-
this.unsubs.push(this.turnEvents.
|
|
270
|
-
const reasoning =
|
|
300
|
+
this.unsubs.push(this.turnEvents.onEnvelope('STREAM_DELTA', (envelope) => {
|
|
301
|
+
const reasoning = envelope.payload.reasoning;
|
|
271
302
|
if (reasoning && currentBlock) {
|
|
272
303
|
currentBlock.content += reasoning;
|
|
304
|
+
currentBlock.updatedAt = envelope.ts;
|
|
273
305
|
this.autoScroll = true;
|
|
274
306
|
this.markDirty();
|
|
275
307
|
}
|
|
276
308
|
}));
|
|
277
309
|
|
|
278
|
-
this.unsubs.push(this.turnEvents.
|
|
310
|
+
this.unsubs.push(this.turnEvents.onEnvelope('STREAM_END', (envelope) => {
|
|
279
311
|
if (currentBlock) {
|
|
280
312
|
currentBlock.active = false;
|
|
313
|
+
currentBlock.updatedAt = envelope.ts;
|
|
281
314
|
currentBlock = null;
|
|
282
315
|
this.markDirty();
|
|
283
316
|
}
|
|
284
317
|
}));
|
|
285
318
|
|
|
286
|
-
this.unsubs.push(this.turnEvents.
|
|
319
|
+
this.unsubs.push(this.turnEvents.onEnvelope('TURN_COMPLETED', (envelope) => {
|
|
287
320
|
if (currentBlock) {
|
|
288
321
|
currentBlock.active = false;
|
|
322
|
+
currentBlock.updatedAt = envelope.ts;
|
|
289
323
|
currentBlock = null;
|
|
290
324
|
this.markDirty();
|
|
291
325
|
}
|
|
@@ -3,12 +3,21 @@ 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,
|
|
16
|
+
buildKeyboardHints,
|
|
17
|
+
buildMeterLine,
|
|
18
|
+
buildStatusPill,
|
|
11
19
|
buildStyledPanelLine,
|
|
20
|
+
buildTable,
|
|
12
21
|
buildPanelWorkspace,
|
|
13
22
|
resolveScrollablePanelSection,
|
|
14
23
|
DEFAULT_PANEL_PALETTE,
|
|
@@ -51,6 +60,9 @@ const WARN_RED = 0.90;
|
|
|
51
60
|
// Maximum turns to keep in per-turn history
|
|
52
61
|
const MAX_TURN_HISTORY = 10;
|
|
53
62
|
|
|
63
|
+
// Rows scrolled per pageup/pagedown key press through the Recent Turns history.
|
|
64
|
+
const PAGE_SCROLL_ROWS = 5;
|
|
65
|
+
|
|
54
66
|
// ---------------------------------------------------------------------------
|
|
55
67
|
// Helpers
|
|
56
68
|
// ---------------------------------------------------------------------------
|
|
@@ -71,10 +83,15 @@ function fmtTok(n: number): string {
|
|
|
71
83
|
*
|
|
72
84
|
* Displays:
|
|
73
85
|
* - Stacked bar: input / output / cache-read / cache-write
|
|
74
|
-
* - Context window fill percentage with progress bar
|
|
75
|
-
* - Per-turn token usage (last 10 turns)
|
|
76
|
-
* - 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
|
|
77
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.
|
|
78
95
|
*/
|
|
79
96
|
export class TokenBudgetPanel extends BasePanel {
|
|
80
97
|
/** Snapshot of cumulative usage from the Orchestrator after each turn. */
|
|
@@ -92,19 +109,38 @@ export class TokenBudgetPanel extends BasePanel {
|
|
|
92
109
|
private orchestrator: Orchestrator | null = null;
|
|
93
110
|
private getContextWindow: (() => number) | null = null;
|
|
94
111
|
private sessionReadModel: UiReadModel<UiSessionSnapshot> | null = null;
|
|
112
|
+
/** Resolver for the active model id, for inline cost display. */
|
|
113
|
+
private getCurrentModelId: (() => string) | null = null;
|
|
95
114
|
private readonly sessionMemoryStore: SessionMemoryQuery;
|
|
96
115
|
private readonly configManager: Pick<ConfigManager, 'get'>;
|
|
97
116
|
private readonly requestRender: () => void;
|
|
98
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
|
+
|
|
99
133
|
constructor(
|
|
100
134
|
sessionMemoryStore: SessionMemoryQuery,
|
|
101
135
|
configManager: Pick<ConfigManager, 'get'>,
|
|
102
136
|
requestRender: () => void = () => {},
|
|
137
|
+
turnEvents?: UiEventFeed<TurnEvent>,
|
|
103
138
|
) {
|
|
104
|
-
super('tokens', 'Tokens', '
|
|
139
|
+
super('tokens', 'Tokens', '▢', 'providers');
|
|
105
140
|
this.sessionMemoryStore = sessionMemoryStore;
|
|
106
141
|
this.configManager = configManager;
|
|
107
142
|
this.requestRender = requestRender;
|
|
143
|
+
if (turnEvents) this._attachBus(turnEvents);
|
|
108
144
|
}
|
|
109
145
|
|
|
110
146
|
// ---------------------------------------------------------------------------
|
|
@@ -114,17 +150,21 @@ export class TokenBudgetPanel extends BasePanel {
|
|
|
114
150
|
/**
|
|
115
151
|
* Wire the panel to live data sources. Call once after construction.
|
|
116
152
|
*
|
|
117
|
-
* @param orchestrator
|
|
118
|
-
* @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.
|
|
119
157
|
*/
|
|
120
158
|
wire(
|
|
121
159
|
orchestrator: Orchestrator,
|
|
122
160
|
getCtxWindow: () => number,
|
|
123
161
|
sessionReadModel?: UiReadModel<UiSessionSnapshot>,
|
|
162
|
+
getCurrentModelId?: () => string,
|
|
124
163
|
): void {
|
|
125
164
|
this.orchestrator = orchestrator;
|
|
126
165
|
this.getContextWindow = getCtxWindow;
|
|
127
166
|
this.sessionReadModel = sessionReadModel ?? null;
|
|
167
|
+
this.getCurrentModelId = getCurrentModelId ?? null;
|
|
128
168
|
}
|
|
129
169
|
|
|
130
170
|
/**
|
|
@@ -149,10 +189,27 @@ export class TokenBudgetPanel extends BasePanel {
|
|
|
149
189
|
if (this.turnHistory.length > MAX_TURN_HISTORY) {
|
|
150
190
|
this.turnHistory.shift();
|
|
151
191
|
}
|
|
192
|
+
// A freshly completed turn always pulls the view back to the latest entry.
|
|
193
|
+
this.turnScrollIndex = -1;
|
|
152
194
|
|
|
153
195
|
this._refreshAndRender();
|
|
154
196
|
}
|
|
155
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
|
+
|
|
156
213
|
// ---------------------------------------------------------------------------
|
|
157
214
|
// Lifecycle
|
|
158
215
|
// ---------------------------------------------------------------------------
|
|
@@ -167,6 +224,7 @@ export class TokenBudgetPanel extends BasePanel {
|
|
|
167
224
|
|
|
168
225
|
override onDeactivate(): void {
|
|
169
226
|
super.onDeactivate();
|
|
227
|
+
this.confirm = null;
|
|
170
228
|
// Stop the off-screen poll so a backgrounded Tokens tab does not keep
|
|
171
229
|
// refreshing (and, once requestRender is wired, force a repaint) every 2 s.
|
|
172
230
|
if (this.refreshTimer !== null) {
|
|
@@ -180,6 +238,78 @@ export class TokenBudgetPanel extends BasePanel {
|
|
|
180
238
|
clearInterval(this.refreshTimer);
|
|
181
239
|
this.refreshTimer = null;
|
|
182
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;
|
|
183
313
|
}
|
|
184
314
|
|
|
185
315
|
// ---------------------------------------------------------------------------
|
|
@@ -216,6 +346,17 @@ export class TokenBudgetPanel extends BasePanel {
|
|
|
216
346
|
|
|
217
347
|
override render(width: number, height: number): Line[] {
|
|
218
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
|
+
|
|
219
360
|
const sections: PanelWorkspaceSection[] = [];
|
|
220
361
|
|
|
221
362
|
if (this.contextWindow > 0) {
|
|
@@ -233,16 +374,33 @@ export class TokenBudgetPanel extends BasePanel {
|
|
|
233
374
|
],
|
|
234
375
|
});
|
|
235
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
|
+
|
|
236
390
|
if (this.turnHistory.length > 0) {
|
|
237
391
|
const priorSections = [...sections];
|
|
392
|
+
const scrollIndex = this.turnScrollIndex === -1 ? this.turnHistory.length : this.turnScrollIndex;
|
|
238
393
|
const turnsSection = resolveScrollablePanelSection(width, height, {
|
|
239
394
|
intro: 'Live context pressure, session token composition, cache usage, and recent turn deltas.',
|
|
395
|
+
footerLines: [
|
|
396
|
+
buildKeyboardHints(width, footerHints, DEFAULT_PANEL_PALETTE),
|
|
397
|
+
],
|
|
240
398
|
palette: DEFAULT_PANEL_PALETTE,
|
|
241
399
|
beforeSections: priorSections,
|
|
242
400
|
section: {
|
|
243
401
|
title: 'Recent Turns',
|
|
244
402
|
scrollableLines: this.renderTurnHistory(width, this.turnHistory.length + 1),
|
|
245
|
-
scrollOffset:
|
|
403
|
+
scrollOffset: scrollIndex,
|
|
246
404
|
minRows: 6,
|
|
247
405
|
},
|
|
248
406
|
afterSections: [{
|
|
@@ -273,6 +431,9 @@ export class TokenBudgetPanel extends BasePanel {
|
|
|
273
431
|
title: ' Token Budget',
|
|
274
432
|
intro: 'Live context pressure, session token composition, cache usage, and recent turn deltas.',
|
|
275
433
|
sections,
|
|
434
|
+
footerLines: [
|
|
435
|
+
buildKeyboardHints(width, footerHints, DEFAULT_PANEL_PALETTE),
|
|
436
|
+
],
|
|
276
437
|
palette: DEFAULT_PANEL_PALETTE,
|
|
277
438
|
});
|
|
278
439
|
});
|
|
@@ -298,7 +459,17 @@ export class TokenBudgetPanel extends BasePanel {
|
|
|
298
459
|
}
|
|
299
460
|
|
|
300
461
|
if (status.guidanceMode !== 'off' && status.nextSteps.length > 0) {
|
|
301
|
-
|
|
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
|
+
}
|
|
302
473
|
}
|
|
303
474
|
|
|
304
475
|
return lines;
|
|
@@ -373,7 +544,7 @@ export class TokenBudgetPanel extends BasePanel {
|
|
|
373
544
|
return lines;
|
|
374
545
|
}
|
|
375
546
|
|
|
376
|
-
/** 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). */
|
|
377
548
|
private renderContextBar(width: number): Line[] {
|
|
378
549
|
const lines: Line[] = [];
|
|
379
550
|
const pct = this.contextWindow > 0
|
|
@@ -398,16 +569,26 @@ export class TokenBudgetPanel extends BasePanel {
|
|
|
398
569
|
const suffix = ` ${fmtTok(this.lastInputTokens)}/${fmtTok(this.contextWindow)} (${pctInt}%)${warnSuffix}`;
|
|
399
570
|
const BAR_W = Math.max(8, width - label.length - suffix.length - 2);
|
|
400
571
|
const filled = Math.round(pct * BAR_W);
|
|
572
|
+
// Context fill is the headline metric — render it with the shared meter
|
|
573
|
+
// primitive so the bar glyphs and width handling match every other panel.
|
|
574
|
+
lines.push(buildMeterLine(width, filled, BAR_W,
|
|
575
|
+
{ filled: barColor, empty: C.barBg, label: C.label },
|
|
576
|
+
{ prefix: label, suffix },
|
|
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';
|
|
401
583
|
lines.push(buildStyledPanelLine(width, [
|
|
402
|
-
{ text:
|
|
403
|
-
|
|
404
|
-
{ text: '.'.repeat(Math.max(0, BAR_W - filled)), fg: C.barBg, dim: pct < WARN_YELLOW },
|
|
405
|
-
{ text: suffix, fg: barColor, dim: pct < WARN_YELLOW },
|
|
584
|
+
{ text: ' ', fg: C.dim },
|
|
585
|
+
...buildStatusPill(pressureState, pressureLabel),
|
|
406
586
|
]));
|
|
587
|
+
|
|
407
588
|
return lines;
|
|
408
589
|
}
|
|
409
590
|
|
|
410
|
-
/** Session cumulative totals with color-coded labels. */
|
|
591
|
+
/** Session cumulative totals with color-coded labels, plus an inline cost estimate. */
|
|
411
592
|
private renderTotals(width: number): Line[] {
|
|
412
593
|
const lines: Line[] = [];
|
|
413
594
|
lines.push(this.paintTextLine(' Session Totals:', width, C.label));
|
|
@@ -431,46 +612,52 @@ export class TokenBudgetPanel extends BasePanel {
|
|
|
431
612
|
]));
|
|
432
613
|
}
|
|
433
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
|
+
|
|
434
625
|
return lines;
|
|
435
626
|
}
|
|
436
627
|
|
|
437
628
|
/** Last N turns table: turn#, in, out, CR, CW, total. */
|
|
438
629
|
private renderTurnHistory(width: number, maxRows: number): Line[] {
|
|
439
|
-
const lines: Line[] = [];
|
|
440
|
-
|
|
441
|
-
// Column headers
|
|
442
|
-
const colLine = createEmptyLine(width);
|
|
443
|
-
const headers = [' #', ' Input', ' Output', ' CR', ' CW', ' Total'];
|
|
444
|
-
const cols = [3, 9, 9, 6, 6, 9];
|
|
445
|
-
let hx = 0;
|
|
446
|
-
for (let i = 0; i < headers.length; i++) {
|
|
447
|
-
const h = headers[i]!;
|
|
448
|
-
for (const ch of h.slice(0, cols[i]!)) {
|
|
449
|
-
if (hx >= width) break;
|
|
450
|
-
colLine[hx++] = createStyledCell(ch, { fg: C.turnHeader, dim: true });
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
lines.push(colLine);
|
|
454
|
-
|
|
455
630
|
const available = Math.max(0, maxRows - 1); // minus col header
|
|
456
631
|
const toShow = this.turnHistory.slice(-Math.max(0, available));
|
|
457
632
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
]
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
633
|
+
// Width-aware aligned table via the shared primitive (replaces manual
|
|
634
|
+
// per-cell column math, which miscounts on wide glyphs).
|
|
635
|
+
return buildTable(
|
|
636
|
+
width,
|
|
637
|
+
[
|
|
638
|
+
{ label: '#', width: 4, align: 'right' },
|
|
639
|
+
{ label: 'Input', width: 9, align: 'right' },
|
|
640
|
+
{ label: 'Output', width: 9, align: 'right' },
|
|
641
|
+
{ label: 'CR', width: 7, align: 'right' },
|
|
642
|
+
{ label: 'CW', width: 7, align: 'right' },
|
|
643
|
+
{ label: 'Total', align: 'right' },
|
|
644
|
+
],
|
|
645
|
+
toShow.map((t, i) => {
|
|
646
|
+
const turnNum = this.turnHistory.length - toShow.length + i + 1;
|
|
647
|
+
const total = t.input + t.output + t.cacheRead + t.cacheWrite;
|
|
648
|
+
return {
|
|
649
|
+
cells: [
|
|
650
|
+
{ text: String(turnNum), fg: C.dim },
|
|
651
|
+
{ text: fmtTok(t.input), fg: C.input },
|
|
652
|
+
{ text: fmtTok(t.output), fg: C.output },
|
|
653
|
+
{ text: fmtTok(t.cacheRead), fg: C.cacheRead },
|
|
654
|
+
{ text: fmtTok(t.cacheWrite), fg: C.cacheWrite },
|
|
655
|
+
{ text: fmtTok(total), fg: C.value },
|
|
656
|
+
],
|
|
657
|
+
};
|
|
658
|
+
}),
|
|
659
|
+
{ ...DEFAULT_PANEL_PALETTE, label: C.turnHeader },
|
|
660
|
+
);
|
|
474
661
|
}
|
|
475
662
|
|
|
476
663
|
/** Paint a plain text string into a new Line. */
|