@pellux/goodvibes-tui 0.18.23 → 0.19.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.
- package/CHANGELOG.md +34 -0
- package/README.md +1 -1
- package/docs/foundation-artifacts/operator-contract.json +1 -1
- package/package.json +7 -3
- package/src/core/conversation-rendering.ts +8 -6
- package/src/core/orchestrator.ts +1 -1
- package/src/input/commands/diff-runtime.ts +6 -5
- package/src/input/commands/guidance-runtime.ts +1 -1
- package/src/input/commands/health-runtime.ts +2 -2
- package/src/input/commands/local-setup-review.ts +1 -1
- package/src/input/commands/session-content.ts +1 -1
- package/src/input/commands/shell-core.ts +3 -2
- package/src/input/commands/skills-runtime.ts +2 -2
- package/src/input/commands/subscription-runtime.ts +4 -4
- package/src/input/handler.ts +8 -10
- package/src/input/panel-integration-actions.ts +2 -1
- package/src/input/settings-modal-types.ts +60 -0
- package/src/input/settings-modal.ts +83 -65
- package/src/panels/agent-inspector-panel.ts +10 -9
- package/src/panels/agent-logs-panel.ts +26 -6
- package/src/panels/approval-panel.ts +1 -0
- package/src/panels/automation-control-panel.ts +1 -0
- package/src/panels/base-panel.ts +108 -3
- package/src/panels/communication-panel.ts +1 -0
- package/src/panels/context-visualizer-panel.ts +2 -0
- package/src/panels/control-plane-panel.ts +1 -0
- package/src/panels/diff-panel.ts +2 -0
- package/src/panels/file-explorer-panel.ts +51 -31
- package/src/panels/file-preview-panel.ts +57 -35
- package/src/panels/git-panel.ts +12 -13
- package/src/panels/hooks-panel.ts +3 -1
- package/src/panels/incident-review-panel.ts +4 -2
- package/src/panels/knowledge-panel.ts +75 -107
- package/src/panels/local-auth-panel.ts +1 -0
- package/src/panels/marketplace-panel.ts +51 -69
- package/src/panels/mcp-panel.ts +3 -1
- package/src/panels/memory-panel.ts +90 -158
- package/src/panels/ops-control-panel.ts +1 -0
- package/src/panels/orchestration-panel.ts +70 -51
- package/src/panels/panel-list-panel.ts +5 -4
- package/src/panels/panel-manager.ts +3 -0
- package/src/panels/plan-dashboard-panel.ts +2 -0
- package/src/panels/plugins-panel.ts +1 -0
- package/src/panels/polish.ts +51 -2
- package/src/panels/provider-accounts-panel.ts +1 -0
- package/src/panels/provider-health-panel.ts +6 -8
- package/src/panels/routes-panel.ts +3 -1
- package/src/panels/schedule-panel.ts +7 -6
- package/src/panels/scrollable-list-panel.ts +19 -2
- package/src/panels/security-panel.ts +17 -15
- package/src/panels/services-panel.ts +6 -4
- package/src/panels/session-browser-panel.ts +19 -18
- package/src/panels/settings-sync-panel.ts +3 -1
- package/src/panels/skills-panel.ts +114 -230
- package/src/panels/subscription-panel.ts +1 -0
- package/src/panels/system-messages-panel.ts +147 -141
- package/src/panels/tasks-panel.ts +1 -0
- package/src/panels/token-budget-panel.ts +2 -0
- package/src/panels/watchers-panel.ts +1 -0
- package/src/panels/worktree-panel.ts +1 -0
- package/src/panels/wrfc-panel.ts +2 -0
- package/src/renderer/agent-detail-modal.ts +2 -2
- package/src/renderer/ansi-sanitize.ts +76 -0
- package/src/renderer/buffer.ts +12 -1
- package/src/renderer/help-overlay.ts +14 -3
- package/src/renderer/settings-modal-helpers.ts +27 -0
- package/src/renderer/settings-modal.ts +18 -1
- package/src/renderer/status-glyphs.ts +21 -0
- package/src/renderer/status-token.ts +4 -8
- package/src/renderer/tool-call.ts +4 -3
- package/src/runtime/bootstrap-core.ts +1 -1
- package/src/runtime/bootstrap-hook-bridge.ts +1 -1
- package/src/runtime/bootstrap.ts +7 -8
- package/src/runtime/diagnostics/panels/policy.ts +2 -1
- package/src/shell/ui-openers.ts +1 -1
- package/src/version.ts +1 -1
|
@@ -1,26 +1,20 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { promises as fsPromises } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import type { Line } from '../types/grid.ts';
|
|
4
4
|
import { createEmptyLine } from '../types/grid.ts';
|
|
5
5
|
import { type ConfirmState, handleConfirmInput, renderConfirmLines } from './confirm-state.ts';
|
|
6
6
|
import { getDisplayWidth, truncateDisplay } from '../utils/terminal-width.ts';
|
|
7
|
-
import {
|
|
7
|
+
import { SearchableListPanel } from './scrollable-list-panel.ts';
|
|
8
8
|
import type { ComponentHealthMonitor } from '../runtime/perf/panel-health-monitor.ts';
|
|
9
9
|
import type { ShellPathService } from '@pellux/goodvibes-sdk/platform/runtime/shell-paths';
|
|
10
10
|
import {
|
|
11
|
-
buildEmptyState,
|
|
12
11
|
buildPanelLine,
|
|
13
|
-
buildSearchInputLine,
|
|
14
12
|
buildPanelWorkspace,
|
|
15
13
|
DEFAULT_PANEL_PALETTE,
|
|
16
|
-
resolvePrimaryScrollableSection,
|
|
17
|
-
type PanelWorkspaceSection,
|
|
18
14
|
} from './polish.ts';
|
|
19
15
|
import {
|
|
20
16
|
getPanelSearchFocusTransition,
|
|
21
|
-
isPanelSearchBackspace,
|
|
22
17
|
isPanelSearchCancel,
|
|
23
|
-
isPanelSearchPrintable,
|
|
24
18
|
} from './search-focus.ts';
|
|
25
19
|
|
|
26
20
|
const C = {
|
|
@@ -81,11 +75,10 @@ function getSkillDirectories(cwd: string, homeDir: string): Array<{ root: string
|
|
|
81
75
|
];
|
|
82
76
|
}
|
|
83
77
|
|
|
84
|
-
function readSkillFile(path: string, origin: SkillOrigin): SkillRecord | null {
|
|
85
|
-
if (!existsSync(path)) return null;
|
|
78
|
+
async function readSkillFile(path: string, origin: SkillOrigin): Promise<SkillRecord | null> {
|
|
86
79
|
let content = '';
|
|
87
80
|
try {
|
|
88
|
-
content =
|
|
81
|
+
content = await fsPromises.readFile(path, 'utf-8');
|
|
89
82
|
} catch {
|
|
90
83
|
return null;
|
|
91
84
|
}
|
|
@@ -115,11 +108,10 @@ function readSkillFile(path: string, origin: SkillOrigin): SkillRecord | null {
|
|
|
115
108
|
};
|
|
116
109
|
}
|
|
117
110
|
|
|
118
|
-
function scanSkillDirectory(root: string, origin: SkillOrigin): SkillRecord[] {
|
|
119
|
-
if (!existsSync(root)) return [];
|
|
111
|
+
async function scanSkillDirectory(root: string, origin: SkillOrigin): Promise<SkillRecord[]> {
|
|
120
112
|
let entries: string[] = [];
|
|
121
113
|
try {
|
|
122
|
-
entries =
|
|
114
|
+
entries = await fsPromises.readdir(root);
|
|
123
115
|
} catch {
|
|
124
116
|
return [];
|
|
125
117
|
}
|
|
@@ -127,27 +119,27 @@ function scanSkillDirectory(root: string, origin: SkillOrigin): SkillRecord[] {
|
|
|
127
119
|
const records: SkillRecord[] = [];
|
|
128
120
|
for (const entry of entries.sort((a, b) => a.localeCompare(b))) {
|
|
129
121
|
if (entry.endsWith('.md')) {
|
|
130
|
-
const record = readSkillFile(join(root, entry), origin);
|
|
122
|
+
const record = await readSkillFile(join(root, entry), origin);
|
|
131
123
|
if (record) records.push(record);
|
|
132
124
|
continue;
|
|
133
125
|
}
|
|
134
126
|
|
|
135
127
|
const markerPath = join(root, entry, 'SKILL.md');
|
|
136
|
-
const record = readSkillFile(markerPath, origin);
|
|
128
|
+
const record = await readSkillFile(markerPath, origin);
|
|
137
129
|
if (record) records.push(record);
|
|
138
130
|
}
|
|
139
131
|
|
|
140
132
|
return records;
|
|
141
133
|
}
|
|
142
134
|
|
|
143
|
-
export function discoverSkills(shellPaths: Pick<ShellPathService, 'workingDirectory' | 'homeDirectory'>): SkillRecord[] {
|
|
135
|
+
export async function discoverSkills(shellPaths: Pick<ShellPathService, 'workingDirectory' | 'homeDirectory'>): Promise<SkillRecord[]> {
|
|
144
136
|
const cwd = shellPaths.workingDirectory;
|
|
145
137
|
const homeDir = shellPaths.homeDirectory;
|
|
146
138
|
const seen = new Set<string>();
|
|
147
139
|
const records: SkillRecord[] = [];
|
|
148
140
|
|
|
149
141
|
for (const { root, origin } of getSkillDirectories(cwd, homeDir)) {
|
|
150
|
-
for (const record of scanSkillDirectory(root, origin)) {
|
|
142
|
+
for (const record of await scanSkillDirectory(root, origin)) {
|
|
151
143
|
if (seen.has(record.name.toLowerCase())) continue;
|
|
152
144
|
seen.add(record.name.toLowerCase());
|
|
153
145
|
records.push(record);
|
|
@@ -234,29 +226,99 @@ function originColor(origin: SkillOrigin): string {
|
|
|
234
226
|
}
|
|
235
227
|
}
|
|
236
228
|
|
|
237
|
-
export class SkillsPanel extends
|
|
229
|
+
export class SkillsPanel extends SearchableListPanel<SkillRecord> {
|
|
238
230
|
private readonly shellPaths: Pick<ShellPathService, 'workingDirectory' | 'homeDirectory'>;
|
|
239
|
-
|
|
231
|
+
/** Whether the filter input row is focused for typing (vs. list navigation). */
|
|
240
232
|
private filterFocused = false;
|
|
241
|
-
private selectedIndex = 0;
|
|
242
|
-
private scrollOffset = 0;
|
|
243
233
|
private cached: SkillRecord[] | null = null;
|
|
244
234
|
private cacheDirty = true;
|
|
245
235
|
// I1: confirm state for destructive delete
|
|
246
236
|
private confirm: ConfirmState | null = null;
|
|
237
|
+
private readyPromise: Promise<void> | null = null;
|
|
247
238
|
|
|
248
239
|
public constructor(options: SkillsPanelOptions) {
|
|
249
240
|
super('skills', 'Skills', 'K', 'monitoring', options.componentHealthMonitor);
|
|
241
|
+
this.showSelectionGutter = true; // I5: non-color selection affordance
|
|
250
242
|
this.shellPaths = options.shellPaths;
|
|
251
243
|
}
|
|
252
244
|
|
|
245
|
+
// -------------------------------------------------------------------------
|
|
246
|
+
// SearchableListPanel implementation
|
|
247
|
+
// -------------------------------------------------------------------------
|
|
248
|
+
|
|
249
|
+
protected getAllItems(): readonly SkillRecord[] {
|
|
250
|
+
return this.cached ?? [];
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private _loadSkillsAsync(): Promise<void> {
|
|
254
|
+
const p = (async () => {
|
|
255
|
+
try {
|
|
256
|
+
await this.withLoading('Scanning skills\u2026', async () => {
|
|
257
|
+
this.cached = await discoverSkills(this.shellPaths);
|
|
258
|
+
this.cacheDirty = false;
|
|
259
|
+
this.invalidateFilter();
|
|
260
|
+
});
|
|
261
|
+
} catch (err) {
|
|
262
|
+
this.setError(err instanceof Error ? err.message : String(err));
|
|
263
|
+
}
|
|
264
|
+
this.markDirty();
|
|
265
|
+
})();
|
|
266
|
+
this.readyPromise = p;
|
|
267
|
+
return p;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** Resolves when the current load cycle has settled. */
|
|
271
|
+
public awaitReady(): Promise<void> {
|
|
272
|
+
return this.readyPromise ?? Promise.resolve();
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
protected matchesSearch(skill: SkillRecord, query: string): boolean {
|
|
276
|
+
const q = query.trim().toLowerCase();
|
|
277
|
+
if (!q) return true;
|
|
278
|
+
const haystack = [
|
|
279
|
+
skill.name,
|
|
280
|
+
skill.description,
|
|
281
|
+
skill.path,
|
|
282
|
+
skill.origin,
|
|
283
|
+
skill.dependencies.join(' '),
|
|
284
|
+
skill.includes.join(' '),
|
|
285
|
+
].join(' ').toLowerCase();
|
|
286
|
+
return haystack.includes(q);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
protected renderItem(skill: SkillRecord, index: number, selected: boolean, width: number): Line {
|
|
290
|
+
const bg = selected ? C.selectBg : undefined;
|
|
291
|
+
const dot = skill.origin === 'project-local' ? '\u25c6' : '\u2022';
|
|
292
|
+
const desc = skill.description || 'No description provided.';
|
|
293
|
+
const descWidth = Math.max(1, width - 4 - skill.name.length - 6);
|
|
294
|
+
const descLines = wordWrap(desc, descWidth);
|
|
295
|
+
return buildPanelLine(width, [
|
|
296
|
+
[selected ? '\u25b8' : ' ', C.selectedFg, bg],
|
|
297
|
+
[' ', C.dim, bg],
|
|
298
|
+
[dot, originColor(skill.origin), bg],
|
|
299
|
+
[' ', C.dim, bg],
|
|
300
|
+
[skill.name, selected ? C.selectedFg : C.value, bg],
|
|
301
|
+
[' ', C.dim, bg],
|
|
302
|
+
[descLines[0] ?? '', selected ? C.selectedFg : C.dim, bg],
|
|
303
|
+
]);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
protected override getPalette() { return C; }
|
|
307
|
+
protected override getEmptyStateMessage() { return ' No skills discovered.'; }
|
|
308
|
+
protected override getEmptyStateActions() {
|
|
309
|
+
return [
|
|
310
|
+
{ command: '.goodvibes/skills', summary: 'place skill .md files here (project-local) or ~/.goodvibes/skills (global)' },
|
|
311
|
+
{ command: '/registry search skills', summary: 'inspect the same skill directories from the shell' },
|
|
312
|
+
];
|
|
313
|
+
}
|
|
314
|
+
|
|
253
315
|
public override onActivate(): void {
|
|
254
316
|
super.onActivate();
|
|
255
|
-
this.
|
|
317
|
+
this.searchQuery = '';
|
|
318
|
+
this.invalidateFilter();
|
|
256
319
|
this.filterFocused = false;
|
|
257
|
-
this.selectedIndex = 0;
|
|
258
|
-
this.scrollOffset = 0;
|
|
259
320
|
this.cacheDirty = true;
|
|
321
|
+
void this._loadSkillsAsync();
|
|
260
322
|
}
|
|
261
323
|
|
|
262
324
|
public override onDestroy(): void {}
|
|
@@ -280,110 +342,51 @@ export class SkillsPanel extends BasePanel {
|
|
|
280
342
|
}
|
|
281
343
|
if (confirmResult === 'absorbed') return true;
|
|
282
344
|
|
|
283
|
-
const
|
|
345
|
+
const items = this.getItems();
|
|
346
|
+
|
|
347
|
+
// Filter-focus mode: typing goes into the search query
|
|
284
348
|
if (this.filterFocused) {
|
|
285
|
-
const transition = getPanelSearchFocusTransition(key, { selectedIndex: this.selectedIndex, itemCount:
|
|
349
|
+
const transition = getPanelSearchFocusTransition(key, { selectedIndex: this.selectedIndex, itemCount: items.length });
|
|
286
350
|
if (transition === 'focus-list') {
|
|
287
351
|
this.filterFocused = false;
|
|
288
|
-
this.selectedIndex = 0;
|
|
289
|
-
this.scrollOffset = 0;
|
|
290
|
-
this.markDirty();
|
|
291
|
-
return true;
|
|
292
|
-
}
|
|
293
|
-
if (isPanelSearchBackspace(key)) {
|
|
294
|
-
if (this.query.length === 0) return true;
|
|
295
|
-
this.query = this.query.slice(0, -1);
|
|
296
|
-
this.selectedIndex = 0;
|
|
297
|
-
this.scrollOffset = 0;
|
|
298
352
|
this.markDirty();
|
|
299
353
|
return true;
|
|
300
354
|
}
|
|
355
|
+
// Escape: also blur filter focus (clear + return to list navigation)
|
|
301
356
|
if (isPanelSearchCancel(key)) {
|
|
302
357
|
this.filterFocused = false;
|
|
303
|
-
|
|
304
|
-
|
|
358
|
+
// Delegate to super to clear the query. If the query is empty, super
|
|
359
|
+
// returns false and escape propagates to the panel dismissal handler —
|
|
360
|
+
// this is the intentional double-escape UX (blur filter, then close).
|
|
361
|
+
return super.handleInput(key);
|
|
305
362
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
this.selectedIndex = 0;
|
|
309
|
-
this.scrollOffset = 0;
|
|
310
|
-
this.markDirty();
|
|
311
|
-
return true;
|
|
312
|
-
}
|
|
313
|
-
return false;
|
|
363
|
+
// Delegate backspace/printable to SearchableListPanel.handleInput
|
|
364
|
+
return super.handleInput(key);
|
|
314
365
|
}
|
|
315
366
|
|
|
316
|
-
const transition = getPanelSearchFocusTransition(key, { selectedIndex: this.selectedIndex, itemCount:
|
|
367
|
+
const transition = getPanelSearchFocusTransition(key, { selectedIndex: this.selectedIndex, itemCount: items.length });
|
|
317
368
|
if (transition === 'focus-search') {
|
|
318
369
|
this.filterFocused = true;
|
|
319
370
|
this.markDirty();
|
|
320
371
|
return true;
|
|
321
372
|
}
|
|
322
373
|
|
|
323
|
-
if (key === 'up' || key === 'k') {
|
|
324
|
-
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
|
|
325
|
-
this.markDirty();
|
|
326
|
-
return true;
|
|
327
|
-
}
|
|
328
|
-
if (key === 'down' || key === 'j') {
|
|
329
|
-
this.selectedIndex = Math.min(Math.max(0, records.length - 1), this.selectedIndex + 1);
|
|
330
|
-
this.markDirty();
|
|
331
|
-
return true;
|
|
332
|
-
}
|
|
333
|
-
if (key === 'home') {
|
|
334
|
-
this.selectedIndex = 0;
|
|
335
|
-
this.markDirty();
|
|
336
|
-
return true;
|
|
337
|
-
}
|
|
338
|
-
if (key === 'end') {
|
|
339
|
-
this.selectedIndex = Math.max(0, records.length - 1);
|
|
340
|
-
this.markDirty();
|
|
341
|
-
return true;
|
|
342
|
-
}
|
|
343
|
-
if (key === 'pageup') {
|
|
344
|
-
this.selectedIndex = Math.max(0, this.selectedIndex - 5);
|
|
345
|
-
this.markDirty();
|
|
346
|
-
return true;
|
|
347
|
-
}
|
|
348
|
-
if (key === 'pagedown') {
|
|
349
|
-
this.selectedIndex = Math.min(Math.max(0, records.length - 1), this.selectedIndex + 5);
|
|
350
|
-
this.markDirty();
|
|
351
|
-
return true;
|
|
352
|
-
}
|
|
353
374
|
// I1: 'd' prompts delete confirmation
|
|
354
375
|
if (key === 'd') {
|
|
355
|
-
const skill =
|
|
376
|
+
const skill = items[this.selectedIndex];
|
|
356
377
|
if (skill) {
|
|
357
378
|
this.confirm = { subject: skill.path, label: skill.name };
|
|
358
379
|
this.markDirty();
|
|
359
380
|
}
|
|
360
381
|
return true;
|
|
361
382
|
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
this.selectedIndex = 0;
|
|
366
|
-
this.scrollOffset = 0;
|
|
367
|
-
this.markDirty();
|
|
368
|
-
return true;
|
|
369
|
-
}
|
|
370
|
-
if (isPanelSearchCancel(key)) {
|
|
371
|
-
if (this.query.length === 0) return false;
|
|
372
|
-
this.query = '';
|
|
373
|
-
this.selectedIndex = 0;
|
|
374
|
-
this.scrollOffset = 0;
|
|
375
|
-
this.markDirty();
|
|
376
|
-
return true;
|
|
377
|
-
}
|
|
378
|
-
return false;
|
|
383
|
+
|
|
384
|
+
// Navigation + search: delegate to SearchableListPanel (up/down/g/G/page/enter + backspace/escape)
|
|
385
|
+
return super.handleInput(key);
|
|
379
386
|
}
|
|
380
387
|
|
|
381
388
|
public render(width: number, height: number): Line[] {
|
|
382
|
-
|
|
383
|
-
return Array.from({ length: height }, () => createEmptyLine(width));
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
const start = Date.now();
|
|
389
|
+
return this.trackedRender(() => {
|
|
387
390
|
this.needsRender = false;
|
|
388
391
|
|
|
389
392
|
// I1: show confirm dialog in place of normal content
|
|
@@ -395,50 +398,15 @@ export class SkillsPanel extends BasePanel {
|
|
|
395
398
|
palette: C,
|
|
396
399
|
});
|
|
397
400
|
while (lines.length < height) lines.push(createEmptyLine(width));
|
|
398
|
-
this.reportRenderDuration(Date.now() - start);
|
|
399
|
-
return lines.slice(0, height);
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
const intro = 'Discover project-local and global skill packs, filter by name or description, and inspect path, dependencies, and includes.';
|
|
403
|
-
const skills = this._filteredSkills();
|
|
404
|
-
|
|
405
|
-
if (skills.length === 0) {
|
|
406
|
-
const lines = buildPanelWorkspace(width, height, {
|
|
407
|
-
title: 'Skills - discover project-local and global skill packs',
|
|
408
|
-
intro,
|
|
409
|
-
sections: [{
|
|
410
|
-
title: 'Filter',
|
|
411
|
-
lines: [buildSearchInputLine(width, ' query: ', `${this.query}${this.filterFocused ? '_' : ''}`, C, {
|
|
412
|
-
active: this.filterFocused,
|
|
413
|
-
emptyLabel: this.filterFocused ? '(type to filter)' : '(/ or up at top)',
|
|
414
|
-
valueColor: this.query ? C.searchFg : undefined,
|
|
415
|
-
})],
|
|
416
|
-
}, {
|
|
417
|
-
lines: buildEmptyState(
|
|
418
|
-
width,
|
|
419
|
-
' No skills discovered.',
|
|
420
|
-
'Create .goodvibes/skills or .goodvibes/tui/skills in this repo, or ~/.goodvibes/skills and ~/.goodvibes/tui/skills for global packs.',
|
|
421
|
-
[{ command: '/registry search skills', summary: 'inspect the same skill directories from the shell' }],
|
|
422
|
-
C,
|
|
423
|
-
),
|
|
424
|
-
}],
|
|
425
|
-
palette: C,
|
|
426
|
-
});
|
|
427
|
-
while (lines.length < height) lines.push(createEmptyLine(width));
|
|
428
|
-
this.reportRenderDuration(Date.now() - start);
|
|
429
401
|
return lines.slice(0, height);
|
|
430
402
|
}
|
|
431
403
|
|
|
432
|
-
|
|
433
|
-
const
|
|
434
|
-
const fixedDiscoveryLines: Line[] = [
|
|
435
|
-
buildSearchInputLine(width, ' query: ', `${this.query}${this.filterFocused ? '_' : ''}`, C, {
|
|
436
|
-
active: this.filterFocused,
|
|
437
|
-
emptyLabel: this.filterFocused ? '(type to filter)' : '(/ or up at top)',
|
|
438
|
-
valueColor: this.query ? C.searchFg : undefined,
|
|
439
|
-
}),
|
|
440
|
-
];
|
|
404
|
+
// Build filter input line (provided by SearchableListPanel base)
|
|
405
|
+
const filterLine = this.buildFilterInputLine(width, 'Filter', this.filterFocused);
|
|
441
406
|
|
|
407
|
+
// Build detail footer for the currently selected skill
|
|
408
|
+
const items = this.getItems();
|
|
409
|
+
const selected = items[this.selectedIndex];
|
|
442
410
|
const detailLines: Line[] = [];
|
|
443
411
|
if (selected) {
|
|
444
412
|
detailLines.push(
|
|
@@ -448,99 +416,15 @@ export class SkillsPanel extends BasePanel {
|
|
|
448
416
|
buildPanelLine(width, [[' Depends: ', C.label], [selected.dependencies.length > 0 ? selected.dependencies.join(', ') : 'none', C.dim]]),
|
|
449
417
|
buildPanelLine(width, [[' Includes: ', C.label], [selected.includes.length > 0 ? selected.includes.join(', ') : 'none', C.dim]]),
|
|
450
418
|
);
|
|
451
|
-
} else {
|
|
452
|
-
detailLines.push(buildPanelLine(width, [[' No selection.', C.dim]]));
|
|
453
419
|
}
|
|
454
|
-
|
|
455
|
-
const resolvedDiscoverySection = resolvePrimaryScrollableSection(width, height, {
|
|
456
|
-
intro,
|
|
457
|
-
footerLines: [buildPanelLine(width, [[' Up/Down navigate / or Up-at-top focus filter Esc blur Backspace clear', C.hint]])],
|
|
458
|
-
palette: C,
|
|
459
|
-
section: {
|
|
460
|
-
title: 'Discovery',
|
|
461
|
-
fixedLines: fixedDiscoveryLines,
|
|
462
|
-
scrollableLines: skills.map((skill, absolute) => {
|
|
463
|
-
const isSelected = absolute === this.selectedIndex;
|
|
464
|
-
const bg = isSelected ? C.selectBg : undefined;
|
|
465
|
-
const dot = skill.origin === 'project-local' ? '◆' : '•';
|
|
466
|
-
const desc = skill.description || 'No description provided.';
|
|
467
|
-
const descWidth = Math.max(1, width - 4 - skill.name.length - 6);
|
|
468
|
-
const descLines = wordWrap(desc, descWidth);
|
|
469
|
-
return buildPanelLine(width, [
|
|
470
|
-
[isSelected ? '▸' : ' ', C.selectedFg, bg],
|
|
471
|
-
[' ', C.dim, bg],
|
|
472
|
-
[dot, originColor(skill.origin), bg],
|
|
473
|
-
[' ', C.dim, bg],
|
|
474
|
-
[skill.name, isSelected ? C.selectedFg : C.value, bg],
|
|
475
|
-
[' ', C.dim, bg],
|
|
476
|
-
[descLines[0] ?? '', isSelected ? C.selectedFg : C.dim, bg],
|
|
477
|
-
]);
|
|
478
|
-
}),
|
|
479
|
-
selectedIndex: this.selectedIndex,
|
|
480
|
-
scrollOffset: this.scrollOffset,
|
|
481
|
-
guardRows: 1,
|
|
482
|
-
minRows: 4,
|
|
483
|
-
appendWindowSummary: {
|
|
484
|
-
dimColor: C.dim,
|
|
485
|
-
formatter: (window) => buildPanelLine(width, [[` showing ${window.start + 1}-${window.end} of ${window.total}`, C.dim]]),
|
|
486
|
-
},
|
|
487
|
-
},
|
|
488
|
-
afterSections: [detailSection],
|
|
489
|
-
});
|
|
490
|
-
this.scrollOffset = resolvedDiscoverySection.scrollOffset;
|
|
491
|
-
this._clampScroll(skills, resolvedDiscoverySection.window.count);
|
|
420
|
+
detailLines.push(buildPanelLine(width, [[' Up/Down navigate / or Up-at-top focus filter Esc blur Backspace clear', C.hint]]));
|
|
492
421
|
|
|
493
|
-
const
|
|
494
|
-
resolvedDiscoverySection.section,
|
|
495
|
-
detailSection,
|
|
496
|
-
];
|
|
497
|
-
const lines = buildPanelWorkspace(width, height, {
|
|
422
|
+
const lines = this.renderList(width, height, {
|
|
498
423
|
title: 'Skills - discover project-local and global skill packs',
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
footerLines: [buildPanelLine(width, [[' Up/Down navigate / or Up-at-top focus filter Esc blur Backspace clear', C.hint]])],
|
|
502
|
-
palette: C,
|
|
424
|
+
header: [filterLine],
|
|
425
|
+
footer: detailLines,
|
|
503
426
|
});
|
|
504
|
-
|
|
505
|
-
this.reportRenderDuration(Date.now() - start);
|
|
506
|
-
return lines.slice(0, height);
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
private _filteredSkills(): SkillRecord[] {
|
|
510
|
-
if (this.cached === null || this.cacheDirty) {
|
|
511
|
-
this.cached = discoverSkills(this.shellPaths);
|
|
512
|
-
this.cacheDirty = false;
|
|
513
|
-
}
|
|
514
|
-
const q = this.query.trim().toLowerCase();
|
|
515
|
-
if (!q) return this.cached;
|
|
516
|
-
return this.cached.filter((skill) => {
|
|
517
|
-
const haystack = [
|
|
518
|
-
skill.name,
|
|
519
|
-
skill.description,
|
|
520
|
-
skill.path,
|
|
521
|
-
skill.origin,
|
|
522
|
-
skill.dependencies.join(' '),
|
|
523
|
-
skill.includes.join(' '),
|
|
524
|
-
].join(' ').toLowerCase();
|
|
525
|
-
return haystack.includes(q);
|
|
427
|
+
return lines;
|
|
526
428
|
});
|
|
527
429
|
}
|
|
528
|
-
|
|
529
|
-
private _clampSelection(records: SkillRecord[]): void {
|
|
530
|
-
if (records.length === 0) {
|
|
531
|
-
this.selectedIndex = 0;
|
|
532
|
-
return;
|
|
533
|
-
}
|
|
534
|
-
this.selectedIndex = Math.max(0, Math.min(this.selectedIndex, records.length - 1));
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
private _clampScroll(records: SkillRecord[], listHeight: number): void {
|
|
538
|
-
const maxScroll = Math.max(0, records.length - listHeight);
|
|
539
|
-
if (this.selectedIndex < this.scrollOffset) {
|
|
540
|
-
this.scrollOffset = this.selectedIndex;
|
|
541
|
-
} else if (this.selectedIndex >= this.scrollOffset + listHeight) {
|
|
542
|
-
this.scrollOffset = this.selectedIndex - listHeight + 1;
|
|
543
|
-
}
|
|
544
|
-
this.scrollOffset = Math.max(0, Math.min(this.scrollOffset, maxScroll));
|
|
545
|
-
}
|
|
546
430
|
}
|
|
@@ -64,6 +64,7 @@ export class SubscriptionPanel extends ScrollableListPanel<SubscriptionRow> {
|
|
|
64
64
|
subscriptionManager: SubscriptionAccessQuery,
|
|
65
65
|
) {
|
|
66
66
|
super('subscription', 'Subscriptions', 'B', 'monitoring');
|
|
67
|
+
this.showSelectionGutter = true; // I5: non-color selection affordance
|
|
67
68
|
this.serviceRegistry = serviceRegistry;
|
|
68
69
|
this.subscriptionManager = subscriptionManager;
|
|
69
70
|
}
|