@pellux/goodvibes-tui 0.27.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 (101) hide show
  1. package/CHANGELOG.md +23 -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/eval.ts +4 -3
  9. package/src/input/handler-feed-routes.ts +61 -4
  10. package/src/input/handler-feed.ts +13 -0
  11. package/src/input/handler-picker-routes.ts +18 -13
  12. package/src/input/handler-ui-state.ts +4 -0
  13. package/src/input/keybindings.ts +5 -0
  14. package/src/input/model-picker.ts +5 -0
  15. package/src/input/panel-integration-actions.ts +10 -0
  16. package/src/main.ts +0 -1
  17. package/src/panels/agent-inspector-panel.ts +18 -13
  18. package/src/panels/agent-inspector-shared.ts +3 -3
  19. package/src/panels/agent-logs-panel.ts +22 -53
  20. package/src/panels/approval-panel.ts +146 -95
  21. package/src/panels/automation-control-panel.ts +24 -1
  22. package/src/panels/base-panel.ts +26 -2
  23. package/src/panels/builtin/development.ts +1 -1
  24. package/src/panels/cockpit-panel.ts +24 -5
  25. package/src/panels/cockpit-read-model.ts +2 -1
  26. package/src/panels/communication-panel.ts +108 -43
  27. package/src/panels/context-visualizer-panel.ts +25 -1
  28. package/src/panels/control-plane-panel.ts +27 -1
  29. package/src/panels/cost-tracker-panel.ts +82 -54
  30. package/src/panels/debug-panel.ts +51 -16
  31. package/src/panels/diff-panel.ts +59 -30
  32. package/src/panels/docs-panel.ts +19 -3
  33. package/src/panels/eval-panel.ts +130 -81
  34. package/src/panels/expandable-list-panel.ts +189 -0
  35. package/src/panels/file-explorer-panel.ts +40 -41
  36. package/src/panels/file-preview-panel.ts +9 -2
  37. package/src/panels/forensics-panel.ts +27 -13
  38. package/src/panels/git-panel.ts +58 -70
  39. package/src/panels/hooks-panel.ts +36 -2
  40. package/src/panels/incident-review-panel.ts +31 -10
  41. package/src/panels/intelligence-panel.ts +152 -71
  42. package/src/panels/knowledge-graph-panel.ts +89 -27
  43. package/src/panels/local-auth-panel.ts +17 -11
  44. package/src/panels/marketplace-panel.ts +31 -12
  45. package/src/panels/memory-panel.ts +5 -4
  46. package/src/panels/ops-control-panel.ts +69 -7
  47. package/src/panels/ops-strategy-panel.ts +61 -43
  48. package/src/panels/orchestration-panel.ts +34 -4
  49. package/src/panels/panel-list-panel.ts +33 -8
  50. package/src/panels/panel-manager.ts +23 -4
  51. package/src/panels/plan-dashboard-panel.ts +183 -66
  52. package/src/panels/plugins-panel.ts +48 -10
  53. package/src/panels/policy-panel.ts +60 -23
  54. package/src/panels/polish-core.ts +157 -0
  55. package/src/panels/polish-tables.ts +258 -0
  56. package/src/panels/polish.ts +44 -145
  57. package/src/panels/project-planning-panel.ts +22 -0
  58. package/src/panels/provider-accounts-panel.ts +55 -18
  59. package/src/panels/provider-health-panel.ts +65 -23
  60. package/src/panels/provider-stats-panel.ts +47 -22
  61. package/src/panels/qr-panel.ts +23 -11
  62. package/src/panels/remote-panel.ts +12 -5
  63. package/src/panels/routes-panel.ts +27 -5
  64. package/src/panels/sandbox-panel.ts +62 -27
  65. package/src/panels/schedule-panel.ts +40 -17
  66. package/src/panels/scrollable-list-panel.ts +124 -10
  67. package/src/panels/security-panel.ts +72 -20
  68. package/src/panels/services-panel.ts +68 -22
  69. package/src/panels/session-browser-panel.ts +29 -5
  70. package/src/panels/session-maintenance.ts +2 -2
  71. package/src/panels/settings-sync-panel.ts +30 -15
  72. package/src/panels/subscription-panel.ts +21 -3
  73. package/src/panels/symbol-outline-panel.ts +40 -47
  74. package/src/panels/system-messages-panel.ts +60 -27
  75. package/src/panels/tasks-panel.ts +34 -7
  76. package/src/panels/thinking-panel.ts +26 -25
  77. package/src/panels/token-budget-panel.ts +49 -38
  78. package/src/panels/tool-inspector-panel.ts +27 -21
  79. package/src/panels/types.ts +25 -0
  80. package/src/panels/watchers-panel.ts +25 -5
  81. package/src/panels/work-plan-panel.ts +125 -34
  82. package/src/panels/worktree-panel.ts +65 -20
  83. package/src/panels/wrfc-panel-format.ts +133 -0
  84. package/src/panels/wrfc-panel.ts +31 -127
  85. package/src/renderer/compaction-preview.ts +2 -1
  86. package/src/renderer/compositor.ts +46 -43
  87. package/src/renderer/model-picker-overlay.ts +9 -4
  88. package/src/renderer/model-workspace.ts +2 -3
  89. package/src/renderer/panel-composite.ts +7 -20
  90. package/src/renderer/panel-workspace-bar.ts +14 -8
  91. package/src/renderer/progress.ts +3 -2
  92. package/src/renderer/tab-strip.ts +148 -34
  93. package/src/renderer/ui-factory.ts +4 -6
  94. package/src/runtime/ui/model-picker/health-enrichment.ts +3 -0
  95. package/src/runtime/ui/model-picker/types.ts +4 -0
  96. package/src/utils/format-duration.ts +55 -0
  97. package/src/utils/format-number.ts +71 -0
  98. package/src/version.ts +1 -1
  99. package/src/panels/panel-picker.ts +0 -106
  100. package/src/renderer/panel-picker-overlay.ts +0 -202
  101. package/src/renderer/panel-tab-bar.ts +0 -69
@@ -1,4 +1,5 @@
1
1
  import type { Line } from '../types/grid.ts';
2
+ import { fitDisplay, truncateDisplay } from '../utils/terminal-width.ts';
2
3
  import type { ForensicsRegistry } from '@/runtime/index.ts';
3
4
  import { ScrollableListPanel } from './scrollable-list-panel.ts';
4
5
  import {
@@ -42,6 +43,8 @@ export class IncidentReviewPanel extends ScrollableListPanel<FailureReport> {
42
43
  public constructor(registry?: ForensicsRegistry) {
43
44
  super('incident', 'Incident Review', 'N', 'monitoring');
44
45
  this.showSelectionGutter = true; // I5: non-color selection affordance
46
+ this.filterEnabled = true;
47
+ this.filterLabel = 'Filter incidents';
45
48
  this.registry = registry;
46
49
  this.unsub = registry ? registry.subscribe(() => this.markDirty()) : null;
47
50
  }
@@ -58,13 +61,19 @@ export class IncidentReviewPanel extends ScrollableListPanel<FailureReport> {
58
61
  return this.registry?.getAll() ?? [];
59
62
  }
60
63
 
64
+ protected override filterMatches(report: FailureReport, q: string): boolean {
65
+ return report.classification.toLowerCase().includes(q)
66
+ || report.id.toLowerCase().includes(q)
67
+ || (report.summary ?? '').toLowerCase().includes(q);
68
+ }
69
+
61
70
  protected renderItem(report: FailureReport, index: number, selected: boolean, width: number): Line {
62
71
  const bg = selected ? C.selectBg : undefined;
63
72
  return buildPanelLine(width, [
64
73
  [' ', C.label, bg],
65
- [report.id.slice(0, 8).padEnd(9), C.dim, bg],
66
- [report.classification.padEnd(20), classificationColor(report.classification), bg],
67
- [report.summary.slice(0, Math.max(0, width - 31)), C.value, bg],
74
+ [fitDisplay(report.id, 9), C.dim, bg],
75
+ [fitDisplay(report.classification, 20), classificationColor(report.classification), bg],
76
+ [truncateDisplay(report.summary, Math.max(0, width - 31)), C.value, bg],
68
77
  ]);
69
78
  }
70
79
 
@@ -117,7 +126,6 @@ export class IncidentReviewPanel extends ScrollableListPanel<FailureReport> {
117
126
  { label: 'selected', value: `${this.selectedIndex + 1}/${reports.length}`, valueColor: C.info },
118
127
  { label: 'classification', value: selected.classification, valueColor: classificationColor(selected.classification) },
119
128
  ], C),
120
- buildPanelLine(width, [[' Up/Down move Home/End jump selected incident drives the action rail below', C.dim]]),
121
129
  ];
122
130
 
123
131
  const footerLines: Line[] = [];
@@ -134,26 +142,26 @@ export class IncidentReviewPanel extends ScrollableListPanel<FailureReport> {
134
142
  ], C));
135
143
  footerLines.push(buildPanelLine(width, [
136
144
  [' Related IDs: ', C.label],
137
- [`turn=${bundle.evidence.relatedIds.turnId ?? 'n/a'} task=${bundle.evidence.relatedIds.taskId ?? 'n/a'} agent=${bundle.evidence.relatedIds.agentId ?? 'n/a'}`.slice(0, Math.max(0, width - 14)), C.info],
145
+ [truncateDisplay(`turn=${bundle.evidence.relatedIds.turnId ?? 'n/a'} task=${bundle.evidence.relatedIds.taskId ?? 'n/a'} agent=${bundle.evidence.relatedIds.agentId ?? 'n/a'}`, Math.max(0, width - 14)), C.info],
138
146
  ]));
139
147
  if (bundle.evidence.slowPhases.length > 0) {
140
148
  footerLines.push(buildPanelLine(width, [
141
149
  [' Slow phases: ', C.label],
142
- ...buildStatusPill('warn', bundle.evidence.slowPhases.join(', ').slice(0, Math.max(0, width - 15))),
150
+ ...buildStatusPill('warn', truncateDisplay(bundle.evidence.slowPhases.join(', '), Math.max(0, width - 15))),
143
151
  ]));
144
152
  }
145
153
  const rootCause = selected.causalChain.find((entry) => entry.isRootCause);
146
154
  if (rootCause) {
147
155
  footerLines.push(buildPanelLine(width, [
148
156
  [' Root event: ', C.label],
149
- [`${rootCause.sourceEventType} - ${rootCause.description}`.slice(0, Math.max(0, width - 14)), C.dim],
157
+ [truncateDisplay(`${rootCause.sourceEventType} - ${rootCause.description}`, Math.max(0, width - 14)), C.dim],
150
158
  ]));
151
159
  }
152
160
  const denied = selected.permissionEvidence.find((entry) => entry.approved === false);
153
161
  if (denied) {
154
162
  footerLines.push(buildPanelLine(width, [
155
163
  [' Permission: ', C.label],
156
- [`${denied.tool} denied${denied.riskLevel ? ` (${denied.riskLevel})` : ''}${denied.summary ? ` - ${denied.summary}` : ''}`.slice(0, Math.max(0, width - 14)), C.warn],
164
+ [truncateDisplay(`${denied.tool} denied${denied.riskLevel ? ` (${denied.riskLevel})` : ''}${denied.summary ? ` - ${denied.summary}` : ''}`, Math.max(0, width - 14)), C.warn],
157
165
  ]));
158
166
  }
159
167
  if (bundle.replay.relatedMismatches.length > 0) {
@@ -168,7 +176,7 @@ export class IncidentReviewPanel extends ScrollableListPanel<FailureReport> {
168
176
  : `Replay link: ${mismatch.kind}${mismatch.ownerDomain ? `/${mismatch.ownerDomain}` : ''} - ${mismatch.description}`;
169
177
  footerLines.push(buildPanelLine(width, [
170
178
  [' ', C.label],
171
- ...buildStatusPill('bad', replayDetail.slice(0, Math.max(0, width - 2))),
179
+ ...buildStatusPill('bad', truncateDisplay(replayDetail, Math.max(0, width - 2))),
172
180
  ]));
173
181
  } else {
174
182
  const ownerBreakdown = Object.entries(bundle.replay.mismatchBreakdown.byOwnerDomain)
@@ -179,7 +187,7 @@ export class IncidentReviewPanel extends ScrollableListPanel<FailureReport> {
179
187
  if (ownerBreakdown.length > 0) {
180
188
  footerLines.push(buildPanelLine(width, [
181
189
  [' Replay owners: ', C.label],
182
- [ownerBreakdown.slice(0, Math.max(0, width - 17)), C.info],
190
+ [truncateDisplay(ownerBreakdown, Math.max(0, width - 17)), C.info],
183
191
  ]));
184
192
  }
185
193
  }
@@ -188,10 +196,23 @@ export class IncidentReviewPanel extends ScrollableListPanel<FailureReport> {
188
196
  footerLines.push(buildPanelLine(width, [[` /incident latest /incident export ${selected.id} /recall capture incident ${selected.id}`, C.info]]));
189
197
  footerLines.push(buildGuidanceLine(width, '/security', 'open the broader trust and incident posture control room', C));
190
198
 
199
+ const hints = this.filterActive
200
+ ? [
201
+ { keys: 'type', label: 'filter incidents' },
202
+ { keys: 'Enter', label: 'apply' },
203
+ { keys: 'Esc', label: 'clear' },
204
+ ]
205
+ : [
206
+ { keys: '↑/↓', label: 'select incident' },
207
+ { keys: 'Home/End', label: 'jump' },
208
+ { keys: '/', label: 'filter' },
209
+ ];
210
+
191
211
  return this.renderList(width, height, {
192
212
  title: 'Incident Review Workspace',
193
213
  header: headerLines,
194
214
  footer: footerLines,
215
+ hints,
195
216
  });
196
217
  }
197
218
  }
@@ -1,15 +1,20 @@
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 { BasePanel } from './base-panel.ts';
4
5
  import type { UiIntelligenceSnapshot, UiReadModel } from '../runtime/ui-read-models.ts';
5
6
  import {
7
+ buildDetailBlock,
6
8
  buildEmptyState,
7
9
  buildGuidanceLine,
10
+ buildKeyboardHints,
8
11
  buildKeyValueLine,
9
12
  buildPanelLine,
10
13
  buildPanelWorkspace,
14
+ buildStatusPill,
11
15
  DEFAULT_PANEL_PALETTE,
12
16
  type PanelWorkspaceSection,
17
+ type StyledPanelSegment,
13
18
  } from './polish.ts';
14
19
 
15
20
  const C = {
@@ -21,22 +26,29 @@ const C = {
21
26
  headerBg: '#1e293b',
22
27
  } as const;
23
28
 
24
- function statusColor(status: string): string {
25
- switch (status) {
26
- case 'ready':
27
- return C.good;
28
- case 'loading':
29
- return C.info;
30
- case 'degraded':
31
- return C.warn;
32
- case 'unavailable':
33
- default:
34
- return C.bad;
35
- }
29
+ interface DiagnosticFile {
30
+ readonly filePath: string;
31
+ readonly errors: number;
32
+ readonly warnings: number;
33
+ readonly diagnostics: ReadonlyArray<{
34
+ readonly line: number;
35
+ readonly column: number;
36
+ readonly severity: string;
37
+ readonly message: string;
38
+ readonly source?: string | undefined;
39
+ readonly code?: string | undefined;
40
+ }>;
41
+ }
42
+
43
+ /** Map an intelligence-surface status string to a status-pill segment. */
44
+ function postureSegments(label: string, status: string): StyledPanelSegment[] {
45
+ const state = status === 'ready' ? 'good' : status === 'degraded' ? 'warn' : status === 'loading' ? 'info' : 'bad';
46
+ return buildStatusPill(state, `${label} ${status}`);
36
47
  }
37
48
 
38
49
  export class IntelligencePanel extends BasePanel {
39
50
  private readonly unsub: (() => void) | null;
51
+ private selectedIndex = 0;
40
52
 
41
53
  public constructor(private readonly readModel?: UiReadModel<UiIntelligenceSnapshot>) {
42
54
  super('intelligence', 'Intelligence', 'J', 'development');
@@ -47,6 +59,53 @@ export class IntelligencePanel extends BasePanel {
47
59
  this.unsub?.();
48
60
  }
49
61
 
62
+ /** Diagnostic files sorted error-first so the most actionable file is first. */
63
+ private diagnosticFiles(): DiagnosticFile[] {
64
+ if (!this.readModel) return [];
65
+ const state = this.readModel.getSnapshot();
66
+ return [...state.diagnostics.entries()]
67
+ .map(([filePath, diagnostics]) => ({
68
+ filePath,
69
+ errors: diagnostics.filter((entry) => entry.severity === 'error').length,
70
+ warnings: diagnostics.filter((entry) => entry.severity === 'warning').length,
71
+ diagnostics: diagnostics.map((entry) => ({
72
+ line: entry.line,
73
+ column: entry.column,
74
+ severity: entry.severity,
75
+ message: entry.message,
76
+ source: entry.source,
77
+ code: entry.code,
78
+ })),
79
+ }))
80
+ .sort((a, b) => (b.errors - a.errors) || (b.warnings - a.warnings) || a.filePath.localeCompare(b.filePath));
81
+ }
82
+
83
+ public handleInput(key: string): boolean {
84
+ const files = this.diagnosticFiles();
85
+ if (files.length === 0) return false;
86
+ if (key === 'up' || key === 'k') {
87
+ this.selectedIndex = Math.max(0, this.selectedIndex - 1);
88
+ this.markDirty();
89
+ return true;
90
+ }
91
+ if (key === 'down' || key === 'j') {
92
+ this.selectedIndex = Math.min(files.length - 1, this.selectedIndex + 1);
93
+ this.markDirty();
94
+ return true;
95
+ }
96
+ if (key === 'home' || key === 'g') {
97
+ this.selectedIndex = 0;
98
+ this.markDirty();
99
+ return true;
100
+ }
101
+ if (key === 'end' || key === 'G') {
102
+ this.selectedIndex = files.length - 1;
103
+ this.markDirty();
104
+ return true;
105
+ }
106
+ return false;
107
+ }
108
+
50
109
  public render(width: number, height: number): Line[] {
51
110
  this.needsRender = false;
52
111
  if (!this.readModel) {
@@ -76,16 +135,25 @@ export class IntelligencePanel extends BasePanel {
76
135
  state.hoverStatus,
77
136
  ].filter((status) => status !== 'ready').length;
78
137
 
138
+ const diagnosticFiles = this.diagnosticFiles();
139
+ this.selectedIndex = Math.max(0, Math.min(this.selectedIndex, Math.max(0, diagnosticFiles.length - 1)));
140
+
79
141
  const sections: PanelWorkspaceSection[] = [
80
142
  {
81
143
  title: 'Intelligence posture',
82
144
  lines: [
83
- buildKeyValueLine(width, [
84
- { label: 'diagnostics', value: state.diagnosticsStatus, valueColor: statusColor(state.diagnosticsStatus) },
85
- { label: 'symbols', value: state.symbolSearchStatus, valueColor: statusColor(state.symbolSearchStatus) },
86
- { label: 'completions', value: state.completionsStatus, valueColor: statusColor(state.completionsStatus) },
87
- { label: 'hover', value: state.hoverStatus, valueColor: statusColor(state.hoverStatus) },
88
- ], C),
145
+ buildPanelLine(width, [
146
+ { text: ' ', fg: C.dim },
147
+ ...postureSegments('diagnostics', state.diagnosticsStatus),
148
+ { text: ' ', fg: C.dim },
149
+ ...postureSegments('symbols', state.symbolSearchStatus),
150
+ ]),
151
+ buildPanelLine(width, [
152
+ { text: ' ', fg: C.dim },
153
+ ...postureSegments('completions', state.completionsStatus),
154
+ { text: ' ', fg: C.dim },
155
+ ...postureSegments('hover', state.hoverStatus),
156
+ ]),
89
157
  buildKeyValueLine(width, [
90
158
  { label: 'errors', value: String(state.errorCount), valueColor: state.errorCount > 0 ? C.bad : C.dim },
91
159
  { label: 'warnings', value: String(state.warningCount), valueColor: state.warningCount > 0 ? C.warn : C.dim },
@@ -94,80 +162,93 @@ export class IntelligencePanel extends BasePanel {
94
162
  ], C),
95
163
  ],
96
164
  },
97
- {
98
- title: 'Next Actions',
99
- lines: [
100
- buildGuidanceLine(width, '/intelligence diagnostics', 'review readiness posture and current diagnostics activity', C),
101
- buildGuidanceLine(width, '/intelligence repair', 'surface repair-oriented guidance when symbols, hover, or completions degrade', C),
102
- ],
103
- },
104
- {
105
- title: 'Readiness',
106
- lines: [
107
- buildPanelLine(width, [[` Diagnostics are ${state.diagnosticsStatus}. Symbol search is ${state.symbolSearchStatus}.`, state.diagnosticsStatus === 'ready' && state.symbolSearchStatus === 'ready' ? C.dim : C.warn]]),
108
- buildPanelLine(width, [[` Hover is ${state.hoverStatus}. Completions are ${state.completionsStatus}.`, state.hoverStatus === 'ready' && state.completionsStatus === 'ready' ? C.dim : C.warn]]),
109
- ...(state.hover.active && state.hover.filePath
110
- ? [buildPanelLine(width, [[` Active hover: ${state.hover.filePath}`, C.info]])]
111
- : []),
112
- ],
113
- },
114
165
  ];
115
166
 
116
- const diagnosticFiles = [...state.diagnostics.entries()]
117
- .map(([filePath, diagnostics]) => ({
118
- filePath,
119
- errors: diagnostics.filter((entry) => entry.severity === 'error').length,
120
- warnings: diagnostics.filter((entry) => entry.severity === 'warning').length,
121
- }))
122
- .sort((a, b) => (b.errors - a.errors) || (b.warnings - a.warnings) || a.filePath.localeCompare(b.filePath))
123
- .slice(0, 4);
124
- sections.push({
125
- title: 'Diagnostics',
126
- lines: diagnosticFiles.length > 0
127
- ? diagnosticFiles.map((entry) => buildPanelLine(width, [[
128
- ` ${entry.filePath} errors=${entry.errors} warnings=${entry.warnings}`,
129
- entry.errors > 0 ? C.bad : entry.warnings > 0 ? C.warn : C.dim,
130
- ]]))
131
- : [buildPanelLine(width, [[' No tracked diagnostics yet.', C.dim]])],
132
- });
167
+ // Diagnostics the most actionable surface. Render as a selectable list so
168
+ // the operator can drill into a specific file's findings.
169
+ const visibleFiles = diagnosticFiles.slice(0, 6);
170
+ const diagnosticsLines: Line[] = diagnosticFiles.length > 0
171
+ ? visibleFiles.map((entry, idx) => {
172
+ const selected = idx === this.selectedIndex;
173
+ const tone = entry.errors > 0 ? C.bad : entry.warnings > 0 ? C.warn : C.dim;
174
+ const counts = ` ${entry.errors} err ${entry.warnings} warn`;
175
+ const pathBudget = Math.max(8, width - counts.length - 4);
176
+ return buildPanelLine(width, [
177
+ [selected ? ' ▸ ' : ' ', selected ? C.info : C.dim, selected ? C.headerBg : undefined],
178
+ [truncateDisplay(entry.filePath, pathBudget), selected ? C.value : C.label, selected ? C.headerBg : undefined],
179
+ [counts, tone, selected ? C.headerBg : undefined],
180
+ ]);
181
+ })
182
+ : [buildPanelLine(width, [[' No tracked diagnostics. Symbol/diagnostic surfaces are clear.', C.dim]])];
183
+ if (diagnosticFiles.length > visibleFiles.length) {
184
+ diagnosticsLines.push(buildPanelLine(width, [[` +${diagnosticFiles.length - visibleFiles.length} more files`, C.dim]]));
185
+ }
186
+ sections.push({ title: `Diagnostics (${diagnosticFiles.length})`, lines: diagnosticsLines });
133
187
 
134
- sections.push({
135
- title: 'Workflows',
136
- lines: [
137
- buildGuidanceLine(width, '/intelligence symbols <file>', 'inspect document symbols for a file and verify symbol-surface readiness', C),
138
- buildGuidanceLine(width, '/intelligence outline <file>', 'review structural outline extraction without leaving the control room', C),
139
- buildGuidanceLine(width, '/intelligence definition <file> <line> <column>', 'check definition lookup for an exact source position', C),
140
- buildGuidanceLine(width, '/intelligence references <file> <line> <column>', 'review reference lookup for a symbol under the cursor', C),
141
- buildGuidanceLine(width, '/intelligence hover <file> <line> <column>', 'inspect hover/details posture for a specific source position', C),
142
- ],
143
- });
188
+ // Drill-down detail for the selected diagnostic file.
189
+ const selectedFile = diagnosticFiles[this.selectedIndex];
190
+ if (selectedFile) {
191
+ const detailRows: Line[] = [
192
+ buildKeyValueLine(width, [
193
+ { label: 'errors', value: String(selectedFile.errors), valueColor: selectedFile.errors > 0 ? C.bad : C.dim },
194
+ { label: 'warnings', value: String(selectedFile.warnings), valueColor: selectedFile.warnings > 0 ? C.warn : C.dim },
195
+ ], C),
196
+ ];
197
+ const topFindings = [...selectedFile.diagnostics]
198
+ .sort((a, b) => (a.severity === 'error' ? 0 : 1) - (b.severity === 'error' ? 0 : 1))
199
+ .slice(0, 3);
200
+ for (const finding of topFindings) {
201
+ const loc = `${finding.line + 1}:${finding.column + 1}`;
202
+ const tag = finding.source ? `${finding.source}${finding.code ? `(${finding.code})` : ''}` : finding.severity;
203
+ const prefix = ` ${loc} ${tag}: `;
204
+ detailRows.push(buildPanelLine(width, [
205
+ [prefix, finding.severity === 'error' ? C.bad : C.warn],
206
+ [truncateDisplay(finding.message, Math.max(8, width - prefix.length - 1)), C.value],
207
+ ]));
208
+ }
209
+ if (selectedFile.diagnostics.length > topFindings.length) {
210
+ detailRows.push(buildPanelLine(width, [[` +${selectedFile.diagnostics.length - topFindings.length} more findings`, C.dim]]));
211
+ }
212
+ detailRows.push(buildGuidanceLine(width, `/intelligence diagnostics ${selectedFile.filePath}`, 'open the full diagnostic list for this file', C));
213
+ sections.push({ lines: buildDetailBlock(width, truncateDisplay(selectedFile.filePath, Math.max(8, width - 4)), detailRows, C) });
214
+ }
144
215
 
145
216
  if (degraded > 0) {
146
217
  sections.push({
147
218
  title: 'Recovery',
148
219
  lines: [
149
- buildPanelLine(width, [[' Workspace intelligence is not fully ready. Review LSP/tree-sitter setup and workspace language configuration.', C.warn]]),
150
- buildGuidanceLine(width, '/health review', 'check setup and readiness failures that could block diagnostics and symbol search', C),
151
- buildGuidanceLine(width, '/setup review', 'review startup and environment posture for intelligence dependencies', C),
152
- buildGuidanceLine(width, '/intelligence repair', 'show repair-oriented commands for diagnostics, symbols, hover, and completions', C),
153
- buildGuidanceLine(width, '/health repair intelligence', 'show repair commands plus post-repair verification for the intelligence domain', C),
220
+ buildPanelLine(width, [[' Workspace intelligence is not fully ready. Review LSP/tree-sitter setup and language configuration.', C.warn]]),
221
+ buildGuidanceLine(width, '/intelligence repair', 'show repair commands for diagnostics, symbols, hover, and completions', C),
222
+ buildGuidanceLine(width, '/health repair intelligence', 'show repair commands plus post-repair verification', C),
154
223
  ],
155
224
  });
156
225
  } else {
157
226
  sections.push({
158
- title: 'Recovery',
227
+ title: 'Workflows',
159
228
  lines: [
160
- buildPanelLine(width, [[' Intelligence surfaces are healthy and ready for code-aware workflows.', C.dim]]),
161
- buildGuidanceLine(width, '/health intelligence', 'verify readiness posture after setup changes or dependency recovery', C),
229
+ buildGuidanceLine(width, '/intelligence symbols <file>', 'inspect document symbols and verify symbol-surface readiness', C),
230
+ buildGuidanceLine(width, '/intelligence definition <file> <line> <column>', 'check definition lookup for an exact source position', C),
231
+ buildGuidanceLine(width, '/intelligence hover <file> <line> <column>', 'inspect hover/details posture for a source position', C),
162
232
  ],
163
233
  });
164
234
  }
165
235
 
236
+ const footerLines = diagnosticFiles.length > 0
237
+ ? [buildKeyboardHints(width, [
238
+ { keys: '↑/↓', label: 'select file' },
239
+ { keys: 'Home/End', label: 'jump' },
240
+ { keys: '/intelligence', label: 'commands' },
241
+ ], C)]
242
+ : [buildKeyboardHints(width, [
243
+ { keys: '/intelligence diagnostics', label: 'review' },
244
+ { keys: '/intelligence symbols <file>', label: 'inspect symbols' },
245
+ ], C)];
246
+
166
247
  const lines = buildPanelWorkspace(width, height, {
167
248
  title: 'Intelligence Control Room',
168
249
  intro: 'Workspace intelligence posture across diagnostics, symbol search, hover, and completion readiness.',
169
250
  sections,
170
- footerLines: [buildPanelLine(width, [[' /symbols /intelligence diagnostics /intelligence symbols <file> /intelligence definition <file> <line> <column> ', C.dim]])],
251
+ footerLines,
171
252
  palette: C,
172
253
  });
173
254
  while (lines.length < height) lines.push(createEmptyLine(width));
@@ -8,13 +8,20 @@
8
8
  * This panel is a thin information surface that explains the graph's capabilities
9
9
  * and routes the user to the /knowledge command suite for ingest/RAG operations.
10
10
  * The full graph UI is command-driven (/knowledge ask, ingest-url, list, search…).
11
+ *
12
+ * It is selectable: the operator can move a cursor across the command catalogue
13
+ * so the currently-highlighted command is unambiguous and a one-line "what this
14
+ * does" detail is surfaced for it.
11
15
  */
12
16
 
13
17
  import type { Line } from '../types/grid.ts';
18
+ import { createEmptyLine } from '../types/grid.ts';
14
19
  import { BasePanel } from './base-panel.ts';
15
20
  import {
16
21
  buildBodyText,
22
+ buildDetailBlock,
17
23
  buildGuidanceLine,
24
+ buildKeyboardHints,
18
25
  buildPanelLine,
19
26
  buildPanelWorkspace,
20
27
  DEFAULT_PANEL_PALETTE,
@@ -26,59 +33,114 @@ const C = {
26
33
  headerBg: '#1e293b',
27
34
  } as const;
28
35
 
36
+ interface CommandEntry {
37
+ readonly command: string;
38
+ readonly summary: string;
39
+ readonly detail: string;
40
+ }
41
+
42
+ const GRAPH_COMMANDS: readonly CommandEntry[] = [
43
+ { command: '/knowledge status', summary: 'check the graph status and source counts', detail: 'Reports node/edge totals, ingested source count, and embedding-provider readiness.' },
44
+ { command: '/knowledge ask <query>', summary: 'ask a question against the ingested knowledge', detail: 'Runs retrieval-augmented Q&A over ingested sources and returns a cited answer.' },
45
+ { command: '/knowledge ingest-url <url>', summary: 'ingest a URL as a knowledge source', detail: 'Fetches a URL, extracts content, and stores it as graph nodes and edges.' },
46
+ { command: '/knowledge list', summary: 'list ingested sources or graph nodes', detail: 'Enumerates the current sources and top-level nodes in the graph.' },
47
+ { command: '/knowledge search <query>', summary: 'search the graph for nodes and sources', detail: 'Keyword/semantic search across stored nodes without generating an answer.' },
48
+ { command: '/knowledge packet <task>', summary: 'build a compact prompt packet for a task', detail: 'Assembles a token-budgeted context packet of the most relevant facts for a task.' },
49
+ ];
50
+
51
+ const MEMORY_COMMANDS: readonly CommandEntry[] = [
52
+ { command: '/recall add <class> <summary>', summary: 'capture a new memory record', detail: 'Stores a durable decision, risk, runbook, incident, or architecture record.' },
53
+ { command: '/recall queue', summary: 'show the operator review queue', detail: 'Lists stale and contradicted records awaiting operator review.' },
54
+ { command: '/project-memory (pmem)', summary: 'project-memory alias for /recall front-door', detail: 'Shorthand entry point to the same durable project-memory surface.' },
55
+ ];
56
+
29
57
  export class KnowledgeGraphPanel extends BasePanel {
58
+ private selectedIndex = 0;
59
+
30
60
  constructor() {
31
61
  super('knowledge', 'Knowledge', 'K', 'agent');
32
62
  }
33
63
 
34
- handleInput(_key: string): boolean {
64
+ private get entries(): readonly CommandEntry[] {
65
+ return [...GRAPH_COMMANDS, ...MEMORY_COMMANDS];
66
+ }
67
+
68
+ handleInput(key: string): boolean {
69
+ const count = this.entries.length;
70
+ if (key === 'up' || key === 'k') {
71
+ this.selectedIndex = (this.selectedIndex - 1 + count) % count;
72
+ this.markDirty();
73
+ return true;
74
+ }
75
+ if (key === 'down' || key === 'j') {
76
+ this.selectedIndex = (this.selectedIndex + 1) % count;
77
+ this.markDirty();
78
+ return true;
79
+ }
80
+ if (key === 'home' || key === 'g') {
81
+ this.selectedIndex = 0;
82
+ this.markDirty();
83
+ return true;
84
+ }
85
+ if (key === 'end' || key === 'G') {
86
+ this.selectedIndex = count - 1;
87
+ this.markDirty();
88
+ return true;
89
+ }
35
90
  return false;
36
91
  }
37
92
 
93
+ private commandLines(width: number, group: readonly CommandEntry[], offset: number): Line[] {
94
+ return group.map((entry, idx) => {
95
+ const absIdx = offset + idx;
96
+ const selected = absIdx === this.selectedIndex;
97
+ if (selected) {
98
+ return buildPanelLine(width, [
99
+ [' ▸ ', C.info, C.headerBg],
100
+ [`${entry.command} `, C.value, C.headerBg],
101
+ [entry.summary, C.dim, C.headerBg],
102
+ ]);
103
+ }
104
+ return buildGuidanceLine(width, entry.command, entry.summary, C);
105
+ });
106
+ }
107
+
38
108
  render(width: number, height: number): Line[] {
109
+ this.selectedIndex = Math.max(0, Math.min(this.selectedIndex, this.entries.length - 1));
110
+ const selected = this.entries[this.selectedIndex]!;
111
+
39
112
  const sections = [
40
113
  {
41
114
  title: 'SDK Knowledge Graph',
42
115
  lines: [
43
- ...buildBodyText(
44
- width,
45
- 'The knowledge graph stores ingested URLs, bookmarks, and structured facts as nodes and edges. ' +
46
- 'Use /knowledge commands to ingest sources, search the graph, and build task-context packets.',
47
- C,
48
- C.value,
49
- ),
50
- buildPanelLine(width, [['', C.dim]]),
51
- buildGuidanceLine(width, '/knowledge status', 'check the graph status and source counts', C),
52
- buildGuidanceLine(width, '/knowledge ask <query>', 'ask a question against the ingested knowledge', C),
53
- buildGuidanceLine(width, '/knowledge ingest-url <url>', 'ingest a URL as a knowledge source', C),
54
- buildGuidanceLine(width, '/knowledge list', 'list ingested sources or graph nodes', C),
55
- buildGuidanceLine(width, '/knowledge search <query>', 'search the graph for nodes and sources', C),
56
- buildGuidanceLine(width, '/knowledge packet <task>', 'build a compact prompt packet for a task', C),
116
+ buildPanelLine(width, [[' Ingest URLs and facts as graph nodes; search and build task-context packets.', C.dim]]),
117
+ ...this.commandLines(width, GRAPH_COMMANDS, 0),
57
118
  ],
58
119
  },
59
120
  {
60
121
  title: 'Project Memory',
61
122
  lines: [
62
- ...buildBodyText(
63
- width,
64
- 'For durable decisions, risks, runbooks, incidents, and architecture records, use the Memory panel ' +
65
- 'or the /recall command surface. Durable memory is a sub-namespace of the knowledge graph.',
66
- C,
67
- C.dim,
68
- ),
69
- buildPanelLine(width, [['', C.dim]]),
70
- buildGuidanceLine(width, '/recall add <class> <summary>', 'capture a new memory record', C),
71
- buildGuidanceLine(width, '/recall queue', 'show the operator review queue', C),
72
- buildGuidanceLine(width, '/project-memory (pmem)', 'project-memory alias for /recall front-door', C),
123
+ buildPanelLine(width, [[' Durable decisions, risks, runbooks, and incidents — a sub-namespace of the graph.', C.dim]]),
124
+ ...this.commandLines(width, MEMORY_COMMANDS, GRAPH_COMMANDS.length),
73
125
  ],
74
126
  },
127
+ {
128
+ lines: buildDetailBlock(width, selected.command, buildBodyText(width, selected.detail, C, C.value), C),
129
+ },
75
130
  ];
76
131
 
77
- return buildPanelWorkspace(width, height, {
132
+ const lines = buildPanelWorkspace(width, height, {
78
133
  title: 'Knowledge Graph',
79
134
  intro: 'Ingested sources, graph nodes, and the durable memory bridge.',
80
135
  sections,
136
+ footerLines: [buildKeyboardHints(width, [
137
+ { keys: '↑/↓', label: 'browse commands' },
138
+ { keys: 'Home/End', label: 'jump' },
139
+ { keys: 'M', label: 'Memory panel' },
140
+ ], C)],
81
141
  palette: C,
82
142
  });
143
+ while (lines.length < height) lines.push(createEmptyLine(width));
144
+ return lines.slice(0, height);
83
145
  }
84
146
  }
@@ -1,5 +1,6 @@
1
1
  import type { Line } from '../types/grid.ts';
2
2
  import { createEmptyLine } from '../types/grid.ts';
3
+ import { fitDisplay, truncateDisplay } from '../utils/terminal-width.ts';
3
4
  import { ScrollableListPanel } from './scrollable-list-panel.ts';
4
5
  import {
5
6
  buildDetailBlock,
@@ -136,8 +137,8 @@ export class LocalAuthPanel extends ScrollableListPanel<LocalAuthUser> {
136
137
 
137
138
  protected renderItem(user: LocalAuthUser, _index: number, selected: boolean, width: number): Line {
138
139
  return buildPanelListRow(width, [
139
- { text: user.username.padEnd(20), fg: C.value },
140
- { text: ` roles=${formatRoles(user.roles)}`.slice(0, Math.max(0, width - 24)), fg: C.info },
140
+ { text: fitDisplay(user.username, 20), fg: C.value },
141
+ { text: truncateDisplay(` roles=${formatRoles(user.roles)}`, Math.max(0, width - 24)), fg: C.info },
141
142
  ], C, { selected });
142
143
  }
143
144
 
@@ -171,10 +172,10 @@ export class LocalAuthPanel extends ScrollableListPanel<LocalAuthUser> {
171
172
  [' bootstrap ', C.label],
172
173
  [snapshot.bootstrapCredentialPresent ? 'present' : 'cleared', snapshot.bootstrapCredentialPresent ? C.warn : C.good],
173
174
  ]),
174
- buildPanelLine(width, [[' user store ', C.label], [snapshot.userStorePath.slice(0, Math.max(0, width - 13)), C.dim]]),
175
- buildPanelLine(width, [[' bootstrap file ', C.label], [snapshot.bootstrapCredentialPath.slice(0, Math.max(0, width - 18)), C.dim]]),
175
+ buildPanelLine(width, [[' user store ', C.label], [truncateDisplay(snapshot.userStorePath, Math.max(0, width - 13)), C.dim]]),
176
+ buildPanelLine(width, [[' bootstrap file ', C.label], [truncateDisplay(snapshot.bootstrapCredentialPath, Math.max(0, width - 18)), C.dim]]),
176
177
  ...(issueMessages.length > 0
177
- ? issueMessages.map((issue) => buildPanelLine(width, [[` issue: ${issue}`.slice(0, Math.max(0, width)), C.warn]]))
178
+ ? issueMessages.map((issue) => buildPanelLine(width, [[truncateDisplay(` issue: ${issue}`, width), C.warn]]))
178
179
  : [buildPanelLine(width, [[' local auth posture looks healthy.', C.good]])]),
179
180
  buildGuidanceLine(width, '/auth local rotate-password <user>', 'open masked password entry for the selected user (no plaintext in history)', C),
180
181
  ], C),
@@ -198,19 +199,20 @@ export class LocalAuthPanel extends ScrollableListPanel<LocalAuthUser> {
198
199
  if (selected) {
199
200
  footerLines.push(
200
201
  ...buildDetailBlock(width, 'Selected user', [
201
- buildPanelLine(width, [[' username ', C.label], [selected.username, C.value], [' roles ', C.label], [formatRoles(selected.roles).slice(0, Math.max(0, width - 23)), C.info]]),
202
- buildPanelLine(width, [[` next: /auth local rotate-password ${selected.username}`.slice(0, Math.max(0, width)), C.dim]]),
203
- buildPanelLine(width, [[` next: /auth local delete-user ${selected.username}`.slice(0, Math.max(0, width)), C.dim]]),
202
+ buildPanelLine(width, [[' username ', C.label], [selected.username, C.value], [' roles ', C.label], [truncateDisplay(formatRoles(selected.roles), Math.max(0, width - 23)), C.info]]),
203
+ buildPanelLine(width, [[truncateDisplay(` next: /auth local rotate-password ${selected.username}`, width), C.dim]]),
204
+ buildPanelLine(width, [[truncateDisplay(` next: /auth local delete-user ${selected.username}`, width), C.dim]]),
204
205
  ], C),
205
206
  );
206
207
  }
207
208
 
208
209
  if (snapshot.sessions.length > 0) {
210
+ footerLines.push(buildPanelLine(width, [[` Active sessions (${snapshot.sessions.length})`, C.label]]));
209
211
  footerLines.push(
210
212
  ...snapshot.sessions.slice(0, 8).map((session) => buildPanelLine(width, [
211
213
  [' ', C.label],
212
- [session.username.padEnd(18), C.value],
213
- [` expires ${new Date(session.expiresAt).toLocaleString()}`.slice(0, Math.max(0, width - 20)), C.dim],
214
+ [fitDisplay(session.username, 18), C.value],
215
+ [truncateDisplay(` expires ${new Date(session.expiresAt).toLocaleString()}`, Math.max(0, width - 20)), C.dim],
214
216
  ])),
215
217
  );
216
218
  }
@@ -220,6 +222,10 @@ export class LocalAuthPanel extends ScrollableListPanel<LocalAuthUser> {
220
222
  title: 'Local Auth Control Room',
221
223
  header: headerLines,
222
224
  footer: footerLines,
225
+ hints: [
226
+ { keys: '↑/↓', label: 'select user' },
227
+ { keys: '/auth local rotate-password', label: 'masked entry' },
228
+ ],
223
229
  });
224
230
  }
225
231
 
@@ -233,7 +239,7 @@ export class LocalAuthPanel extends ScrollableListPanel<LocalAuthUser> {
233
239
  const promptLines: Line[] = [
234
240
  buildPanelLine(width, [[` ${actionLabel}: ${state.username}`, C.value]]),
235
241
  buildPanelLine(width, [['', C.label]]),
236
- buildPanelLine(width, [[' Password ', C.label], [maskedDisplay.slice(0, Math.max(0, width - 12)), C.info]]),
242
+ buildPanelLine(width, [[' Password ', C.label], [truncateDisplay(maskedDisplay, Math.max(0, width - 12)), C.info]]),
237
243
  buildPanelLine(width, [['', C.label]]),
238
244
  buildPanelLine(width, [[' [Enter] Confirm [Esc] Cancel [Backspace] Delete char', C.dim]]),
239
245
  ];