@pellux/goodvibes-tui 0.26.0 → 0.28.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 +40 -0
- package/README.md +12 -7
- package/docs/foundation-artifacts/operator-contract.json +4 -1
- package/package.json +2 -2
- package/src/cli/bundle-command.ts +2 -1
- package/src/cli/service-posture.ts +2 -1
- package/src/core/context-usage.ts +53 -0
- package/src/core/conversation-rendering.ts +2 -2
- package/src/core/conversation.ts +53 -8
- package/src/core/session-recovery.ts +26 -18
- package/src/core/system-message-router.ts +42 -26
- package/src/core/turn-event-wiring.ts +13 -16
- package/src/daemon/handlers/calendar/caldav-client.ts +2 -1
- package/src/daemon/handlers/inbox/index.ts +2 -1
- package/src/daemon/handlers/inbox/poller.ts +2 -1
- package/src/daemon/handlers/inbox/providers/discord.ts +2 -1
- package/src/daemon/handlers/inbox/providers/email.ts +2 -1
- package/src/daemon/handlers/inbox/providers/route-util.ts +2 -1
- package/src/daemon/handlers/inbox/providers/slack.ts +2 -1
- package/src/daemon/handlers/triage/integration.ts +2 -1
- package/src/daemon/handlers/triage/pipeline.ts +2 -1
- package/src/daemon/handlers/triage/tagger/discord.ts +2 -1
- package/src/daemon/handlers/triage/tagger/imap.ts +4 -3
- package/src/export/gist-uploader.ts +3 -1
- package/src/input/command-registry.ts +1 -0
- package/src/input/commands/cloudflare-runtime.ts +2 -1
- package/src/input/commands/eval.ts +4 -3
- package/src/input/commands/guidance-runtime.ts +2 -4
- package/src/input/commands/health-runtime.ts +13 -7
- package/src/input/commands/knowledge.ts +2 -1
- package/src/input/commands/runtime-services.ts +40 -10
- package/src/input/handler-command-route.ts +1 -1
- package/src/input/handler-feed-routes.ts +61 -4
- package/src/input/handler-feed.ts +13 -0
- package/src/input/handler-interactions.ts +2 -1
- package/src/input/handler-modal-routes.ts +2 -1
- package/src/input/handler-onboarding-cloudflare.ts +2 -1
- package/src/input/handler-onboarding.ts +8 -8
- package/src/input/handler-picker-routes.ts +18 -13
- package/src/input/handler-ui-state.ts +5 -1
- package/src/input/keybindings.ts +5 -0
- package/src/input/model-picker.ts +5 -0
- package/src/input/panel-integration-actions.ts +10 -0
- package/src/input/tts-settings-actions.ts +2 -1
- package/src/main.ts +52 -60
- package/src/panels/agent-inspector-panel.ts +90 -15
- package/src/panels/agent-inspector-shared.ts +3 -3
- package/src/panels/agent-logs-panel.ts +149 -72
- package/src/panels/approval-panel.ts +146 -95
- package/src/panels/automation-control-panel.ts +24 -1
- package/src/panels/base-panel.ts +28 -3
- package/src/panels/builtin/agent.ts +3 -1
- package/src/panels/builtin/development.ts +2 -1
- package/src/panels/builtin/session.ts +1 -1
- package/src/panels/cockpit-panel.ts +24 -5
- package/src/panels/cockpit-read-model.ts +2 -1
- package/src/panels/communication-panel.ts +108 -43
- package/src/panels/context-visualizer-panel.ts +49 -15
- package/src/panels/control-plane-panel.ts +27 -1
- package/src/panels/cost-tracker-panel.ts +87 -65
- package/src/panels/debug-panel.ts +61 -37
- package/src/panels/diff-panel.ts +59 -30
- package/src/panels/docs-panel.ts +30 -24
- package/src/panels/eval-panel.ts +130 -81
- package/src/panels/expandable-list-panel.ts +189 -0
- package/src/panels/file-explorer-panel.ts +42 -42
- package/src/panels/file-preview-panel.ts +11 -3
- package/src/panels/forensics-panel.ts +27 -13
- package/src/panels/git-panel.ts +65 -84
- package/src/panels/hooks-panel.ts +36 -2
- package/src/panels/incident-review-panel.ts +31 -10
- package/src/panels/intelligence-panel.ts +152 -71
- package/src/panels/knowledge-graph-panel.ts +89 -27
- package/src/panels/local-auth-panel.ts +17 -11
- package/src/panels/marketplace-panel.ts +33 -13
- package/src/panels/memory-panel.ts +7 -5
- package/src/panels/ops-control-panel.ts +69 -7
- package/src/panels/ops-strategy-panel.ts +61 -43
- package/src/panels/orchestration-panel.ts +34 -4
- package/src/panels/panel-list-panel.ts +33 -8
- package/src/panels/panel-manager.ts +23 -4
- package/src/panels/plan-dashboard-panel.ts +183 -66
- package/src/panels/plugins-panel.ts +48 -10
- package/src/panels/policy-panel.ts +60 -23
- package/src/panels/polish-core.ts +157 -0
- package/src/panels/polish-tables.ts +258 -0
- package/src/panels/polish.ts +44 -145
- package/src/panels/project-planning-panel.ts +27 -4
- package/src/panels/provider-accounts-panel.ts +55 -18
- package/src/panels/provider-health-panel.ts +118 -87
- package/src/panels/provider-stats-panel.ts +47 -22
- package/src/panels/qr-panel.ts +23 -11
- package/src/panels/remote-panel.ts +12 -5
- package/src/panels/routes-panel.ts +27 -5
- package/src/panels/sandbox-panel.ts +62 -27
- package/src/panels/schedule-panel.ts +44 -24
- package/src/panels/scrollable-list-panel.ts +124 -10
- package/src/panels/security-panel.ts +72 -20
- package/src/panels/services-panel.ts +68 -22
- package/src/panels/session-browser-panel.ts +42 -26
- package/src/panels/session-maintenance.ts +2 -2
- package/src/panels/settings-sync-panel.ts +30 -15
- package/src/panels/skills-panel.ts +2 -1
- package/src/panels/subscription-panel.ts +21 -3
- package/src/panels/symbol-outline-panel.ts +40 -47
- package/src/panels/system-messages-panel.ts +60 -27
- package/src/panels/tasks-panel.ts +36 -14
- package/src/panels/thinking-panel.ts +32 -36
- package/src/panels/token-budget-panel.ts +80 -53
- package/src/panels/tool-inspector-panel.ts +37 -39
- package/src/panels/types.ts +25 -0
- package/src/panels/watchers-panel.ts +25 -5
- package/src/panels/work-plan-panel.ts +127 -35
- package/src/panels/worktree-panel.ts +65 -20
- package/src/panels/wrfc-panel-format.ts +133 -0
- package/src/panels/wrfc-panel.ts +53 -147
- package/src/renderer/agent-detail-modal.ts +2 -2
- package/src/renderer/compaction-preview.ts +2 -1
- package/src/renderer/compositor.ts +46 -43
- package/src/renderer/modal-utils.ts +0 -10
- package/src/renderer/model-picker-overlay.ts +9 -4
- package/src/renderer/model-workspace.ts +2 -3
- package/src/renderer/panel-composite.ts +7 -20
- package/src/renderer/panel-workspace-bar.ts +14 -8
- package/src/renderer/process-modal.ts +2 -2
- package/src/renderer/progress.ts +3 -2
- package/src/renderer/tab-strip.ts +148 -34
- package/src/renderer/ui-factory.ts +4 -6
- package/src/runtime/bootstrap-command-context.ts +3 -0
- package/src/runtime/bootstrap-command-parts.ts +3 -1
- package/src/runtime/bootstrap-core.ts +31 -31
- package/src/runtime/bootstrap-shell.ts +1 -0
- package/src/runtime/onboarding/apply.ts +4 -3
- package/src/runtime/onboarding/snapshot.ts +4 -3
- package/src/runtime/process-lifecycle.ts +195 -0
- package/src/runtime/services.ts +4 -1
- package/src/runtime/ui/model-picker/health-enrichment.ts +3 -0
- package/src/runtime/ui/model-picker/types.ts +4 -0
- package/src/runtime/wrfc-persistence.ts +20 -5
- package/src/shell/ui-openers.ts +3 -2
- package/src/utils/format-duration.ts +55 -0
- package/src/utils/format-number.ts +71 -0
- package/src/verification/live-verifier.ts +4 -3
- package/src/version.ts +1 -1
- package/src/core/context-auto-compact.ts +0 -110
- package/src/panels/panel-picker.ts +0 -106
- package/src/renderer/panel-picker-overlay.ts +0 -202
- package/src/renderer/panel-tab-bar.ts +0 -69
|
@@ -4,13 +4,17 @@ import type { UiCommunicationSnapshot, UiReadModel } from '../runtime/ui-read-mo
|
|
|
4
4
|
import { truncateDisplay } from '../utils/terminal-width.ts';
|
|
5
5
|
import {
|
|
6
6
|
buildBodyText,
|
|
7
|
+
buildDetailBlock,
|
|
7
8
|
buildEmptyState,
|
|
8
9
|
buildGuidanceLine,
|
|
9
10
|
buildKeyValueLine,
|
|
11
|
+
buildKeyboardHints,
|
|
10
12
|
buildPanelLine,
|
|
11
13
|
buildPanelWorkspace,
|
|
14
|
+
buildStatusBadge,
|
|
12
15
|
DEFAULT_PANEL_PALETTE,
|
|
13
16
|
type PanelPalette,
|
|
17
|
+
type StatusBadgeKind,
|
|
14
18
|
} from './polish.ts';
|
|
15
19
|
import { createEmptyLine } from '../types/grid.ts';
|
|
16
20
|
|
|
@@ -26,13 +30,28 @@ const C = {
|
|
|
26
30
|
|
|
27
31
|
type CommunicationRecord = UiCommunicationSnapshot['records'][number];
|
|
28
32
|
|
|
33
|
+
function statusBadgeKind(status: CommunicationRecord['status']): StatusBadgeKind {
|
|
34
|
+
return status === 'blocked' ? 'blocked' : status === 'delivered' ? 'completed' : 'running';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function fmtAgo(ts: number): string {
|
|
38
|
+
const sec = Math.max(0, Math.floor((Date.now() - ts) / 1000));
|
|
39
|
+
if (sec < 60) return `${sec}s`;
|
|
40
|
+
if (sec < 3600) return `${Math.floor(sec / 60)}m`;
|
|
41
|
+
return `${Math.floor(sec / 3600)}h`;
|
|
42
|
+
}
|
|
43
|
+
|
|
29
44
|
export class CommunicationPanel extends ScrollableListPanel<CommunicationRecord> {
|
|
30
45
|
private readonly readModel?: UiReadModel<UiCommunicationSnapshot>;
|
|
31
46
|
private readonly unsub: (() => void) | null;
|
|
47
|
+
/** When true, the list is narrowed to blocked messages only (b toggles). */
|
|
48
|
+
private blockedOnly = false;
|
|
32
49
|
|
|
33
50
|
public constructor(readModel?: UiReadModel<UiCommunicationSnapshot>) {
|
|
34
51
|
super('communication', 'Communication', 'Y', 'monitoring');
|
|
35
52
|
this.showSelectionGutter = true; // I5: non-color selection affordance
|
|
53
|
+
this.filterEnabled = true;
|
|
54
|
+
this.filterLabel = 'Filter messages';
|
|
36
55
|
this.readModel = readModel;
|
|
37
56
|
this.unsub = readModel ? readModel.subscribe(() => this.markDirty()) : null;
|
|
38
57
|
}
|
|
@@ -47,18 +66,47 @@ export class CommunicationPanel extends ScrollableListPanel<CommunicationRecord>
|
|
|
47
66
|
|
|
48
67
|
protected getItems(): readonly CommunicationRecord[] {
|
|
49
68
|
if (!this.readModel) return [];
|
|
50
|
-
|
|
69
|
+
const records = this.readModel.getSnapshot().records;
|
|
70
|
+
return this.blockedOnly ? records.filter((r) => r.status === 'blocked') : records;
|
|
51
71
|
}
|
|
52
72
|
|
|
53
|
-
protected
|
|
73
|
+
protected override filterMatches(record: CommunicationRecord, q: string): boolean {
|
|
74
|
+
return (record.content ?? '').toLowerCase().includes(q)
|
|
75
|
+
|| record.kind.toLowerCase().includes(q)
|
|
76
|
+
|| String(record.fromId ?? '').toLowerCase().includes(q)
|
|
77
|
+
|| String(record.toId ?? '').toLowerCase().includes(q);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
public override handleInput(key: string): boolean {
|
|
81
|
+
// `b` toggles the blocked-lanes-only view — guarded so it only acts when not
|
|
82
|
+
// typing into the filter input.
|
|
83
|
+
if (key === 'b' && !this.filterActive) {
|
|
84
|
+
this.blockedOnly = !this.blockedOnly;
|
|
85
|
+
this.selectedIndex = 0;
|
|
86
|
+
this.markDirty();
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
return super.handleInput(key);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
protected renderItem(record: CommunicationRecord, _index: number, selected: boolean, width: number): Line {
|
|
54
93
|
const bg = selected ? C.selectBg : undefined;
|
|
55
|
-
const
|
|
94
|
+
const badge = buildStatusBadge(statusBadgeKind(record.status), record.status)[0]!;
|
|
95
|
+
const route = truncateDisplay(`${record.fromId} → ${record.toId}`, 26);
|
|
96
|
+
const contentWidth = Math.max(0, width - 50);
|
|
97
|
+
// Blocked messages have no delivered content; surface the policy reason
|
|
98
|
+
// inline so the most important detail (why a lane is blocked) is visible
|
|
99
|
+
// without selecting the row.
|
|
100
|
+
const tail = record.status === 'blocked' && record.reason
|
|
101
|
+
? { text: record.reason, fg: C.warn }
|
|
102
|
+
: { text: record.content, fg: C.dim };
|
|
56
103
|
return buildPanelLine(width, [
|
|
57
104
|
[' ', C.label, bg],
|
|
58
|
-
[record.
|
|
59
|
-
[` ${
|
|
60
|
-
[
|
|
61
|
-
[
|
|
105
|
+
[`${fmtAgo(record.timestamp)} `.padStart(5), C.dim, bg],
|
|
106
|
+
[` ${badge.text} `.padEnd(12), badge.fg, bg],
|
|
107
|
+
[`${record.kind} `.padEnd(9), C.info, bg],
|
|
108
|
+
[`${route} `.padEnd(27), C.value, bg],
|
|
109
|
+
[truncateDisplay(tail.text, contentWidth), tail.fg, bg],
|
|
62
110
|
]);
|
|
63
111
|
}
|
|
64
112
|
|
|
@@ -68,7 +116,7 @@ export class CommunicationPanel extends ScrollableListPanel<CommunicationRecord>
|
|
|
68
116
|
|
|
69
117
|
protected override getEmptyStateActions(): Array<{ command: string; summary: string }> {
|
|
70
118
|
return [
|
|
71
|
-
{ command: '/orchestration', summary: 'review graphs and recursive agent activity' },
|
|
119
|
+
{ command: '/orchestration', summary: 'review graphs and recursive agent activity that emit messages' },
|
|
72
120
|
{ command: '/communication', summary: 'reopen this workspace once the runtime emits message traffic' },
|
|
73
121
|
];
|
|
74
122
|
}
|
|
@@ -98,56 +146,73 @@ export class CommunicationPanel extends ScrollableListPanel<CommunicationRecord>
|
|
|
98
146
|
const snapshot = this.readModel.getSnapshot();
|
|
99
147
|
const records = this.getItems();
|
|
100
148
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
149
|
+
// Posture: blocked lanes / message flow surfaced first per the panel's purpose.
|
|
150
|
+
const postureLines = (): Line[] => [
|
|
151
|
+
buildPanelLine(width, [
|
|
152
|
+
[' ', C.label],
|
|
153
|
+
...buildStatusBadge('blocked', 'blocked', { count: snapshot.totalBlocked }),
|
|
154
|
+
[' ', C.dim],
|
|
155
|
+
...buildStatusBadge('completed', 'delivered', { count: snapshot.totalDelivered }),
|
|
156
|
+
[' ', C.dim],
|
|
157
|
+
...buildStatusBadge('running', 'sent', { count: snapshot.totalSent }),
|
|
158
|
+
...(this.blockedOnly ? ([[' (blocked-only view)', C.warn]] as Array<[string, string]>) : []),
|
|
159
|
+
]),
|
|
160
|
+
snapshot.totalBlocked > 0
|
|
161
|
+
? buildPanelLine(width, [[` ${snapshot.totalBlocked} message${snapshot.totalBlocked !== 1 ? 's' : ''} blocked by routing policy — press b to isolate blocked lanes.`, C.warn]])
|
|
162
|
+
: buildGuidanceLine(width, '/orchestration', 'inspect recursive routing, message handoff, and broadcast posture', C),
|
|
109
163
|
];
|
|
110
164
|
|
|
111
165
|
if (records.length === 0) {
|
|
112
166
|
return this.renderList(width, height, {
|
|
113
167
|
title: 'Communication Control Room',
|
|
114
|
-
header: postureLines,
|
|
168
|
+
header: postureLines(),
|
|
169
|
+
footer: [buildKeyboardHints(width, this.footerHints(false), C)],
|
|
115
170
|
});
|
|
116
171
|
}
|
|
117
172
|
|
|
118
173
|
this.clampSelection();
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
// Update posture with selected info
|
|
122
|
-
const postureWithSelected: Line[] = [
|
|
123
|
-
buildPanelLine(width, [[' Communication posture', C.label]]),
|
|
124
|
-
buildKeyValueLine(width, [
|
|
125
|
-
{ label: 'sent', value: String(snapshot.totalSent), valueColor: snapshot.totalSent > 0 ? C.info : C.dim },
|
|
126
|
-
{ label: 'delivered', value: String(snapshot.totalDelivered), valueColor: snapshot.totalDelivered > 0 ? C.ok : C.dim },
|
|
127
|
-
{ label: 'blocked', value: String(snapshot.totalBlocked), valueColor: snapshot.totalBlocked > 0 ? C.error : C.dim },
|
|
128
|
-
{ label: 'selected', value: `${selected?.fromId ?? 'n/a'} -> ${selected?.toId ?? 'n/a'}`, valueColor: C.value },
|
|
129
|
-
], C),
|
|
130
|
-
buildGuidanceLine(width, '/orchestration', 'inspect recursive routing, message handoff, and blocked broadcast posture', C),
|
|
131
|
-
];
|
|
174
|
+
const visible = this.getVisibleItems();
|
|
175
|
+
const selected = visible[this.selectedIndex];
|
|
132
176
|
|
|
133
|
-
const
|
|
177
|
+
const detailLines: Line[] = [];
|
|
134
178
|
if (selected) {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
179
|
+
detailLines.push(...buildDetailBlock(width, `Message · ${selected.kind}`, [
|
|
180
|
+
buildKeyValueLine(width, [
|
|
181
|
+
{ label: 'status', value: selected.status, valueColor: selected.status === 'blocked' ? C.error : C.ok },
|
|
182
|
+
{ label: 'scope', value: selected.scope, valueColor: C.value },
|
|
183
|
+
{ label: 'when', value: `${fmtAgo(selected.timestamp)} ago`, valueColor: C.dim },
|
|
184
|
+
], C),
|
|
185
|
+
buildKeyValueLine(width, [
|
|
186
|
+
{ label: 'from', value: `${selected.fromId} (${selected.fromRole ?? 'unknown'})`, valueColor: C.value },
|
|
187
|
+
{ label: 'to', value: `${selected.toId} (${selected.toRole ?? 'unknown'})`, valueColor: C.value },
|
|
188
|
+
], C),
|
|
189
|
+
...(selected.reason
|
|
190
|
+
? buildBodyText(width, `blocked: ${selected.reason}`, C, C.warn)
|
|
191
|
+
: []),
|
|
192
|
+
...buildBodyText(width, selected.content, C, C.value),
|
|
193
|
+
], C));
|
|
144
194
|
}
|
|
145
|
-
footerLines.push(buildPanelLine(width, [[' Up/Down move through messages', C.dim]]));
|
|
146
195
|
|
|
147
196
|
return this.renderList(width, height, {
|
|
148
197
|
title: 'Communication Control Room',
|
|
149
|
-
header:
|
|
150
|
-
footer:
|
|
198
|
+
header: postureLines(),
|
|
199
|
+
footer: [...detailLines, buildKeyboardHints(width, this.footerHints(true), C)],
|
|
151
200
|
});
|
|
152
201
|
}
|
|
202
|
+
|
|
203
|
+
// Context-aware hints: filter keys reflect filter state, `b` reflects the
|
|
204
|
+
// blocked-only toggle, and the inspect hint only appears when rows exist.
|
|
205
|
+
private footerHints(hasRows: boolean): Array<{ keys: string; label: string }> {
|
|
206
|
+
const hints: Array<{ keys: string; label: string }> = [];
|
|
207
|
+
if (hasRows) hints.push({ keys: '↑/↓', label: 'select' });
|
|
208
|
+
hints.push({ keys: 'b', label: this.blockedOnly ? 'show all lanes' : 'blocked only' });
|
|
209
|
+
if (this.filterActive) {
|
|
210
|
+
hints.push({ keys: 'Esc', label: 'clear filter' });
|
|
211
|
+
} else if (this.filterQuery) {
|
|
212
|
+
hints.push({ keys: '/', label: 'edit filter' }, { keys: 'Esc', label: 'clear filter' });
|
|
213
|
+
} else {
|
|
214
|
+
hints.push({ keys: '/', label: 'filter' });
|
|
215
|
+
}
|
|
216
|
+
return hints;
|
|
217
|
+
}
|
|
153
218
|
}
|
|
@@ -13,19 +13,19 @@ import type { SessionMemoryQuery } from '../runtime/ui-service-queries.ts';
|
|
|
13
13
|
import {
|
|
14
14
|
buildEmptyState,
|
|
15
15
|
buildGuidanceLine,
|
|
16
|
+
buildKeyboardHints,
|
|
16
17
|
buildMeterLine,
|
|
17
18
|
buildPanelLine,
|
|
19
|
+
buildStatusPill,
|
|
18
20
|
buildStyledPanelLine,
|
|
19
21
|
buildPanelWorkspace,
|
|
20
22
|
DEFAULT_PANEL_PALETTE,
|
|
23
|
+
extendPalette,
|
|
21
24
|
} from './polish.ts';
|
|
22
25
|
|
|
23
|
-
const C = {
|
|
26
|
+
const C = extendPalette(DEFAULT_PANEL_PALETTE, {
|
|
24
27
|
convFg: '#cc99ff',
|
|
25
|
-
|
|
26
|
-
barEmpty: '#333344',
|
|
27
|
-
labelFg: '#8888bb',
|
|
28
|
-
} as const;
|
|
28
|
+
});
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
|
|
@@ -49,8 +49,9 @@ export class ContextVisualizerPanel extends BasePanel {
|
|
|
49
49
|
sessionMemoryStore: SessionMemoryQuery,
|
|
50
50
|
private readonly configManager: Pick<ConfigManager, 'get'>,
|
|
51
51
|
private getUsage?: () => { input: number; output: number; cacheRead: number; cacheWrite: number; model?: string },
|
|
52
|
-
private contextLimit?: number,
|
|
52
|
+
private contextLimit?: number | (() => number),
|
|
53
53
|
private sessionReadModel?: UiReadModel<UiSessionSnapshot>,
|
|
54
|
+
private getLastInputTokens?: () => number,
|
|
54
55
|
) {
|
|
55
56
|
super('context', 'Context', 'C', 'ai');
|
|
56
57
|
this.sessionMemoryStore = sessionMemoryStore;
|
|
@@ -80,7 +81,7 @@ export class ContextVisualizerPanel extends BasePanel {
|
|
|
80
81
|
const pct = limit > 0 ? Math.min(100, Math.round((input / limit) * 100)) : 0;
|
|
81
82
|
const barWidth = Math.max(1, width - 2);
|
|
82
83
|
const overLimit = limit > 0 && input > limit;
|
|
83
|
-
const fg = overLimit ? C.
|
|
84
|
+
const fg = overLimit ? C.bad : C.convFg;
|
|
84
85
|
if (limit <= 0) {
|
|
85
86
|
return buildPanelWorkspace(width, height, {
|
|
86
87
|
title: ' Context Usage',
|
|
@@ -91,7 +92,10 @@ export class ContextVisualizerPanel extends BasePanel {
|
|
|
91
92
|
width,
|
|
92
93
|
' Context limit unavailable',
|
|
93
94
|
'Select a model with a known context window and submit or complete a turn to populate live context usage.',
|
|
94
|
-
[
|
|
95
|
+
[
|
|
96
|
+
{ command: '/model', summary: 'pick an active model so its context window is known' },
|
|
97
|
+
{ command: '/context', summary: 'review current context composition and pressure' },
|
|
98
|
+
],
|
|
95
99
|
DEFAULT_PANEL_PALETTE,
|
|
96
100
|
),
|
|
97
101
|
},
|
|
@@ -100,6 +104,11 @@ export class ContextVisualizerPanel extends BasePanel {
|
|
|
100
104
|
});
|
|
101
105
|
}
|
|
102
106
|
|
|
107
|
+
// Pressure state drives the headline pill and the footer hint so the most
|
|
108
|
+
// important signal — am I about to overflow context — is obvious at a glance.
|
|
109
|
+
const pressureState = overLimit ? 'bad' : pct >= 90 ? 'bad' : pct >= 75 ? 'warn' : 'good';
|
|
110
|
+
const pressureLabel = overLimit ? 'over limit' : pct >= 90 ? 'critical' : pct >= 75 ? 'elevated' : 'healthy';
|
|
111
|
+
|
|
103
112
|
return buildPanelWorkspace(width, height, {
|
|
104
113
|
title: ' Context Usage',
|
|
105
114
|
intro: 'Visualize current input-token pressure against the active model context window.',
|
|
@@ -114,6 +123,8 @@ export class ContextVisualizerPanel extends BasePanel {
|
|
|
114
123
|
[formatK(limit), DEFAULT_PANEL_PALETTE.info],
|
|
115
124
|
[' Fill ', DEFAULT_PANEL_PALETTE.label],
|
|
116
125
|
[`${pct}%`, overLimit ? DEFAULT_PANEL_PALETTE.bad : DEFAULT_PANEL_PALETTE.good],
|
|
126
|
+
[' ', DEFAULT_PANEL_PALETTE.dim],
|
|
127
|
+
...buildStatusPill(pressureState, pressureLabel),
|
|
117
128
|
]),
|
|
118
129
|
],
|
|
119
130
|
},
|
|
@@ -130,6 +141,18 @@ export class ContextVisualizerPanel extends BasePanel {
|
|
|
130
141
|
lines: this._renderMaintenance(width),
|
|
131
142
|
},
|
|
132
143
|
],
|
|
144
|
+
footerLines: [
|
|
145
|
+
buildKeyboardHints(width, pressureState === 'good'
|
|
146
|
+
? [
|
|
147
|
+
{ keys: '/context', label: 'composition' },
|
|
148
|
+
{ keys: '/model', label: 'switch model' },
|
|
149
|
+
]
|
|
150
|
+
: [
|
|
151
|
+
{ keys: '/compact', label: 'reduce context now' },
|
|
152
|
+
{ keys: '/context', label: 'composition' },
|
|
153
|
+
{ keys: '/model', label: 'larger window' },
|
|
154
|
+
], DEFAULT_PANEL_PALETTE),
|
|
155
|
+
],
|
|
133
156
|
palette: DEFAULT_PANEL_PALETTE,
|
|
134
157
|
});
|
|
135
158
|
});
|
|
@@ -138,10 +161,10 @@ export class ContextVisualizerPanel extends BasePanel {
|
|
|
138
161
|
private _renderBar(width: number, barWidth: number, input: number, limit: number): Line {
|
|
139
162
|
const filled = limit > 0 ? Math.min(barWidth, Math.round((input / limit) * barWidth)) : 0;
|
|
140
163
|
const overLimit = limit > 0 && input > limit;
|
|
141
|
-
const barFg = overLimit ? C.
|
|
164
|
+
const barFg = overLimit ? C.bad : C.convFg;
|
|
142
165
|
return buildMeterLine(width, filled, barWidth, {
|
|
143
166
|
filled: barFg,
|
|
144
|
-
empty: C.
|
|
167
|
+
empty: C.empty,
|
|
145
168
|
label: DEFAULT_PANEL_PALETTE.dim,
|
|
146
169
|
});
|
|
147
170
|
}
|
|
@@ -151,7 +174,7 @@ export class ContextVisualizerPanel extends BasePanel {
|
|
|
151
174
|
const valStr = formatK(val).padStart(7);
|
|
152
175
|
const pctStr = `${pct}%`.padStart(5);
|
|
153
176
|
return buildStyledPanelLine(width, [
|
|
154
|
-
{ text: labelPadded, fg: C.
|
|
177
|
+
{ text: labelPadded, fg: C.label },
|
|
155
178
|
{ text: valStr, fg },
|
|
156
179
|
{ text: ' ', fg: DEFAULT_PANEL_PALETTE.dim },
|
|
157
180
|
{ text: pctStr, fg },
|
|
@@ -159,10 +182,21 @@ export class ContextVisualizerPanel extends BasePanel {
|
|
|
159
182
|
}
|
|
160
183
|
|
|
161
184
|
private _refresh(): void {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
this.
|
|
185
|
+
// Resolve the context window live so the panel tracks /model switches when a
|
|
186
|
+
// getter is supplied; a plain number stays fixed at its provided value.
|
|
187
|
+
this.snapshot.limit =
|
|
188
|
+
typeof this.contextLimit === 'function' ? this.contextLimit() : (this.contextLimit ?? 0);
|
|
189
|
+
// Prefer the live per-call input occupancy (cache-inclusive, matching the
|
|
190
|
+
// Tokens panel and the auto-compaction threshold) when a getter is wired;
|
|
191
|
+
// otherwise fall back to the cumulative usage input.
|
|
192
|
+
const lastInput = this.getLastInputTokens?.();
|
|
193
|
+
if (lastInput !== undefined) {
|
|
194
|
+
this.snapshot.input = lastInput;
|
|
195
|
+
} else {
|
|
196
|
+
const usage = this.getUsage?.();
|
|
197
|
+
if (usage) {
|
|
198
|
+
this.snapshot.input = usage.input;
|
|
199
|
+
}
|
|
166
200
|
}
|
|
167
201
|
this.markDirty();
|
|
168
202
|
}
|
|
@@ -7,8 +7,10 @@ import {
|
|
|
7
7
|
buildEmptyState,
|
|
8
8
|
buildGuidanceLine,
|
|
9
9
|
buildKeyValueLine,
|
|
10
|
+
buildKeyboardHints,
|
|
10
11
|
buildPanelLine,
|
|
11
12
|
buildPanelWorkspace,
|
|
13
|
+
buildSectionHeader,
|
|
12
14
|
DEFAULT_PANEL_PALETTE,
|
|
13
15
|
type PanelPalette,
|
|
14
16
|
} from './polish.ts';
|
|
@@ -44,6 +46,8 @@ export class ControlPlanePanel extends ScrollableListPanel<ControlPlaneClient> {
|
|
|
44
46
|
public constructor(private readonly readModel?: UiReadModel<UiControlPlaneSnapshot>) {
|
|
45
47
|
super('control-plane', 'Control Plane', 'C', 'monitoring');
|
|
46
48
|
this.showSelectionGutter = true; // I5: non-color selection affordance
|
|
49
|
+
this.filterEnabled = true;
|
|
50
|
+
this.filterLabel = 'Filter clients';
|
|
47
51
|
this.unsub = readModel ? readModel.subscribe(() => this.markDirty()) : null;
|
|
48
52
|
}
|
|
49
53
|
|
|
@@ -55,6 +59,12 @@ export class ControlPlanePanel extends ScrollableListPanel<ControlPlaneClient> {
|
|
|
55
59
|
return C;
|
|
56
60
|
}
|
|
57
61
|
|
|
62
|
+
protected override filterMatches(client: ControlPlaneClient, q: string): boolean {
|
|
63
|
+
return client.kind.toLowerCase().includes(q)
|
|
64
|
+
|| client.label.toLowerCase().includes(q)
|
|
65
|
+
|| client.transport.toLowerCase().includes(q);
|
|
66
|
+
}
|
|
67
|
+
|
|
58
68
|
protected getItems(): readonly ControlPlaneClient[] {
|
|
59
69
|
if (!this.readModel) return [];
|
|
60
70
|
return this.readModel.getSnapshot().clients;
|
|
@@ -170,6 +180,8 @@ export class ControlPlanePanel extends ScrollableListPanel<ControlPlaneClient> {
|
|
|
170
180
|
}
|
|
171
181
|
|
|
172
182
|
if (approvals.length > 0) {
|
|
183
|
+
const pending = approvals.filter((entry) => entry.status === 'pending').length;
|
|
184
|
+
footerLines.push(buildSectionHeader(width, `Approvals (${pending} pending / ${approvals.length})`, C));
|
|
173
185
|
footerLines.push(
|
|
174
186
|
...approvals.slice(0, 6).map((approval) => buildPanelLine(width, [
|
|
175
187
|
[' ', C.label],
|
|
@@ -181,6 +193,8 @@ export class ControlPlanePanel extends ScrollableListPanel<ControlPlaneClient> {
|
|
|
181
193
|
}
|
|
182
194
|
|
|
183
195
|
if (sessions.length > 0) {
|
|
196
|
+
const activeSessions = sessions.filter((session) => session.status === 'active').length;
|
|
197
|
+
footerLines.push(buildSectionHeader(width, `Sessions (${activeSessions} active / ${sessions.length})`, C));
|
|
184
198
|
footerLines.push(
|
|
185
199
|
...sessions.slice(0, 6).map((session) => buildPanelLine(width, [
|
|
186
200
|
[' ', C.label],
|
|
@@ -192,6 +206,7 @@ export class ControlPlanePanel extends ScrollableListPanel<ControlPlaneClient> {
|
|
|
192
206
|
}
|
|
193
207
|
|
|
194
208
|
if (recentEvents.length > 0) {
|
|
209
|
+
footerLines.push(buildSectionHeader(width, `Recent Events (${recentEvents.length})`, C));
|
|
195
210
|
footerLines.push(
|
|
196
211
|
...recentEvents.slice(0, 6).map((event) => buildPanelLine(width, [
|
|
197
212
|
[' ', C.label],
|
|
@@ -200,7 +215,18 @@ export class ControlPlanePanel extends ScrollableListPanel<ControlPlaneClient> {
|
|
|
200
215
|
])),
|
|
201
216
|
);
|
|
202
217
|
}
|
|
203
|
-
footerLines.push(
|
|
218
|
+
footerLines.push(
|
|
219
|
+
this.filterActive
|
|
220
|
+
? buildKeyboardHints(width, [
|
|
221
|
+
{ keys: 'type', label: 'filter clients' },
|
|
222
|
+
{ keys: 'Enter', label: 'apply' },
|
|
223
|
+
{ keys: 'Esc', label: 'clear' },
|
|
224
|
+
], C)
|
|
225
|
+
: buildKeyboardHints(width, [
|
|
226
|
+
{ keys: 'Up/Down', label: 'select client' },
|
|
227
|
+
{ keys: '/', label: 'filter' },
|
|
228
|
+
], C),
|
|
229
|
+
);
|
|
204
230
|
|
|
205
231
|
return this.renderList(width, height, {
|
|
206
232
|
title: 'Control Plane',
|