@pellux/goodvibes-tui 0.27.0 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/CHANGELOG.md +124 -0
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  4. package/src/core/context-usage.ts +53 -0
  5. package/src/core/conversation.ts +4 -6
  6. package/src/core/session-recovery.ts +2 -0
  7. package/src/core/turn-event-wiring.ts +1 -0
  8. package/src/input/commands/cost-runtime.ts +49 -0
  9. package/src/input/commands/eval.ts +4 -3
  10. package/src/input/commands/operator-runtime.ts +5 -1
  11. package/src/input/commands.ts +2 -0
  12. package/src/input/handler-feed-routes.ts +60 -26
  13. package/src/input/handler-feed.ts +17 -8
  14. package/src/input/handler-picker-routes.ts +18 -13
  15. package/src/input/handler-shortcuts.ts +51 -0
  16. package/src/input/handler-ui-state.ts +4 -0
  17. package/src/input/handler.ts +43 -6
  18. package/src/input/keybindings.ts +53 -8
  19. package/src/input/model-picker.ts +5 -0
  20. package/src/input/panel-integration-actions.ts +117 -1
  21. package/src/main.ts +5 -2
  22. package/src/panels/agent-inspector-panel.ts +139 -45
  23. package/src/panels/agent-inspector-shared.ts +147 -3
  24. package/src/panels/approval-panel.ts +204 -102
  25. package/src/panels/automation-control-panel.ts +370 -103
  26. package/src/panels/base-panel.ts +26 -2
  27. package/src/panels/builtin/agent.ts +28 -37
  28. package/src/panels/builtin/development.ts +40 -32
  29. package/src/panels/builtin/knowledge.ts +15 -6
  30. package/src/panels/builtin/operations.ts +178 -117
  31. package/src/panels/builtin/session.ts +35 -9
  32. package/src/panels/builtin/shared.ts +98 -6
  33. package/src/panels/cockpit-panel.ts +253 -75
  34. package/src/panels/cockpit-read-model.ts +2 -1
  35. package/src/panels/communication-panel.ts +159 -56
  36. package/src/panels/confirm-state.ts +8 -1
  37. package/src/panels/control-plane-panel.ts +354 -95
  38. package/src/panels/cost-tracker-panel.ts +243 -57
  39. package/src/panels/debug-panel.ts +347 -159
  40. package/src/panels/diff-panel.ts +257 -82
  41. package/src/panels/docs-panel.ts +166 -64
  42. package/src/panels/eval-panel.ts +336 -109
  43. package/src/panels/expandable-list-panel.ts +189 -0
  44. package/src/panels/file-explorer-panel.ts +238 -159
  45. package/src/panels/file-preview-panel.ts +141 -59
  46. package/src/panels/git-panel.ts +360 -191
  47. package/src/panels/hooks-panel.ts +181 -19
  48. package/src/panels/incident-review-panel.ts +252 -43
  49. package/src/panels/index.ts +0 -4
  50. package/src/panels/intelligence-panel.ts +310 -103
  51. package/src/panels/knowledge-graph-panel.ts +480 -58
  52. package/src/panels/local-auth-panel.ts +251 -33
  53. package/src/panels/marketplace-panel.ts +219 -33
  54. package/src/panels/memory-panel.ts +83 -81
  55. package/src/panels/ops-control-panel.ts +211 -32
  56. package/src/panels/ops-strategy-panel.ts +131 -45
  57. package/src/panels/orchestration-panel.ts +259 -67
  58. package/src/panels/panel-list-panel.ts +184 -136
  59. package/src/panels/panel-manager.ts +120 -13
  60. package/src/panels/plan-dashboard-panel.ts +507 -74
  61. package/src/panels/plugins-panel.ts +227 -34
  62. package/src/panels/policy-panel.ts +264 -55
  63. package/src/panels/polish-core.ts +162 -0
  64. package/src/panels/polish-tables.ts +258 -0
  65. package/src/panels/polish.ts +67 -149
  66. package/src/panels/project-planning-answer-actions.ts +134 -0
  67. package/src/panels/project-planning-panel.ts +84 -113
  68. package/src/panels/provider-health-panel.ts +438 -480
  69. package/src/panels/provider-health-routes.ts +203 -0
  70. package/src/panels/provider-health-tracker.ts +194 -6
  71. package/src/panels/provider-health-views.ts +560 -0
  72. package/src/panels/qr-panel.ts +136 -38
  73. package/src/panels/remote-panel.ts +123 -38
  74. package/src/panels/routes-panel.ts +87 -24
  75. package/src/panels/sandbox-panel.ts +232 -65
  76. package/src/panels/scrollable-list-panel.ts +143 -145
  77. package/src/panels/security-panel.ts +204 -52
  78. package/src/panels/services-panel.ts +174 -76
  79. package/src/panels/session-browser-panel.ts +101 -6
  80. package/src/panels/session-maintenance.ts +4 -122
  81. package/src/panels/settings-sync-panel.ts +346 -68
  82. package/src/panels/skills-panel.ts +157 -89
  83. package/src/panels/subscription-panel.ts +105 -34
  84. package/src/panels/symbol-outline-panel.ts +287 -154
  85. package/src/panels/system-messages-panel.ts +172 -38
  86. package/src/panels/tasks-panel.ts +348 -134
  87. package/src/panels/thinking-panel.ts +66 -32
  88. package/src/panels/token-budget-panel.ts +233 -46
  89. package/src/panels/tool-inspector-panel.ts +170 -54
  90. package/src/panels/types.ts +65 -4
  91. package/src/panels/work-plan-panel.ts +393 -39
  92. package/src/panels/worktree-panel.ts +239 -61
  93. package/src/panels/wrfc-panel-format.ts +133 -0
  94. package/src/panels/wrfc-panel.ts +146 -150
  95. package/src/renderer/compaction-preview.ts +2 -1
  96. package/src/renderer/compositor.ts +46 -43
  97. package/src/renderer/conversation-overlays.ts +25 -11
  98. package/src/renderer/footer-tips.ts +41 -0
  99. package/src/renderer/fullscreen-primitives.ts +22 -16
  100. package/src/renderer/help-overlay.ts +91 -14
  101. package/src/renderer/hint-grammar.ts +52 -0
  102. package/src/renderer/layout.ts +7 -7
  103. package/src/renderer/modal-factory.ts +23 -15
  104. package/src/renderer/model-picker-overlay.ts +30 -11
  105. package/src/renderer/model-workspace.ts +2 -3
  106. package/src/renderer/overlay-box.ts +16 -10
  107. package/src/renderer/panel-composite.ts +7 -20
  108. package/src/renderer/panel-workspace-bar.ts +14 -8
  109. package/src/renderer/process-indicator.ts +3 -1
  110. package/src/renderer/progress.ts +8 -6
  111. package/src/renderer/search-overlay.ts +27 -6
  112. package/src/renderer/session-picker-modal.ts +6 -4
  113. package/src/renderer/settings-modal.ts +70 -10
  114. package/src/renderer/shell-surface.ts +41 -15
  115. package/src/renderer/status-glyphs.ts +11 -9
  116. package/src/renderer/tab-strip.ts +148 -34
  117. package/src/renderer/theme.ts +60 -3
  118. package/src/renderer/ui-factory.ts +45 -36
  119. package/src/renderer/ui-primitives.ts +23 -2
  120. package/src/runtime/bootstrap-shell.ts +35 -18
  121. package/src/runtime/diagnostics/panels/index.ts +0 -3
  122. package/src/runtime/ui/model-picker/health-enrichment.ts +3 -0
  123. package/src/runtime/ui/model-picker/types.ts +4 -0
  124. package/src/shell/ui-openers.ts +14 -22
  125. package/src/utils/format-duration.ts +55 -0
  126. package/src/utils/format-number.ts +71 -0
  127. package/src/utils/splash-lines.ts +44 -3
  128. package/src/version.ts +1 -1
  129. package/src/work-plans/work-plan-store.ts +13 -0
  130. package/src/panels/agent-logs-panel.ts +0 -666
  131. package/src/panels/agent-logs-shared.ts +0 -129
  132. package/src/panels/context-visualizer-panel.ts +0 -214
  133. package/src/panels/forensics-panel.ts +0 -364
  134. package/src/panels/panel-picker.ts +0 -106
  135. package/src/panels/provider-account-snapshot.ts +0 -259
  136. package/src/panels/provider-accounts-panel.ts +0 -218
  137. package/src/panels/provider-stats-panel.ts +0 -366
  138. package/src/panels/schedule-panel.ts +0 -342
  139. package/src/panels/watchers-panel.ts +0 -193
  140. package/src/renderer/panel-picker-overlay.ts +0 -202
  141. package/src/renderer/panel-tab-bar.ts +0 -69
  142. package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
  143. package/src/runtime/diagnostics/panels/policy.ts +0 -177
@@ -1,84 +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…).
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.
11
16
  */
12
17
 
13
18
  import type { Line } from '../types/grid.ts';
14
- 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';
15
30
  import {
16
31
  buildBodyText,
17
- buildGuidanceLine,
32
+ buildKeyValueLine,
18
33
  buildPanelLine,
19
34
  buildPanelWorkspace,
35
+ buildSearchInputLine,
20
36
  DEFAULT_PANEL_PALETTE,
21
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];
54
+
55
+ type BrowseKind = 'node' | 'source' | 'issue' | 'result';
56
+
57
+ interface BrowseRow {
58
+ readonly kind: BrowseKind;
59
+ readonly id: string;
60
+ readonly title: string;
61
+ readonly tag: string;
62
+ readonly detail: string;
63
+ }
64
+
65
+ type Mode = 'browse' | 'review';
22
66
 
23
- const C = {
24
- ...DEFAULT_PANEL_PALETTE,
25
- header: '#94a3b8',
26
- headerBg: '#1e293b',
27
- } as const;
67
+ const LIST_LIMIT = 60;
68
+ const SEARCH_LIMIT = 30;
28
69
 
29
- export class KnowledgeGraphPanel extends BasePanel {
30
- constructor() {
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) {
31
159
  super('knowledge', 'Knowledge', 'K', 'agent');
160
+ this.knowledge = knowledge;
161
+ this.openMemoryPanel = openMemoryPanel;
32
162
  }
33
163
 
34
- handleInput(_key: string): boolean {
35
- return false;
164
+ // ---------------------------------------------------------------------------
165
+ // Lifecycle
166
+ // ---------------------------------------------------------------------------
167
+
168
+ override onActivate(): void {
169
+ super.onActivate();
170
+ this.confirm = null;
171
+ this.refreshBrowseRows();
172
+ this.refreshReviewRows();
36
173
  }
37
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();
210
+ }
211
+
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;
237
+ }
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
+
269
+ handleInput(key: string): boolean {
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();
293
+ this.markDirty();
294
+ return true;
295
+ }
296
+
297
+ if (key === 'm' || key === 'M') {
298
+ this.openMemoryPanel?.();
299
+ return true;
300
+ }
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
+ }
361
+ return true;
362
+ }
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;
368
+ this.markDirty();
369
+ return true;
370
+ }
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
+
38
389
  render(width: number, height: number): Line[] {
39
- const sections = [
40
- {
41
- title: 'SDK Knowledge Graph',
42
- 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),
57
- ],
58
- },
59
- {
60
- title: 'Project Memory',
61
- 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),
73
- ],
74
- },
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
+ ]);
420
+ }
421
+
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
+ ),
75
439
  ];
76
440
 
77
- return buildPanelWorkspace(width, height, {
78
- title: 'Knowledge Graph',
79
- intro: 'Ingested sources, graph nodes, and the durable memory bridge.',
80
- sections,
81
- palette: C,
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
+ ]));
464
+ }
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
+ ],
474
+ });
475
+ }
476
+
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),
483
+ ];
484
+
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
+ ],
82
504
  });
83
505
  }
84
506
  }