@pellux/goodvibes-tui 0.27.0 → 0.29.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 (143) hide show
  1. package/CHANGELOG.md +124 -0
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  4. package/src/core/context-usage.ts +53 -0
  5. package/src/core/conversation.ts +4 -6
  6. package/src/core/session-recovery.ts +2 -0
  7. package/src/core/turn-event-wiring.ts +1 -0
  8. package/src/input/commands/cost-runtime.ts +49 -0
  9. package/src/input/commands/eval.ts +4 -3
  10. package/src/input/commands/operator-runtime.ts +5 -1
  11. package/src/input/commands.ts +2 -0
  12. package/src/input/handler-feed-routes.ts +60 -26
  13. package/src/input/handler-feed.ts +17 -8
  14. package/src/input/handler-picker-routes.ts +18 -13
  15. package/src/input/handler-shortcuts.ts +51 -0
  16. package/src/input/handler-ui-state.ts +4 -0
  17. package/src/input/handler.ts +43 -6
  18. package/src/input/keybindings.ts +53 -8
  19. package/src/input/model-picker.ts +5 -0
  20. package/src/input/panel-integration-actions.ts +117 -1
  21. package/src/main.ts +5 -2
  22. package/src/panels/agent-inspector-panel.ts +139 -45
  23. package/src/panels/agent-inspector-shared.ts +147 -3
  24. package/src/panels/approval-panel.ts +204 -102
  25. package/src/panels/automation-control-panel.ts +370 -103
  26. package/src/panels/base-panel.ts +26 -2
  27. package/src/panels/builtin/agent.ts +28 -37
  28. package/src/panels/builtin/development.ts +40 -32
  29. package/src/panels/builtin/knowledge.ts +15 -6
  30. package/src/panels/builtin/operations.ts +178 -117
  31. package/src/panels/builtin/session.ts +35 -9
  32. package/src/panels/builtin/shared.ts +98 -6
  33. package/src/panels/cockpit-panel.ts +253 -75
  34. package/src/panels/cockpit-read-model.ts +2 -1
  35. package/src/panels/communication-panel.ts +159 -56
  36. package/src/panels/confirm-state.ts +8 -1
  37. package/src/panels/control-plane-panel.ts +354 -95
  38. package/src/panels/cost-tracker-panel.ts +243 -57
  39. package/src/panels/debug-panel.ts +347 -159
  40. package/src/panels/diff-panel.ts +257 -82
  41. package/src/panels/docs-panel.ts +166 -64
  42. package/src/panels/eval-panel.ts +336 -109
  43. package/src/panels/expandable-list-panel.ts +189 -0
  44. package/src/panels/file-explorer-panel.ts +238 -159
  45. package/src/panels/file-preview-panel.ts +141 -59
  46. package/src/panels/git-panel.ts +360 -191
  47. package/src/panels/hooks-panel.ts +181 -19
  48. package/src/panels/incident-review-panel.ts +252 -43
  49. package/src/panels/index.ts +0 -4
  50. package/src/panels/intelligence-panel.ts +310 -103
  51. package/src/panels/knowledge-graph-panel.ts +480 -58
  52. package/src/panels/local-auth-panel.ts +251 -33
  53. package/src/panels/marketplace-panel.ts +219 -33
  54. package/src/panels/memory-panel.ts +83 -81
  55. package/src/panels/ops-control-panel.ts +211 -32
  56. package/src/panels/ops-strategy-panel.ts +131 -45
  57. package/src/panels/orchestration-panel.ts +259 -67
  58. package/src/panels/panel-list-panel.ts +184 -136
  59. package/src/panels/panel-manager.ts +120 -13
  60. package/src/panels/plan-dashboard-panel.ts +507 -74
  61. package/src/panels/plugins-panel.ts +227 -34
  62. package/src/panels/policy-panel.ts +264 -55
  63. package/src/panels/polish-core.ts +162 -0
  64. package/src/panels/polish-tables.ts +258 -0
  65. package/src/panels/polish.ts +67 -149
  66. package/src/panels/project-planning-answer-actions.ts +134 -0
  67. package/src/panels/project-planning-panel.ts +84 -113
  68. package/src/panels/provider-health-panel.ts +438 -480
  69. package/src/panels/provider-health-routes.ts +203 -0
  70. package/src/panels/provider-health-tracker.ts +194 -6
  71. package/src/panels/provider-health-views.ts +560 -0
  72. package/src/panels/qr-panel.ts +136 -38
  73. package/src/panels/remote-panel.ts +123 -38
  74. package/src/panels/routes-panel.ts +87 -24
  75. package/src/panels/sandbox-panel.ts +232 -65
  76. package/src/panels/scrollable-list-panel.ts +143 -145
  77. package/src/panels/security-panel.ts +204 -52
  78. package/src/panels/services-panel.ts +174 -76
  79. package/src/panels/session-browser-panel.ts +101 -6
  80. package/src/panels/session-maintenance.ts +4 -122
  81. package/src/panels/settings-sync-panel.ts +346 -68
  82. package/src/panels/skills-panel.ts +157 -89
  83. package/src/panels/subscription-panel.ts +105 -34
  84. package/src/panels/symbol-outline-panel.ts +287 -154
  85. package/src/panels/system-messages-panel.ts +172 -38
  86. package/src/panels/tasks-panel.ts +348 -134
  87. package/src/panels/thinking-panel.ts +66 -32
  88. package/src/panels/token-budget-panel.ts +233 -46
  89. package/src/panels/tool-inspector-panel.ts +170 -54
  90. package/src/panels/types.ts +65 -4
  91. package/src/panels/work-plan-panel.ts +393 -39
  92. package/src/panels/worktree-panel.ts +239 -61
  93. package/src/panels/wrfc-panel-format.ts +133 -0
  94. package/src/panels/wrfc-panel.ts +146 -150
  95. package/src/renderer/compaction-preview.ts +2 -1
  96. package/src/renderer/compositor.ts +46 -43
  97. package/src/renderer/conversation-overlays.ts +25 -11
  98. package/src/renderer/footer-tips.ts +41 -0
  99. package/src/renderer/fullscreen-primitives.ts +22 -16
  100. package/src/renderer/help-overlay.ts +91 -14
  101. package/src/renderer/hint-grammar.ts +52 -0
  102. package/src/renderer/layout.ts +7 -7
  103. package/src/renderer/modal-factory.ts +23 -15
  104. package/src/renderer/model-picker-overlay.ts +30 -11
  105. package/src/renderer/model-workspace.ts +2 -3
  106. package/src/renderer/overlay-box.ts +16 -10
  107. package/src/renderer/panel-composite.ts +7 -20
  108. package/src/renderer/panel-workspace-bar.ts +14 -8
  109. package/src/renderer/process-indicator.ts +3 -1
  110. package/src/renderer/progress.ts +8 -6
  111. package/src/renderer/search-overlay.ts +27 -6
  112. package/src/renderer/session-picker-modal.ts +6 -4
  113. package/src/renderer/settings-modal.ts +70 -10
  114. package/src/renderer/shell-surface.ts +41 -15
  115. package/src/renderer/status-glyphs.ts +11 -9
  116. package/src/renderer/tab-strip.ts +148 -34
  117. package/src/renderer/theme.ts +60 -3
  118. package/src/renderer/ui-factory.ts +45 -36
  119. package/src/renderer/ui-primitives.ts +23 -2
  120. package/src/runtime/bootstrap-shell.ts +35 -18
  121. package/src/runtime/diagnostics/panels/index.ts +0 -3
  122. package/src/runtime/ui/model-picker/health-enrichment.ts +3 -0
  123. package/src/runtime/ui/model-picker/types.ts +4 -0
  124. package/src/shell/ui-openers.ts +14 -22
  125. package/src/utils/format-duration.ts +55 -0
  126. package/src/utils/format-number.ts +71 -0
  127. package/src/utils/splash-lines.ts +44 -3
  128. package/src/version.ts +1 -1
  129. package/src/work-plans/work-plan-store.ts +13 -0
  130. package/src/panels/agent-logs-panel.ts +0 -666
  131. package/src/panels/agent-logs-shared.ts +0 -129
  132. package/src/panels/context-visualizer-panel.ts +0 -214
  133. package/src/panels/forensics-panel.ts +0 -364
  134. package/src/panels/panel-picker.ts +0 -106
  135. package/src/panels/provider-account-snapshot.ts +0 -259
  136. package/src/panels/provider-accounts-panel.ts +0 -218
  137. package/src/panels/provider-stats-panel.ts +0 -366
  138. package/src/panels/schedule-panel.ts +0 -342
  139. package/src/panels/watchers-panel.ts +0 -193
  140. package/src/renderer/panel-picker-overlay.ts +0 -202
  141. package/src/renderer/panel-tab-bar.ts +0 -69
  142. package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
  143. package/src/runtime/diagnostics/panels/policy.ts +0 -177
@@ -1,40 +1,58 @@
1
1
  import type { Line } from '../types/grid.ts';
2
2
  import { createEmptyLine } from '../types/grid.ts';
3
+ import { truncateDisplay } from '../utils/terminal-width.ts';
3
4
  import { ScrollableListPanel } from './scrollable-list-panel.ts';
4
- import type { PluginManagerObserver, PluginStatus } from '@pellux/goodvibes-sdk/platform/plugins';
5
+ import type { PanelIntegrationContext } from './types.ts';
6
+ import type { PluginManager, PluginManagerObserver, PluginStatus } from '@pellux/goodvibes-sdk/platform/plugins';
5
7
  import {
8
+ type ConfirmState,
9
+ handleConfirmInput,
10
+ renderConfirmLines,
11
+ } from './confirm-state.ts';
12
+ import {
13
+ buildDetailBlock,
6
14
  buildEmptyState,
15
+ buildKeyValueLine,
16
+ buildKeyboardHints,
7
17
  buildPanelLine,
8
18
  buildPanelWorkspace,
9
19
  DEFAULT_PANEL_PALETTE,
10
20
  type PanelPalette,
11
21
  } from './polish.ts';
12
22
 
13
- const C = {
14
- ...DEFAULT_PANEL_PALETTE,
15
- header: '#94a3b8',
16
- headerBg: '#1e293b',
17
- ok: '#22c55e',
18
- warn: '#eab308',
19
- error: '#ef4444',
20
- info: '#38bdf8',
21
- selectBg: '#0f172a',
22
- } as const;
23
+ // Base chrome only — title band, state colors, and text tokens all come
24
+ // straight from DEFAULT_PANEL_PALETTE (WO-002).
25
+ const C = DEFAULT_PANEL_PALETTE;
26
+
27
+ /**
28
+ * WO-134: PluginsPanel drives real plugin-manager mutations (enable/disable/
29
+ * verify/lift-quarantine) from the keyboard, not just read-only inspection.
30
+ * `PluginManagerObserver` alone (the read-only surface most panels need) has
31
+ * no mutation methods, so the constructor param is widened to also require
32
+ * the handful of `PluginManager` methods this panel actually calls. The real
33
+ * runtime object passed at bootstrap (`services.pluginManager`, a full
34
+ * `PluginManager` instance — see src/runtime/services.ts) already satisfies
35
+ * this widened shape.
36
+ */
37
+ export type PluginManagerControls = PluginManagerObserver
38
+ & Pick<PluginManager, 'enable' | 'disable' | 'verify' | 'liftQuarantine'>;
39
+
40
+ type PluginConfirmSubject = { readonly kind: 'disable' | 'lift-quarantine'; readonly name: string };
23
41
 
24
42
  function trustColor(tier: PluginStatus['trustTier']): string {
25
43
  switch (tier) {
26
44
  case 'trusted':
27
- return C.ok;
45
+ return C.good;
28
46
  case 'limited':
29
47
  return C.warn;
30
48
  case 'untrusted':
31
- return C.error;
49
+ return C.bad;
32
50
  }
33
51
  }
34
52
 
35
53
  function statusColor(status: PluginStatus): string {
36
- if (status.quarantined) return C.error;
37
- if (status.active) return C.ok;
54
+ if (status.quarantined) return C.bad;
55
+ if (status.active) return C.good;
38
56
  if (status.enabled) return C.warn;
39
57
  return C.dim;
40
58
  }
@@ -47,25 +65,158 @@ function statusLabel(status: PluginStatus): string {
47
65
  }
48
66
 
49
67
  export class PluginsPanel extends ScrollableListPanel<PluginStatus> {
50
- private readonly manager: PluginManagerObserver;
68
+ private readonly manager: PluginManagerControls;
51
69
  private readonly unsub: (() => void) | null;
70
+ /** Pending destructive/state-changing confirm for 'd' disable and 'q' lift-quarantine. */
71
+ private confirmAction: ConfirmState<PluginConfirmSubject> | null = null;
72
+ /** Result of the last 'v' verify press, rendered in the detail block until the selection or a new verify changes it. */
73
+ private _verifyResult: { name: string; valid: boolean; fingerprint?: string | undefined; reason?: string | undefined } | null = null;
52
74
 
53
- public constructor(manager: PluginManagerObserver) {
54
- super('plugins', 'Plugins', 'P', 'monitoring');
75
+ public constructor(manager: PluginManagerControls) {
76
+ super('plugins', 'Plugins', '', 'automation-control');
55
77
  this.showSelectionGutter = true; // I5: non-color selection affordance
78
+ this.filterEnabled = true;
79
+ this.filterLabel = 'Filter plugins';
56
80
  this.manager = manager;
57
81
  this.unsub = manager.subscribe(() => this.markDirty());
58
82
  }
59
83
 
60
- public override onActivate(): void {
61
- super.onActivate();
62
- this.selectedIndex = 0;
84
+ protected override filterMatches(plugin: PluginStatus, q: string): boolean {
85
+ return plugin.name.toLowerCase().includes(q)
86
+ || plugin.trustTier.toLowerCase().includes(q)
87
+ || plugin.version.toLowerCase().includes(q);
63
88
  }
64
89
 
90
+ // WO-134: selection is preserved across onActivate — BasePanel's default
91
+ // (mark dirty, no index reset) is exactly what we want, so no override.
92
+
65
93
  public override onDestroy(): void {
66
94
  this.unsub?.();
67
95
  }
68
96
 
97
+ // -------------------------------------------------------------------------
98
+ // Input — e=enable, d=disable (confirm), v=verify, q=lift quarantine (confirm)
99
+ // -------------------------------------------------------------------------
100
+
101
+ public handleInput(key: string): boolean {
102
+ if (this.lastError !== null) this.clearError();
103
+
104
+ if (this.confirmAction) {
105
+ const result = handleConfirmInput(this.confirmAction, key);
106
+ if (result === 'confirmed') {
107
+ this._executeConfirmed(this.confirmAction.subject);
108
+ this.confirmAction = null;
109
+ this.markDirty();
110
+ return true;
111
+ }
112
+ if (result === 'cancelled') {
113
+ this.confirmAction = null;
114
+ this.markDirty();
115
+ }
116
+ return true;
117
+ }
118
+
119
+ if (!this.filterActive) {
120
+ switch (key) {
121
+ case 'e':
122
+ this._enableSelected();
123
+ return true;
124
+ case 'd':
125
+ this._requestDisable();
126
+ return true;
127
+ case 'v':
128
+ this._verifySelected();
129
+ return true;
130
+ case 'q': {
131
+ const plugin = this.getSelectedItem();
132
+ if (!plugin?.quarantined) return false;
133
+ this._requestLiftQuarantine();
134
+ return true;
135
+ }
136
+ case 'm': {
137
+ // Consumed here only when there is a quarantined plugin selected;
138
+ // the actual /recall dispatch happens in handlePanelIntegrationAction
139
+ // below, which needs the executeCommand bridge from the router.
140
+ const plugin = this.getSelectedItem();
141
+ return Boolean(plugin?.quarantined);
142
+ }
143
+ default:
144
+ break;
145
+ }
146
+ }
147
+
148
+ return super.handleInput(key);
149
+ }
150
+
151
+ /**
152
+ * WO-134: optional one-key capture-to-memory for a quarantined plugin — 'm'
153
+ * dispatches `/recall capture plugin <name>` (recall-capture.ts:146 pattern)
154
+ * instead of requiring the operator to type the command by hand.
155
+ */
156
+ public handlePanelIntegrationAction(key: string, ctx: PanelIntegrationContext): boolean {
157
+ if (key !== 'm' || !ctx.executeCommand) return false;
158
+ const plugin = this.getSelectedItem();
159
+ if (!plugin?.quarantined) return false;
160
+ void ctx.executeCommand('recall', ['capture', 'plugin', plugin.name]).catch(() => {});
161
+ return true;
162
+ }
163
+
164
+ private _enableSelected(): void {
165
+ const plugin = this.getSelectedItem();
166
+ if (!plugin) return;
167
+ void this.manager.enable(plugin.name).then((result) => {
168
+ if (!result.ok) this.setError(`Enable failed: ${result.error ?? 'unknown error'}`);
169
+ this.markDirty();
170
+ }).catch((err) => {
171
+ this.setError(`Enable failed: ${err instanceof Error ? err.message : String(err)}`);
172
+ this.markDirty();
173
+ });
174
+ }
175
+
176
+ private _requestDisable(): void {
177
+ const plugin = this.getSelectedItem();
178
+ if (!plugin) return;
179
+ this.confirmAction = { subject: { kind: 'disable', name: plugin.name }, label: plugin.name, verb: 'Disable' };
180
+ this.markDirty();
181
+ }
182
+
183
+ private _requestLiftQuarantine(): void {
184
+ const plugin = this.getSelectedItem();
185
+ if (!plugin?.quarantined) return;
186
+ this.confirmAction = { subject: { kind: 'lift-quarantine', name: plugin.name }, label: plugin.name, verb: 'Lift quarantine on' };
187
+ this.markDirty();
188
+ }
189
+
190
+ private _verifySelected(): void {
191
+ const plugin = this.getSelectedItem();
192
+ if (!plugin) return;
193
+ const result = this.manager.verify(plugin.name);
194
+ if (!result.ok && result.reason?.toLowerCase().includes('not found')) {
195
+ this._verifyResult = null;
196
+ this.setError(`Verify failed: ${result.reason}`);
197
+ this.markDirty();
198
+ return;
199
+ }
200
+ this._verifyResult = { name: plugin.name, valid: result.valid, fingerprint: result.fingerprint, reason: result.reason };
201
+ this.markDirty();
202
+ }
203
+
204
+ private _executeConfirmed(subject: PluginConfirmSubject): void {
205
+ if (subject.kind === 'disable') {
206
+ void this.manager.disable(subject.name).then((result) => {
207
+ if (!result.ok) this.setError(`Disable failed: ${result.error ?? 'unknown error'}`);
208
+ this.markDirty();
209
+ }).catch((err) => {
210
+ this.setError(`Disable failed: ${err instanceof Error ? err.message : String(err)}`);
211
+ this.markDirty();
212
+ });
213
+ return;
214
+ }
215
+ const result = this.manager.liftQuarantine(subject.name);
216
+ if (!result.ok) this.setError(`Lift quarantine failed: ${result.error ?? 'unknown error'}`);
217
+ this.markDirty();
218
+ }
219
+
69
220
  protected override getPalette(): PanelPalette {
70
221
  return C;
71
222
  }
@@ -122,15 +273,30 @@ export class PluginsPanel extends ScrollableListPanel<PluginStatus> {
122
273
  return workspace;
123
274
  }
124
275
 
276
+ // Provenance/error posture header — surface trust + quarantine pressure first.
277
+ const quarantined = plugins.filter((p) => p.quarantined).length;
278
+ const untrusted = plugins.filter((p) => p.trustTier === 'untrusted').length;
279
+ const active = plugins.filter((p) => p.active).length;
280
+ const headerLines: Line[] = [
281
+ buildKeyValueLine(width, [
282
+ { label: 'plugins', value: String(plugins.length), valueColor: C.info },
283
+ { label: 'active', value: String(active), valueColor: active > 0 ? C.good : C.dim },
284
+ { label: 'untrusted', value: String(untrusted), valueColor: untrusted > 0 ? C.warn : C.dim },
285
+ { label: 'quarantined', value: String(quarantined), valueColor: quarantined > 0 ? C.bad : C.dim },
286
+ ], C),
287
+ ];
288
+
125
289
  this.clampSelection();
126
- const selected = plugins[this.selectedIndex]!;
290
+ const selected = this.getSelectedItem()!;
127
291
  const selectedCaps = this.manager.capabilities(selected.name);
128
292
  const trustRecord = this.manager.getTrustRecord(selected.name);
129
293
  const quarantineRecord = this.manager.getQuarantineRecord(selected.name);
130
- const detailLines: Line[] = [
294
+ const detailRows: Line[] = [
131
295
  buildPanelLine(width, [
132
296
  [' Plugin: ', C.label],
133
297
  [selected.name, C.value],
298
+ [' v', C.label],
299
+ [selected.version, C.dim],
134
300
  [' State: ', C.label],
135
301
  [statusLabel(selected), statusColor(selected)],
136
302
  [' Trust: ', C.label],
@@ -138,41 +304,68 @@ export class PluginsPanel extends ScrollableListPanel<PluginStatus> {
138
304
  ]),
139
305
  buildPanelLine(width, [
140
306
  [' Description: ', C.label],
141
- [selected.description.slice(0, Math.max(0, width - 15)), C.dim],
307
+ [truncateDisplay(selected.description, Math.max(0, width - 15)), C.dim],
142
308
  ]),
143
309
  ];
144
310
 
145
311
  if (selectedCaps) {
146
- detailLines.push(buildPanelLine(width, [
312
+ detailRows.push(buildPanelLine(width, [
147
313
  [' Capabilities: ', C.label],
148
314
  [String(selectedCaps.requested.length), C.value],
149
315
  [' High-risk: ', C.label],
150
- [String(selectedCaps.highRisk.length), selectedCaps.highRisk.length > 0 ? C.warn : C.ok],
316
+ [String(selectedCaps.highRisk.length), selectedCaps.highRisk.length > 0 ? C.warn : C.good],
151
317
  [' Blocked: ', C.label],
152
- [String(selectedCaps.blocked.length), selectedCaps.blocked.length > 0 ? C.error : C.ok],
318
+ [String(selectedCaps.blocked.length), selectedCaps.blocked.length > 0 ? C.bad : C.good],
153
319
  ]));
154
320
  }
155
321
 
156
322
  if (trustRecord?.signatureFingerprint) {
157
- detailLines.push(buildPanelLine(width, [
323
+ detailRows.push(buildPanelLine(width, [
158
324
  [' Signature: ', C.label],
159
- [trustRecord.signatureFingerprint, C.info],
325
+ [truncateDisplay(trustRecord.signatureFingerprint, Math.max(0, width - 14)), C.info],
160
326
  ]));
327
+ } else {
328
+ detailRows.push(buildPanelLine(width, [[' Signature: unsigned (no provenance fingerprint on record)', C.warn]]));
161
329
  }
162
330
 
163
331
  if (quarantineRecord) {
164
- detailLines.push(buildPanelLine(width, [
332
+ detailRows.push(buildPanelLine(width, [
165
333
  [' Quarantine: ', C.label],
166
- [quarantineRecord.reason.slice(0, Math.max(0, width - 14)), C.error],
334
+ [truncateDisplay(quarantineRecord.reason, Math.max(0, width - 14)), C.bad],
167
335
  ]));
168
336
  }
169
337
 
170
- detailLines.push(buildPanelLine(width, [[' Inspect trust and capability state here, then use /plugin to take action.', C.dim]]));
171
- detailLines.push(buildPanelLine(width, [[' Up/Down move through discovered plugins', C.dim]]));
338
+ if (this._verifyResult && this._verifyResult.name === selected.name) {
339
+ const vr = this._verifyResult;
340
+ const verifySegments: Array<[string, string, string?]> = [
341
+ [' Verify: ', C.label],
342
+ [vr.valid ? 'VALID' : 'INVALID', vr.valid ? C.good : C.bad],
343
+ ];
344
+ if (vr.fingerprint) verifySegments.push([` fp=${truncateDisplay(vr.fingerprint, 20)}`, C.info]);
345
+ if (!vr.valid && vr.reason) verifySegments.push([` ${truncateDisplay(vr.reason, Math.max(0, width - 40))}`, C.warn]);
346
+ detailRows.push(buildPanelLine(width, verifySegments));
347
+ }
348
+
349
+ if (this.confirmAction) detailRows.push(...renderConfirmLines(width, this.confirmAction));
350
+
351
+ const hints = this.filterActive
352
+ ? [{ keys: 'type', label: 'filter' }, { keys: 'Enter', label: 'apply' }, { keys: 'Esc', label: 'clear' }]
353
+ : [
354
+ { keys: 'Up/Down', label: 'move' },
355
+ { keys: 'e', label: 'enable' },
356
+ { keys: 'd', label: 'disable' },
357
+ { keys: 'v', label: 'verify' },
358
+ ...(selected.quarantined ? [{ keys: 'q', label: 'lift quarantine' }, { keys: 'm', label: 'capture to memory' }] : []),
359
+ { keys: '/', label: 'filter' },
360
+ ];
172
361
 
173
362
  return this.renderList(width, height, {
174
363
  title: 'Plugin Control Room',
175
- footer: detailLines,
364
+ header: headerLines,
365
+ footer: [
366
+ ...buildDetailBlock(width, `Plugin · ${selected.name}`, detailRows, C),
367
+ buildKeyboardHints(width, hints, C),
368
+ ],
176
369
  });
177
370
  }
178
371
  }