@pellux/goodvibes-tui 0.21.0 → 0.23.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 (70) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +1 -1
  3. package/package.json +2 -1
  4. package/src/cli/completions/generate.ts +4 -8
  5. package/src/cli/entrypoint.ts +6 -0
  6. package/src/cli/management-commands.ts +1 -1
  7. package/src/cli/management-utils.ts +352 -0
  8. package/src/cli/management.ts +36 -334
  9. package/src/cli/parser.ts +17 -0
  10. package/src/cli/surface-command.ts +1 -1
  11. package/src/cli/types.ts +2 -0
  12. package/src/config/goodvibes-home-audit.ts +2 -0
  13. package/src/core/context-auto-compact.ts +110 -0
  14. package/src/core/conversation-rendering.ts +5 -2
  15. package/src/core/conversation-types.ts +24 -0
  16. package/src/core/conversation.ts +7 -12
  17. package/src/core/stream-event-wiring.ts +125 -7
  18. package/src/core/turn-event-wiring.ts +124 -0
  19. package/src/daemon/cli.ts +5 -0
  20. package/src/input/command-registry.ts +1 -0
  21. package/src/input/commands/channel-runtime.ts +139 -0
  22. package/src/input/commands/control-room-runtime.ts +5 -5
  23. package/src/input/commands/provider.ts +57 -3
  24. package/src/input/commands/runtime-services.ts +30 -1
  25. package/src/input/commands/session-workflow.ts +8 -16
  26. package/src/input/commands/session.ts +70 -20
  27. package/src/input/commands/share-runtime.ts +1 -1
  28. package/src/input/commands/shell-core.ts +54 -4
  29. package/src/input/commands.ts +2 -2
  30. package/src/input/handler-modal-routes.ts +37 -0
  31. package/src/input/handler-modal-token-routes.ts +19 -5
  32. package/src/input/handler-onboarding.ts +18 -0
  33. package/src/input/handler.ts +1 -0
  34. package/src/input/onboarding/onboarding-wizard-apply.ts +10 -0
  35. package/src/input/onboarding/onboarding-wizard-cloudflare-step.ts +14 -0
  36. package/src/input/onboarding/onboarding-wizard-steps.ts +6 -0
  37. package/src/input/onboarding/onboarding-wizard-validation.ts +77 -0
  38. package/src/input/settings-modal-behavior.ts +5 -0
  39. package/src/input/settings-modal-data.ts +77 -3
  40. package/src/input/settings-modal-mutations.ts +3 -0
  41. package/src/input/settings-modal-reset.ts +154 -0
  42. package/src/input/settings-modal.ts +55 -13
  43. package/src/main.ts +58 -50
  44. package/src/panels/agent-inspector-panel.ts +120 -18
  45. package/src/panels/agent-inspector-shared.ts +29 -0
  46. package/src/panels/builtin/development.ts +1 -0
  47. package/src/panels/builtin/knowledge.ts +14 -13
  48. package/src/panels/builtin/operations.ts +22 -1
  49. package/src/panels/builtin/shared.ts +7 -0
  50. package/src/panels/cockpit-panel.ts +123 -3
  51. package/src/panels/cockpit-read-model.ts +232 -0
  52. package/src/panels/index.ts +1 -1
  53. package/src/panels/knowledge-graph-panel.ts +84 -0
  54. package/src/panels/memory-panel.ts +370 -40
  55. package/src/panels/session-maintenance.ts +66 -15
  56. package/src/renderer/agent-detail-modal.ts +107 -3
  57. package/src/renderer/compaction-history-modal.ts +55 -0
  58. package/src/renderer/compaction-preview.ts +146 -0
  59. package/src/renderer/context-status-hint.ts +54 -0
  60. package/src/renderer/settings-modal-helpers.ts +2 -2
  61. package/src/renderer/settings-modal.ts +14 -3
  62. package/src/renderer/shell-surface.ts +10 -0
  63. package/src/runtime/bootstrap-command-parts.ts +4 -0
  64. package/src/runtime/bootstrap-core.ts +116 -0
  65. package/src/runtime/bootstrap-shell.ts +11 -0
  66. package/src/runtime/bootstrap.ts +7 -0
  67. package/src/runtime/services.ts +6 -1
  68. package/src/utils/browser.ts +29 -0
  69. package/src/version.ts +1 -1
  70. package/src/panels/knowledge-panel.ts +0 -343
@@ -40,6 +40,11 @@ export interface BootstrapShellState {
40
40
  readonly lastGitInfoRef: { value: GitHeaderInfo | undefined };
41
41
  readonly inputHistory: InputHistory;
42
42
  readonly systemMessageRouter: SystemMessageRouter;
43
+ /**
44
+ * Wire the agent detail modal opener after InputHandler is constructed.
45
+ * Call with `(id) => input.agentDetailModal.open(id)` from main.ts.
46
+ */
47
+ readonly setOpenAgentDetail: (fn: (agentId: string) => void) => void;
43
48
  }
44
49
 
45
50
  export interface BootstrapShellOptions {
@@ -103,6 +108,8 @@ export function createBootstrapShell(options: BootstrapShellOptions): BootstrapS
103
108
  providerRegistry: services.providerRegistry,
104
109
  });
105
110
 
111
+ const openAgentDetailRef: { fn: (agentId: string) => void } = { fn: (_agentId: string) => {} };
112
+
106
113
  let commandContextRef: CommandContext | null = null;
107
114
  registerBuiltinPanels(services.panelManager, {
108
115
  configManager,
@@ -143,6 +150,7 @@ export function createBootstrapShell(options: BootstrapShellOptions): BootstrapS
143
150
  hookActivityTracker: services.hookActivityTracker,
144
151
  hookWorkbench: services.hookWorkbench,
145
152
  mcpRegistry: services.mcpRegistry,
153
+ openAgentDetail: (agentId: string) => openAgentDetailRef.fn(agentId),
146
154
  daemonHomeDir: join(services.homeDirectory, '.goodvibes', 'daemon'),
147
155
  });
148
156
  services.panelManager.prewarmRegistered();
@@ -278,5 +286,8 @@ export function createBootstrapShell(options: BootstrapShellOptions): BootstrapS
278
286
  lastGitInfoRef,
279
287
  inputHistory,
280
288
  systemMessageRouter,
289
+ setOpenAgentDetail: (fn: (agentId: string) => void) => {
290
+ openAgentDetailRef.fn = fn;
291
+ },
281
292
  };
282
293
  }
@@ -118,6 +118,11 @@ export type BootstrapContext = RuntimeContext & {
118
118
  * stay out of the main conversation and go to the SystemMessagesPanel instead.
119
119
  */
120
120
  systemMessageRouter: SystemMessageRouter;
121
+ /**
122
+ * Wire the agent detail modal opener after InputHandler is constructed in main.ts.
123
+ * Call with `(id) => input.agentDetailModal.open(id)` once the InputHandler is ready.
124
+ */
125
+ setOpenAgentDetail: (fn: (agentId: string) => void) => void;
121
126
  };
122
127
 
123
128
  // ── Bootstrap function ────────────────────────────────────────────────────
@@ -292,6 +297,7 @@ export async function bootstrapRuntime(
292
297
  const gitStatusProvider = shell.gitStatusProvider;
293
298
  const inputHistory = shell.inputHistory;
294
299
  const lastGitInfoRef = shell.lastGitInfoRef;
300
+ const setOpenAgentDetail = shell.setOpenAgentDetail;
295
301
  const pluginCommandRegistry = {
296
302
  register(command: {
297
303
  readonly name: string;
@@ -622,6 +628,7 @@ export async function bootstrapRuntime(
622
628
  _getConfiguredProviderIds: () => services.providerRegistry.getConfiguredProviderIds(),
623
629
  commandRegistry,
624
630
  systemMessageRouter,
631
+ setOpenAgentDetail,
625
632
  shutdown: async (sessionData) => {
626
633
  // Clear bootstrap-owned subscriptions
627
634
  bootstrapUnsubs.forEach(fn => fn());
@@ -78,6 +78,7 @@ import { ComponentHealthMonitor } from '@/runtime/index.ts';
78
78
  import { WorktreeRegistry } from '@/runtime/index.ts';
79
79
  import { SandboxSessionRegistry } from '@/runtime/index.ts';
80
80
  import { createShellPathService, type ShellPathService } from '@/runtime/index.ts';
81
+ import { isFeatureFlagEnabled } from './surface-feature-flags.ts';
81
82
  import type { FeatureFlagManager } from '@/runtime/index.ts';
82
83
  import { createFeatureFlagManager } from '@/runtime/index.ts';
83
84
  import { PolicyRuntimeState } from '@/runtime/index.ts';
@@ -530,7 +531,11 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
530
531
  const worktreeRegistry = new WorktreeRegistry(workingDirectory);
531
532
  const webhookNotifier = new WebhookNotifier();
532
533
  const replayEngine = new DeterministicReplayEngine(workingDirectory);
533
- const providerOptimizer = new ProviderOptimizer(providerRegistry, providerCapabilityRegistry, false);
534
+ const providerOptimizer = new ProviderOptimizer(
535
+ providerRegistry,
536
+ providerCapabilityRegistry,
537
+ isFeatureFlagEnabled(configManager, 'provider-optimizer'),
538
+ );
534
539
  const sessionMemoryStore = new SessionMemoryStore();
535
540
  const sessionLineageTracker = new SessionLineageTracker();
536
541
  const sessionChangeTracker = new SessionChangeTracker();
@@ -0,0 +1,29 @@
1
+ /**
2
+ * browser.ts — cross-platform browser launcher utility.
3
+ *
4
+ * Extracted from cli/management.ts so it can be used by input-layer commands
5
+ * without creating an upward cli→input dependency. Lives in utils (Layer 0)
6
+ * and has no imports from shell-UI or entrypoint layers.
7
+ */
8
+
9
+ import { spawn } from 'node:child_process';
10
+ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
11
+
12
+ /**
13
+ * Open the given URL in the user's default browser.
14
+ * Returns a status string (success or error description).
15
+ * Does not throw — errors are returned as a descriptive string.
16
+ */
17
+ export function openBrowser(url: string): string {
18
+ const platform = process.platform;
19
+ const command = platform === 'darwin' ? 'open' : platform === 'win32' ? 'cmd' : 'xdg-open';
20
+ const args = platform === 'win32' ? ['/c', 'start', '', url] : [url];
21
+ try {
22
+ const child = spawn(command, args, { detached: true, stdio: 'ignore' });
23
+ child.once('error', () => {});
24
+ child.unref();
25
+ return 'browser open requested';
26
+ } catch (error) {
27
+ return `browser open failed: ${summarizeError(error)}`;
28
+ }
29
+ }
package/src/version.ts CHANGED
@@ -6,7 +6,7 @@ import { join } from 'node:path';
6
6
  // The prebuild script updates the fallback value before compilation.
7
7
  // Uses import.meta.dir (Bun) to locate package.json relative to this file,
8
8
  // which is correct regardless of the process working directory.
9
- let _version = '0.21.0';
9
+ let _version = '0.23.0';
10
10
  try {
11
11
  const pkg = JSON.parse(readFileSync(join(import.meta.dir, '..', 'package.json'), 'utf-8'));
12
12
  _version = pkg.version ?? _version;
@@ -1,343 +0,0 @@
1
- import type { Line } from '../types/grid.ts';
2
- import { ScrollableListPanel } from './scrollable-list-panel.ts';
3
- import type { KeyName } from './types.ts';
4
- import { type ConfirmState, handleConfirmInput, renderConfirmLines } from './confirm-state.ts';
5
- import type { MemoryClass, MemoryRecord, MemoryRegistry, MemoryReviewState } from '@pellux/goodvibes-sdk/platform/state';
6
- import {
7
- buildBodyText,
8
- buildEmptyState,
9
- buildGuidanceLine,
10
- buildKeyValueLine,
11
- buildPanelLine,
12
- buildPanelWorkspace,
13
- DEFAULT_PANEL_PALETTE,
14
- } from './polish.ts';
15
-
16
- function summarize(records: MemoryRecord[], cls: MemoryClass): MemoryRecord[] {
17
- return records.filter((record) => record.cls === cls).slice(0, 3);
18
- }
19
-
20
- const C = {
21
- ...DEFAULT_PANEL_PALETTE,
22
- header: '#94a3b8',
23
- headerBg: '#1e293b',
24
- } as const;
25
-
26
- function reviewStateColor(state: MemoryReviewState): string {
27
- switch (state) {
28
- case 'reviewed':
29
- return C.good;
30
- case 'stale':
31
- return C.warn;
32
- case 'contradicted':
33
- return C.bad;
34
- case 'fresh':
35
- default:
36
- return C.info;
37
- }
38
- }
39
-
40
- function formatConfidence(confidence: number): string {
41
- return `${confidence.toString().padStart(3, ' ')}%`;
42
- }
43
-
44
- export class KnowledgePanel extends ScrollableListPanel<MemoryRecord> {
45
- private readonly registry: MemoryRegistry;
46
- private unsubscribe?: () => void;
47
- private records: MemoryRecord[] = [];
48
- // I1: confirm for destructive review-state mutations
49
- private confirm: ConfirmState<{ id: string; action: 'stale' | 'contradicted' }> | null = null;
50
-
51
- public constructor(registry: MemoryRegistry) {
52
- super('knowledge', 'Knowledge', 'K', 'agent');
53
- this.registry = registry;
54
- }
55
-
56
- public override onActivate(): void {
57
- super.onActivate();
58
- this.refresh();
59
- this.unsubscribe = this.registry.subscribe(() => {
60
- this.refresh();
61
- this.markDirty();
62
- });
63
- }
64
-
65
- public override onDeactivate(): void {
66
- super.onDeactivate();
67
- }
68
-
69
- public override onDestroy(): void {
70
- this.unsubscribe?.();
71
- this.unsubscribe = undefined;
72
- }
73
-
74
- // ---------------------------------------------------------------------------
75
- // ScrollableListPanel implementation
76
- // ---------------------------------------------------------------------------
77
-
78
- protected getItems(): readonly MemoryRecord[] {
79
- return this.records;
80
- }
81
-
82
- protected renderItem(record: MemoryRecord, index: number, selected: boolean, width: number): Line {
83
- const bg = selected ? C.selectBg : undefined;
84
- return buildPanelLine(width, [
85
- [' ', C.label, bg],
86
- [record.reviewState.padEnd(13), reviewStateColor(record.reviewState), bg],
87
- [` ${formatConfidence(record.confidence)} `, C.value, bg],
88
- [record.summary.slice(0, Math.max(0, width - 26)), C.value, bg],
89
- ]);
90
- }
91
-
92
- protected override getPalette() { return C; }
93
- protected override getEmptyStateMessage() { return 'No durable project knowledge'; }
94
- protected override getEmptyStateActions() {
95
- return [
96
- { command: '/recall add fact <summary>', summary: 'capture a durable fact directly' },
97
- { command: '/recall capture incident latest', summary: 'promote the latest incident into project memory' },
98
- { command: '/recall capture policy', summary: 'store the current policy posture as durable evidence' },
99
- ];
100
- }
101
-
102
- // ---------------------------------------------------------------------------
103
- // Input
104
- // ---------------------------------------------------------------------------
105
-
106
- public handleInput(key: KeyName): boolean {
107
- // I1: y/n confirm for stale/contradict
108
- if (this.confirm) {
109
- const result = handleConfirmInput(this.confirm, key);
110
- if (result === 'confirmed') {
111
- const { id, action } = this.confirm.subject;
112
- this.confirm = null;
113
- const selected = this.records.find((r) => r.id === id);
114
- if (selected) {
115
- try {
116
- if (action === 'stale') {
117
- this.registry.review(id, {
118
- state: 'stale',
119
- confidence: Math.min(selected.confidence, 40),
120
- reviewedBy: 'operator',
121
- staleReason: 'marked stale from the knowledge panel',
122
- });
123
- } else {
124
- this.registry.review(id, {
125
- state: 'contradicted',
126
- confidence: 0,
127
- reviewedBy: 'operator',
128
- staleReason: 'marked contradicted from the knowledge panel',
129
- });
130
- }
131
- } catch (e) {
132
- // I2: surface async failure
133
- this.setError(`Review update failed: ${e instanceof Error ? e.message : String(e)}`);
134
- }
135
- }
136
- this.refresh();
137
- this.markDirty();
138
- return true;
139
- }
140
- if (result === 'cancelled') {
141
- this.confirm = null;
142
- this.markDirty();
143
- return true;
144
- }
145
- if (result === 'absorbed') return true;
146
- }
147
-
148
- // I2: auto-clear error on next keypress (inherited via super.handleInput)
149
- if (this.records.length === 0) return super.handleInput(key);
150
-
151
- const selected = this.records[this.selectedIndex];
152
-
153
- if (key === 'enter' || key === 'return' || key === 'r') {
154
- if (!selected) return false;
155
- this.registry.review(selected.id, {
156
- state: 'reviewed',
157
- confidence: Math.max(selected.confidence, 85),
158
- reviewedBy: 'operator',
159
- });
160
- this.refresh();
161
- this.markDirty();
162
- return true;
163
- }
164
- if (key === 's') {
165
- if (!selected) return false;
166
- // I1: prompt confirm before marking stale
167
- this.confirm = { subject: { id: selected.id, action: 'stale' }, label: selected.summary.slice(0, 40) };
168
- this.markDirty();
169
- return true;
170
- }
171
- if (key === 'c') {
172
- if (!selected) return false;
173
- // I1: prompt confirm before marking contradicted
174
- this.confirm = { subject: { id: selected.id, action: 'contradicted' }, label: selected.summary.slice(0, 40) };
175
- this.markDirty();
176
- return true;
177
- }
178
- if (key === 'f') {
179
- if (!selected) return false;
180
- this.registry.review(selected.id, {
181
- state: 'fresh',
182
- confidence: Math.max(selected.confidence, 60),
183
- reviewedBy: 'operator',
184
- });
185
- this.refresh();
186
- this.markDirty();
187
- return true;
188
- }
189
-
190
- return super.handleInput(key);
191
- }
192
-
193
- private refresh(): void {
194
- const queue = this.registry.reviewQueue(24);
195
- this.records = queue.length > 0 ? queue : this.registry.search({ limit: 24 });
196
- this.clampSelection();
197
- }
198
-
199
- public render(width: number, height: number): Line[] {
200
- this.clampSelection();
201
-
202
- // I1: show confirm dialog in place of normal content
203
- if (this.confirm) {
204
- return buildPanelWorkspace(width, height, {
205
- title: 'Knowledge Control Room',
206
- intro: '',
207
- sections: [{ title: 'Confirmation', lines: renderConfirmLines(width, this.confirm) }],
208
- footerLines: [buildPanelLine(width, [[' y confirm n / Esc cancel', C.dim]])],
209
- palette: C,
210
- });
211
- }
212
-
213
- if (this.records.length === 0) this.refresh();
214
-
215
- const intro = 'Typed project knowledge, reviewed evidence, and operator-governed memory across session, project, and team scopes.';
216
- const records = this.registry.search({ limit: 200 });
217
-
218
- if (records.length === 0) {
219
- return this.renderList(width, height, {
220
- title: 'Knowledge Control Room',
221
- footer: [buildPanelLine(width, [[' Review keys: Up/Down move r/Enter review s stale c contradicted f fresh', C.dim]])],
222
- });
223
- }
224
-
225
- const queue = this.registry.reviewQueue(24);
226
- const byClass = new Map<MemoryClass, number>();
227
- const byReview = new Map<MemoryReviewState, number>();
228
- const byScope = new Map<string, number>();
229
- for (const record of records) {
230
- byClass.set(record.cls, (byClass.get(record.cls) ?? 0) + 1);
231
- byReview.set(record.reviewState, (byReview.get(record.reviewState) ?? 0) + 1);
232
- byScope.set(record.scope, (byScope.get(record.scope) ?? 0) + 1);
233
- }
234
-
235
- const classLines: Line[] = [
236
- buildPanelLine(width, [
237
- [' facts ', C.label], [String(byClass.get('fact') ?? 0), C.good],
238
- [' risks ', C.label], [String(byClass.get('risk') ?? 0), (byClass.get('risk') ?? 0) > 0 ? C.warn : C.good],
239
- [' runbooks ', C.label], [String(byClass.get('runbook') ?? 0), C.info],
240
- [' architecture ', C.label], [String(byClass.get('architecture') ?? 0), C.info],
241
- [' incidents ', C.label], [String(byClass.get('incident') ?? 0), (byClass.get('incident') ?? 0) > 0 ? C.bad : C.good],
242
- ]),
243
- buildPanelLine(width, [
244
- [' decisions ', C.label], [String(byClass.get('decision') ?? 0), C.value],
245
- [' constraints ', C.label], [String(byClass.get('constraint') ?? 0), C.value],
246
- [' ownership ', C.label], [String(byClass.get('ownership') ?? 0), C.value],
247
- [' patterns ', C.label], [String(byClass.get('pattern') ?? 0), C.value],
248
- [' total ', C.label], [String(records.length), C.value],
249
- ]),
250
- ];
251
-
252
- const reviewLines: Line[] = [
253
- buildPanelLine(width, [
254
- [' reviewed ', C.label], [String(byReview.get('reviewed') ?? 0), C.good],
255
- [' fresh ', C.label], [String(byReview.get('fresh') ?? 0), C.info],
256
- [' stale ', C.label], [String(byReview.get('stale') ?? 0), C.warn],
257
- [' contradicted ', C.label], [String(byReview.get('contradicted') ?? 0), C.bad],
258
- [' Review Queue ', C.label], [String(queue.length), queue.length > 0 ? C.warn : C.good],
259
- ]),
260
- buildPanelLine(width, [
261
- [' session ', C.label], [String(byScope.get('session') ?? 0), C.info],
262
- [' project ', C.label], [String(byScope.get('project') ?? 0), C.value],
263
- [' team ', C.label], [String(byScope.get('team') ?? 0), C.good],
264
- ]),
265
- buildGuidanceLine(width, '/recall review', 'work the stale and contradicted queue from the command surface', C),
266
- ];
267
-
268
- const recentSummaryLines: Line[] = [];
269
- for (const [title, items, color] of [
270
- ['Recent Risks', summarize(records, 'risk'), C.warn],
271
- ['Runbooks', summarize(records, 'runbook'), C.info],
272
- ['Architecture Notes', summarize(records, 'architecture'), C.info],
273
- ['Recent Incidents', summarize(records, 'incident'), C.bad],
274
- ] as const) {
275
- if (recentSummaryLines.length > 0) {
276
- recentSummaryLines.push(buildPanelLine(width, [['', C.dim]]));
277
- }
278
- recentSummaryLines.push(buildPanelLine(width, [[` ${title}`, C.label]]));
279
- if (items.length === 0) {
280
- recentSummaryLines.push(buildPanelLine(width, [[' none recorded', C.dim]]));
281
- continue;
282
- }
283
- for (const item of items) {
284
- recentSummaryLines.push(buildPanelLine(width, [
285
- [' ', C.label],
286
- [item.summary.slice(0, Math.max(0, width - 2)), color],
287
- ]));
288
- }
289
- }
290
-
291
- const selectedRecord = this.records[this.selectedIndex];
292
- const selectedLines: Line[] = [];
293
- if (selectedRecord) {
294
- selectedLines.push(buildPanelLine(width, [[' Selected', C.label]]));
295
- selectedLines.push(buildKeyValueLine(width, [
296
- { label: 'Class', value: selectedRecord.cls, valueColor: C.value },
297
- { label: 'Scope', value: selectedRecord.scope, valueColor: C.info },
298
- { label: 'Review', value: selectedRecord.reviewState, valueColor: reviewStateColor(selectedRecord.reviewState) },
299
- { label: 'Confidence', value: formatConfidence(selectedRecord.confidence), valueColor: C.value },
300
- ], C));
301
- selectedLines.push(...buildBodyText(width, `Summary: ${selectedRecord.summary}`, C, C.value));
302
- if (selectedRecord.detail) selectedLines.push(...buildBodyText(width, `Detail: ${selectedRecord.detail}`, C, C.dim));
303
- if (selectedRecord.provenance.length) {
304
- selectedLines.push(...buildBodyText(
305
- width,
306
- `Provenance: ${selectedRecord.provenance.map((p) => `${p.kind}:${p.ref}`).join(', ')}`,
307
- C,
308
- C.dim,
309
- ));
310
- }
311
- if (selectedRecord.staleReason) {
312
- selectedLines.push(...buildBodyText(
313
- width,
314
- `Stale reason: ${selectedRecord.staleReason}`,
315
- C,
316
- selectedRecord.reviewState === 'contradicted' ? C.bad : C.warn,
317
- ));
318
- }
319
- if (selectedRecord.reviewedAt) {
320
- selectedLines.push(buildPanelLine(width, [
321
- [' Reviewed: ', C.label],
322
- [new Date(selectedRecord.reviewedAt).toLocaleString(), C.dim],
323
- ]));
324
- if (selectedRecord.reviewedBy) {
325
- selectedLines.push(buildPanelLine(width, [
326
- [' Reviewer: ', C.label],
327
- [selectedRecord.reviewedBy, C.dim],
328
- ]));
329
- }
330
- }
331
- }
332
-
333
- return this.renderList(width, height, {
334
- title: 'Knowledge Control Room',
335
- header: [...classLines, ...reviewLines],
336
- footer: [
337
- ...(selectedLines.length > 0 ? selectedLines : []),
338
- ...recentSummaryLines,
339
- buildPanelLine(width, [[' Up/Down move r/Enter reviewed s stale c contradicted f fresh', C.dim]]),
340
- ],
341
- });
342
- }
343
- }