@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.
- package/CHANGELOG.md +154 -0
- package/README.md +1 -1
- package/docs/foundation-artifacts/operator-contract.json +1 -1
- package/package.json +7 -3
- package/src/core/conversation-rendering.ts +22 -6
- package/src/core/orchestrator.ts +1 -1
- package/src/input/commands/diff-runtime.ts +6 -5
- package/src/input/commands/guidance-runtime.ts +1 -1
- package/src/input/commands/health-runtime.ts +2 -2
- package/src/input/commands/local-setup-review.ts +1 -1
- package/src/input/commands/session-content.ts +1 -1
- package/src/input/commands/session.ts +0 -1
- package/src/input/commands/shell-core.ts +3 -2
- package/src/input/commands/skills-runtime.ts +2 -2
- package/src/input/commands/subscription-runtime.ts +4 -4
- package/src/input/feed-context-factory.ts +236 -0
- package/src/input/handler-feed.ts +44 -6
- package/src/input/handler-shortcuts.ts +138 -125
- package/src/input/handler.ts +119 -119
- package/src/input/keybindings.ts +30 -0
- package/src/input/panel-integration-actions.ts +2 -1
- package/src/input/settings-modal-types.ts +60 -0
- package/src/input/settings-modal.ts +83 -65
- package/src/panels/agent-inspector-panel.ts +10 -9
- package/src/panels/agent-logs-panel.ts +26 -6
- package/src/panels/approval-panel.ts +55 -82
- package/src/panels/automation-control-panel.ts +120 -161
- package/src/panels/base-panel.ts +108 -3
- package/src/panels/communication-panel.ts +69 -107
- package/src/panels/context-visualizer-panel.ts +2 -0
- package/src/panels/control-plane-panel.ts +117 -172
- package/src/panels/diff-panel.ts +2 -0
- package/src/panels/file-explorer-panel.ts +51 -31
- package/src/panels/file-preview-panel.ts +57 -35
- package/src/panels/git-panel.ts +12 -13
- package/src/panels/hooks-panel.ts +103 -138
- package/src/panels/incident-review-panel.ts +59 -109
- package/src/panels/knowledge-panel.ts +75 -107
- package/src/panels/local-auth-panel.ts +77 -93
- package/src/panels/marketplace-panel.ts +51 -69
- package/src/panels/mcp-panel.ts +110 -155
- package/src/panels/memory-panel.ts +90 -158
- package/src/panels/ops-control-panel.ts +51 -85
- package/src/panels/orchestration-panel.ts +70 -51
- package/src/panels/panel-list-panel.ts +5 -4
- package/src/panels/panel-manager.ts +25 -2
- package/src/panels/plan-dashboard-panel.ts +2 -0
- package/src/panels/plugins-panel.ts +37 -60
- package/src/panels/polish.ts +51 -2
- package/src/panels/provider-accounts-panel.ts +1 -0
- package/src/panels/provider-health-panel.ts +6 -8
- package/src/panels/routes-panel.ts +91 -141
- package/src/panels/schedule-panel.ts +7 -6
- package/src/panels/scrollable-list-panel.ts +64 -16
- package/src/panels/security-panel.ts +118 -152
- package/src/panels/services-panel.ts +63 -105
- package/src/panels/session-browser-panel.ts +19 -18
- package/src/panels/settings-sync-panel.ts +79 -123
- package/src/panels/skills-panel.ts +114 -230
- package/src/panels/subscription-panel.ts +64 -86
- package/src/panels/system-messages-panel.ts +147 -141
- package/src/panels/tasks-panel.ts +130 -179
- package/src/panels/token-budget-panel.ts +2 -0
- package/src/panels/watchers-panel.ts +89 -137
- package/src/panels/worktree-panel.ts +1 -0
- package/src/panels/wrfc-panel.ts +2 -0
- package/src/renderer/agent-detail-modal.ts +2 -2
- package/src/renderer/ansi-sanitize.ts +76 -0
- package/src/renderer/buffer.ts +23 -1
- package/src/renderer/diff.ts +8 -0
- package/src/renderer/help-overlay.ts +48 -28
- package/src/renderer/markdown.ts +3 -145
- package/src/renderer/settings-modal-helpers.ts +27 -0
- package/src/renderer/settings-modal.ts +18 -1
- package/src/renderer/status-glyphs.ts +21 -0
- package/src/renderer/status-token.ts +4 -8
- package/src/renderer/tool-call.ts +4 -3
- package/src/runtime/bootstrap-core.ts +1 -1
- package/src/runtime/bootstrap-hook-bridge.ts +1 -1
- package/src/runtime/bootstrap.ts +7 -8
- package/src/runtime/diagnostics/panels/policy.ts +2 -1
- package/src/shell/ui-openers.ts +1 -1
- 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 {
|
|
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
|
|
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
|
|
185
|
-
|
|
186
|
-
lines.push(...buildEmptyState(
|
|
195
|
+
const summaryLines = buildSummaryBlock(width, 'Subscription posture', postureLines, C);
|
|
196
|
+
const emptyLines = buildEmptyState(
|
|
187
197
|
width,
|
|
188
|
-
|
|
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
|
|
200
|
-
sections: [{ lines
|
|
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
|
|
217
|
+
const selectedRow = this.rows[this.selectedIndex];
|
|
209
218
|
const detailRows: Line[] = [];
|
|
210
|
-
if (
|
|
219
|
+
if (selectedRow) {
|
|
211
220
|
detailRows.push(buildKeyValueLine(width, [
|
|
212
|
-
{ label: 'provider', value:
|
|
213
|
-
{ label: 'status', value: statusOf(
|
|
214
|
-
{ label: 'oauth config', value:
|
|
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 (
|
|
217
|
-
const expires =
|
|
218
|
-
? new Date(
|
|
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:
|
|
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
|
-
` ${
|
|
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 ===
|
|
231
|
-
detailRows.push(buildPanelLine(width, [[` Press Enter or X again to sign out ${
|
|
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 (
|
|
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 (
|
|
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
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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 {
|
|
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
|
|
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.
|
|
109
|
+
if (this.selectedIndex > 0) this.selectedIndex--;
|
|
67
110
|
}
|
|
68
|
-
|
|
69
|
-
this.
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
117
|
-
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
// Render — multi-section layout (posture + list + detail)
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
118
131
|
|
|
119
|
-
|
|
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
|
|
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
|
}
|