@pellux/goodvibes-agent 0.1.70 → 0.1.71
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 +6 -0
- package/package.json +42 -1
- package/src/agent/skill-discovery.ts +119 -0
- package/src/input/commands/delegation-runtime.ts +0 -8
- package/src/input/commands/experience-runtime.ts +0 -177
- package/src/input/commands/guidance-runtime.ts +0 -69
- package/src/input/commands/local-runtime.ts +1 -57
- package/src/input/commands/local-setup-review.ts +1 -1
- package/src/input/commands/operator-runtime.ts +1 -145
- package/src/input/commands/platform-access-runtime.ts +2 -195
- package/src/input/commands/product-runtime.ts +0 -116
- package/src/input/commands/security-runtime.ts +88 -0
- package/src/input/commands/session-content.ts +0 -97
- package/src/input/commands/shell-core.ts +0 -13
- package/src/input/commands.ts +2 -95
- package/src/panels/builtin/operations.ts +3 -184
- package/src/panels/index.ts +0 -11
- package/src/version.ts +1 -1
- package/src/input/commands/branch-runtime.ts +0 -72
- package/src/input/commands/control-room-runtime.ts +0 -234
- package/src/input/commands/discovery-runtime.ts +0 -61
- package/src/input/commands/hooks-runtime.ts +0 -207
- package/src/input/commands/incident-runtime.ts +0 -106
- package/src/input/commands/integration-runtime.ts +0 -437
- package/src/input/commands/local-setup.ts +0 -288
- package/src/input/commands/managed-runtime.ts +0 -240
- package/src/input/commands/marketplace-runtime.ts +0 -305
- package/src/input/commands/memory-product-runtime.ts +0 -148
- package/src/input/commands/operator-panel-runtime.ts +0 -146
- package/src/input/commands/platform-services-runtime.ts +0 -271
- package/src/input/commands/profile-sync-runtime.ts +0 -110
- package/src/input/commands/provider.ts +0 -363
- package/src/input/commands/remote-runtime-pool.ts +0 -89
- package/src/input/commands/remote-runtime-setup.ts +0 -226
- package/src/input/commands/remote-runtime.ts +0 -432
- package/src/input/commands/replay-runtime.ts +0 -25
- package/src/input/commands/services-runtime.ts +0 -220
- package/src/input/commands/settings-sync-runtime.ts +0 -197
- package/src/input/commands/share-runtime.ts +0 -127
- package/src/input/commands/skills-runtime.ts +0 -226
- package/src/input/commands/teleport-runtime.ts +0 -68
- package/src/panels/cockpit-panel.ts +0 -183
- package/src/panels/communication-panel.ts +0 -153
- package/src/panels/control-plane-panel.ts +0 -211
- package/src/panels/forensics-panel.ts +0 -364
- package/src/panels/hooks-panel.ts +0 -239
- package/src/panels/incident-review-panel.ts +0 -197
- package/src/panels/marketplace-panel.ts +0 -212
- package/src/panels/ops-control-panel.ts +0 -150
- package/src/panels/ops-strategy-panel.ts +0 -235
- package/src/panels/orchestration-panel.ts +0 -272
- package/src/panels/plugins-panel.ts +0 -178
- package/src/panels/remote-panel.ts +0 -449
- package/src/panels/routes-panel.ts +0 -178
- package/src/panels/services-panel.ts +0 -231
- package/src/panels/settings-sync-panel.ts +0 -120
- package/src/panels/skills-panel.ts +0 -431
- package/src/panels/watchers-panel.ts +0 -193
- package/src/verification/live-verifier.ts +0 -588
- package/src/verification/verification-ledger.ts +0 -239
|
@@ -1,431 +0,0 @@
|
|
|
1
|
-
import { promises as fsPromises } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
import type { Line } from '../types/grid.ts';
|
|
4
|
-
import { createEmptyLine } from '../types/grid.ts';
|
|
5
|
-
import { type ConfirmState, handleConfirmInput, renderConfirmLines } from './confirm-state.ts';
|
|
6
|
-
import { getDisplayWidth, truncateDisplay } from '../utils/terminal-width.ts';
|
|
7
|
-
import { SearchableListPanel } from './scrollable-list-panel.ts';
|
|
8
|
-
import type { ComponentHealthMonitor } from '../runtime/perf/panel-health-monitor.ts';
|
|
9
|
-
import type { ShellPathService } from '@/runtime/index.ts';
|
|
10
|
-
import {
|
|
11
|
-
buildPanelLine,
|
|
12
|
-
buildPanelWorkspace,
|
|
13
|
-
DEFAULT_PANEL_PALETTE,
|
|
14
|
-
} from './polish.ts';
|
|
15
|
-
import {
|
|
16
|
-
getPanelSearchFocusTransition,
|
|
17
|
-
isPanelSearchCancel,
|
|
18
|
-
} from './search-focus.ts';
|
|
19
|
-
import { GOODVIBES_AGENT_SURFACE_ROOT } from '../config/surface.ts';
|
|
20
|
-
|
|
21
|
-
const C = {
|
|
22
|
-
...DEFAULT_PANEL_PALETTE,
|
|
23
|
-
header: '#94a3b8',
|
|
24
|
-
headerBg: '#1e293b',
|
|
25
|
-
searchFg: '#f97316',
|
|
26
|
-
searchBg: '#1e293b',
|
|
27
|
-
label: '#64748b',
|
|
28
|
-
value: '#e2e8f0',
|
|
29
|
-
dim: '#64748b',
|
|
30
|
-
empty: '#334155',
|
|
31
|
-
selectedFg: '#e2e8f0',
|
|
32
|
-
selectedBg: '#1e3a5f',
|
|
33
|
-
project: '#38bdf8',
|
|
34
|
-
global: '#a78bfa',
|
|
35
|
-
hint: '#475569',
|
|
36
|
-
path: '#94a3b8',
|
|
37
|
-
selectBg: '#1e3a5f',
|
|
38
|
-
} as const;
|
|
39
|
-
|
|
40
|
-
export type SkillOrigin = 'project-local' | 'global' | 'custom';
|
|
41
|
-
|
|
42
|
-
export interface SkillRecord {
|
|
43
|
-
name: string;
|
|
44
|
-
description: string;
|
|
45
|
-
path: string;
|
|
46
|
-
origin: SkillOrigin;
|
|
47
|
-
dependencies: string[];
|
|
48
|
-
includes: string[];
|
|
49
|
-
frontmatter: Record<string, string>;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export interface SkillsPanelOptions {
|
|
53
|
-
shellPaths: Pick<ShellPathService, 'workingDirectory' | 'homeDirectory'>;
|
|
54
|
-
componentHealthMonitor?: ComponentHealthMonitor;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function parseFrontmatter(content: string): Record<string, string> {
|
|
58
|
-
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
59
|
-
if (!match) return {};
|
|
60
|
-
const result: Record<string, string> = {};
|
|
61
|
-
for (const line of match[1].split('\n')) {
|
|
62
|
-
const [key, ...rest] = line.split(':');
|
|
63
|
-
if (key && rest.length > 0) {
|
|
64
|
-
result[key.trim()] = rest.join(':').trim();
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
return result;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function getSkillDirectories(cwd: string, homeDir: string): Array<{ root: string; origin: SkillOrigin }> {
|
|
71
|
-
return [
|
|
72
|
-
{ root: join(cwd, '.goodvibes', 'skills'), origin: 'project-local' },
|
|
73
|
-
{ root: join(cwd, '.goodvibes', GOODVIBES_AGENT_SURFACE_ROOT, 'skills'), origin: 'project-local' },
|
|
74
|
-
{ root: join(homeDir, '.goodvibes', 'skills'), origin: 'global' },
|
|
75
|
-
{ root: join(homeDir, '.goodvibes', GOODVIBES_AGENT_SURFACE_ROOT, 'skills'), origin: 'global' },
|
|
76
|
-
];
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
async function readSkillFile(path: string, origin: SkillOrigin): Promise<SkillRecord | null> {
|
|
80
|
-
let content = '';
|
|
81
|
-
try {
|
|
82
|
-
content = await fsPromises.readFile(path, 'utf-8');
|
|
83
|
-
} catch {
|
|
84
|
-
return null;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const frontmatter = parseFrontmatter(content);
|
|
88
|
-
const body = content.replace(/^---\n[\s\S]*?\n---\n?/, '');
|
|
89
|
-
const name = frontmatter.name ?? path.split(/[\\/]/).pop()?.replace(/\.md$/, '') ?? 'skill';
|
|
90
|
-
const description = frontmatter.description ?? frontmatter.summary ?? '';
|
|
91
|
-
const dependencies = frontmatter.depends_on
|
|
92
|
-
? frontmatter.depends_on.split(',').map((item) => item.trim()).filter(Boolean)
|
|
93
|
-
: [];
|
|
94
|
-
const includes: string[] = [];
|
|
95
|
-
const includeRegex = /^@([\w/-]+)/gm;
|
|
96
|
-
let match: RegExpExecArray | null;
|
|
97
|
-
while ((match = includeRegex.exec(body)) !== null) {
|
|
98
|
-
includes.push(match[1]);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
return {
|
|
102
|
-
name,
|
|
103
|
-
description,
|
|
104
|
-
path,
|
|
105
|
-
origin,
|
|
106
|
-
dependencies,
|
|
107
|
-
includes,
|
|
108
|
-
frontmatter,
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
async function scanSkillDirectory(root: string, origin: SkillOrigin): Promise<SkillRecord[]> {
|
|
113
|
-
let entries: string[] = [];
|
|
114
|
-
try {
|
|
115
|
-
entries = await fsPromises.readdir(root);
|
|
116
|
-
} catch {
|
|
117
|
-
return [];
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const records: SkillRecord[] = [];
|
|
121
|
-
for (const entry of entries.sort((a, b) => a.localeCompare(b))) {
|
|
122
|
-
if (entry.endsWith('.md')) {
|
|
123
|
-
const record = await readSkillFile(join(root, entry), origin);
|
|
124
|
-
if (record) records.push(record);
|
|
125
|
-
continue;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const markerPath = join(root, entry, 'SKILL.md');
|
|
129
|
-
const record = await readSkillFile(markerPath, origin);
|
|
130
|
-
if (record) records.push(record);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
return records;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
export async function discoverSkills(shellPaths: Pick<ShellPathService, 'workingDirectory' | 'homeDirectory'>): Promise<SkillRecord[]> {
|
|
137
|
-
const cwd = shellPaths.workingDirectory;
|
|
138
|
-
const homeDir = shellPaths.homeDirectory;
|
|
139
|
-
const seen = new Set<string>();
|
|
140
|
-
const records: SkillRecord[] = [];
|
|
141
|
-
|
|
142
|
-
for (const { root, origin } of getSkillDirectories(cwd, homeDir)) {
|
|
143
|
-
for (const record of await scanSkillDirectory(root, origin)) {
|
|
144
|
-
if (seen.has(record.name.toLowerCase())) continue;
|
|
145
|
-
seen.add(record.name.toLowerCase());
|
|
146
|
-
records.push(record);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
return records.sort((a, b) => {
|
|
151
|
-
const originRank = a.origin === b.origin
|
|
152
|
-
? 0
|
|
153
|
-
: a.origin === 'project-local'
|
|
154
|
-
? -1
|
|
155
|
-
: 1;
|
|
156
|
-
return originRank || a.name.localeCompare(b.name);
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function wordWrap(text: string, maxWidth: number): string[] {
|
|
161
|
-
if (maxWidth <= 0) return [''];
|
|
162
|
-
const words = text.split(/\s+/).filter(Boolean);
|
|
163
|
-
if (words.length === 0) return [''];
|
|
164
|
-
const lines: string[] = [];
|
|
165
|
-
let line = '';
|
|
166
|
-
for (const word of words) {
|
|
167
|
-
if (!line) {
|
|
168
|
-
line = word;
|
|
169
|
-
continue;
|
|
170
|
-
}
|
|
171
|
-
if (line.length + 1 + word.length <= maxWidth) {
|
|
172
|
-
line += ` ${word}`;
|
|
173
|
-
} else {
|
|
174
|
-
lines.push(line);
|
|
175
|
-
line = word;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
if (line) lines.push(line);
|
|
179
|
-
return lines.length > 0 ? lines : [''];
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function truncatePathDisplay(path: string, width: number): string {
|
|
183
|
-
if (width <= 0) return '';
|
|
184
|
-
if (getDisplayWidth(path) <= width) return path;
|
|
185
|
-
|
|
186
|
-
const ellipsis = '…';
|
|
187
|
-
const ellipsisWidth = getDisplayWidth(ellipsis);
|
|
188
|
-
if (ellipsisWidth >= width) return truncateDisplay(path, width);
|
|
189
|
-
|
|
190
|
-
const available = width - ellipsisWidth;
|
|
191
|
-
const prefixBudget = Math.max(1, Math.floor(available * 0.35));
|
|
192
|
-
const suffixBudget = Math.max(1, available - prefixBudget);
|
|
193
|
-
const prefix = truncateDisplay(path, prefixBudget, '');
|
|
194
|
-
|
|
195
|
-
let suffix = '';
|
|
196
|
-
let suffixWidth = 0;
|
|
197
|
-
for (let index = path.length - 1; index >= 0; index -= 1) {
|
|
198
|
-
const char = path[index]!;
|
|
199
|
-
const charWidth = getDisplayWidth(char);
|
|
200
|
-
if (suffixWidth + charWidth > suffixBudget) break;
|
|
201
|
-
suffix = char + suffix;
|
|
202
|
-
suffixWidth += charWidth;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
return `${prefix}${ellipsis}${suffix}`;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function originLabel(origin: SkillOrigin): string {
|
|
209
|
-
switch (origin) {
|
|
210
|
-
case 'project-local':
|
|
211
|
-
return 'project';
|
|
212
|
-
case 'global':
|
|
213
|
-
return 'global';
|
|
214
|
-
case 'custom':
|
|
215
|
-
return 'custom';
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function originColor(origin: SkillOrigin): string {
|
|
220
|
-
switch (origin) {
|
|
221
|
-
case 'project-local':
|
|
222
|
-
return C.project;
|
|
223
|
-
case 'global':
|
|
224
|
-
return C.global;
|
|
225
|
-
case 'custom':
|
|
226
|
-
return C.dim;
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
export class SkillsPanel extends SearchableListPanel<SkillRecord> {
|
|
231
|
-
private readonly shellPaths: Pick<ShellPathService, 'workingDirectory' | 'homeDirectory'>;
|
|
232
|
-
/** Whether the filter input row is focused for typing (vs. list navigation). */
|
|
233
|
-
private filterFocused = false;
|
|
234
|
-
private cached: SkillRecord[] | null = null;
|
|
235
|
-
private cacheDirty = true;
|
|
236
|
-
// I1: confirm state for destructive delete
|
|
237
|
-
private confirm: ConfirmState | null = null;
|
|
238
|
-
private readyPromise: Promise<void> | null = null;
|
|
239
|
-
|
|
240
|
-
public constructor(options: SkillsPanelOptions) {
|
|
241
|
-
super('skills', 'Skills', 'K', 'monitoring', options.componentHealthMonitor);
|
|
242
|
-
this.showSelectionGutter = true; // I5: non-color selection affordance
|
|
243
|
-
this.shellPaths = options.shellPaths;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// -------------------------------------------------------------------------
|
|
247
|
-
// SearchableListPanel implementation
|
|
248
|
-
// -------------------------------------------------------------------------
|
|
249
|
-
|
|
250
|
-
protected getAllItems(): readonly SkillRecord[] {
|
|
251
|
-
return this.cached ?? [];
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
private _loadSkillsAsync(): Promise<void> {
|
|
255
|
-
const p = (async () => {
|
|
256
|
-
try {
|
|
257
|
-
await this.withLoading('Scanning skills\u2026', async () => {
|
|
258
|
-
this.cached = await discoverSkills(this.shellPaths);
|
|
259
|
-
this.cacheDirty = false;
|
|
260
|
-
this.invalidateFilter();
|
|
261
|
-
});
|
|
262
|
-
} catch (err) {
|
|
263
|
-
this.setError(err instanceof Error ? err.message : String(err));
|
|
264
|
-
}
|
|
265
|
-
this.markDirty();
|
|
266
|
-
})();
|
|
267
|
-
this.readyPromise = p;
|
|
268
|
-
return p;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
/** Resolves when the current load cycle has settled. */
|
|
272
|
-
public awaitReady(): Promise<void> {
|
|
273
|
-
return this.readyPromise ?? Promise.resolve();
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
protected matchesSearch(skill: SkillRecord, query: string): boolean {
|
|
277
|
-
const q = query.trim().toLowerCase();
|
|
278
|
-
if (!q) return true;
|
|
279
|
-
const haystack = [
|
|
280
|
-
skill.name,
|
|
281
|
-
skill.description,
|
|
282
|
-
skill.path,
|
|
283
|
-
skill.origin,
|
|
284
|
-
skill.dependencies.join(' '),
|
|
285
|
-
skill.includes.join(' '),
|
|
286
|
-
].join(' ').toLowerCase();
|
|
287
|
-
return haystack.includes(q);
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
protected renderItem(skill: SkillRecord, index: number, selected: boolean, width: number): Line {
|
|
291
|
-
const bg = selected ? C.selectBg : undefined;
|
|
292
|
-
const dot = skill.origin === 'project-local' ? '\u25c6' : '\u2022';
|
|
293
|
-
const desc = skill.description || 'No description provided.';
|
|
294
|
-
const descWidth = Math.max(1, width - 4 - skill.name.length - 6);
|
|
295
|
-
const descLines = wordWrap(desc, descWidth);
|
|
296
|
-
return buildPanelLine(width, [
|
|
297
|
-
[selected ? '\u25b8' : ' ', C.selectedFg, bg],
|
|
298
|
-
[' ', C.dim, bg],
|
|
299
|
-
[dot, originColor(skill.origin), bg],
|
|
300
|
-
[' ', C.dim, bg],
|
|
301
|
-
[skill.name, selected ? C.selectedFg : C.value, bg],
|
|
302
|
-
[' ', C.dim, bg],
|
|
303
|
-
[descLines[0] ?? '', selected ? C.selectedFg : C.dim, bg],
|
|
304
|
-
]);
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
protected override getPalette() { return C; }
|
|
308
|
-
protected override getEmptyStateMessage() { return ' No skills discovered.'; }
|
|
309
|
-
protected override getEmptyStateActions() {
|
|
310
|
-
return [
|
|
311
|
-
{ command: '.goodvibes/skills', summary: 'place skill .md files here (project-local) or ~/.goodvibes/skills (global)' },
|
|
312
|
-
{ command: '/registry search skills', summary: 'inspect the same skill directories from the shell' },
|
|
313
|
-
];
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
public override onActivate(): void {
|
|
317
|
-
super.onActivate();
|
|
318
|
-
this.searchQuery = '';
|
|
319
|
-
this.invalidateFilter();
|
|
320
|
-
this.filterFocused = false;
|
|
321
|
-
this.cacheDirty = true;
|
|
322
|
-
void this._loadSkillsAsync();
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
public override onDestroy(): void {}
|
|
326
|
-
|
|
327
|
-
public handleInput(key: string): boolean {
|
|
328
|
-
// I1: y/n confirmation dialog for delete
|
|
329
|
-
const confirmResult = handleConfirmInput(this.confirm, key);
|
|
330
|
-
if (confirmResult === 'confirmed') {
|
|
331
|
-
const toDelete = this.confirm!.subject;
|
|
332
|
-
this.confirm = null;
|
|
333
|
-
// Skills are read from the filesystem — deletion requires a shell command.
|
|
334
|
-
// Surface an error directing the user to remove the file manually.
|
|
335
|
-
this.setError(`Delete via shell: rm "${toDelete}"`);
|
|
336
|
-
this.markDirty();
|
|
337
|
-
return true;
|
|
338
|
-
}
|
|
339
|
-
if (confirmResult === 'cancelled') {
|
|
340
|
-
this.confirm = null;
|
|
341
|
-
this.markDirty();
|
|
342
|
-
return true;
|
|
343
|
-
}
|
|
344
|
-
if (confirmResult === 'absorbed') return true;
|
|
345
|
-
|
|
346
|
-
const items = this.getItems();
|
|
347
|
-
|
|
348
|
-
// Filter-focus mode: typing goes into the search query
|
|
349
|
-
if (this.filterFocused) {
|
|
350
|
-
const transition = getPanelSearchFocusTransition(key, { selectedIndex: this.selectedIndex, itemCount: items.length });
|
|
351
|
-
if (transition === 'focus-list') {
|
|
352
|
-
this.filterFocused = false;
|
|
353
|
-
this.markDirty();
|
|
354
|
-
return true;
|
|
355
|
-
}
|
|
356
|
-
// Escape: also blur filter focus (clear + return to list navigation)
|
|
357
|
-
if (isPanelSearchCancel(key)) {
|
|
358
|
-
this.filterFocused = false;
|
|
359
|
-
// Delegate to super to clear the query. If the query is empty, super
|
|
360
|
-
// returns false and escape propagates to the panel dismissal handler —
|
|
361
|
-
// this is the intentional double-escape UX (blur filter, then close).
|
|
362
|
-
return super.handleInput(key);
|
|
363
|
-
}
|
|
364
|
-
// Delegate backspace/printable to SearchableListPanel.handleInput
|
|
365
|
-
return super.handleInput(key);
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
const transition = getPanelSearchFocusTransition(key, { selectedIndex: this.selectedIndex, itemCount: items.length });
|
|
369
|
-
if (transition === 'focus-search') {
|
|
370
|
-
this.filterFocused = true;
|
|
371
|
-
this.markDirty();
|
|
372
|
-
return true;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
// I1: 'd' prompts delete confirmation
|
|
376
|
-
if (key === 'd') {
|
|
377
|
-
const skill = items[this.selectedIndex];
|
|
378
|
-
if (skill) {
|
|
379
|
-
this.confirm = { subject: skill.path, label: skill.name };
|
|
380
|
-
this.markDirty();
|
|
381
|
-
}
|
|
382
|
-
return true;
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
// Navigation + search: delegate to SearchableListPanel (up/down/g/G/page/enter + backspace/escape)
|
|
386
|
-
return super.handleInput(key);
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
public render(width: number, height: number): Line[] {
|
|
390
|
-
return this.trackedRender(() => {
|
|
391
|
-
this.needsRender = false;
|
|
392
|
-
|
|
393
|
-
// I1: show confirm dialog in place of normal content
|
|
394
|
-
if (this.confirm) {
|
|
395
|
-
const lines = buildPanelWorkspace(width, height, {
|
|
396
|
-
title: 'Skills - confirm action',
|
|
397
|
-
intro: '',
|
|
398
|
-
sections: [{ title: 'Confirmation', lines: renderConfirmLines(width, this.confirm) }],
|
|
399
|
-
palette: C,
|
|
400
|
-
});
|
|
401
|
-
while (lines.length < height) lines.push(createEmptyLine(width));
|
|
402
|
-
return lines.slice(0, height);
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
// Build filter input line (provided by SearchableListPanel base)
|
|
406
|
-
const filterLine = this.buildFilterInputLine(width, 'Filter', this.filterFocused);
|
|
407
|
-
|
|
408
|
-
// Build detail footer for the currently selected skill
|
|
409
|
-
const items = this.getItems();
|
|
410
|
-
const selected = items[this.selectedIndex];
|
|
411
|
-
const detailLines: Line[] = [];
|
|
412
|
-
if (selected) {
|
|
413
|
-
detailLines.push(
|
|
414
|
-
buildPanelLine(width, [[' Selected: ', C.label], [selected.name, C.value], [' [', C.dim], [originLabel(selected.origin), originColor(selected.origin)], [']', C.dim]]),
|
|
415
|
-
buildPanelLine(width, [[' Path: ', C.label], [truncatePathDisplay(selected.path, Math.max(1, width - 8)), C.path]]),
|
|
416
|
-
buildPanelLine(width, [[' Desc: ', C.label], [selected.description || 'No description provided.', C.value]]),
|
|
417
|
-
buildPanelLine(width, [[' Depends: ', C.label], [selected.dependencies.length > 0 ? selected.dependencies.join(', ') : 'none', C.dim]]),
|
|
418
|
-
buildPanelLine(width, [[' Includes: ', C.label], [selected.includes.length > 0 ? selected.includes.join(', ') : 'none', C.dim]]),
|
|
419
|
-
);
|
|
420
|
-
}
|
|
421
|
-
detailLines.push(buildPanelLine(width, [[' Up/Down navigate / or Up-at-top focus filter Esc blur Backspace clear', C.hint]]));
|
|
422
|
-
|
|
423
|
-
const lines = this.renderList(width, height, {
|
|
424
|
-
title: 'Skills - discover project-local and global skill packs',
|
|
425
|
-
header: [filterLine],
|
|
426
|
-
footer: detailLines,
|
|
427
|
-
});
|
|
428
|
-
return lines;
|
|
429
|
-
});
|
|
430
|
-
}
|
|
431
|
-
}
|
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
import type { Line } from '../types/grid.ts';
|
|
2
|
-
import { createEmptyLine } from '../types/grid.ts';
|
|
3
|
-
import { ScrollableListPanel } from './scrollable-list-panel.ts';
|
|
4
|
-
import type { UiReadModel, UiWatchersSnapshot } from '../runtime/ui-read-models.ts';
|
|
5
|
-
import { truncateDisplay } from '../utils/terminal-width.ts';
|
|
6
|
-
import {
|
|
7
|
-
buildEmptyState,
|
|
8
|
-
buildGuidanceLine,
|
|
9
|
-
buildKeyValueLine,
|
|
10
|
-
buildPanelLine,
|
|
11
|
-
buildPanelWorkspace,
|
|
12
|
-
DEFAULT_PANEL_PALETTE,
|
|
13
|
-
type PanelPalette,
|
|
14
|
-
} from './polish.ts';
|
|
15
|
-
|
|
16
|
-
const C = {
|
|
17
|
-
...DEFAULT_PANEL_PALETTE,
|
|
18
|
-
header: '#94a3b8',
|
|
19
|
-
headerBg: '#1e293b',
|
|
20
|
-
ok: '#22c55e',
|
|
21
|
-
warn: '#eab308',
|
|
22
|
-
error: '#ef4444',
|
|
23
|
-
info: '#38bdf8',
|
|
24
|
-
selectBg: '#0f172a',
|
|
25
|
-
} as const;
|
|
26
|
-
|
|
27
|
-
function stateColor(state: string): string {
|
|
28
|
-
if (state === 'running') return C.ok;
|
|
29
|
-
if (state === 'degraded') return C.warn;
|
|
30
|
-
if (state === 'failed') return C.error;
|
|
31
|
-
return C.dim;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function sourceStatusColor(state?: string): string {
|
|
35
|
-
if (state === 'healthy') return C.ok;
|
|
36
|
-
if (state === 'lagging' || state === 'stale' || state === 'degraded') return C.warn;
|
|
37
|
-
if (state === 'failed') return C.error;
|
|
38
|
-
return C.dim;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function formatLag(value?: number): string {
|
|
42
|
-
if (!value || value <= 0) return 'n/a';
|
|
43
|
-
if (value < 1000) return `${value}ms`;
|
|
44
|
-
if (value < 60_000) return `${Math.round(value / 1000)}s`;
|
|
45
|
-
return `${Math.round(value / 60_000)}m`;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function formatTime(value?: number): string {
|
|
49
|
-
if (!value) return 'n/a';
|
|
50
|
-
return new Date(value).toLocaleString();
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
type WatcherEntry = UiWatchersSnapshot['watchers'][number];
|
|
54
|
-
|
|
55
|
-
export class WatchersPanel extends ScrollableListPanel<WatcherEntry> {
|
|
56
|
-
private readonly readModel?: UiReadModel<UiWatchersSnapshot>;
|
|
57
|
-
private readonly unsub: (() => void) | null;
|
|
58
|
-
|
|
59
|
-
public constructor(readModel?: UiReadModel<UiWatchersSnapshot>) {
|
|
60
|
-
super('watchers', 'Watchers', 'W', 'monitoring');
|
|
61
|
-
this.showSelectionGutter = true; // I5: non-color selection affordance
|
|
62
|
-
this.readModel = readModel;
|
|
63
|
-
this.unsub = readModel ? readModel.subscribe(() => this.markDirty()) : null;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
public override onDestroy(): void {
|
|
67
|
-
this.unsub?.();
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
protected override getPalette(): PanelPalette {
|
|
71
|
-
return C;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
protected getItems(): readonly WatcherEntry[] {
|
|
75
|
-
if (!this.readModel) return [];
|
|
76
|
-
return this.readModel.getSnapshot().watchers;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
protected renderItem(watcher: WatcherEntry, _index: number, selected: boolean, width: number): Line {
|
|
80
|
-
const bg = selected ? C.selectBg : undefined;
|
|
81
|
-
return buildPanelLine(width, [
|
|
82
|
-
[' ', C.label, bg],
|
|
83
|
-
[watcher.state.padEnd(10), stateColor(watcher.state), bg],
|
|
84
|
-
[` ${truncateDisplay(watcher.label, 18).padEnd(18)}`, C.value, bg],
|
|
85
|
-
[` ${String(watcher.sourceStatus ?? 'unknown').padEnd(10)}`, sourceStatusColor(watcher.sourceStatus), bg],
|
|
86
|
-
[` ${truncateDisplay(formatLag(watcher.sourceLagMs), Math.max(0, width - 43))}`, C.dim, bg],
|
|
87
|
-
]);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
protected override getEmptyStateMessage(): string {
|
|
91
|
-
return ' No watchers registered.';
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
protected override getEmptyStateActions(): Array<{ command: string; summary: string }> {
|
|
95
|
-
return [
|
|
96
|
-
{ command: '/schedule list', summary: 'review automation that will consume watcher events' },
|
|
97
|
-
{ command: '/services auth-review', summary: 'validate integration credentials before enabling remote watchers' },
|
|
98
|
-
];
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
public render(width: number, height: number): Line[] {
|
|
102
|
-
const intro = 'Managed watchers and source health used to trigger automation, refresh routes, and surface degraded upstream conditions.';
|
|
103
|
-
|
|
104
|
-
if (!this.readModel) {
|
|
105
|
-
const workspace = buildPanelWorkspace(width, height, {
|
|
106
|
-
title: 'Watchers',
|
|
107
|
-
intro,
|
|
108
|
-
sections: [{
|
|
109
|
-
lines: buildEmptyState(
|
|
110
|
-
width,
|
|
111
|
-
' Runtime store not wired.',
|
|
112
|
-
'This panel needs the shared runtime store to inspect watcher health and source lag.',
|
|
113
|
-
[{ command: '/services auth-review', summary: 'inspect supporting services until watcher wiring is available' }],
|
|
114
|
-
C,
|
|
115
|
-
),
|
|
116
|
-
}],
|
|
117
|
-
palette: C,
|
|
118
|
-
});
|
|
119
|
-
while (workspace.length < height) workspace.push(createEmptyLine(width));
|
|
120
|
-
return workspace;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const snapshot = this.readModel.getSnapshot();
|
|
124
|
-
const watchers = this.getItems();
|
|
125
|
-
|
|
126
|
-
const headerLines: Line[] = [
|
|
127
|
-
buildKeyValueLine(width, [
|
|
128
|
-
{ label: 'watchers', value: String(snapshot.totalWatchers), valueColor: snapshot.totalWatchers > 0 ? C.info : C.dim },
|
|
129
|
-
{ label: 'active', value: String(snapshot.activeWatcherIds.length), valueColor: snapshot.activeWatcherIds.length > 0 ? C.ok : C.dim },
|
|
130
|
-
{ label: 'degraded', value: String(snapshot.totalDegraded), valueColor: snapshot.totalDegraded > 0 ? C.warn : C.dim },
|
|
131
|
-
{ label: 'lagged', value: String(snapshot.totalLagged), valueColor: snapshot.totalLagged > 0 ? C.warn : C.dim },
|
|
132
|
-
], C),
|
|
133
|
-
buildGuidanceLine(width, '/schedule list', 'verify jobs consuming these sources; Agent keeps watcher lifecycle read-only here', C),
|
|
134
|
-
];
|
|
135
|
-
|
|
136
|
-
if (watchers.length === 0) {
|
|
137
|
-
return this.renderList(width, height, {
|
|
138
|
-
title: 'Watchers',
|
|
139
|
-
header: headerLines,
|
|
140
|
-
emptyMessage: ' No watchers registered.',
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
this.clampSelection();
|
|
145
|
-
const selected = watchers[this.selectedIndex]!;
|
|
146
|
-
|
|
147
|
-
const footerLines: Line[] = [
|
|
148
|
-
buildPanelLine(width, [
|
|
149
|
-
[' Watcher: ', C.label],
|
|
150
|
-
[selected.label, C.value],
|
|
151
|
-
[' Kind: ', C.label],
|
|
152
|
-
[selected.kind, C.info],
|
|
153
|
-
]),
|
|
154
|
-
buildPanelLine(width, [
|
|
155
|
-
[' State: ', C.label],
|
|
156
|
-
[selected.state, stateColor(selected.state)],
|
|
157
|
-
[' Source: ', C.label],
|
|
158
|
-
[selected.source.kind, C.value],
|
|
159
|
-
]),
|
|
160
|
-
buildPanelLine(width, [
|
|
161
|
-
[' Source status: ', C.label],
|
|
162
|
-
[selected.sourceStatus ?? 'unknown', sourceStatusColor(selected.sourceStatus)],
|
|
163
|
-
[' Lag: ', C.label],
|
|
164
|
-
[formatLag(selected.sourceLagMs), selected.sourceLagMs ? C.warn : C.dim],
|
|
165
|
-
]),
|
|
166
|
-
buildPanelLine(width, [
|
|
167
|
-
[' Heartbeat: ', C.label],
|
|
168
|
-
[formatTime(selected.lastHeartbeatAt), C.dim],
|
|
169
|
-
[' Checkpoint: ', C.label],
|
|
170
|
-
[truncateDisplay(selected.lastCheckpoint ?? 'n/a', Math.max(0, width - 38)), C.dim],
|
|
171
|
-
]),
|
|
172
|
-
];
|
|
173
|
-
if (selected.degradedReason) {
|
|
174
|
-
footerLines.push(buildPanelLine(width, [
|
|
175
|
-
[' Reason: ', C.label],
|
|
176
|
-
[truncateDisplay(selected.degradedReason, Math.max(0, width - 11)), C.warn],
|
|
177
|
-
]));
|
|
178
|
-
}
|
|
179
|
-
if (selected.lastError) {
|
|
180
|
-
footerLines.push(buildPanelLine(width, [
|
|
181
|
-
[' Error: ', C.label],
|
|
182
|
-
[truncateDisplay(selected.lastError, Math.max(0, width - 10)), C.error],
|
|
183
|
-
]));
|
|
184
|
-
}
|
|
185
|
-
footerLines.push(buildPanelLine(width, [[' Up/Down move through watchers', C.dim]]));
|
|
186
|
-
|
|
187
|
-
return this.renderList(width, height, {
|
|
188
|
-
title: 'Watchers',
|
|
189
|
-
header: headerLines,
|
|
190
|
-
footer: footerLines,
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
-
}
|