@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
@@ -9,6 +9,8 @@ import type { HookChain, HookDefinition } from '@pellux/goodvibes-sdk/platform/h
9
9
  import type { HookWorkbench } from '@pellux/goodvibes-sdk/platform/hooks';
10
10
  import { truncateDisplay } from '../utils/terminal-width.ts';
11
11
  import {
12
+ buildKeyValueLine,
13
+ buildKeyboardHints,
12
14
  buildPanelLine,
13
15
  buildStatusPill,
14
16
  DEFAULT_PANEL_PALETTE,
@@ -68,9 +70,17 @@ export class HooksPanel extends ScrollableListPanel<HookEntry> {
68
70
  ) {
69
71
  super('hooks', 'Hooks', 'H', 'monitoring');
70
72
  this.showSelectionGutter = true; // I5: non-color selection affordance
73
+ this.filterEnabled = true;
74
+ this.filterLabel = 'Filter hooks';
71
75
  this.dataSource = dataSource;
72
76
  }
73
77
 
78
+ protected override filterMatches(entry: HookEntry, q: string): boolean {
79
+ return (entry.hook.name ?? '').toLowerCase().includes(q)
80
+ || entry.pattern.toLowerCase().includes(q)
81
+ || entry.hook.type.toLowerCase().includes(q);
82
+ }
83
+
74
84
  protected override getPalette() { return C; }
75
85
  protected override getEmptyStateMessage() { return ' No hooks are currently registered.'; }
76
86
  protected override getEmptyStateActions() {
@@ -96,7 +106,7 @@ export class HooksPanel extends ScrollableListPanel<HookEntry> {
96
106
  }
97
107
 
98
108
  public handleInput(key: string): boolean {
99
- if (key === 'r') {
109
+ if (!this.filterActive && key === 'r') {
100
110
  this.markDirty();
101
111
  return true;
102
112
  }
@@ -224,15 +234,39 @@ export class HooksPanel extends ScrollableListPanel<HookEntry> {
224
234
  });
225
235
  }
226
236
 
237
+ // Summary header — surface registration + activity counts first so the most
238
+ // important "what's firing" signal is visible without scrolling to the footer.
239
+ const denials = recentActivity.filter((r) => r.ok && r.decision === 'deny').length;
240
+ const errors = recentActivity.filter((r) => !r.ok).length;
241
+ const headerLines: Line[] = [
242
+ buildKeyValueLine(width, [
243
+ { label: 'hooks', value: String(hooks.length), valueColor: C.info },
244
+ { label: 'chains', value: String(chains.length), valueColor: C.value },
245
+ { label: 'contracts', value: String(contracts.length), valueColor: C.value },
246
+ { label: 'recent denials', value: String(denials), valueColor: denials > 0 ? C.warn : C.dim },
247
+ { label: 'errors', value: String(errors), valueColor: errors > 0 ? C.error : C.dim },
248
+ ], C),
249
+ ];
250
+
251
+ const hints = this.filterActive
252
+ ? [{ keys: 'type', label: 'filter' }, { keys: 'Enter', label: 'apply' }, { keys: 'Esc', label: 'clear' }]
253
+ : [
254
+ { keys: 'Up/Down', label: 'move' },
255
+ { keys: 'r', label: 'refresh' },
256
+ { keys: '/hooks', label: 'full listing' },
257
+ { keys: '/', label: 'filter' },
258
+ ];
259
+
227
260
  return this.renderList(width, height, {
228
261
  title: 'Hooks Control Room',
262
+ header: headerLines,
229
263
  footer: [
230
264
  ...detailLines,
231
265
  buildPanelLine(width, [[' Recent Activity', C.label]]),
232
266
  ...activityLines,
233
267
  buildPanelLine(width, [[' Authoring', C.label]]),
234
268
  ...authoringLines,
235
- buildPanelLine(width, [[' Up/Down move r refresh /hooks for full contract listing', C.dim]]),
269
+ buildKeyboardHints(width, hints, C),
236
270
  ],
237
271
  });
238
272
  }
@@ -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
  }