@pellux/goodvibes-tui 0.18.20 → 0.19.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 (83) hide show
  1. package/CHANGELOG.md +154 -0
  2. package/README.md +1 -1
  3. package/docs/foundation-artifacts/operator-contract.json +1 -1
  4. package/package.json +7 -3
  5. package/src/core/conversation-rendering.ts +22 -6
  6. package/src/core/orchestrator.ts +1 -1
  7. package/src/input/commands/diff-runtime.ts +6 -5
  8. package/src/input/commands/guidance-runtime.ts +1 -1
  9. package/src/input/commands/health-runtime.ts +2 -2
  10. package/src/input/commands/local-setup-review.ts +1 -1
  11. package/src/input/commands/session-content.ts +1 -1
  12. package/src/input/commands/session.ts +0 -1
  13. package/src/input/commands/shell-core.ts +3 -2
  14. package/src/input/commands/skills-runtime.ts +2 -2
  15. package/src/input/commands/subscription-runtime.ts +4 -4
  16. package/src/input/feed-context-factory.ts +236 -0
  17. package/src/input/handler-feed.ts +44 -6
  18. package/src/input/handler-shortcuts.ts +138 -125
  19. package/src/input/handler.ts +119 -119
  20. package/src/input/keybindings.ts +30 -0
  21. package/src/input/panel-integration-actions.ts +2 -1
  22. package/src/input/settings-modal-types.ts +60 -0
  23. package/src/input/settings-modal.ts +83 -65
  24. package/src/panels/agent-inspector-panel.ts +10 -9
  25. package/src/panels/agent-logs-panel.ts +26 -6
  26. package/src/panels/approval-panel.ts +55 -82
  27. package/src/panels/automation-control-panel.ts +120 -161
  28. package/src/panels/base-panel.ts +108 -3
  29. package/src/panels/communication-panel.ts +69 -107
  30. package/src/panels/context-visualizer-panel.ts +2 -0
  31. package/src/panels/control-plane-panel.ts +117 -172
  32. package/src/panels/diff-panel.ts +2 -0
  33. package/src/panels/file-explorer-panel.ts +51 -31
  34. package/src/panels/file-preview-panel.ts +57 -35
  35. package/src/panels/git-panel.ts +12 -13
  36. package/src/panels/hooks-panel.ts +103 -138
  37. package/src/panels/incident-review-panel.ts +59 -109
  38. package/src/panels/knowledge-panel.ts +75 -107
  39. package/src/panels/local-auth-panel.ts +77 -93
  40. package/src/panels/marketplace-panel.ts +51 -69
  41. package/src/panels/mcp-panel.ts +110 -155
  42. package/src/panels/memory-panel.ts +90 -158
  43. package/src/panels/ops-control-panel.ts +51 -85
  44. package/src/panels/orchestration-panel.ts +70 -51
  45. package/src/panels/panel-list-panel.ts +5 -4
  46. package/src/panels/panel-manager.ts +25 -2
  47. package/src/panels/plan-dashboard-panel.ts +2 -0
  48. package/src/panels/plugins-panel.ts +37 -60
  49. package/src/panels/polish.ts +51 -2
  50. package/src/panels/provider-accounts-panel.ts +1 -0
  51. package/src/panels/provider-health-panel.ts +6 -8
  52. package/src/panels/routes-panel.ts +91 -141
  53. package/src/panels/schedule-panel.ts +7 -6
  54. package/src/panels/scrollable-list-panel.ts +64 -16
  55. package/src/panels/security-panel.ts +118 -152
  56. package/src/panels/services-panel.ts +63 -105
  57. package/src/panels/session-browser-panel.ts +19 -18
  58. package/src/panels/settings-sync-panel.ts +79 -123
  59. package/src/panels/skills-panel.ts +114 -230
  60. package/src/panels/subscription-panel.ts +64 -86
  61. package/src/panels/system-messages-panel.ts +147 -141
  62. package/src/panels/tasks-panel.ts +130 -179
  63. package/src/panels/token-budget-panel.ts +2 -0
  64. package/src/panels/watchers-panel.ts +89 -137
  65. package/src/panels/worktree-panel.ts +1 -0
  66. package/src/panels/wrfc-panel.ts +2 -0
  67. package/src/renderer/agent-detail-modal.ts +2 -2
  68. package/src/renderer/ansi-sanitize.ts +76 -0
  69. package/src/renderer/buffer.ts +23 -1
  70. package/src/renderer/diff.ts +8 -0
  71. package/src/renderer/help-overlay.ts +48 -28
  72. package/src/renderer/markdown.ts +3 -145
  73. package/src/renderer/settings-modal-helpers.ts +27 -0
  74. package/src/renderer/settings-modal.ts +18 -1
  75. package/src/renderer/status-glyphs.ts +21 -0
  76. package/src/renderer/status-token.ts +4 -8
  77. package/src/renderer/tool-call.ts +4 -3
  78. package/src/runtime/bootstrap-core.ts +1 -1
  79. package/src/runtime/bootstrap-hook-bridge.ts +1 -1
  80. package/src/runtime/bootstrap.ts +7 -8
  81. package/src/runtime/diagnostics/panels/policy.ts +2 -1
  82. package/src/shell/ui-openers.ts +1 -1
  83. package/src/version.ts +1 -1
@@ -1,12 +1,10 @@
1
1
  import type { Line } from '../types/grid.ts';
2
2
  import { createEmptyLine } from '../types/grid.ts';
3
- import { BasePanel } from './base-panel.ts';
4
- import { handleConfirmInput, renderConfirmLines } from './confirm-state.ts';
3
+ import { ScrollableListPanel } from './scrollable-list-panel.ts';
5
4
  import type { ProviderSubscription, PendingSubscriptionLogin } from '@pellux/goodvibes-sdk/platform/config/subscriptions';
6
5
  import { listBuiltinSubscriptionProviders } from '@pellux/goodvibes-sdk/platform/config/subscription-providers';
7
6
  import type { ServiceInspectionQuery, SubscriptionAccessQuery } from '../runtime/ui-service-queries.ts';
8
7
  import {
9
- buildDetailBlock,
10
8
  buildEmptyState,
11
9
  buildGuidanceLine,
12
10
  buildKeyValueLine,
@@ -15,8 +13,6 @@ import {
15
13
  buildSummaryBlock,
16
14
  buildPanelWorkspace,
17
15
  DEFAULT_PANEL_PALETTE,
18
- resolvePrimaryScrollableSection,
19
- type PanelWorkspaceSection,
20
16
  } from './polish.ts';
21
17
 
22
18
  const C = {
@@ -57,12 +53,10 @@ function statusColor(status: ReturnType<typeof statusOf>): string {
57
53
  }
58
54
  }
59
55
 
60
- export class SubscriptionPanel extends BasePanel {
56
+ export class SubscriptionPanel extends ScrollableListPanel<SubscriptionRow> {
61
57
  private readonly serviceRegistry: Pick<ServiceInspectionQuery, 'getAll'>;
62
58
  private readonly subscriptionManager: SubscriptionAccessQuery;
63
59
  private rows: SubscriptionRow[] = [];
64
- private selectedIndex = 0;
65
- private scrollOffset = 0;
66
60
  private logoutConfirmationTarget: string | null = null;
67
61
 
68
62
  public constructor(
@@ -70,6 +64,7 @@ export class SubscriptionPanel extends BasePanel {
70
64
  subscriptionManager: SubscriptionAccessQuery,
71
65
  ) {
72
66
  super('subscription', 'Subscriptions', 'B', 'monitoring');
67
+ this.showSelectionGutter = true; // I5: non-color selection affordance
73
68
  this.serviceRegistry = serviceRegistry;
74
69
  this.subscriptionManager = subscriptionManager;
75
70
  }
@@ -79,6 +74,30 @@ export class SubscriptionPanel extends BasePanel {
79
74
  this.refresh();
80
75
  }
81
76
 
77
+ protected override getPalette() { return C; }
78
+ protected override getEmptyStateMessage() { return ' No provider subscriptions are active yet.'; }
79
+ protected override getEmptyStateActions() {
80
+ return [
81
+ { command: '/subscription login openai start', summary: 'start the first-class OpenAI subscription flow' },
82
+ { command: '/login provider <name> start', summary: 'use the front-door auth surface for supported providers' },
83
+ { command: '/services auth-review', summary: 'inspect configured service auth posture and stored secrets' },
84
+ ];
85
+ }
86
+
87
+ protected getItems(): readonly SubscriptionRow[] {
88
+ return this.rows;
89
+ }
90
+
91
+ protected renderItem(row: SubscriptionRow, index: number, selected: boolean, width: number): Line {
92
+ const status = statusOf(row);
93
+ return buildPanelListRow(width, [
94
+ { text: row.provider.padEnd(16).slice(0, 16), fg: C.value },
95
+ { text: ` ${status.toUpperCase().padEnd(12)}`, fg: statusColor(status) },
96
+ { text: ` oauth=${row.hasOAuthConfig ? 'yes' : 'no'} `, fg: row.hasOAuthConfig ? C.info : C.dim },
97
+ { text: ` override=${row.subscription ? 'active' : 'off'}`, fg: row.subscription ? C.good : C.dim },
98
+ ], C, { selected, selectedBg: C.selectedBg });
99
+ }
100
+
82
101
  public handleInput(key: string): boolean {
83
102
  if (this.rows.length === 0) return false;
84
103
  const selected = this.rows[this.selectedIndex] ?? null;
@@ -96,11 +115,6 @@ export class SubscriptionPanel extends BasePanel {
96
115
  }
97
116
  if (key === 'enter' || key === 'x') {
98
117
  if (!selected?.subscription) return false;
99
- // I1: use confirm helper — first press sets target, second (y) executes
100
- const confirmResult = handleConfirmInput(
101
- this.logoutConfirmationTarget ? { subject: this.logoutConfirmationTarget, label: this.logoutConfirmationTarget } : null,
102
- key,
103
- );
104
118
  if (this.logoutConfirmationTarget === null || this.logoutConfirmationTarget !== selected.provider) {
105
119
  this.logoutConfirmationTarget = selected.provider;
106
120
  this.markDirty();
@@ -112,7 +126,6 @@ export class SubscriptionPanel extends BasePanel {
112
126
  this.markDirty();
113
127
  return true;
114
128
  }
115
- // Allow n/Esc to cancel pending logout confirm
116
129
  if ((key === 'n' || key === 'escape') && this.logoutConfirmationTarget) {
117
130
  this.logoutConfirmationTarget = null;
118
131
  this.markDirty();
@@ -157,8 +170,9 @@ export class SubscriptionPanel extends BasePanel {
157
170
  }
158
171
 
159
172
  public render(width: number, height: number): Line[] {
160
- this.needsRender = false;
161
173
  this.refresh();
174
+ this.clampSelection();
175
+ const intro = 'Review provider login state, subscription-backed routing, and pending browser auth handshakes.';
162
176
 
163
177
  const activeCount = this.rows.filter((row) => row.subscription).length;
164
178
  const pendingCount = this.rows.filter((row) => row.pending).length;
@@ -175,111 +189,75 @@ export class SubscriptionPanel extends BasePanel {
175
189
  ], C),
176
190
  buildGuidanceLine(width, '/subscription login <provider> start', 'start or repair browser login for the selected provider route', C),
177
191
  ];
178
- const footerLines = [
179
- buildGuidanceLine(width, '/subscription login <provider> start', 'start browser-based provider login from the packaged subscription surface', C),
180
- buildPanelLine(width, [[' Up/Down move Enter/X sign out selected provider r refresh', C.dim]]),
181
- ] as const;
182
192
 
193
+ // Empty state: render posture + base empty state
183
194
  if (this.rows.length === 0) {
184
- const lines: Line[] = [];
185
- lines.push(...buildSummaryBlock(width, 'Subscription posture', postureLines, C));
186
- lines.push(...buildEmptyState(
195
+ const summaryLines = buildSummaryBlock(width, 'Subscription posture', postureLines, C);
196
+ const emptyLines = buildEmptyState(
187
197
  width,
188
- ' No provider subscriptions are active yet.',
198
+ this.getEmptyStateMessage(),
189
199
  'Built-in OAuth-capable providers and configured service providers will appear here once available for browser login or session import.',
190
- [
191
- { command: '/subscription login openai start', summary: 'start the first-class OpenAI subscription flow' },
192
- { command: '/login provider <name> start', summary: 'use the front-door auth surface for supported providers' },
193
- { command: '/services auth-review', summary: 'inspect configured service auth posture and stored secrets' },
194
- ],
200
+ this.getEmptyStateActions(),
195
201
  C,
196
- ));
202
+ );
197
203
  const workspace = buildPanelWorkspace(width, height, {
198
204
  title: 'Provider Subscriptions',
199
- intro: 'Review provider login state, subscription-backed routing, and pending browser auth handshakes.',
200
- sections: [{ lines }] satisfies readonly PanelWorkspaceSection[],
201
- footerLines,
205
+ intro,
206
+ sections: [{ lines: [...summaryLines, ...emptyLines] }],
207
+ footerLines: [
208
+ buildGuidanceLine(width, '/subscription login <provider> start', 'start browser-based provider login from the packaged subscription surface', C),
209
+ buildPanelLine(width, [[' Up/Down move Enter/X sign out selected provider r refresh', C.dim]]),
210
+ ],
202
211
  palette: C,
203
212
  });
204
213
  while (workspace.length < height) workspace.push(createEmptyLine(width));
205
214
  return workspace.slice(0, height);
206
215
  }
207
216
 
208
- const selected = this.rows[this.selectedIndex];
217
+ const selectedRow = this.rows[this.selectedIndex];
209
218
  const detailRows: Line[] = [];
210
- if (selected) {
219
+ if (selectedRow) {
211
220
  detailRows.push(buildKeyValueLine(width, [
212
- { label: 'provider', value: selected.provider, valueColor: C.value },
213
- { label: 'status', value: statusOf(selected), valueColor: statusColor(statusOf(selected)) },
214
- { label: 'oauth config', value: selected.hasOAuthConfig ? 'present' : 'missing', valueColor: selected.hasOAuthConfig ? C.good : C.bad },
221
+ { label: 'provider', value: selectedRow.provider, valueColor: C.value },
222
+ { label: 'status', value: statusOf(selectedRow), valueColor: statusColor(statusOf(selectedRow)) },
223
+ { label: 'oauth config', value: selectedRow.hasOAuthConfig ? 'present' : 'missing', valueColor: selectedRow.hasOAuthConfig ? C.good : C.bad },
215
224
  ], C));
216
- if (selected.subscription) {
217
- const expires = selected.subscription.expiresAt
218
- ? new Date(selected.subscription.expiresAt).toISOString()
225
+ if (selectedRow.subscription) {
226
+ const expires = selectedRow.subscription.expiresAt
227
+ ? new Date(selectedRow.subscription.expiresAt).toISOString()
219
228
  : 'n/a';
220
229
  detailRows.push(buildKeyValueLine(width, [
221
- { label: 'token type', value: selected.subscription.tokenType, valueColor: C.info },
230
+ { label: 'token type', value: selectedRow.subscription.tokenType, valueColor: C.info },
222
231
  { label: 'expires', value: expires, valueColor: C.dim },
223
232
  ], C));
224
233
  detailRows.push(buildPanelLine(width, [[
225
- ` ${selected.subscription.overrideAmbientApiKeys
234
+ ` ${selectedRow.subscription.overrideAmbientApiKeys
226
235
  ? 'Provider subscription overrides ambient API-key resolution for this provider.'
227
236
  : 'Stored for subscription-backed flows. Ambient API-key resolution remains unchanged.'}`,
228
237
  C.dim,
229
238
  ]]));
230
- if (this.logoutConfirmationTarget === selected.provider) {
231
- detailRows.push(buildPanelLine(width, [[` Press Enter or X again to sign out ${selected.provider}.`, C.warn]]));
239
+ if (this.logoutConfirmationTarget === selectedRow.provider) {
240
+ detailRows.push(buildPanelLine(width, [[` Press Enter or X again to sign out ${selectedRow.provider}.`, C.warn]]));
232
241
  }
233
- } else if (selected.pending) {
242
+ } else if (selectedRow.pending) {
234
243
  detailRows.push(buildPanelLine(width, [[' Login is pending. Finish with /subscription login <provider> finish <code>.', C.warn]]));
235
- } else if (selected.hasOAuthConfig) {
244
+ } else if (selectedRow.hasOAuthConfig) {
236
245
  detailRows.push(buildPanelLine(width, [[' Ready for login. Start with /subscription login <provider> start.', C.dim]]));
237
246
  } else {
238
247
  detailRows.push(buildPanelLine(width, [[' Add a provider-specific OAuth config or enable a built-in subscription provider to use subscription login.', C.bad]]));
239
248
  }
240
249
  }
241
- const postureSection: PanelWorkspaceSection = { lines: buildSummaryBlock(width, 'Subscription posture', postureLines, C) };
242
- const detailSection: PanelWorkspaceSection = { lines: buildDetailBlock(width, 'Selected provider', detailRows, C) };
243
- const rawProviderLines: Line[] = this.rows.map((row, absolute) => {
244
- const status = statusOf(row);
245
- return buildPanelListRow(width, [
246
- { text: row.provider.padEnd(16).slice(0, 16), fg: C.value },
247
- { text: ` ${status.toUpperCase().padEnd(12)}`, fg: statusColor(status) },
248
- { text: ` oauth=${row.hasOAuthConfig ? 'yes' : 'no'} `, fg: row.hasOAuthConfig ? C.info : C.dim },
249
- { text: ` override=${row.subscription ? 'active' : 'off'}`, fg: row.subscription ? C.good : C.dim },
250
- ], C, { selected: absolute === this.selectedIndex, selectedBg: C.selectedBg });
251
- });
252
- const resolvedProvidersSection = resolvePrimaryScrollableSection(width, height, {
253
- intro: 'Review provider login state, subscription-backed routing, and pending browser auth handshakes.',
254
- footerLines,
255
- palette: C,
256
- beforeSections: [postureSection],
257
- section: {
258
- title: 'Providers',
259
- scrollableLines: rawProviderLines,
260
- selectedIndex: this.selectedIndex,
261
- scrollOffset: this.scrollOffset,
262
- guardRows: 1,
263
- minRows: 4,
264
- appendWindowSummary: { dimColor: C.dim },
265
- },
266
- afterSections: [detailSection],
267
- });
268
- this.scrollOffset = resolvedProvidersSection.scrollOffset;
269
250
 
270
- const sections: PanelWorkspaceSection[] = [
271
- postureSection,
272
- resolvedProvidersSection.section,
273
- detailSection,
274
- ];
275
- const lines = buildPanelWorkspace(width, height, {
251
+ const headerLines: Line[] = buildSummaryBlock(width, 'Subscription posture', postureLines, C);
252
+
253
+ return this.renderList(width, height, {
276
254
  title: 'Provider Subscriptions',
277
- intro: 'Review provider login state, subscription-backed routing, and pending browser auth handshakes.',
278
- sections,
279
- footerLines,
280
- palette: C,
255
+ header: headerLines,
256
+ footer: [
257
+ ...detailRows,
258
+ buildGuidanceLine(width, '/subscription login <provider> start', 'start browser-based provider login from the packaged subscription surface', C),
259
+ buildPanelLine(width, [[' Up/Down move Enter/X sign out selected provider r refresh', C.dim]]),
260
+ ],
281
261
  });
282
- while (lines.length < height) lines.push(createEmptyLine(width));
283
- return lines.slice(0, height);
284
262
  }
285
263
  }
@@ -1,9 +1,12 @@
1
1
  /**
2
2
  * SystemMessagesPanel — displays operational system messages routed away
3
3
  * from the main conversation.
4
+ *
5
+ * Migrated (Wave B2): extends ScrollableListPanel<SystemMessageEntry>.
6
+ * Navigation (up/down/j/k/pageup/pagedown/g/G) is handled by the base class.
4
7
  */
5
8
 
6
- import { BasePanel } from './base-panel.ts';
9
+ import { ScrollableListPanel } from './scrollable-list-panel.ts';
7
10
  import type { Line } from '../types/grid.ts';
8
11
  import type { ComponentHealthMonitor } from '../runtime/perf/panel-health-monitor.ts';
9
12
  import {
@@ -17,20 +20,21 @@ import {
17
20
  buildPanelWorkspace,
18
21
  resolvePrimaryScrollableSection,
19
22
  DEFAULT_PANEL_PALETTE,
23
+ extendPalette,
24
+ type PanelPalette,
20
25
  type PanelWorkspaceSection,
21
26
  } from './polish.ts';
22
27
  import { ConfigManager } from '@pellux/goodvibes-sdk/platform/config/manager';
23
28
 
24
29
  const MAX_MESSAGES = 500;
25
30
 
26
- const C = {
27
- ...DEFAULT_PANEL_PALETTE,
31
+ const C = extendPalette(DEFAULT_PANEL_PALETTE, {
28
32
  header: '#00ffff',
29
33
  headerBg: '#0f172a',
30
34
  high: '#fbbf24',
31
35
  low: '#9ca3af',
32
36
  ts: '#6b7280',
33
- } as const;
37
+ } as const);
34
38
 
35
39
  export type SystemMessagePriority = 'high' | 'low';
36
40
 
@@ -48,10 +52,8 @@ function fmtTime(ts: number): string {
48
52
  return `${hh}:${mm}:${ss}`;
49
53
  }
50
54
 
51
- export class SystemMessagesPanel extends BasePanel {
55
+ export class SystemMessagesPanel extends ScrollableListPanel<SystemMessageEntry> {
52
56
  private _messages: SystemMessageEntry[] = [];
53
- private _lastVisibleIdx = 0;
54
- private _scrollOffset = 0;
55
57
  private readonly configManager: ConfigManager;
56
58
 
57
59
  constructor(configManager: ConfigManager, componentHealthMonitor?: ComponentHealthMonitor) {
@@ -59,14 +61,55 @@ export class SystemMessagesPanel extends BasePanel {
59
61
  this.configManager = configManager;
60
62
  }
61
63
 
64
+ // ---------------------------------------------------------------------------
65
+ // ScrollableListPanel contract
66
+ // ---------------------------------------------------------------------------
67
+
68
+ protected getItems(): readonly SystemMessageEntry[] {
69
+ return this._messages;
70
+ }
71
+
72
+ protected renderItem(
73
+ entry: SystemMessageEntry,
74
+ index: number,
75
+ selected: boolean,
76
+ width: number,
77
+ ): Line {
78
+ const preview = entry.text.replace(/\s+/g, ' ').trim();
79
+ return buildPanelListRow(width, [
80
+ { text: `${fmtTime(entry.ts)} `, fg: C.ts },
81
+ {
82
+ text: `${entry.priority === 'high' ? 'HIGH' : 'LOW '.padEnd(4)} `,
83
+ fg: entry.priority === 'high' ? C.high : C.low,
84
+ bold: entry.priority === 'high',
85
+ },
86
+ { text: preview, fg: C.value },
87
+ ], C, {
88
+ selected,
89
+ marker: entry.priority === 'high' ? '!' : '\u00b7',
90
+ });
91
+ }
92
+
93
+ protected override getPalette(): PanelPalette {
94
+ return C;
95
+ }
96
+
97
+ protected override getEmptyStateMessage(): string {
98
+ return ' No system messages yet.';
99
+ }
100
+
101
+ // ---------------------------------------------------------------------------
102
+ // Public API
103
+ // ---------------------------------------------------------------------------
104
+
62
105
  push(text: string, priority: SystemMessagePriority): void {
63
106
  this._messages.push({ ts: Date.now(), text, priority });
64
107
  if (this._messages.length > MAX_MESSAGES) {
65
108
  this._messages.shift();
66
- if (this._lastVisibleIdx > 0) this._lastVisibleIdx--;
109
+ if (this.selectedIndex > 0) this.selectedIndex--;
67
110
  }
68
- this._lastVisibleIdx = Math.max(0, this._messages.length - 1);
69
- this._scrollOffset = Math.max(0, this._messages.length - 1);
111
+ // Auto-follow: jump to latest message
112
+ this.selectedIndex = Math.max(0, this._messages.length - 1);
70
113
  this.markDirty();
71
114
  }
72
115
 
@@ -78,147 +121,110 @@ export class SystemMessagesPanel extends BasePanel {
78
121
  return this._messages;
79
122
  }
80
123
 
81
- handleInput(key: string): boolean {
82
- const prev = this._lastVisibleIdx;
83
- switch (key) {
84
- case 'j':
85
- case '\x1b[B':
86
- this._lastVisibleIdx = Math.min(this._lastVisibleIdx + 1, Math.max(0, this._messages.length - 1));
87
- break;
88
- case 'k':
89
- case '\x1b[A':
90
- this._lastVisibleIdx = Math.max(this._lastVisibleIdx - 1, 0);
91
- break;
92
- case '\x1b[6~':
93
- this._lastVisibleIdx = Math.min(this._lastVisibleIdx + 20, Math.max(0, this._messages.length - 1));
94
- break;
95
- case '\x1b[5~':
96
- this._lastVisibleIdx = Math.max(this._lastVisibleIdx - 20, 0);
97
- break;
98
- case 'g':
99
- this._lastVisibleIdx = 0;
100
- break;
101
- case 'G':
102
- this._lastVisibleIdx = Math.max(0, this._messages.length - 1);
103
- break;
104
- default:
105
- return false;
106
- }
107
- if (this._lastVisibleIdx !== prev) this.markDirty();
108
- return true;
109
- }
110
-
111
- override render(width: number, height: number): Line[] {
112
- if (!this.canRenderNow()) {
113
- return Array.from({ length: height }, () => buildPanelLine(width, [['', C.dim]]));
114
- }
124
+ // ---------------------------------------------------------------------------
125
+ // Input base class handles all navigation; nothing custom here
126
+ // ---------------------------------------------------------------------------
115
127
 
116
- const start = Date.now();
117
- const intro = 'Operational system traffic routed out of the main conversation to reduce noise and keep runtime status reviewable.';
128
+ // ---------------------------------------------------------------------------
129
+ // Render multi-section layout (posture + list + detail)
130
+ // ---------------------------------------------------------------------------
118
131
 
119
- if (this._messages.length === 0) {
132
+ override render(width: number, height: number): Line[] {
133
+ return this.trackedRender(() => {
134
+ const intro = 'Operational system traffic routed out of the main conversation to reduce noise and keep runtime status reviewable.';
135
+
136
+ if (this._messages.length === 0) {
137
+ this.needsRender = false;
138
+ const lines = buildPanelWorkspace(width, height, {
139
+ title: 'System Messages',
140
+ intro,
141
+ sections: [{
142
+ lines: buildEmptyState(
143
+ width,
144
+ this.getEmptyStateMessage(),
145
+ 'Model switches, scan notices, provider/system state, and other operational updates will appear here once the runtime starts emitting them.',
146
+ [
147
+ { command: '/help', summary: 'review command and workflow surfaces' },
148
+ { command: '/cockpit', summary: 'open the unified runtime control room' },
149
+ ],
150
+ C,
151
+ ),
152
+ }],
153
+ footerLines: [
154
+ buildPanelLine(width, [[' j/k or Up/Down scroll g/G jump low-priority system traffic lands here by default', C.dim]]),
155
+ ],
156
+ palette: C,
157
+ });
158
+ return lines;
159
+ }
160
+
161
+ const highCount = this._messages.filter((entry) => entry.priority === 'high').length;
162
+ const lowCount = this._messages.length - highCount;
163
+ this.selectedIndex = Math.min(this.selectedIndex, this._messages.length - 1);
164
+ const ui = this.configManager.getRaw().ui;
165
+ const postureLines = [
166
+ buildKeyValueLine(width, [
167
+ { label: 'messages', value: String(this._messages.length), valueColor: C.value },
168
+ { label: 'high', value: String(highCount), valueColor: highCount > 0 ? C.high : C.dim },
169
+ { label: 'low', value: String(lowCount), valueColor: lowCount > 0 ? C.low : C.dim },
170
+ ], C),
171
+ buildKeyValueLine(width, [
172
+ { label: 'system route', value: ui.systemMessages, valueColor: C.info },
173
+ { label: 'ops route', value: ui.operationalMessages, valueColor: C.info },
174
+ { label: 'wrfc route', value: ui.wrfcMessages, valueColor: C.info },
175
+ ], C),
176
+ buildGuidanceLine(width, '/settings', 'adjust where operational and WRFC messages render across panels and conversation', C),
177
+ ];
178
+
179
+ const selected = this._messages[this.selectedIndex]!;
180
+ const messageRows: Line[] = this._messages.map((entry, index) =>
181
+ this.renderItem(entry, index, index === this.selectedIndex, width),
182
+ );
183
+
184
+ const postureSection: PanelWorkspaceSection = { lines: buildSummaryBlock(width, 'System posture', postureLines, C) };
185
+ const detailSection: PanelWorkspaceSection = {
186
+ title: 'Selected Message',
187
+ lines: [
188
+ buildPanelLine(width, [
189
+ [' Time ', C.label],
190
+ [fmtTime(selected.ts), C.value],
191
+ [' Priority ', C.label],
192
+ [selected.priority, selected.priority === 'high' ? C.high : C.low],
193
+ ]),
194
+ ...buildBodyText(width, selected.text, C, C.value),
195
+ ],
196
+ };
197
+ const messagesSection = resolvePrimaryScrollableSection(width, height, {
198
+ intro,
199
+ palette: C,
200
+ beforeSections: [postureSection],
201
+ section: {
202
+ title: 'Timeline',
203
+ scrollableLines: messageRows,
204
+ selectedIndex: this.selectedIndex,
205
+ scrollOffset: this.scrollStart,
206
+ minRows: 4,
207
+ appendWindowSummary: { dimColor: C.ts },
208
+ },
209
+ afterSections: [detailSection],
210
+ });
211
+ this.scrollStart = messagesSection.scrollOffset;
212
+ const sections: PanelWorkspaceSection[] = [
213
+ postureSection,
214
+ messagesSection.section,
215
+ detailSection,
216
+ ];
217
+ this.needsRender = false;
120
218
  const lines = buildPanelWorkspace(width, height, {
121
219
  title: 'System Messages',
122
220
  intro,
123
- sections: [{
124
- lines: buildEmptyState(
125
- width,
126
- ' No system messages yet.',
127
- 'Model switches, scan notices, provider/system state, and other operational updates will appear here once the runtime starts emitting them.',
128
- [
129
- { command: '/help', summary: 'review command and workflow surfaces' },
130
- { command: '/cockpit', summary: 'open the unified runtime control room' },
131
- ],
132
- C,
133
- ),
134
- }],
221
+ sections,
135
222
  footerLines: [
136
- buildPanelLine(width, [[' j/k or Up/Down scroll g/G jump low-priority system traffic lands here by default', C.dim]]),
223
+ buildPanelLine(width, [[' j/k or Up/Down scroll PgUp/PgDn page g/G jump', C.dim]]),
137
224
  ],
138
225
  palette: C,
139
226
  });
140
- this.reportRenderDuration(Date.now() - start);
141
227
  return lines;
142
- }
143
-
144
- const highCount = this._messages.filter((entry) => entry.priority === 'high').length;
145
- const lowCount = this._messages.length - highCount;
146
- this._lastVisibleIdx = Math.min(this._lastVisibleIdx, this._messages.length - 1);
147
- const ui = this.configManager.getRaw().ui;
148
- const postureLines = [
149
- buildKeyValueLine(width, [
150
- { label: 'messages', value: String(this._messages.length), valueColor: C.value },
151
- { label: 'high', value: String(highCount), valueColor: highCount > 0 ? C.high : C.dim },
152
- { label: 'low', value: String(lowCount), valueColor: lowCount > 0 ? C.low : C.dim },
153
- ], C),
154
- buildKeyValueLine(width, [
155
- { label: 'system route', value: ui.systemMessages, valueColor: C.info },
156
- { label: 'ops route', value: ui.operationalMessages, valueColor: C.info },
157
- { label: 'wrfc route', value: ui.wrfcMessages, valueColor: C.info },
158
- ], C),
159
- buildGuidanceLine(width, '/settings', 'adjust where operational and WRFC messages render across panels and conversation', C),
160
- ];
161
-
162
- const selected = this._messages[this._lastVisibleIdx]!;
163
- const messageRows: Line[] = this._messages.map((entry, index) => {
164
- const preview = entry.text.replace(/\s+/g, ' ').trim();
165
- return buildPanelListRow(width, [
166
- { text: `${fmtTime(entry.ts)} `, fg: C.ts },
167
- {
168
- text: `${entry.priority === 'high' ? 'HIGH' : 'LOW '.padEnd(4)} `,
169
- fg: entry.priority === 'high' ? C.high : C.low,
170
- bold: entry.priority === 'high',
171
- },
172
- { text: preview, fg: C.value },
173
- ], C, {
174
- selected: index === this._lastVisibleIdx,
175
- marker: entry.priority === 'high' ? '!' : '·',
176
- });
177
- });
178
-
179
- const postureSection: PanelWorkspaceSection = { lines: buildSummaryBlock(width, 'System posture', postureLines, C) };
180
- const detailSection: PanelWorkspaceSection = {
181
- title: 'Selected Message',
182
- lines: [
183
- buildPanelLine(width, [
184
- [' Time ', C.label],
185
- [fmtTime(selected.ts), C.value],
186
- [' Priority ', C.label],
187
- [selected.priority, selected.priority === 'high' ? C.high : C.low],
188
- ]),
189
- ...buildBodyText(width, selected.text, C, C.value),
190
- ],
191
- };
192
- const messagesSection = resolvePrimaryScrollableSection(width, height, {
193
- intro,
194
- palette: C,
195
- beforeSections: [postureSection],
196
- section: {
197
- title: 'Timeline',
198
- scrollableLines: messageRows,
199
- selectedIndex: this._lastVisibleIdx,
200
- scrollOffset: this._scrollOffset,
201
- minRows: 4,
202
- appendWindowSummary: { dimColor: C.ts },
203
- },
204
- afterSections: [detailSection],
205
- });
206
- this._scrollOffset = messagesSection.scrollOffset;
207
- const sections: PanelWorkspaceSection[] = [
208
- postureSection,
209
- messagesSection.section,
210
- detailSection,
211
- ];
212
- const lines = buildPanelWorkspace(width, height, {
213
- title: 'System Messages',
214
- intro,
215
- sections,
216
- footerLines: [
217
- buildPanelLine(width, [[' j/k or Up/Down scroll PgUp/PgDn page g/G jump', C.dim]]),
218
- ],
219
- palette: C,
220
228
  });
221
- this.reportRenderDuration(Date.now() - start);
222
- return lines;
223
229
  }
224
230
  }