@pellux/goodvibes-tui 0.28.0 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/CHANGELOG.md +101 -0
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  4. package/src/input/commands/cost-runtime.ts +49 -0
  5. package/src/input/commands/operator-runtime.ts +5 -1
  6. package/src/input/commands.ts +2 -0
  7. package/src/input/handler-feed-routes.ts +12 -35
  8. package/src/input/handler-feed.ts +4 -8
  9. package/src/input/handler-shortcuts.ts +51 -0
  10. package/src/input/handler.ts +43 -6
  11. package/src/input/keybindings.ts +48 -8
  12. package/src/input/panel-integration-actions.ts +107 -1
  13. package/src/main.ts +5 -1
  14. package/src/panels/agent-inspector-panel.ts +125 -36
  15. package/src/panels/agent-inspector-shared.ts +144 -0
  16. package/src/panels/approval-panel.ts +67 -16
  17. package/src/panels/automation-control-panel.ts +368 -124
  18. package/src/panels/builtin/agent.ts +28 -37
  19. package/src/panels/builtin/development.ts +40 -32
  20. package/src/panels/builtin/knowledge.ts +15 -6
  21. package/src/panels/builtin/operations.ts +178 -117
  22. package/src/panels/builtin/session.ts +35 -9
  23. package/src/panels/builtin/shared.ts +98 -6
  24. package/src/panels/cockpit-panel.ts +232 -73
  25. package/src/panels/communication-panel.ts +58 -20
  26. package/src/panels/confirm-state.ts +8 -1
  27. package/src/panels/control-plane-panel.ts +351 -118
  28. package/src/panels/cost-tracker-panel.ts +165 -7
  29. package/src/panels/debug-panel.ts +312 -159
  30. package/src/panels/diff-panel.ts +203 -57
  31. package/src/panels/docs-panel.ts +152 -66
  32. package/src/panels/eval-panel.ts +220 -42
  33. package/src/panels/file-explorer-panel.ts +202 -122
  34. package/src/panels/file-preview-panel.ts +132 -57
  35. package/src/panels/git-panel.ts +309 -128
  36. package/src/panels/hooks-panel.ts +150 -22
  37. package/src/panels/incident-review-panel.ts +223 -35
  38. package/src/panels/index.ts +0 -4
  39. package/src/panels/intelligence-panel.ts +212 -86
  40. package/src/panels/knowledge-graph-panel.ts +461 -101
  41. package/src/panels/local-auth-panel.ts +240 -28
  42. package/src/panels/marketplace-panel.ts +193 -26
  43. package/src/panels/memory-panel.ts +78 -77
  44. package/src/panels/ops-control-panel.ts +146 -29
  45. package/src/panels/ops-strategy-panel.ts +72 -4
  46. package/src/panels/orchestration-panel.ts +231 -69
  47. package/src/panels/panel-list-panel.ts +154 -131
  48. package/src/panels/panel-manager.ts +97 -9
  49. package/src/panels/plan-dashboard-panel.ts +333 -17
  50. package/src/panels/plugins-panel.ts +184 -29
  51. package/src/panels/policy-panel.ts +210 -38
  52. package/src/panels/polish-core.ts +7 -2
  53. package/src/panels/polish.ts +23 -4
  54. package/src/panels/project-planning-answer-actions.ts +134 -0
  55. package/src/panels/project-planning-panel.ts +62 -113
  56. package/src/panels/provider-health-panel.ts +434 -518
  57. package/src/panels/provider-health-routes.ts +203 -0
  58. package/src/panels/provider-health-tracker.ts +194 -6
  59. package/src/panels/provider-health-views.ts +560 -0
  60. package/src/panels/qr-panel.ts +116 -30
  61. package/src/panels/remote-panel.ts +114 -36
  62. package/src/panels/routes-panel.ts +63 -22
  63. package/src/panels/sandbox-panel.ts +174 -42
  64. package/src/panels/scrollable-list-panel.ts +19 -135
  65. package/src/panels/security-panel.ts +133 -33
  66. package/src/panels/services-panel.ts +116 -64
  67. package/src/panels/session-browser-panel.ts +73 -2
  68. package/src/panels/session-maintenance.ts +4 -122
  69. package/src/panels/settings-sync-panel.ts +335 -72
  70. package/src/panels/skills-panel.ts +157 -89
  71. package/src/panels/subscription-panel.ts +86 -33
  72. package/src/panels/symbol-outline-panel.ts +248 -108
  73. package/src/panels/system-messages-panel.ts +114 -13
  74. package/src/panels/tasks-panel.ts +326 -139
  75. package/src/panels/thinking-panel.ts +43 -10
  76. package/src/panels/token-budget-panel.ts +194 -18
  77. package/src/panels/tool-inspector-panel.ts +144 -34
  78. package/src/panels/types.ts +40 -4
  79. package/src/panels/work-plan-panel.ts +280 -17
  80. package/src/panels/worktree-panel.ts +175 -42
  81. package/src/panels/wrfc-panel.ts +116 -24
  82. package/src/renderer/conversation-overlays.ts +25 -11
  83. package/src/renderer/footer-tips.ts +41 -0
  84. package/src/renderer/fullscreen-primitives.ts +22 -16
  85. package/src/renderer/help-overlay.ts +91 -14
  86. package/src/renderer/hint-grammar.ts +52 -0
  87. package/src/renderer/layout.ts +7 -7
  88. package/src/renderer/modal-factory.ts +23 -15
  89. package/src/renderer/model-picker-overlay.ts +21 -7
  90. package/src/renderer/overlay-box.ts +16 -10
  91. package/src/renderer/process-indicator.ts +3 -1
  92. package/src/renderer/progress.ts +5 -4
  93. package/src/renderer/search-overlay.ts +27 -6
  94. package/src/renderer/session-picker-modal.ts +6 -4
  95. package/src/renderer/settings-modal.ts +70 -10
  96. package/src/renderer/shell-surface.ts +41 -15
  97. package/src/renderer/status-glyphs.ts +11 -9
  98. package/src/renderer/theme.ts +60 -3
  99. package/src/renderer/ui-factory.ts +41 -30
  100. package/src/renderer/ui-primitives.ts +23 -2
  101. package/src/runtime/bootstrap-shell.ts +35 -18
  102. package/src/runtime/diagnostics/panels/index.ts +0 -3
  103. package/src/shell/ui-openers.ts +14 -22
  104. package/src/utils/format-duration.ts +2 -2
  105. package/src/utils/splash-lines.ts +44 -3
  106. package/src/version.ts +1 -1
  107. package/src/work-plans/work-plan-store.ts +13 -0
  108. package/src/panels/agent-logs-panel.ts +0 -635
  109. package/src/panels/agent-logs-shared.ts +0 -129
  110. package/src/panels/context-visualizer-panel.ts +0 -238
  111. package/src/panels/forensics-panel.ts +0 -378
  112. package/src/panels/provider-account-snapshot.ts +0 -259
  113. package/src/panels/provider-accounts-panel.ts +0 -255
  114. package/src/panels/provider-stats-panel.ts +0 -391
  115. package/src/panels/schedule-panel.ts +0 -365
  116. package/src/panels/watchers-panel.ts +0 -213
  117. package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
  118. package/src/runtime/diagnostics/panels/policy.ts +0 -177
@@ -1,146 +1,506 @@
1
1
  /**
2
2
  * KnowledgeGraphPanel — SDK knowledge graph front-door.
3
3
  *
4
- * TASK-040: The 'knowledge' panel id is repointed here (the SDK graph), fixing
5
- * the naming inversion where the former panel named 'Knowledge' was actually
6
- * rendering memory records.
4
+ * WO-123: Replaces the former static GRAPH_COMMANDS/MEMORY_COMMANDS command
5
+ * catalogue with a live console over the injected KnowledgeApi surface:
6
+ * - 'browse' mode: header counts + a combined node/source/issue list,
7
+ * with '/' search dispatched through graph.items.search, plus a
8
+ * schedules summary from jobs.schedules.list.
9
+ * - 'review' mode (Tab): the open-issue review queue, with
10
+ * accept/reject/resolve/reopen mutations via graph.issues.review,
11
+ * ConfirmState-gated — mirrors MemoryPanel's proven review pattern.
7
12
  *
8
- * This panel is a thin information surface that explains the graph's capabilities
9
- * and routes the user to the /knowledge command suite for ingest/RAG operations.
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.
13
+ * 'M' opens the Memory panel directly via panelManager.open (no more dead
14
+ * hint text) the two panels stay distinct: this one is the SDK graph,
15
+ * memory-panel.ts is the durable project-memory substrate.
15
16
  */
16
17
 
17
18
  import type { Line } from '../types/grid.ts';
18
- import { createEmptyLine } from '../types/grid.ts';
19
- import { BasePanel } from './base-panel.ts';
19
+ import type { KnowledgeApi } from '@pellux/goodvibes-sdk/platform/knowledge';
20
+ import type {
21
+ KnowledgeIssueRecord,
22
+ KnowledgeNodeRecord,
23
+ KnowledgeScheduleRecord,
24
+ KnowledgeSearchResult,
25
+ KnowledgeSourceRecord,
26
+ } from '@pellux/goodvibes-sdk/platform/knowledge';
27
+ import { ScrollableListPanel } from './scrollable-list-panel.ts';
28
+ import { truncateDisplay } from '../utils/terminal-width.ts';
29
+ import { type ConfirmState, handleConfirmInput, renderConfirmLines } from './confirm-state.ts';
20
30
  import {
21
31
  buildBodyText,
22
- buildDetailBlock,
23
- buildGuidanceLine,
24
- buildKeyboardHints,
32
+ buildKeyValueLine,
25
33
  buildPanelLine,
26
34
  buildPanelWorkspace,
35
+ buildSearchInputLine,
27
36
  DEFAULT_PANEL_PALETTE,
28
37
  } from './polish.ts';
38
+ import {
39
+ getPanelSearchFocusTransition,
40
+ isPanelSearchBackspace,
41
+ isPanelSearchCancel,
42
+ isPanelSearchCommit,
43
+ isPanelSearchPrintable,
44
+ } from './search-focus.ts';
45
+ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
46
+
47
+ const C = DEFAULT_PANEL_PALETTE;
48
+
49
+ // The 'edit' and 'forget' review actions accept structured data the panel has
50
+ // no field to collect, so only the four browsable actions are key-bound here
51
+ // (mirrors the reduced key surface the plan calls out: a/x/r/o).
52
+ const KNOWLEDGE_REVIEW_ACTIONS = ['accept', 'reject', 'resolve', 'reopen'] as const;
53
+ type KnowledgeReviewAction = typeof KNOWLEDGE_REVIEW_ACTIONS[number];
29
54
 
30
- const C = {
31
- ...DEFAULT_PANEL_PALETTE,
32
- header: '#94a3b8',
33
- headerBg: '#1e293b',
34
- } as const;
55
+ type BrowseKind = 'node' | 'source' | 'issue' | 'result';
35
56
 
36
- interface CommandEntry {
37
- readonly command: string;
38
- readonly summary: string;
57
+ interface BrowseRow {
58
+ readonly kind: BrowseKind;
59
+ readonly id: string;
60
+ readonly title: string;
61
+ readonly tag: string;
39
62
  readonly detail: string;
40
63
  }
41
64
 
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
-
57
- export class KnowledgeGraphPanel extends BasePanel {
58
- private selectedIndex = 0;
59
-
60
- constructor() {
65
+ type Mode = 'browse' | 'review';
66
+
67
+ const LIST_LIMIT = 60;
68
+ const SEARCH_LIMIT = 30;
69
+
70
+ function cleanInline(value: string | undefined): string {
71
+ return (value ?? '').replace(/\s+/g, ' ').trim();
72
+ }
73
+
74
+ function nodeRow(node: KnowledgeNodeRecord): BrowseRow {
75
+ return {
76
+ kind: 'node',
77
+ id: node.id,
78
+ title: cleanInline(node.title) || 'untitled',
79
+ tag: `node/${node.kind}`,
80
+ detail: cleanInline(node.summary),
81
+ };
82
+ }
83
+
84
+ function sourceRow(source: KnowledgeSourceRecord): BrowseRow {
85
+ return {
86
+ kind: 'source',
87
+ id: source.id,
88
+ title: cleanInline(source.title) || cleanInline(source.canonicalUri) || cleanInline(source.sourceUri) || source.id,
89
+ tag: `source/${source.status}`,
90
+ detail: cleanInline(source.summary),
91
+ };
92
+ }
93
+
94
+ function issueRow(issue: KnowledgeIssueRecord): BrowseRow {
95
+ return {
96
+ kind: 'issue',
97
+ id: issue.id,
98
+ title: issue.code,
99
+ tag: `issue/${issue.severity}`,
100
+ detail: cleanInline(issue.message),
101
+ };
102
+ }
103
+
104
+ function searchResultRow(result: KnowledgeSearchResult): BrowseRow {
105
+ const title = result.source?.title
106
+ ?? result.source?.canonicalUri
107
+ ?? result.node?.title
108
+ ?? result.id;
109
+ return {
110
+ kind: 'result',
111
+ id: result.id,
112
+ title: cleanInline(title) || result.id,
113
+ tag: `${result.kind} score=${result.score}`,
114
+ detail: cleanInline(result.reason),
115
+ };
116
+ }
117
+
118
+ function kindColor(kind: BrowseKind): string {
119
+ switch (kind) {
120
+ case 'node': return C.info;
121
+ case 'source': return C.good;
122
+ case 'issue': return C.warn;
123
+ case 'result': return C.value;
124
+ }
125
+ }
126
+
127
+ function severityColor(severity: KnowledgeIssueRecord['severity']): string {
128
+ switch (severity) {
129
+ case 'error': return C.bad;
130
+ case 'warning': return C.warn;
131
+ case 'info':
132
+ default: return C.info;
133
+ }
134
+ }
135
+
136
+ export class KnowledgeGraphPanel extends ScrollableListPanel<BrowseRow> {
137
+ private readonly knowledge: KnowledgeApi;
138
+ private readonly openMemoryPanel?: () => void;
139
+
140
+ private mode: Mode = 'browse';
141
+
142
+ // Browse-mode data + search state
143
+ private browseRows: BrowseRow[] = [];
144
+ private searchRows: BrowseRow[] = [];
145
+ private searchFocused = false;
146
+ private searchQuery = '';
147
+ private sourceCount = 0;
148
+ private nodeCount = 0;
149
+ private issueCount = 0;
150
+ private schedules: readonly KnowledgeScheduleRecord[] = [];
151
+
152
+ // Review-mode data
153
+ private reviewRows: BrowseRow[] = [];
154
+ private reviewIssues = new Map<string, KnowledgeIssueRecord>();
155
+
156
+ private confirm: ConfirmState<{ issueId: string; action: KnowledgeReviewAction }> | null = null;
157
+
158
+ constructor(knowledge: KnowledgeApi, openMemoryPanel?: () => void) {
61
159
  super('knowledge', 'Knowledge', 'K', 'agent');
160
+ this.knowledge = knowledge;
161
+ this.openMemoryPanel = openMemoryPanel;
162
+ }
163
+
164
+ // ---------------------------------------------------------------------------
165
+ // Lifecycle
166
+ // ---------------------------------------------------------------------------
167
+
168
+ override onActivate(): void {
169
+ super.onActivate();
170
+ this.confirm = null;
171
+ this.refreshBrowseRows();
172
+ this.refreshReviewRows();
173
+ }
174
+
175
+ // ---------------------------------------------------------------------------
176
+ // Data refresh
177
+ // ---------------------------------------------------------------------------
178
+
179
+ private refreshBrowseRows(): void {
180
+ try {
181
+ const nodes = this.knowledge.graph.nodes.list(LIST_LIMIT);
182
+ const sources = this.knowledge.sources.list(LIST_LIMIT);
183
+ const issues = this.knowledge.graph.issues.list(LIST_LIMIT);
184
+ this.sourceCount = sources.length;
185
+ this.nodeCount = nodes.length;
186
+ this.issueCount = issues.length;
187
+ this.browseRows = [
188
+ ...sources.map(sourceRow),
189
+ ...nodes.map(nodeRow),
190
+ ...issues.map(issueRow),
191
+ ];
192
+ this.schedules = this.knowledge.jobs.schedules.list(20);
193
+ } catch (e) {
194
+ this.setError(`Knowledge graph load failed: ${summarizeError(e)}`);
195
+ }
196
+ this.clampSelection();
197
+ this.markDirty();
198
+ }
199
+
200
+ private refreshReviewRows(): void {
201
+ try {
202
+ const issues = this.knowledge.graph.issues.list(LIST_LIMIT).filter((issue) => issue.status === 'open');
203
+ this.reviewIssues = new Map(issues.map((issue) => [issue.id, issue]));
204
+ this.reviewRows = issues.map(issueRow);
205
+ } catch (e) {
206
+ this.setError(`Knowledge review queue load failed: ${summarizeError(e)}`);
207
+ }
208
+ this.clampSelection();
209
+ this.markDirty();
62
210
  }
63
211
 
64
- private get entries(): readonly CommandEntry[] {
65
- return [...GRAPH_COMMANDS, ...MEMORY_COMMANDS];
212
+ private runSearch(): void {
213
+ const query = this.searchQuery.trim();
214
+ if (!query) {
215
+ this.searchRows = [];
216
+ this.markDirty();
217
+ return;
218
+ }
219
+ try {
220
+ this.searchRows = this.knowledge.graph.items.search(query, SEARCH_LIMIT).map(searchResultRow);
221
+ } catch (e) {
222
+ this.setError(`Knowledge search failed: ${summarizeError(e)}`);
223
+ this.searchRows = [];
224
+ }
225
+ this.selectedIndex = 0;
226
+ this.markDirty();
227
+ }
228
+
229
+ // ---------------------------------------------------------------------------
230
+ // ScrollableListPanel implementation
231
+ // ---------------------------------------------------------------------------
232
+
233
+ protected getItems(): readonly BrowseRow[] {
234
+ if (this.mode === 'review') return this.reviewRows;
235
+ if (this.searchQuery.trim()) return this.searchRows;
236
+ return this.browseRows;
66
237
  }
67
238
 
239
+ protected renderItem(row: BrowseRow, _index: number, selected: boolean, width: number): Line {
240
+ const bg = selected ? C.selectBg : undefined;
241
+ return buildPanelLine(width, [
242
+ [' ', C.label, bg],
243
+ [row.tag.padEnd(16), kindColor(row.kind), bg],
244
+ [truncateDisplay(row.title, Math.max(0, Math.floor((width - 22) * 0.45))), C.value, bg],
245
+ [' ', C.label, bg],
246
+ [truncateDisplay(row.detail, Math.max(0, width - 22 - Math.floor((width - 22) * 0.45) - 2)), C.dim, bg],
247
+ ]);
248
+ }
249
+
250
+ protected override getPalette() { return C; }
251
+
252
+ protected override getEmptyStateMessage(): string {
253
+ if (this.mode === 'review') return 'No issues waiting for review.';
254
+ if (this.searchQuery.trim()) return ` No results matching "${this.searchQuery.trim()}"`;
255
+ return 'No ingested knowledge yet.';
256
+ }
257
+
258
+ protected override getEmptyStateActions() {
259
+ if (this.mode === 'review' || this.searchQuery.trim()) return [];
260
+ return [
261
+ { command: '/knowledge ingest-url <url>', summary: 'ingest a URL as the first graph source' },
262
+ ];
263
+ }
264
+
265
+ // ---------------------------------------------------------------------------
266
+ // Input
267
+ // ---------------------------------------------------------------------------
268
+
68
269
  handleInput(key: string): boolean {
69
- const count = this.entries.length;
70
- if (key === 'up' || key === 'k') {
71
- this.selectedIndex = (this.selectedIndex - 1 + count) % count;
270
+ if (this.lastError !== null) this.clearError();
271
+
272
+ if (this.confirm) {
273
+ const result = handleConfirmInput(this.confirm, key);
274
+ if (result === 'confirmed') {
275
+ const { issueId, action } = this.confirm.subject;
276
+ this.confirm = null;
277
+ void this.applyReview(issueId, action);
278
+ return true;
279
+ }
280
+ if (result === 'cancelled') {
281
+ this.confirm = null;
282
+ this.markDirty();
283
+ return true;
284
+ }
285
+ if (result === 'absorbed') return true;
286
+ }
287
+
288
+ if (key === 'tab') {
289
+ this.mode = this.mode === 'browse' ? 'review' : 'browse';
290
+ this.selectedIndex = 0;
291
+ if (this.mode === 'review') this.refreshReviewRows();
292
+ else this.refreshBrowseRows();
72
293
  this.markDirty();
73
294
  return true;
74
295
  }
75
- if (key === 'down' || key === 'j') {
76
- this.selectedIndex = (this.selectedIndex + 1) % count;
77
- this.markDirty();
296
+
297
+ if (key === 'm' || key === 'M') {
298
+ this.openMemoryPanel?.();
78
299
  return true;
79
300
  }
80
- if (key === 'home' || key === 'g') {
81
- this.selectedIndex = 0;
82
- this.markDirty();
301
+
302
+ if (this.mode === 'review') {
303
+ // getItems() returns reviewRows in review mode, so the shared
304
+ // getSelectedItem() accessor resolves the highlighted issue row.
305
+ const selected = this.getSelectedItem();
306
+ if (key === 'a' && selected) {
307
+ this.confirm = { subject: { issueId: selected.id, action: 'accept' }, label: truncateDisplay(selected.detail || selected.title, 40), verb: 'Accept' };
308
+ this.markDirty();
309
+ return true;
310
+ }
311
+ if (key === 'x' && selected) {
312
+ this.confirm = { subject: { issueId: selected.id, action: 'reject' }, label: truncateDisplay(selected.detail || selected.title, 40), verb: 'Reject' };
313
+ this.markDirty();
314
+ return true;
315
+ }
316
+ if (key === 'r' && selected) {
317
+ this.confirm = { subject: { issueId: selected.id, action: 'resolve' }, label: truncateDisplay(selected.detail || selected.title, 40), verb: 'Resolve' };
318
+ this.markDirty();
319
+ return true;
320
+ }
321
+ if (key === 'o' && selected) {
322
+ this.confirm = { subject: { issueId: selected.id, action: 'reopen' }, label: truncateDisplay(selected.detail || selected.title, 40), verb: 'Reopen' };
323
+ this.markDirty();
324
+ return true;
325
+ }
326
+ return ScrollableListPanel.prototype.handleInput.call(this, key);
327
+ }
328
+
329
+ // Browse mode: '/' search focus
330
+ if (this.searchFocused) {
331
+ const items = this.getItems();
332
+ const transition = getPanelSearchFocusTransition(key, { selectedIndex: this.selectedIndex, itemCount: items.length });
333
+ if (transition === 'focus-list') {
334
+ this.searchFocused = false;
335
+ this.markDirty();
336
+ return true;
337
+ }
338
+ if (isPanelSearchCancel(key)) {
339
+ this.searchFocused = false;
340
+ this.searchQuery = '';
341
+ this.searchRows = [];
342
+ this.selectedIndex = 0;
343
+ this.markDirty();
344
+ return true;
345
+ }
346
+ if (isPanelSearchBackspace(key)) {
347
+ this.searchQuery = this.searchQuery.slice(0, -1);
348
+ this.runSearch();
349
+ return true;
350
+ }
351
+ if (isPanelSearchCommit(key)) {
352
+ this.searchFocused = false;
353
+ this.markDirty();
354
+ return true;
355
+ }
356
+ if (isPanelSearchPrintable(key)) {
357
+ this.searchQuery += key;
358
+ this.runSearch();
359
+ return true;
360
+ }
83
361
  return true;
84
362
  }
85
- if (key === 'end' || key === 'G') {
86
- this.selectedIndex = count - 1;
363
+
364
+ const items = this.getItems();
365
+ const transition = getPanelSearchFocusTransition(key, { selectedIndex: this.selectedIndex, itemCount: items.length });
366
+ if (transition === 'focus-search') {
367
+ this.searchFocused = true;
87
368
  this.markDirty();
88
369
  return true;
89
370
  }
90
- return false;
371
+
372
+ return ScrollableListPanel.prototype.handleInput.call(this, key);
373
+ }
374
+
375
+ private async applyReview(issueId: string, action: KnowledgeReviewAction): Promise<void> {
376
+ try {
377
+ await this.knowledge.graph.issues.review({ issueId, action, reviewer: 'tui' });
378
+ } catch (e) {
379
+ this.setError(`Review ${action} failed: ${summarizeError(e)}`);
380
+ }
381
+ this.refreshReviewRows();
382
+ this.refreshBrowseRows();
383
+ }
384
+
385
+ // ---------------------------------------------------------------------------
386
+ // Render
387
+ // ---------------------------------------------------------------------------
388
+
389
+ render(width: number, height: number): Line[] {
390
+ if (this.confirm) {
391
+ return buildPanelWorkspace(width, height, {
392
+ title: 'Knowledge',
393
+ intro: '',
394
+ sections: [{ title: 'Confirmation', lines: renderConfirmLines(width, this.confirm) }],
395
+ footerLines: [buildPanelLine(width, [[' y confirm n / Esc cancel', C.dim]])],
396
+ palette: C,
397
+ });
398
+ }
399
+
400
+ // KnowledgeApi has no subscribe/eventing hook (unlike MemoryRegistry), so
401
+ // pull fresh data on every render — same fetch-per-call pattern as the
402
+ // /knowledge command handlers. These are synchronous in-memory reads.
403
+ if (this.mode === 'review') {
404
+ this.refreshReviewRows();
405
+ this.clampSelection();
406
+ return this.renderReviewMode(width, height);
407
+ }
408
+ this.refreshBrowseRows();
409
+ this.clampSelection();
410
+ return this.renderBrowseMode(width, height);
411
+ }
412
+
413
+ private modeToggleLine(width: number): Line {
414
+ const label = this.mode === 'review' ? 'Review Queue' : 'Browse';
415
+ return buildPanelLine(width, [
416
+ [' Mode: ', C.label],
417
+ [label, C.info],
418
+ [' (Tab to toggle)', C.dim],
419
+ ]);
91
420
  }
92
421
 
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
- ]);
422
+ private renderBrowseMode(width: number, height: number): Line[] {
423
+ const readySources = this.sourceCount > 0 && this.nodeCount > 0;
424
+ const summaryLines: Line[] = [
425
+ buildKeyValueLine(width, [
426
+ { label: 'sources', value: String(this.sourceCount), valueColor: C.good },
427
+ { label: 'nodes', value: String(this.nodeCount), valueColor: C.info },
428
+ { label: 'issues', value: String(this.issueCount), valueColor: this.issueCount > 0 ? C.warn : C.good },
429
+ { label: 'schedules', value: String(this.schedules.length), valueColor: C.value },
430
+ ], C),
431
+ this.modeToggleLine(width),
432
+ buildSearchInputLine(
433
+ width,
434
+ this.searchFocused ? '[Search] ' : 'Search: ',
435
+ this.searchFocused ? `${this.searchQuery}_` : (this.searchQuery || '(/ to search)'),
436
+ C,
437
+ this.searchFocused ? { active: false, bg: C.inputBg, valueColor: C.info } : { active: false },
438
+ ),
439
+ ];
440
+
441
+ const selected = this.getSelectedItem();
442
+ const selectedLines: Line[] = [];
443
+ if (selected) {
444
+ selectedLines.push(buildKeyValueLine(width, [
445
+ { label: 'kind', value: selected.tag, valueColor: kindColor(selected.kind) },
446
+ { label: 'id', value: selected.id.slice(-10), valueColor: C.dim },
447
+ ], C));
448
+ if (selected.detail) selectedLines.push(...buildBodyText(width, selected.detail, C, C.value));
449
+ selectedLines.push(buildPanelLine(width, [
450
+ [' Retrieval readiness: ', C.label],
451
+ [readySources ? 'ready (sources + nodes indexed)' : 'not ready (needs sources and nodes)', readySources ? C.good : C.warn],
452
+ ]));
453
+ }
454
+
455
+ if (this.schedules.length > 0) {
456
+ selectedLines.push(buildPanelLine(width, [[' Schedules', C.label]]));
457
+ for (const schedule of this.schedules.slice(0, 4)) {
458
+ selectedLines.push(buildPanelLine(width, [
459
+ [' ', C.label],
460
+ [schedule.enabled ? 'on ' : 'off', schedule.enabled ? C.good : C.dim],
461
+ [' ', C.label],
462
+ [truncateDisplay(schedule.label, Math.max(0, width - 14)), C.value],
463
+ ]));
103
464
  }
104
- return buildGuidanceLine(width, entry.command, entry.summary, C);
465
+ }
466
+
467
+ return this.renderList(width, height, {
468
+ title: 'Knowledge',
469
+ header: summaryLines,
470
+ footer: [
471
+ ...selectedLines,
472
+ buildPanelLine(width, [[' / search j/k or Up/Down move Tab: Review Queue M memory panel Esc clear search', C.dim]]),
473
+ ],
105
474
  });
106
475
  }
107
476
 
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
-
112
- const sections = [
113
- {
114
- title: 'SDK Knowledge Graph',
115
- lines: [
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),
118
- ],
119
- },
120
- {
121
- title: 'Project Memory',
122
- lines: [
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),
125
- ],
126
- },
127
- {
128
- lines: buildDetailBlock(width, selected.command, buildBodyText(width, selected.detail, C, C.value), C),
129
- },
477
+ private renderReviewMode(width: number, height: number): Line[] {
478
+ const summaryLines: Line[] = [
479
+ buildKeyValueLine(width, [
480
+ { label: 'open issues', value: String(this.reviewRows.length), valueColor: this.reviewRows.length > 0 ? C.warn : C.good },
481
+ ], C),
482
+ this.modeToggleLine(width),
130
483
  ];
131
484
 
132
- const lines = buildPanelWorkspace(width, height, {
133
- title: 'Knowledge Graph',
134
- intro: 'Ingested sources, graph nodes, and the durable memory bridge.',
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)],
141
- palette: C,
485
+ const selectedRow = this.getSelectedItem();
486
+ const selectedIssue = selectedRow ? this.reviewIssues.get(selectedRow.id) : undefined;
487
+ const selectedLines: Line[] = [];
488
+ if (selectedRow && selectedIssue) {
489
+ selectedLines.push(buildKeyValueLine(width, [
490
+ { label: 'severity', value: selectedIssue.severity, valueColor: severityColor(selectedIssue.severity) },
491
+ { label: 'code', value: selectedIssue.code, valueColor: C.value },
492
+ { label: 'status', value: selectedIssue.status, valueColor: C.info },
493
+ ], C));
494
+ selectedLines.push(...buildBodyText(width, selectedIssue.message, C, C.value));
495
+ }
496
+
497
+ return this.renderList(width, height, {
498
+ title: 'Knowledge',
499
+ header: summaryLines,
500
+ footer: [
501
+ ...selectedLines,
502
+ buildPanelLine(width, [[' Up/Down move a accept x reject r resolve o reopen Tab: Browse M memory panel', C.dim]]),
503
+ ],
142
504
  });
143
- while (lines.length < height) lines.push(createEmptyLine(width));
144
- return lines.slice(0, height);
145
505
  }
146
506
  }