@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.
Files changed (148) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +12 -7
  3. package/docs/foundation-artifacts/operator-contract.json +4 -1
  4. package/package.json +2 -2
  5. package/src/cli/bundle-command.ts +2 -1
  6. package/src/cli/service-posture.ts +2 -1
  7. package/src/core/context-usage.ts +53 -0
  8. package/src/core/conversation-rendering.ts +2 -2
  9. package/src/core/conversation.ts +53 -8
  10. package/src/core/session-recovery.ts +26 -18
  11. package/src/core/system-message-router.ts +42 -26
  12. package/src/core/turn-event-wiring.ts +13 -16
  13. package/src/daemon/handlers/calendar/caldav-client.ts +2 -1
  14. package/src/daemon/handlers/inbox/index.ts +2 -1
  15. package/src/daemon/handlers/inbox/poller.ts +2 -1
  16. package/src/daemon/handlers/inbox/providers/discord.ts +2 -1
  17. package/src/daemon/handlers/inbox/providers/email.ts +2 -1
  18. package/src/daemon/handlers/inbox/providers/route-util.ts +2 -1
  19. package/src/daemon/handlers/inbox/providers/slack.ts +2 -1
  20. package/src/daemon/handlers/triage/integration.ts +2 -1
  21. package/src/daemon/handlers/triage/pipeline.ts +2 -1
  22. package/src/daemon/handlers/triage/tagger/discord.ts +2 -1
  23. package/src/daemon/handlers/triage/tagger/imap.ts +4 -3
  24. package/src/export/gist-uploader.ts +3 -1
  25. package/src/input/command-registry.ts +1 -0
  26. package/src/input/commands/cloudflare-runtime.ts +2 -1
  27. package/src/input/commands/eval.ts +4 -3
  28. package/src/input/commands/guidance-runtime.ts +2 -4
  29. package/src/input/commands/health-runtime.ts +13 -7
  30. package/src/input/commands/knowledge.ts +2 -1
  31. package/src/input/commands/runtime-services.ts +40 -10
  32. package/src/input/handler-command-route.ts +1 -1
  33. package/src/input/handler-feed-routes.ts +61 -4
  34. package/src/input/handler-feed.ts +13 -0
  35. package/src/input/handler-interactions.ts +2 -1
  36. package/src/input/handler-modal-routes.ts +2 -1
  37. package/src/input/handler-onboarding-cloudflare.ts +2 -1
  38. package/src/input/handler-onboarding.ts +8 -8
  39. package/src/input/handler-picker-routes.ts +18 -13
  40. package/src/input/handler-ui-state.ts +5 -1
  41. package/src/input/keybindings.ts +5 -0
  42. package/src/input/model-picker.ts +5 -0
  43. package/src/input/panel-integration-actions.ts +10 -0
  44. package/src/input/tts-settings-actions.ts +2 -1
  45. package/src/main.ts +52 -60
  46. package/src/panels/agent-inspector-panel.ts +90 -15
  47. package/src/panels/agent-inspector-shared.ts +3 -3
  48. package/src/panels/agent-logs-panel.ts +149 -72
  49. package/src/panels/approval-panel.ts +146 -95
  50. package/src/panels/automation-control-panel.ts +24 -1
  51. package/src/panels/base-panel.ts +28 -3
  52. package/src/panels/builtin/agent.ts +3 -1
  53. package/src/panels/builtin/development.ts +2 -1
  54. package/src/panels/builtin/session.ts +1 -1
  55. package/src/panels/cockpit-panel.ts +24 -5
  56. package/src/panels/cockpit-read-model.ts +2 -1
  57. package/src/panels/communication-panel.ts +108 -43
  58. package/src/panels/context-visualizer-panel.ts +49 -15
  59. package/src/panels/control-plane-panel.ts +27 -1
  60. package/src/panels/cost-tracker-panel.ts +87 -65
  61. package/src/panels/debug-panel.ts +61 -37
  62. package/src/panels/diff-panel.ts +59 -30
  63. package/src/panels/docs-panel.ts +30 -24
  64. package/src/panels/eval-panel.ts +130 -81
  65. package/src/panels/expandable-list-panel.ts +189 -0
  66. package/src/panels/file-explorer-panel.ts +42 -42
  67. package/src/panels/file-preview-panel.ts +11 -3
  68. package/src/panels/forensics-panel.ts +27 -13
  69. package/src/panels/git-panel.ts +65 -84
  70. package/src/panels/hooks-panel.ts +36 -2
  71. package/src/panels/incident-review-panel.ts +31 -10
  72. package/src/panels/intelligence-panel.ts +152 -71
  73. package/src/panels/knowledge-graph-panel.ts +89 -27
  74. package/src/panels/local-auth-panel.ts +17 -11
  75. package/src/panels/marketplace-panel.ts +33 -13
  76. package/src/panels/memory-panel.ts +7 -5
  77. package/src/panels/ops-control-panel.ts +69 -7
  78. package/src/panels/ops-strategy-panel.ts +61 -43
  79. package/src/panels/orchestration-panel.ts +34 -4
  80. package/src/panels/panel-list-panel.ts +33 -8
  81. package/src/panels/panel-manager.ts +23 -4
  82. package/src/panels/plan-dashboard-panel.ts +183 -66
  83. package/src/panels/plugins-panel.ts +48 -10
  84. package/src/panels/policy-panel.ts +60 -23
  85. package/src/panels/polish-core.ts +157 -0
  86. package/src/panels/polish-tables.ts +258 -0
  87. package/src/panels/polish.ts +44 -145
  88. package/src/panels/project-planning-panel.ts +27 -4
  89. package/src/panels/provider-accounts-panel.ts +55 -18
  90. package/src/panels/provider-health-panel.ts +118 -87
  91. package/src/panels/provider-stats-panel.ts +47 -22
  92. package/src/panels/qr-panel.ts +23 -11
  93. package/src/panels/remote-panel.ts +12 -5
  94. package/src/panels/routes-panel.ts +27 -5
  95. package/src/panels/sandbox-panel.ts +62 -27
  96. package/src/panels/schedule-panel.ts +44 -24
  97. package/src/panels/scrollable-list-panel.ts +124 -10
  98. package/src/panels/security-panel.ts +72 -20
  99. package/src/panels/services-panel.ts +68 -22
  100. package/src/panels/session-browser-panel.ts +42 -26
  101. package/src/panels/session-maintenance.ts +2 -2
  102. package/src/panels/settings-sync-panel.ts +30 -15
  103. package/src/panels/skills-panel.ts +2 -1
  104. package/src/panels/subscription-panel.ts +21 -3
  105. package/src/panels/symbol-outline-panel.ts +40 -47
  106. package/src/panels/system-messages-panel.ts +60 -27
  107. package/src/panels/tasks-panel.ts +36 -14
  108. package/src/panels/thinking-panel.ts +32 -36
  109. package/src/panels/token-budget-panel.ts +80 -53
  110. package/src/panels/tool-inspector-panel.ts +37 -39
  111. package/src/panels/types.ts +25 -0
  112. package/src/panels/watchers-panel.ts +25 -5
  113. package/src/panels/work-plan-panel.ts +127 -35
  114. package/src/panels/worktree-panel.ts +65 -20
  115. package/src/panels/wrfc-panel-format.ts +133 -0
  116. package/src/panels/wrfc-panel.ts +53 -147
  117. package/src/renderer/agent-detail-modal.ts +2 -2
  118. package/src/renderer/compaction-preview.ts +2 -1
  119. package/src/renderer/compositor.ts +46 -43
  120. package/src/renderer/modal-utils.ts +0 -10
  121. package/src/renderer/model-picker-overlay.ts +9 -4
  122. package/src/renderer/model-workspace.ts +2 -3
  123. package/src/renderer/panel-composite.ts +7 -20
  124. package/src/renderer/panel-workspace-bar.ts +14 -8
  125. package/src/renderer/process-modal.ts +2 -2
  126. package/src/renderer/progress.ts +3 -2
  127. package/src/renderer/tab-strip.ts +148 -34
  128. package/src/renderer/ui-factory.ts +4 -6
  129. package/src/runtime/bootstrap-command-context.ts +3 -0
  130. package/src/runtime/bootstrap-command-parts.ts +3 -1
  131. package/src/runtime/bootstrap-core.ts +31 -31
  132. package/src/runtime/bootstrap-shell.ts +1 -0
  133. package/src/runtime/onboarding/apply.ts +4 -3
  134. package/src/runtime/onboarding/snapshot.ts +4 -3
  135. package/src/runtime/process-lifecycle.ts +195 -0
  136. package/src/runtime/services.ts +4 -1
  137. package/src/runtime/ui/model-picker/health-enrichment.ts +3 -0
  138. package/src/runtime/ui/model-picker/types.ts +4 -0
  139. package/src/runtime/wrfc-persistence.ts +20 -5
  140. package/src/shell/ui-openers.ts +3 -2
  141. package/src/utils/format-duration.ts +55 -0
  142. package/src/utils/format-number.ts +71 -0
  143. package/src/verification/live-verifier.ts +4 -3
  144. package/src/version.ts +1 -1
  145. package/src/core/context-auto-compact.ts +0 -110
  146. package/src/panels/panel-picker.ts +0 -106
  147. package/src/renderer/panel-picker-overlay.ts +0 -202
  148. 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
- return this.readModel.getSnapshot().records;
69
+ const records = this.readModel.getSnapshot().records;
70
+ return this.blockedOnly ? records.filter((r) => r.status === 'blocked') : records;
51
71
  }
52
72
 
53
- protected renderItem(record: CommunicationRecord, index: number, selected: boolean, width: number): Line {
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 color = record.status === 'blocked' ? C.error : record.status === 'delivered' ? C.ok : C.info;
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.status.padEnd(10), color, bg],
59
- [` ${record.kind.padEnd(10)}`, C.info, bg],
60
- [` ${truncateDisplay(`${record.fromId} -> ${record.toId}`, 28).padEnd(28)}`, C.value, bg],
61
- [` ${truncateDisplay(record.content, Math.max(0, width - 53))}`, C.dim, bg],
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
- const postureLines: Line[] = [
102
- buildPanelLine(width, [[' Communication posture', C.label]]),
103
- buildKeyValueLine(width, [
104
- { label: 'sent', value: String(snapshot.totalSent), valueColor: snapshot.totalSent > 0 ? C.info : C.dim },
105
- { label: 'delivered', value: String(snapshot.totalDelivered), valueColor: snapshot.totalDelivered > 0 ? C.ok : C.dim },
106
- { label: 'blocked', value: String(snapshot.totalBlocked), valueColor: snapshot.totalBlocked > 0 ? C.error : C.dim },
107
- ], C),
108
- buildGuidanceLine(width, '/orchestration', 'inspect recursive routing, message handoff, and blocked broadcast posture', C),
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 selected = records[this.selectedIndex];
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 footerLines: Line[] = [];
177
+ const detailLines: Line[] = [];
134
178
  if (selected) {
135
- footerLines.push(
136
- buildPanelLine(width, [[' Route: ', C.label], [`${selected.scope} / ${selected.kind}`, C.value], [' Status: ', C.label], [selected.status, selected.status === 'blocked' ? C.error : C.ok]]),
137
- buildPanelLine(width, [[' From: ', C.label], [selected.fromId, C.value], [' To: ', C.label], [selected.toId, C.value]]),
138
- buildPanelLine(width, [[' Roles: ', C.label], [`${selected.fromRole ?? 'unknown'} -> ${selected.toRole ?? 'unknown'}`, C.dim]]),
139
- );
140
- if (selected.reason) {
141
- footerLines.push(buildPanelLine(width, [[' Reason: ', C.label], [truncateDisplay(selected.reason, Math.max(0, width - 11)), C.warn]]));
142
- }
143
- footerLines.push(...buildBodyText(width, ` Content: ${selected.content}`, C));
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: postureWithSelected,
150
- footer: footerLines,
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
- overFg: '#ff6666',
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.overFg : C.convFg;
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.overFg : C.convFg;
164
+ const barFg = overLimit ? C.bad : C.convFg;
142
165
  return buildMeterLine(width, filled, barWidth, {
143
166
  filled: barFg,
144
- empty: C.barEmpty,
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.labelFg },
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
- const usage = this.getUsage?.();
163
- if (usage) {
164
- this.snapshot.input = usage.input;
165
- this.snapshot.limit = this.contextLimit ?? 0;
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(buildPanelLine(width, [[' Up/Down move through connected clients', C.dim]]));
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',