@pellux/goodvibes-tui 0.28.0 → 1.0.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.
Files changed (144) hide show
  1. package/CHANGELOG.md +133 -0
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  4. package/src/core/conversation-line-cache.ts +432 -0
  5. package/src/core/conversation-rendering.ts +11 -3
  6. package/src/core/conversation.ts +50 -4
  7. package/src/input/commands/cost-runtime.ts +49 -0
  8. package/src/input/commands/operator-runtime.ts +5 -1
  9. package/src/input/commands.ts +2 -0
  10. package/src/input/handler-content-actions.ts +8 -3
  11. package/src/input/handler-feed-routes.ts +12 -35
  12. package/src/input/handler-feed.ts +4 -8
  13. package/src/input/handler-shortcuts.ts +51 -0
  14. package/src/input/handler.ts +43 -6
  15. package/src/input/input-history.ts +17 -4
  16. package/src/input/keybindings.ts +48 -8
  17. package/src/input/panel-integration-actions.ts +107 -1
  18. package/src/main.ts +19 -11
  19. package/src/panels/agent-inspector-panel.ts +125 -36
  20. package/src/panels/agent-inspector-shared.ts +144 -0
  21. package/src/panels/approval-panel.ts +67 -16
  22. package/src/panels/automation-control-panel.ts +368 -124
  23. package/src/panels/base-panel.ts +1 -1
  24. package/src/panels/builtin/agent.ts +28 -37
  25. package/src/panels/builtin/development.ts +40 -32
  26. package/src/panels/builtin/knowledge.ts +15 -6
  27. package/src/panels/builtin/operations.ts +178 -117
  28. package/src/panels/builtin/session.ts +35 -9
  29. package/src/panels/builtin/shared.ts +98 -6
  30. package/src/panels/cockpit-panel.ts +232 -73
  31. package/src/panels/communication-panel.ts +58 -20
  32. package/src/panels/confirm-state.ts +8 -1
  33. package/src/panels/control-plane-panel.ts +351 -118
  34. package/src/panels/cost-tracker-panel.ts +165 -7
  35. package/src/panels/debug-panel.ts +312 -159
  36. package/src/panels/diff-panel.ts +209 -57
  37. package/src/panels/docs-panel.ts +152 -66
  38. package/src/panels/eval-panel.ts +220 -42
  39. package/src/panels/file-explorer-panel.ts +202 -122
  40. package/src/panels/file-preview-panel.ts +132 -57
  41. package/src/panels/git-panel.ts +310 -129
  42. package/src/panels/hooks-panel.ts +150 -22
  43. package/src/panels/incident-review-panel.ts +223 -35
  44. package/src/panels/index.ts +0 -4
  45. package/src/panels/intelligence-panel.ts +212 -86
  46. package/src/panels/knowledge-graph-panel.ts +461 -101
  47. package/src/panels/local-auth-panel.ts +240 -28
  48. package/src/panels/marketplace-panel.ts +193 -26
  49. package/src/panels/memory-panel.ts +78 -77
  50. package/src/panels/ops-control-panel.ts +146 -29
  51. package/src/panels/ops-strategy-panel.ts +72 -4
  52. package/src/panels/orchestration-panel.ts +231 -69
  53. package/src/panels/panel-list-panel.ts +154 -131
  54. package/src/panels/panel-manager.ts +97 -9
  55. package/src/panels/plan-dashboard-panel.ts +333 -17
  56. package/src/panels/plugins-panel.ts +184 -29
  57. package/src/panels/policy-panel.ts +210 -38
  58. package/src/panels/polish-core.ts +7 -2
  59. package/src/panels/polish.ts +23 -4
  60. package/src/panels/project-planning-answer-actions.ts +134 -0
  61. package/src/panels/project-planning-panel.ts +62 -113
  62. package/src/panels/provider-health-panel.ts +434 -518
  63. package/src/panels/provider-health-routes.ts +203 -0
  64. package/src/panels/provider-health-tracker.ts +194 -6
  65. package/src/panels/provider-health-views.ts +560 -0
  66. package/src/panels/qr-panel.ts +116 -30
  67. package/src/panels/remote-panel.ts +114 -36
  68. package/src/panels/routes-panel.ts +63 -22
  69. package/src/panels/sandbox-panel.ts +174 -42
  70. package/src/panels/scrollable-list-panel.ts +19 -135
  71. package/src/panels/security-panel.ts +133 -33
  72. package/src/panels/services-panel.ts +116 -64
  73. package/src/panels/session-browser-panel.ts +73 -2
  74. package/src/panels/session-maintenance.ts +4 -122
  75. package/src/panels/settings-sync-panel.ts +335 -72
  76. package/src/panels/skills-panel.ts +157 -89
  77. package/src/panels/subscription-panel.ts +86 -33
  78. package/src/panels/symbol-outline-panel.ts +248 -108
  79. package/src/panels/system-messages-panel.ts +114 -13
  80. package/src/panels/tasks-panel.ts +326 -139
  81. package/src/panels/thinking-panel.ts +43 -10
  82. package/src/panels/token-budget-panel.ts +194 -18
  83. package/src/panels/tool-inspector-panel.ts +144 -34
  84. package/src/panels/types.ts +40 -4
  85. package/src/panels/work-plan-panel.ts +280 -17
  86. package/src/panels/worktree-panel.ts +175 -42
  87. package/src/panels/wrfc-panel.ts +116 -24
  88. package/src/renderer/agent-detail-modal.ts +37 -8
  89. package/src/renderer/code-block.ts +58 -28
  90. package/src/renderer/context-inspector.ts +3 -6
  91. package/src/renderer/conversation-overlays.ts +25 -11
  92. package/src/renderer/conversation-surface.ts +1 -21
  93. package/src/renderer/diff-view.ts +6 -4
  94. package/src/renderer/footer-tips.ts +41 -0
  95. package/src/renderer/fullscreen-primitives.ts +23 -17
  96. package/src/renderer/fullscreen-workspace.ts +2 -7
  97. package/src/renderer/help-overlay.ts +91 -14
  98. package/src/renderer/hint-grammar.ts +52 -0
  99. package/src/renderer/history-search-overlay.ts +10 -16
  100. package/src/renderer/layout.ts +7 -7
  101. package/src/renderer/markdown.ts +9 -1
  102. package/src/renderer/modal-factory.ts +23 -15
  103. package/src/renderer/model-picker-overlay.ts +8 -485
  104. package/src/renderer/model-workspace.ts +9 -24
  105. package/src/renderer/overlay-box.ts +23 -19
  106. package/src/renderer/process-indicator.ts +15 -25
  107. package/src/renderer/profile-picker-modal.ts +13 -14
  108. package/src/renderer/prompt-content-width.ts +16 -0
  109. package/src/renderer/search-overlay.ts +27 -6
  110. package/src/renderer/selection-modal-overlay.ts +3 -1
  111. package/src/renderer/semantic-diff.ts +1 -1
  112. package/src/renderer/session-picker-modal.ts +6 -4
  113. package/src/renderer/settings-modal-helpers.ts +10 -34
  114. package/src/renderer/settings-modal.ts +70 -10
  115. package/src/renderer/shell-surface.ts +54 -15
  116. package/src/renderer/status-glyphs.ts +11 -9
  117. package/src/renderer/surface-layout.ts +0 -12
  118. package/src/renderer/term-caps.ts +1 -1
  119. package/src/renderer/theme.ts +60 -3
  120. package/src/renderer/tool-call.ts +6 -20
  121. package/src/renderer/ui-factory.ts +48 -37
  122. package/src/renderer/ui-primitives.ts +41 -3
  123. package/src/runtime/bootstrap-shell.ts +35 -18
  124. package/src/runtime/diagnostics/panels/index.ts +0 -3
  125. package/src/runtime/render-scheduler.ts +80 -0
  126. package/src/shell/ui-openers.ts +14 -22
  127. package/src/utils/format-duration.ts +2 -2
  128. package/src/utils/splash-lines.ts +54 -5
  129. package/src/version.ts +1 -1
  130. package/src/work-plans/work-plan-store.ts +13 -0
  131. package/src/panels/agent-logs-panel.ts +0 -635
  132. package/src/panels/agent-logs-shared.ts +0 -129
  133. package/src/panels/context-visualizer-panel.ts +0 -238
  134. package/src/panels/forensics-panel.ts +0 -378
  135. package/src/panels/provider-account-snapshot.ts +0 -259
  136. package/src/panels/provider-accounts-panel.ts +0 -255
  137. package/src/panels/provider-stats-panel.ts +0 -391
  138. package/src/panels/schedule-panel.ts +0 -365
  139. package/src/panels/watchers-panel.ts +0 -213
  140. package/src/renderer/file-tree.ts +0 -153
  141. package/src/renderer/progress.ts +0 -99
  142. package/src/renderer/status-token.ts +0 -67
  143. package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
  144. 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
- turnId: number;
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
- [String(selectedBlock.turnId), DEFAULT_PANEL_PALETTE.value],
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
- this.unsubs.push(this.turnEvents.on('STREAM_START', () => {
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: this.nextTurnId++,
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.on('STREAM_DELTA', (env) => {
271
- const reasoning = env.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.on('STREAM_END', () => {
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.on('TURN_COMPLETED', () => {
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', 'T', 'monitoring');
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 The main Orchestrator instance (for usage + lastInputTokens).
121
- * @param getCtxWindow Callback returning the current model's contextWindow value.
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: Math.max(0, this.turnHistory.length),
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
- lines.push(buildGuidanceLine(width, status.nextSteps[0]!, 'open the next maintenance action directly', DEFAULT_PANEL_PALETTE));
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