@pellux/goodvibes-tui 0.18.23 → 0.19.1
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 +71 -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 +8 -6
- package/src/core/orchestrator.ts +1 -1
- package/src/daemon/cli.ts +54 -0
- 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/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/handler.ts +8 -10
- package/src/input/model-picker.ts +6 -2
- 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/main.ts +52 -0
- package/src/panels/agent-inspector-panel.ts +10 -9
- package/src/panels/agent-logs-panel.ts +26 -6
- package/src/panels/approval-panel.ts +1 -0
- package/src/panels/automation-control-panel.ts +1 -0
- package/src/panels/base-panel.ts +108 -3
- package/src/panels/communication-panel.ts +1 -0
- package/src/panels/context-visualizer-panel.ts +2 -0
- package/src/panels/control-plane-panel.ts +1 -0
- 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 +3 -1
- package/src/panels/incident-review-panel.ts +4 -2
- package/src/panels/knowledge-panel.ts +75 -107
- package/src/panels/local-auth-panel.ts +1 -0
- package/src/panels/marketplace-panel.ts +51 -69
- package/src/panels/mcp-panel.ts +3 -1
- package/src/panels/memory-panel.ts +90 -158
- package/src/panels/ops-control-panel.ts +1 -0
- package/src/panels/orchestration-panel.ts +70 -51
- package/src/panels/panel-list-panel.ts +5 -4
- package/src/panels/panel-manager.ts +3 -0
- package/src/panels/plan-dashboard-panel.ts +2 -0
- package/src/panels/plugins-panel.ts +1 -0
- 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 +3 -1
- package/src/panels/schedule-panel.ts +7 -6
- package/src/panels/scrollable-list-panel.ts +19 -2
- package/src/panels/security-panel.ts +17 -15
- package/src/panels/services-panel.ts +6 -4
- package/src/panels/session-browser-panel.ts +19 -18
- package/src/panels/settings-sync-panel.ts +3 -1
- package/src/panels/skills-panel.ts +114 -230
- package/src/panels/subscription-panel.ts +1 -0
- package/src/panels/system-messages-panel.ts +147 -141
- package/src/panels/tasks-panel.ts +1 -0
- package/src/panels/token-budget-panel.ts +2 -0
- package/src/panels/watchers-panel.ts +1 -0
- 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 +12 -1
- package/src/renderer/help-overlay.ts +14 -3
- package/src/renderer/model-picker-overlay.ts +9 -2
- 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 +44 -3
- package/src/version.ts +1 -1
|
@@ -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
|
}
|
|
@@ -156,6 +156,7 @@ export class TasksPanel extends ScrollableListPanel<RuntimeTask> {
|
|
|
156
156
|
worktrees?: UiReadModel<UiWorktreeSnapshot>,
|
|
157
157
|
) {
|
|
158
158
|
super('tasks', 'Tasks', 'J', 'monitoring');
|
|
159
|
+
this.showSelectionGutter = true; // I5: non-color selection affordance
|
|
159
160
|
this.readModel = readModel;
|
|
160
161
|
this.worktrees = worktrees;
|
|
161
162
|
this.unsubscribers = [
|
|
@@ -199,6 +199,7 @@ export class TokenBudgetPanel extends BasePanel {
|
|
|
199
199
|
// ---------------------------------------------------------------------------
|
|
200
200
|
|
|
201
201
|
override render(width: number, height: number): Line[] {
|
|
202
|
+
return this.trackedRender(() => {
|
|
202
203
|
const sections: PanelWorkspaceSection[] = [];
|
|
203
204
|
|
|
204
205
|
if (this.contextWindow > 0) {
|
|
@@ -258,6 +259,7 @@ export class TokenBudgetPanel extends BasePanel {
|
|
|
258
259
|
sections,
|
|
259
260
|
palette: DEFAULT_PANEL_PALETTE,
|
|
260
261
|
});
|
|
262
|
+
});
|
|
261
263
|
}
|
|
262
264
|
|
|
263
265
|
private renderMaintenance(width: number): Line[] {
|
|
@@ -58,6 +58,7 @@ export class WatchersPanel extends ScrollableListPanel<WatcherEntry> {
|
|
|
58
58
|
|
|
59
59
|
public constructor(readModel?: UiReadModel<UiWatchersSnapshot>) {
|
|
60
60
|
super('watchers', 'Watchers', 'W', 'monitoring');
|
|
61
|
+
this.showSelectionGutter = true; // I5: non-color selection affordance
|
|
61
62
|
this.readModel = readModel;
|
|
62
63
|
this.unsub = readModel ? readModel.subscribe(() => this.markDirty()) : null;
|
|
63
64
|
}
|
|
@@ -29,6 +29,7 @@ export class WorktreePanel extends ScrollableListPanel<WorktreeStatusRecord> {
|
|
|
29
29
|
|
|
30
30
|
public constructor(worktreeRegistry: WorktreeRegistry) {
|
|
31
31
|
super('worktrees', 'Worktrees', 'W', 'monitoring');
|
|
32
|
+
this.showSelectionGutter = true; // I5: non-color selection affordance
|
|
32
33
|
this.worktreeRegistry = worktreeRegistry;
|
|
33
34
|
void this.refresh();
|
|
34
35
|
}
|
package/src/panels/wrfc-panel.ts
CHANGED
|
@@ -170,6 +170,7 @@ export class WrfcPanel extends BasePanel {
|
|
|
170
170
|
// Render
|
|
171
171
|
// -------------------------------------------------------------------------
|
|
172
172
|
render(width: number, height: number): Line[] {
|
|
173
|
+
return this.trackedRender(() => {
|
|
173
174
|
const activeCount = this.chains.filter(c => !['passed', 'failed'].includes(c.state)).length;
|
|
174
175
|
const passedCount = this.chains.filter(c => c.state === 'passed').length;
|
|
175
176
|
const failedCount = this.chains.filter(c => c.state === 'failed').length;
|
|
@@ -275,6 +276,7 @@ export class WrfcPanel extends BasePanel {
|
|
|
275
276
|
],
|
|
276
277
|
palette: DEFAULT_PANEL_PALETTE,
|
|
277
278
|
});
|
|
279
|
+
});
|
|
278
280
|
}
|
|
279
281
|
|
|
280
282
|
// -------------------------------------------------------------------------
|
|
@@ -53,11 +53,11 @@ export class AgentDetailModal {
|
|
|
53
53
|
this.active = true;
|
|
54
54
|
this.logEntries = [];
|
|
55
55
|
this.logTotal = 0;
|
|
56
|
-
this.loadLog().catch(() => {});
|
|
56
|
+
this.loadLog().catch((err) => { logger.debug('agent detail log load failed', { err }); });
|
|
57
57
|
// Auto-refresh log every 500ms while modal is open
|
|
58
58
|
if (this.refreshTimer) clearInterval(this.refreshTimer);
|
|
59
59
|
this.refreshTimer = setInterval(() => {
|
|
60
|
-
this.loadLog().then(() => this.onRefresh?.()).catch(() => {});
|
|
60
|
+
this.loadLog().then(() => this.onRefresh?.()).catch((err) => { logger.debug('agent detail log refresh tick failed', { err }); });
|
|
61
61
|
}, 500);
|
|
62
62
|
}
|
|
63
63
|
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ANSI sanitizer for untrusted content entering the renderer.
|
|
3
|
+
*
|
|
4
|
+
* The TUI grid renders content character-by-character via writeStyledText,
|
|
5
|
+
* which already drops zero-width characters (including ESC \x1b) by checking
|
|
6
|
+
* display width. However, that is incidental protection — not a contract.
|
|
7
|
+
* This module provides explicit, intentional sanitization.
|
|
8
|
+
*
|
|
9
|
+
* Strategy:
|
|
10
|
+
* - STRIP all non-SGR escape sequences (cursor moves, OSC, BEL, alt-screen,
|
|
11
|
+
* DECSET/private mode, and any other CSI/ESC sequences).
|
|
12
|
+
* - PRESERVE SGR color/style codes (\x1b[<params>m) — used legitimately by
|
|
13
|
+
* the TUI's own colorized output paths.
|
|
14
|
+
* - STRIP bare BEL (\x07) characters.
|
|
15
|
+
*
|
|
16
|
+
* Safe SGR pattern: \x1b[ followed by digits/semicolons, ending in 'm'.
|
|
17
|
+
* Everything else that starts with \x1b is dangerous and stripped.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
// Matches safe SGR sequences: ESC [ <digits/semicolons> m
|
|
21
|
+
const SGR_PATTERN = /\x1b\[([0-9;]*)m/g;
|
|
22
|
+
|
|
23
|
+
// Matches ALL CSI sequences: ESC [ ... <final byte 0x40-0x7E>
|
|
24
|
+
const CSI_SEQUENCE = /\x1b\[[\x20-\x3f]*[\x40-\x7e]/g;
|
|
25
|
+
|
|
26
|
+
// Matches OSC sequences: ESC ] ... (ESC \ or BEL)
|
|
27
|
+
const OSC_SEQUENCE = /\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g;
|
|
28
|
+
|
|
29
|
+
// Matches other ESC sequences (ESC + single character that is not '[' or ']')
|
|
30
|
+
const ESC_OTHER = /\x1b[^\[\]]/g;
|
|
31
|
+
|
|
32
|
+
// Matches standalone BEL
|
|
33
|
+
const BEL = /\x07/g;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Strip dangerous ANSI escape sequences from untrusted content.
|
|
37
|
+
*
|
|
38
|
+
* Preserves SGR color codes (\x1b[<n>m). Removes:
|
|
39
|
+
* - Cursor movement CSI sequences (\x1b[<n>A/B/C/D, \x1b[H, etc.)
|
|
40
|
+
* - OSC sequences (\x1b]...\x07 or \x1b]...\x1b\\)
|
|
41
|
+
* - Alt-screen and DECSET private mode (\x1b[?...h/l)
|
|
42
|
+
* - Any other CSI or ESC sequences
|
|
43
|
+
* - Bare BEL (\x07)
|
|
44
|
+
*
|
|
45
|
+
* @param input - Raw string that may contain ANSI escape sequences
|
|
46
|
+
* @returns Sanitized string safe for grid rendering
|
|
47
|
+
*/
|
|
48
|
+
export function stripDangerousAnsi(input: string): string {
|
|
49
|
+
// Step 1: Extract and preserve SGR sequences by replacing them with placeholders,
|
|
50
|
+
// then strip all other escape sequences, then restore SGR sequences.
|
|
51
|
+
// This approach avoids complex negative lookahead regexes.
|
|
52
|
+
|
|
53
|
+
// Collect SGR sequences and replace with unique markers
|
|
54
|
+
const sgrTokens: string[] = [];
|
|
55
|
+
const withPlaceholders = input.replace(SGR_PATTERN, (match) => {
|
|
56
|
+
const idx = sgrTokens.length;
|
|
57
|
+
sgrTokens.push(match);
|
|
58
|
+
return `\x00SGR${idx}\x00`;
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Strip all remaining dangerous sequences
|
|
62
|
+
let sanitized = withPlaceholders
|
|
63
|
+
.replace(CSI_SEQUENCE, '') // removes cursor moves, alt-screen, DECSET, etc.
|
|
64
|
+
.replace(OSC_SEQUENCE, '') // removes OSC
|
|
65
|
+
.replace(ESC_OTHER, '') // removes remaining ESC+char sequences
|
|
66
|
+
.replace(/\x1b/g, '') // removes any leftover bare ESC
|
|
67
|
+
.replace(BEL, ''); // removes BEL
|
|
68
|
+
|
|
69
|
+
// Restore SGR sequences from placeholders
|
|
70
|
+
sanitized = sanitized.replace(/\x00SGR(\d+)\x00/g, (_match, idxStr) => {
|
|
71
|
+
const idx = parseInt(idxStr, 10);
|
|
72
|
+
return sgrTokens[idx] ?? '';
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
return sanitized;
|
|
76
|
+
}
|
package/src/renderer/buffer.ts
CHANGED
|
@@ -17,7 +17,18 @@ export class TerminalBuffer {
|
|
|
17
17
|
|
|
18
18
|
public setCell(x: number, y: number, cell: Partial<Cell>): void {
|
|
19
19
|
if (y >= 0 && y < this.height && x >= 0 && x < this.width) {
|
|
20
|
-
|
|
20
|
+
// No-op guard: skip the dirty mark and allocation if every field in `cell`
|
|
21
|
+
// already matches the current cell value (idempotent write).
|
|
22
|
+
const current = this.cells[y][x]!;
|
|
23
|
+
let changed = false;
|
|
24
|
+
for (const k in cell) {
|
|
25
|
+
if ((cell as unknown as Record<string, unknown>)[k] !== (current as unknown as Record<string, unknown>)[k]) {
|
|
26
|
+
changed = true;
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (!changed) return;
|
|
31
|
+
this.cells[y][x] = { ...current, ...cell };
|
|
21
32
|
this.dirtyRows[y] = true;
|
|
22
33
|
}
|
|
23
34
|
}
|
|
@@ -10,6 +10,7 @@ import type { SlashCommand } from '../input/command-registry.ts';
|
|
|
10
10
|
import type { KeybindingsManager } from '../input/keybindings.ts';
|
|
11
11
|
import { getOverlaySurfaceMetrics } from './overlay-viewport.ts';
|
|
12
12
|
import { getVisibleWindow } from './surface-layout.ts';
|
|
13
|
+
import { logger } from '@pellux/goodvibes-sdk/platform/utils/logger';
|
|
13
14
|
|
|
14
15
|
function toModalSections(rows: readonly string[]): import('./modal-factory.ts').ModalSection[] {
|
|
15
16
|
return rows.map((row) => {
|
|
@@ -93,9 +94,19 @@ export function renderHelpOverlay(
|
|
|
93
94
|
}
|
|
94
95
|
|
|
95
96
|
const quickStartRows: string[] = [];
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
try {
|
|
98
|
+
for (const [name, argHint, desc] of FEATURED_COMMANDS) {
|
|
99
|
+
if (!hasCommand(name)) continue; // omit if not in live registry
|
|
100
|
+
quickStartRows.push(featuredRow(name, argHint, desc));
|
|
101
|
+
}
|
|
102
|
+
} catch (err) {
|
|
103
|
+
// A plugin command getter threw during registry traversal. Fall back to an
|
|
104
|
+
// unfiltered quick-start list so /help remains reachable.
|
|
105
|
+
logger.warn(`[help-overlay] registry traversal error during command filter; using unfiltered list: ${err}`);
|
|
106
|
+
quickStartRows.length = 0;
|
|
107
|
+
for (const [name, argHint, desc] of FEATURED_COMMANDS) {
|
|
108
|
+
quickStartRows.push(featuredRow(name, argHint, desc));
|
|
109
|
+
}
|
|
99
110
|
}
|
|
100
111
|
|
|
101
112
|
const commandRows: string[] = [];
|
|
@@ -294,12 +294,19 @@ export function renderModelPickerOverlay(
|
|
|
294
294
|
const isSelected = selectableIdx === picker.selectedIndex;
|
|
295
295
|
const indicator = isSelected ? `${OVERLAY_GLYPHS.selected} ` : ' ';
|
|
296
296
|
const checkmark = item.isConfigured ? '✓ ' : ' ';
|
|
297
|
-
|
|
297
|
+
// configuredVia badge: right-aligned short label (env/sub/anon)
|
|
298
|
+
const viaBadge = item.configuredVia === 'env' ? ' [env]'
|
|
299
|
+
: item.configuredVia === 'secrets' ? ' [key]'
|
|
300
|
+
: item.configuredVia === 'subscription' ? ' [sub]'
|
|
301
|
+
: item.configuredVia === 'anonymous' ? ' [anon]'
|
|
302
|
+
: '';
|
|
303
|
+
const badgeW = viaBadge.length;
|
|
304
|
+
const labelW = contentW - 2 - 2 - badgeW; // indicator(2) + checkmark(2) + badge
|
|
298
305
|
const labelStr = item.label.length > labelW
|
|
299
306
|
? item.label.slice(0, labelW - 3) + '...'
|
|
300
307
|
: item.label.padEnd(labelW);
|
|
301
308
|
const row = createOverlayContentLine(width, layout, borderFg, isSelected ? selectedBg : DEFAULT_OVERLAY_PALETTE.bodyBg);
|
|
302
|
-
const rowText = indicator + checkmark + labelStr;
|
|
309
|
+
const rowText = indicator + checkmark + labelStr + viaBadge;
|
|
303
310
|
putRowText(row, layout.margin + 2, contentW, fitDisplay(truncateDisplay(rowText, contentW), contentW), isSelected ? titleFg : bodyFg, isSelected ? selectedBg : DEFAULT_OVERLAY_PALETTE.bodyBg, isSelected);
|
|
304
311
|
lines.push(row);
|
|
305
312
|
}
|
|
@@ -78,6 +78,7 @@ export const CATEGORY_LABELS: Record<(typeof SETTINGS_CATEGORIES)[number], strin
|
|
|
78
78
|
danger: 'Danger',
|
|
79
79
|
tools: 'Tools',
|
|
80
80
|
flags: 'Flags',
|
|
81
|
+
network: 'Network',
|
|
81
82
|
};
|
|
82
83
|
|
|
83
84
|
export const SETTING_LABELS: Partial<Record<string, string>> = {
|
|
@@ -102,6 +103,32 @@ export const SETTING_LABELS: Partial<Record<string, string>> = {
|
|
|
102
103
|
'helper.enabled': 'Helper Enabled',
|
|
103
104
|
'helper.globalProvider': 'Helper Provider',
|
|
104
105
|
'helper.globalModel': 'Helper Model',
|
|
106
|
+
// Control Plane
|
|
107
|
+
'controlPlane.enabled': 'CP Enabled',
|
|
108
|
+
'controlPlane.hostMode': 'CP Host Mode',
|
|
109
|
+
'controlPlane.host': 'CP Host',
|
|
110
|
+
'controlPlane.port': 'CP Port',
|
|
111
|
+
'controlPlane.baseUrl': 'CP Base URL',
|
|
112
|
+
'controlPlane.streamMode': 'CP Stream Mode',
|
|
113
|
+
'controlPlane.allowRemote': 'CP Allow Remote',
|
|
114
|
+
'controlPlane.trustProxy': 'CP Trust Proxy',
|
|
115
|
+
'controlPlane.tls.mode': 'CP TLS Mode',
|
|
116
|
+
'controlPlane.tls.certFile': 'CP TLS Cert',
|
|
117
|
+
'controlPlane.tls.keyFile': 'CP TLS Key',
|
|
118
|
+
// HTTP Listener
|
|
119
|
+
'httpListener.hostMode': 'HTTP Host Mode',
|
|
120
|
+
'httpListener.host': 'HTTP Host',
|
|
121
|
+
'httpListener.port': 'HTTP Port',
|
|
122
|
+
'httpListener.trustProxy': 'HTTP Trust Proxy',
|
|
123
|
+
'httpListener.tls.mode': 'HTTP TLS Mode',
|
|
124
|
+
'httpListener.tls.certFile': 'HTTP TLS Cert',
|
|
125
|
+
// Web Server
|
|
126
|
+
'web.enabled': 'Web Enabled',
|
|
127
|
+
'web.hostMode': 'Web Host Mode',
|
|
128
|
+
'web.host': 'Web Host',
|
|
129
|
+
'web.port': 'Web Port',
|
|
130
|
+
'web.publicBaseUrl': 'Web Public Base URL',
|
|
131
|
+
'web.staticAssetsDir': 'Web Static Assets Dir',
|
|
105
132
|
};
|
|
106
133
|
|
|
107
134
|
export function getSettingLabel(entry: SettingEntry): string {
|
|
@@ -63,6 +63,7 @@ export function renderSettingsModal(
|
|
|
63
63
|
const isFlagsTab = SETTINGS_CATEGORIES[modal.categoryIndex] === 'flags';
|
|
64
64
|
const isUiTab = SETTINGS_CATEGORIES[modal.categoryIndex] === 'ui';
|
|
65
65
|
const isToolsTab = SETTINGS_CATEGORIES[modal.categoryIndex] === 'tools';
|
|
66
|
+
const isNetworkTab = SETTINGS_CATEGORIES[modal.categoryIndex] === 'network';
|
|
66
67
|
let persistentHelpers: import('./modal-factory.ts').ModalHelperRow[] | undefined;
|
|
67
68
|
sections.push({
|
|
68
69
|
type: 'text',
|
|
@@ -78,12 +79,28 @@ export function renderSettingsModal(
|
|
|
78
79
|
? 'Feature flags control staged or experimental behavior. Some changes may require restart.'
|
|
79
80
|
: isToolsTab
|
|
80
81
|
? 'Configure tool LLM routing and helper model. Provider and model fields are optional — empty means use the active provider.'
|
|
81
|
-
:
|
|
82
|
+
: isNetworkTab
|
|
83
|
+
? 'Configure control-plane and HTTP-listener binding. hostMode local/network use preset hosts; custom enables the host field. Changes trigger auto-restart.'
|
|
84
|
+
: 'Browse and adjust operator-facing runtime settings by category.',
|
|
82
85
|
style: { fg: '246', dim: true },
|
|
83
86
|
});
|
|
84
87
|
|
|
85
88
|
sections.push({ type: 'separator' });
|
|
86
89
|
|
|
90
|
+
// ── Network tab restart notice ─────────────────────────────────
|
|
91
|
+
if (isNetworkTab && modal.lastSaveTriggeredRestart !== null) {
|
|
92
|
+
const restartTarget = modal.lastSaveTriggeredRestart === 'control-plane'
|
|
93
|
+
? 'control-plane server'
|
|
94
|
+
: modal.lastSaveTriggeredRestart === 'http-listener'
|
|
95
|
+
? 'HTTP listener'
|
|
96
|
+
: 'web server';
|
|
97
|
+
sections.push({
|
|
98
|
+
type: 'text',
|
|
99
|
+
content: truncateDisplay(`Restarting ${restartTarget}… server will reconnect momentarily.`, contentW),
|
|
100
|
+
style: { fg: '#38bdf8' },
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
87
104
|
// ── Flags tab ──────────────────────────────────────────────────
|
|
88
105
|
if (isFlagsTab) {
|
|
89
106
|
const flagEntries: FlagEntry[] = modal.flagEntries;
|