pi-hermes-memory 0.7.8 → 0.7.10
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/README.md +21 -17
- package/package.json +1 -1
- package/src/config.ts +14 -1
- package/src/extension-root-migration.ts +101 -0
- package/src/handlers/index-sessions.ts +1 -1
- package/src/handlers/skills-command.ts +544 -75
- package/src/index.ts +37 -11
- package/src/project-memory-migration.ts +1 -1
- package/src/store/memory-store.ts +1 -1
- package/src/store/session-anchor-search.ts +472 -0
- package/src/store/skill-store.ts +142 -43
- package/src/store/skill-utils.ts +23 -6
- package/src/tools/session-search-tool.ts +106 -1
- package/src/types.ts +10 -5
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
* Skills command — /memory-skills opens an interactive skills manager.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
import * as os from "node:os";
|
|
7
|
+
import * as path from "node:path";
|
|
5
8
|
import type { ExtensionAPI, ExtensionCommandContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
6
9
|
import { SkillStore } from "../store/skill-store.js";
|
|
7
10
|
import type { SkillIndex, SkillResult, SkillScope } from "../types.js";
|
|
@@ -24,43 +27,192 @@ export const MEMORY_SKILLS_KEYMAP = {
|
|
|
24
27
|
selectAllFiltered: "a",
|
|
25
28
|
clearSelection: "n",
|
|
26
29
|
focusSearch: "/",
|
|
30
|
+
openFilters: "f",
|
|
27
31
|
toggleSelection: "space",
|
|
28
32
|
switchFocus: "tab",
|
|
29
33
|
close: "esc",
|
|
30
34
|
} as const;
|
|
31
35
|
|
|
36
|
+
export type SkillRowCategory = "G" | "P" | "E";
|
|
37
|
+
|
|
32
38
|
export interface SkillModalRow {
|
|
33
39
|
skillId: string;
|
|
34
|
-
scope
|
|
40
|
+
scope?: SkillScope;
|
|
41
|
+
category: SkillRowCategory;
|
|
42
|
+
mutable: boolean;
|
|
35
43
|
name: string;
|
|
36
44
|
displayName: string;
|
|
37
45
|
description: string;
|
|
38
46
|
path: string;
|
|
47
|
+
displayPath: string;
|
|
39
48
|
projectName?: string;
|
|
40
49
|
selected: boolean;
|
|
41
50
|
searchText: string;
|
|
42
51
|
}
|
|
43
52
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
53
|
+
interface LoadedSkillRow {
|
|
54
|
+
name: string;
|
|
55
|
+
displayName: string;
|
|
56
|
+
description: string;
|
|
57
|
+
path: string;
|
|
58
|
+
displayPath: string;
|
|
59
|
+
sourceScope?: string;
|
|
60
|
+
sourceOrigin?: string;
|
|
61
|
+
sourceLabel?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface SkillCommandInfo {
|
|
65
|
+
name: string;
|
|
66
|
+
description?: string;
|
|
67
|
+
source?: string;
|
|
68
|
+
sourceInfo?: {
|
|
69
|
+
path?: string;
|
|
70
|
+
scope?: string;
|
|
71
|
+
source?: string;
|
|
72
|
+
origin?: string;
|
|
73
|
+
baseDir?: string;
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
interface SkillCategoryFilters {
|
|
78
|
+
global: boolean;
|
|
79
|
+
project: boolean;
|
|
80
|
+
external: boolean;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
84
|
+
return typeof value === "object" && value !== null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function getStringField(value: unknown): string | undefined {
|
|
88
|
+
return typeof value === "string" ? value : undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const DEFAULT_SKILL_FILTERS: SkillCategoryFilters = {
|
|
92
|
+
global: true,
|
|
93
|
+
project: true,
|
|
94
|
+
external: true,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
function cloneFilters(filters: SkillCategoryFilters): SkillCategoryFilters {
|
|
98
|
+
return {
|
|
99
|
+
global: filters.global,
|
|
100
|
+
project: filters.project,
|
|
101
|
+
external: filters.external,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function ensureValidFilters(filters: SkillCategoryFilters): SkillCategoryFilters {
|
|
106
|
+
if (filters.global || filters.project || filters.external) return filters;
|
|
107
|
+
return { ...DEFAULT_SKILL_FILTERS };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function filtersLabel(filters: SkillCategoryFilters): string {
|
|
111
|
+
const active: string[] = [];
|
|
112
|
+
if (filters.global) active.push("[G]");
|
|
113
|
+
if (filters.project) active.push("[P]");
|
|
114
|
+
if (filters.external) active.push("[E]");
|
|
115
|
+
return active.length > 0 ? active.join(" ") : "(none)";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function normalizePathForKey(inputPath: string): string {
|
|
119
|
+
const resolved = path.resolve(inputPath);
|
|
120
|
+
const normalized = resolved.replace(/\\/g, "/");
|
|
121
|
+
return process.platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function formatSkillPath(inputPath: string): string {
|
|
125
|
+
const absolutePath = path.resolve(inputPath);
|
|
126
|
+
const home = os.homedir();
|
|
127
|
+
const relative = path.relative(home, absolutePath);
|
|
128
|
+
const underHome = relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
129
|
+
|
|
130
|
+
if (!underHome) return absolutePath;
|
|
131
|
+
if (relative === "") return "~";
|
|
132
|
+
return `~${path.sep}${relative}`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function categoryForScope(scope: SkillScope): SkillRowCategory {
|
|
136
|
+
return scope === "global" ? "G" : "P";
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function createExternalSkillId(name: string, filePath: string): string {
|
|
140
|
+
const safeName = (name || "skill")
|
|
141
|
+
.toLowerCase()
|
|
142
|
+
.replace(/[^a-z0-9-]+/g, "-")
|
|
143
|
+
.replace(/^-+|-+$/g, "") || "skill";
|
|
144
|
+
const hash = createHash("sha1").update(`${name}|${filePath}`).digest("hex").slice(0, 10);
|
|
145
|
+
return `external:${safeName}:${hash}`;
|
|
49
146
|
}
|
|
50
147
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
148
|
+
function matchesCategoryFilter(row: SkillModalRow, filters: SkillCategoryFilters): boolean {
|
|
149
|
+
if (row.category === "G") return filters.global;
|
|
150
|
+
if (row.category === "P") return filters.project;
|
|
151
|
+
return filters.external;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function categoryOrder(category: SkillRowCategory): number {
|
|
155
|
+
switch (category) {
|
|
156
|
+
case "G":
|
|
157
|
+
return 0;
|
|
158
|
+
case "P":
|
|
159
|
+
return 1;
|
|
160
|
+
case "E":
|
|
161
|
+
return 2;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function collectLoadedSkillsFromCommands(commands: SkillCommandInfo[]): LoadedSkillRow[] {
|
|
166
|
+
const loaded: LoadedSkillRow[] = [];
|
|
167
|
+
|
|
168
|
+
for (const command of commands) {
|
|
169
|
+
if (!isRecord(command)) continue;
|
|
170
|
+
const source = getStringField(command.source);
|
|
171
|
+
if (source !== "skill") continue;
|
|
172
|
+
|
|
173
|
+
const commandName = getStringField(command.name)?.trim();
|
|
174
|
+
if (!commandName) continue;
|
|
175
|
+
|
|
176
|
+
const sourceInfo = isRecord(command.sourceInfo) ? command.sourceInfo : undefined;
|
|
177
|
+
const sourcePath = sourceInfo ? getStringField(sourceInfo.path)?.trim() : undefined;
|
|
178
|
+
if (!sourcePath) continue;
|
|
179
|
+
|
|
180
|
+
const rawName = commandName.startsWith("skill:")
|
|
181
|
+
? commandName.slice("skill:".length)
|
|
182
|
+
: commandName;
|
|
183
|
+
const displayName = rawName || commandName;
|
|
184
|
+
const filePath = path.resolve(sourcePath);
|
|
185
|
+
|
|
186
|
+
loaded.push({
|
|
187
|
+
name: rawName || commandName,
|
|
188
|
+
displayName,
|
|
189
|
+
description: getStringField(command.description) || "",
|
|
190
|
+
path: filePath,
|
|
191
|
+
displayPath: formatSkillPath(filePath),
|
|
192
|
+
sourceScope: sourceInfo ? getStringField(sourceInfo.scope) : undefined,
|
|
193
|
+
sourceOrigin: sourceInfo ? getStringField(sourceInfo.origin) : undefined,
|
|
194
|
+
sourceLabel: sourceInfo ? getStringField(sourceInfo.source) : undefined,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return loaded.sort((a, b) => a.displayName.localeCompare(b.displayName));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function formatSkillsList(rows: SkillModalRow[], projectName: string | null): string {
|
|
202
|
+
const globalSkills = rows.filter((row) => row.category === "G");
|
|
203
|
+
const projectSkills = rows.filter((row) => row.category === "P");
|
|
204
|
+
const externalSkills = rows.filter((row) => row.category === "E");
|
|
54
205
|
|
|
55
206
|
const lines: string[] = [];
|
|
56
207
|
lines.push("");
|
|
57
|
-
lines.push("
|
|
58
|
-
lines.push(" ║
|
|
59
|
-
lines.push("
|
|
208
|
+
lines.push(" ╔═══════════════════════════════════════════════════════════╗");
|
|
209
|
+
lines.push(" ║ 🧠 Procedural Skills ║");
|
|
210
|
+
lines.push(" ╚═══════════════════════════════════════════════════════════╝");
|
|
211
|
+
lines.push(" Legend: [G] global · [P] project · [E] external (read-only)");
|
|
60
212
|
lines.push("");
|
|
61
213
|
|
|
62
|
-
if (
|
|
63
|
-
lines.push(" (no skills
|
|
214
|
+
if (rows.length === 0) {
|
|
215
|
+
lines.push(" (no skills found in this session)");
|
|
64
216
|
lines.push("");
|
|
65
217
|
lines.push(" Skills are auto-created after complex tasks,");
|
|
66
218
|
lines.push(" or you can ask the agent to create one.");
|
|
@@ -68,25 +220,34 @@ export function formatSkillsList(skills: SkillIndex[], projectName: string | nul
|
|
|
68
220
|
}
|
|
69
221
|
|
|
70
222
|
if (globalSkills.length > 0) {
|
|
71
|
-
lines.push(" Global Skills");
|
|
72
|
-
lines.push("
|
|
73
|
-
for (const
|
|
74
|
-
lines.push(` 📄 ${
|
|
75
|
-
lines.push(` ${
|
|
76
|
-
lines.push(` id: ${
|
|
77
|
-
lines.push(` path: ${skill.path}`);
|
|
223
|
+
lines.push(" [G] Global Skills");
|
|
224
|
+
lines.push(" ─────────────────");
|
|
225
|
+
for (const row of globalSkills) {
|
|
226
|
+
lines.push(` 📄 ${row.displayName} (${row.displayPath})`);
|
|
227
|
+
lines.push(` ${row.description || "(no description)"}`);
|
|
228
|
+
lines.push(` id: ${row.skillId}`);
|
|
78
229
|
lines.push("");
|
|
79
230
|
}
|
|
80
231
|
}
|
|
81
232
|
|
|
82
233
|
if (projectSkills.length > 0) {
|
|
83
|
-
lines.push(` Project Skills${projectName ? ` (${projectName})` : ""}`);
|
|
84
|
-
lines.push("
|
|
85
|
-
for (const
|
|
86
|
-
lines.push(` 📄 ${
|
|
87
|
-
lines.push(` ${
|
|
88
|
-
lines.push(` id: ${
|
|
89
|
-
lines.push(
|
|
234
|
+
lines.push(` [P] Project Skills${projectName ? ` (${projectName})` : ""}`);
|
|
235
|
+
lines.push(" ─────────────────────────────────");
|
|
236
|
+
for (const row of projectSkills) {
|
|
237
|
+
lines.push(` 📄 ${row.displayName} (${row.displayPath})`);
|
|
238
|
+
lines.push(` ${row.description || "(no description)"}`);
|
|
239
|
+
lines.push(` id: ${row.skillId}`);
|
|
240
|
+
lines.push("");
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (externalSkills.length > 0) {
|
|
245
|
+
lines.push(" [E] External Skills (read-only)");
|
|
246
|
+
lines.push(" ───────────────────────────────");
|
|
247
|
+
for (const row of externalSkills) {
|
|
248
|
+
lines.push(` 📄 ${row.displayName} (${row.displayPath})`);
|
|
249
|
+
lines.push(` ${row.description || "(no description)"}`);
|
|
250
|
+
lines.push(` id: ${row.skillId}`);
|
|
90
251
|
lines.push("");
|
|
91
252
|
}
|
|
92
253
|
}
|
|
@@ -95,17 +256,64 @@ export function formatSkillsList(skills: SkillIndex[], projectName: string | nul
|
|
|
95
256
|
}
|
|
96
257
|
|
|
97
258
|
export function buildSkillRows(skills: SkillIndex[], selectedSkillIds = new Set<string>()): SkillModalRow[] {
|
|
98
|
-
return skills.map((skill) =>
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
259
|
+
return skills.map((skill) => {
|
|
260
|
+
const displayName = skill.displayName || skill.name;
|
|
261
|
+
const displayPath = formatSkillPath(skill.path);
|
|
262
|
+
return {
|
|
263
|
+
skillId: skill.skillId,
|
|
264
|
+
scope: skill.scope,
|
|
265
|
+
category: categoryForScope(skill.scope),
|
|
266
|
+
mutable: true,
|
|
267
|
+
name: skill.name,
|
|
268
|
+
displayName,
|
|
269
|
+
description: skill.description,
|
|
270
|
+
path: skill.path,
|
|
271
|
+
displayPath,
|
|
272
|
+
projectName: skill.projectName,
|
|
273
|
+
selected: selectedSkillIds.has(skill.skillId),
|
|
274
|
+
searchText: `${displayName} ${skill.name} ${skill.description || ""} ${skill.path} ${displayPath}`.trim(),
|
|
275
|
+
};
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export function buildUnifiedSkillRows(
|
|
280
|
+
managedSkills: SkillIndex[],
|
|
281
|
+
loadedSkills: LoadedSkillRow[],
|
|
282
|
+
selectedSkillIds = new Set<string>(),
|
|
283
|
+
): SkillModalRow[] {
|
|
284
|
+
const managedRows = buildSkillRows(managedSkills, selectedSkillIds);
|
|
285
|
+
const managedPathKeys = new Set(managedRows.map((row) => normalizePathForKey(row.path)));
|
|
286
|
+
const externalPathKeys = new Set<string>();
|
|
287
|
+
|
|
288
|
+
const externalRows: SkillModalRow[] = [];
|
|
289
|
+
for (const loaded of loadedSkills) {
|
|
290
|
+
const loadedKey = normalizePathForKey(loaded.path);
|
|
291
|
+
if (managedPathKeys.has(loadedKey)) continue;
|
|
292
|
+
if (externalPathKeys.has(loadedKey)) continue;
|
|
293
|
+
externalPathKeys.add(loadedKey);
|
|
294
|
+
|
|
295
|
+
const externalSkillId = createExternalSkillId(loaded.name, loaded.path);
|
|
296
|
+
externalRows.push({
|
|
297
|
+
skillId: externalSkillId,
|
|
298
|
+
scope: undefined,
|
|
299
|
+
category: "E",
|
|
300
|
+
mutable: false,
|
|
301
|
+
name: loaded.name,
|
|
302
|
+
displayName: loaded.displayName,
|
|
303
|
+
description: loaded.description,
|
|
304
|
+
path: loaded.path,
|
|
305
|
+
displayPath: loaded.displayPath,
|
|
306
|
+
selected: selectedSkillIds.has(externalSkillId),
|
|
307
|
+
searchText: `${loaded.displayName} ${loaded.name} ${loaded.description || ""} ${loaded.path} ${loaded.displayPath}`.trim(),
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return [...managedRows, ...externalRows]
|
|
312
|
+
.sort((a, b) => {
|
|
313
|
+
const byCategory = categoryOrder(a.category) - categoryOrder(b.category);
|
|
314
|
+
if (byCategory !== 0) return byCategory;
|
|
315
|
+
return a.displayName.localeCompare(b.displayName);
|
|
316
|
+
});
|
|
109
317
|
}
|
|
110
318
|
|
|
111
319
|
export function filterSkillRows(rows: SkillModalRow[], query: string): SkillModalRow[] {
|
|
@@ -157,6 +365,13 @@ type SkillMoveStore = Pick<SkillStore, "move" | "loadIndex" | "getProjectName">;
|
|
|
157
365
|
type SkillDeleteStore = Pick<SkillStore, "delete" | "loadIndex">;
|
|
158
366
|
export type ConfirmDialog = (title: string, message: string) => Promise<boolean>;
|
|
159
367
|
|
|
368
|
+
export interface SkillBatchActionResult {
|
|
369
|
+
skills: SkillIndex[];
|
|
370
|
+
summaryLines: string[];
|
|
371
|
+
retainSelectedSkillIds?: string[];
|
|
372
|
+
focusSkillId?: string;
|
|
373
|
+
}
|
|
374
|
+
|
|
160
375
|
export async function moveSelectedSkills(
|
|
161
376
|
store: SkillMoveStore,
|
|
162
377
|
skillIds: string[],
|
|
@@ -307,15 +522,20 @@ export class SkillsManagerModal implements Focusable {
|
|
|
307
522
|
}
|
|
308
523
|
|
|
309
524
|
private readonly searchInput = new Input();
|
|
525
|
+
private managedSkills: SkillIndex[];
|
|
526
|
+
private readonly loadedSkills: LoadedSkillRow[];
|
|
310
527
|
private rows: SkillModalRow[];
|
|
311
528
|
private selectedIndex = 0;
|
|
312
529
|
private query = "";
|
|
313
|
-
private focusArea: "search" | "list" = "list";
|
|
530
|
+
private focusArea: "search" | "list" | "filters" = "list";
|
|
314
531
|
private busy = false;
|
|
315
532
|
private closed = false;
|
|
316
533
|
private pendingDeleteConfirm: { skillIds: string[] } | null = null;
|
|
534
|
+
private activeFilters: SkillCategoryFilters = { ...DEFAULT_SKILL_FILTERS };
|
|
535
|
+
private pendingFilters: SkillCategoryFilters | null = null;
|
|
536
|
+
private filterCursor = 0;
|
|
317
537
|
private summaryLines: string[] = [
|
|
318
|
-
"Select skills with space, then move with g/p or delete with d.",
|
|
538
|
+
"Select skills with space, then move with g/p or delete with d. Press f for filters.",
|
|
319
539
|
];
|
|
320
540
|
|
|
321
541
|
constructor(
|
|
@@ -323,8 +543,39 @@ export class SkillsManagerModal implements Focusable {
|
|
|
323
543
|
private readonly theme: Theme,
|
|
324
544
|
initialRows: SkillModalRow[],
|
|
325
545
|
private readonly callbacks: SkillsManagerCallbacks,
|
|
546
|
+
options?: {
|
|
547
|
+
managedSkills?: SkillIndex[];
|
|
548
|
+
loadedSkills?: LoadedSkillRow[];
|
|
549
|
+
},
|
|
326
550
|
) {
|
|
327
|
-
|
|
551
|
+
const selectedSkillIds = new Set(initialRows.filter((row) => row.selected).map((row) => row.skillId));
|
|
552
|
+
|
|
553
|
+
this.loadedSkills = options?.loadedSkills
|
|
554
|
+
?? initialRows
|
|
555
|
+
.filter((row) => row.category === "E")
|
|
556
|
+
.map((row) => ({
|
|
557
|
+
name: row.name,
|
|
558
|
+
displayName: row.displayName,
|
|
559
|
+
description: row.description,
|
|
560
|
+
path: row.path,
|
|
561
|
+
displayPath: row.displayPath,
|
|
562
|
+
}));
|
|
563
|
+
|
|
564
|
+
this.managedSkills = options?.managedSkills
|
|
565
|
+
?? initialRows
|
|
566
|
+
.filter((row) => row.category !== "E" && row.scope)
|
|
567
|
+
.map((row) => ({
|
|
568
|
+
skillId: row.skillId,
|
|
569
|
+
scope: row.scope!,
|
|
570
|
+
fileName: path.basename(row.path),
|
|
571
|
+
path: row.path,
|
|
572
|
+
projectName: row.projectName,
|
|
573
|
+
name: row.name,
|
|
574
|
+
displayName: row.displayName,
|
|
575
|
+
description: row.description,
|
|
576
|
+
}));
|
|
577
|
+
|
|
578
|
+
this.rows = buildUnifiedSkillRows(this.managedSkills, this.loadedSkills, selectedSkillIds);
|
|
328
579
|
this.syncSearchFocus();
|
|
329
580
|
}
|
|
330
581
|
|
|
@@ -333,7 +584,8 @@ export class SkillsManagerModal implements Focusable {
|
|
|
333
584
|
}
|
|
334
585
|
|
|
335
586
|
private get filteredRows(): SkillModalRow[] {
|
|
336
|
-
|
|
587
|
+
const categoryFiltered = this.rows.filter((row) => matchesCategoryFilter(row, this.activeFilters));
|
|
588
|
+
return filterSkillRows(categoryFiltered, this.query);
|
|
337
589
|
}
|
|
338
590
|
|
|
339
591
|
private getCurrentRow(): SkillModalRow | null {
|
|
@@ -342,10 +594,22 @@ export class SkillsManagerModal implements Focusable {
|
|
|
342
594
|
return rows[Math.min(this.selectedIndex, rows.length - 1)] ?? null;
|
|
343
595
|
}
|
|
344
596
|
|
|
597
|
+
private getSelectedRows(): SkillModalRow[] {
|
|
598
|
+
return this.rows.filter((row) => row.selected);
|
|
599
|
+
}
|
|
600
|
+
|
|
345
601
|
private getSelectedIds(): string[] {
|
|
346
602
|
return getSelectedSkillIds(this.rows);
|
|
347
603
|
}
|
|
348
604
|
|
|
605
|
+
private getFilterOptions(): Array<{ key: keyof SkillCategoryFilters; label: string }> {
|
|
606
|
+
return [
|
|
607
|
+
{ key: "global", label: "Global [G]" },
|
|
608
|
+
{ key: "project", label: "Project [P]" },
|
|
609
|
+
{ key: "external", label: "External [E] (read-only)" },
|
|
610
|
+
];
|
|
611
|
+
}
|
|
612
|
+
|
|
349
613
|
private syncSearchFocus(): void {
|
|
350
614
|
this.searchInput.focused = this.focused && this.focusArea === "search";
|
|
351
615
|
}
|
|
@@ -360,14 +624,15 @@ export class SkillsManagerModal implements Focusable {
|
|
|
360
624
|
}
|
|
361
625
|
}
|
|
362
626
|
|
|
363
|
-
private setFocusArea(area: "search" | "list"): void {
|
|
627
|
+
private setFocusArea(area: "search" | "list" | "filters"): void {
|
|
364
628
|
this.focusArea = area;
|
|
365
629
|
this.syncSearchFocus();
|
|
366
630
|
this.tui.requestRender();
|
|
367
631
|
}
|
|
368
632
|
|
|
369
|
-
private setRows(
|
|
370
|
-
this.
|
|
633
|
+
private setRows(managedSkills: SkillIndex[], retainSelectedSkillIds: string[] = [], focusSkillId?: string): void {
|
|
634
|
+
this.managedSkills = managedSkills;
|
|
635
|
+
this.rows = buildUnifiedSkillRows(this.managedSkills, this.loadedSkills, new Set(retainSelectedSkillIds));
|
|
371
636
|
this.syncQueryFromInput();
|
|
372
637
|
|
|
373
638
|
const rows = this.filteredRows;
|
|
@@ -422,28 +687,84 @@ export class SkillsManagerModal implements Focusable {
|
|
|
422
687
|
this.tui.requestRender();
|
|
423
688
|
}
|
|
424
689
|
|
|
425
|
-
private
|
|
426
|
-
|
|
427
|
-
|
|
690
|
+
private appendExternalReadOnlySummary(
|
|
691
|
+
result: SkillBatchActionResult,
|
|
692
|
+
blockedExternalRows: SkillModalRow[],
|
|
693
|
+
verb: "move" | "delete",
|
|
694
|
+
): SkillBatchActionResult {
|
|
695
|
+
if (blockedExternalRows.length === 0) return result;
|
|
696
|
+
|
|
697
|
+
const blockedIds = blockedExternalRows.map((row) => row.skillId);
|
|
698
|
+
const retainSet = new Set([...(result.retainSelectedSkillIds || []), ...blockedIds]);
|
|
699
|
+
const focusSkillId = result.focusSkillId || blockedIds[0];
|
|
700
|
+
const blockedLabel = blockedExternalRows.length === 1
|
|
701
|
+
? `Blocked 1 external skill: ${blockedExternalRows[0]!.displayName} is read-only.`
|
|
702
|
+
: `Blocked ${blockedExternalRows.length} external skills: read-only (${verb} unavailable).`;
|
|
703
|
+
|
|
704
|
+
return {
|
|
705
|
+
...result,
|
|
706
|
+
summaryLines: [...result.summaryLines, blockedLabel],
|
|
707
|
+
retainSelectedSkillIds: Array.from(retainSet),
|
|
708
|
+
focusSkillId,
|
|
709
|
+
};
|
|
428
710
|
}
|
|
429
711
|
|
|
430
|
-
private
|
|
431
|
-
|
|
432
|
-
|
|
712
|
+
private prepareMutableSelection(verb: "move" | "delete"):
|
|
713
|
+
| { proceed: false }
|
|
714
|
+
| { proceed: true; mutableIds: string[]; blockedExternalRows: SkillModalRow[] } {
|
|
715
|
+
const selectedRows = this.getSelectedRows();
|
|
716
|
+
if (selectedRows.length === 0) {
|
|
433
717
|
this.summaryLines = ["Select one or more skills first."];
|
|
434
718
|
this.tui.requestRender();
|
|
435
|
-
return;
|
|
719
|
+
return { proceed: false };
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
const mutableRows = selectedRows.filter((row) => row.mutable);
|
|
723
|
+
const blockedExternalRows = selectedRows.filter((row) => !row.mutable);
|
|
724
|
+
|
|
725
|
+
if (mutableRows.length === 0 && blockedExternalRows.length > 0) {
|
|
726
|
+
this.summaryLines = [
|
|
727
|
+
`Blocked ${blockedExternalRows.length} external skill${blockedExternalRows.length === 1 ? "" : "s"}: read-only (${verb} unavailable).`,
|
|
728
|
+
];
|
|
729
|
+
this.tui.requestRender();
|
|
730
|
+
return { proceed: false };
|
|
436
731
|
}
|
|
437
732
|
|
|
438
|
-
|
|
733
|
+
return {
|
|
734
|
+
proceed: true,
|
|
735
|
+
mutableIds: mutableRows.map((row) => row.skillId),
|
|
736
|
+
blockedExternalRows,
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
private async runMove(targetScope: SkillScope): Promise<void> {
|
|
741
|
+
const selection = this.prepareMutableSelection("move");
|
|
742
|
+
if (!selection.proceed) return;
|
|
743
|
+
|
|
744
|
+
const action = this.callbacks.moveSelected(targetScope, selection.mutableIds)
|
|
745
|
+
.then((result) => this.appendExternalReadOnlySummary(result, selection.blockedExternalRows, "move"));
|
|
746
|
+
|
|
747
|
+
await this.runAsyncAction(action);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
private promptDelete(): void {
|
|
751
|
+
const selection = this.prepareMutableSelection("delete");
|
|
752
|
+
if (!selection.proceed) return;
|
|
753
|
+
|
|
754
|
+
this.pendingDeleteConfirm = { skillIds: selection.mutableIds };
|
|
755
|
+
const blockedCount = selection.blockedExternalRows.length;
|
|
439
756
|
this.summaryLines = [
|
|
440
|
-
`Delete ${
|
|
757
|
+
`Delete ${selection.mutableIds.length} selected skill${selection.mutableIds.length === 1 ? "" : "s"}? Press y to confirm or n to cancel.${blockedCount > 0 ? ` (${blockedCount} external read-only item${blockedCount === 1 ? "" : "s"} will be skipped)` : ""}`,
|
|
441
758
|
];
|
|
442
759
|
this.tui.requestRender();
|
|
443
760
|
}
|
|
444
761
|
|
|
445
762
|
private async runDeleteConfirmed(skillIds: string[]): Promise<void> {
|
|
446
|
-
|
|
763
|
+
const blockedExternalRows = this.rows.filter((row) => row.selected && !row.mutable);
|
|
764
|
+
const action = this.callbacks.deleteSelected(skillIds)
|
|
765
|
+
.then((result) => this.appendExternalReadOnlySummary(result, blockedExternalRows, "delete"));
|
|
766
|
+
|
|
767
|
+
await this.runAsyncAction(action);
|
|
447
768
|
}
|
|
448
769
|
|
|
449
770
|
private closeModal(): void {
|
|
@@ -452,6 +773,72 @@ export class SkillsManagerModal implements Focusable {
|
|
|
452
773
|
this.callbacks.close();
|
|
453
774
|
}
|
|
454
775
|
|
|
776
|
+
private openFilterPanel(): void {
|
|
777
|
+
this.pendingFilters = cloneFilters(this.activeFilters);
|
|
778
|
+
this.filterCursor = 0;
|
|
779
|
+
this.setFocusArea("filters");
|
|
780
|
+
this.summaryLines = ["Filter panel open: space toggle · enter apply · esc cancel."];
|
|
781
|
+
this.tui.requestRender();
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
private applyFilterPanel(): void {
|
|
785
|
+
const candidate = ensureValidFilters(this.pendingFilters ? cloneFilters(this.pendingFilters) : cloneFilters(this.activeFilters));
|
|
786
|
+
const wasAllOff = this.pendingFilters
|
|
787
|
+
&& !this.pendingFilters.global
|
|
788
|
+
&& !this.pendingFilters.project
|
|
789
|
+
&& !this.pendingFilters.external;
|
|
790
|
+
|
|
791
|
+
this.activeFilters = candidate;
|
|
792
|
+
this.pendingFilters = null;
|
|
793
|
+
this.syncQueryFromInput();
|
|
794
|
+
this.setFocusArea("list");
|
|
795
|
+
this.summaryLines = [
|
|
796
|
+
wasAllOff
|
|
797
|
+
? "All categories were disabled; restored filters to [G] [P] [E]."
|
|
798
|
+
: `Applied filters: ${filtersLabel(this.activeFilters)}`,
|
|
799
|
+
];
|
|
800
|
+
this.tui.requestRender();
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
private cancelFilterPanel(): void {
|
|
804
|
+
this.pendingFilters = null;
|
|
805
|
+
this.setFocusArea("list");
|
|
806
|
+
this.summaryLines = ["Filter changes cancelled."];
|
|
807
|
+
this.tui.requestRender();
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
private handleFilterInput(data: string): void {
|
|
811
|
+
const options = this.getFilterOptions();
|
|
812
|
+
const draft = this.pendingFilters ?? cloneFilters(this.activeFilters);
|
|
813
|
+
this.pendingFilters = draft;
|
|
814
|
+
|
|
815
|
+
if (matchesKey(data, Key.escape)) {
|
|
816
|
+
this.cancelFilterPanel();
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
if (matchesKey(data, Key.up)) {
|
|
820
|
+
this.filterCursor = Math.max(0, this.filterCursor - 1);
|
|
821
|
+
this.tui.requestRender();
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
if (matchesKey(data, Key.down)) {
|
|
825
|
+
this.filterCursor = Math.min(options.length - 1, this.filterCursor + 1);
|
|
826
|
+
this.tui.requestRender();
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
if (matchesKey(data, Key.space)) {
|
|
830
|
+
const option = options[this.filterCursor];
|
|
831
|
+
if (option) {
|
|
832
|
+
draft[option.key] = !draft[option.key];
|
|
833
|
+
}
|
|
834
|
+
this.tui.requestRender();
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
if (matchesKey(data, Key.enter)) {
|
|
838
|
+
this.applyFilterPanel();
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
455
842
|
private async runAsyncAction(action: Promise<SkillBatchActionResult>): Promise<void> {
|
|
456
843
|
if (this.closed) return;
|
|
457
844
|
|
|
@@ -490,7 +877,7 @@ export class SkillsManagerModal implements Focusable {
|
|
|
490
877
|
}
|
|
491
878
|
|
|
492
879
|
private getMaxVisibleRows(): number {
|
|
493
|
-
return Math.max(6, Math.min(14, this.tui.terminal.rows -
|
|
880
|
+
return Math.max(6, Math.min(14, this.tui.terminal.rows - 22));
|
|
494
881
|
}
|
|
495
882
|
|
|
496
883
|
private focusSearchWithOptionalInput(data?: string): void {
|
|
@@ -530,6 +917,11 @@ export class SkillsManagerModal implements Focusable {
|
|
|
530
917
|
return;
|
|
531
918
|
}
|
|
532
919
|
|
|
920
|
+
if (this.focusArea === "filters") {
|
|
921
|
+
this.handleFilterInput(data);
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
|
|
533
925
|
if (matchesKey(data, Key.escape)) {
|
|
534
926
|
this.closeModal();
|
|
535
927
|
return;
|
|
@@ -547,6 +939,11 @@ export class SkillsManagerModal implements Focusable {
|
|
|
547
939
|
return;
|
|
548
940
|
}
|
|
549
941
|
|
|
942
|
+
if (data === MEMORY_SKILLS_KEYMAP.openFilters) {
|
|
943
|
+
this.openFilterPanel();
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
|
|
550
947
|
if (matchesKey(data, Key.tab) || matchesKey(data, Key.slash)) {
|
|
551
948
|
this.focusSearchWithOptionalInput();
|
|
552
949
|
return;
|
|
@@ -601,7 +998,7 @@ export class SkillsManagerModal implements Focusable {
|
|
|
601
998
|
this.promptDelete();
|
|
602
999
|
return;
|
|
603
1000
|
}
|
|
604
|
-
if (this.isPrintableInput(data) && !["g", "p", "d", "a", "n"].includes(data)) {
|
|
1001
|
+
if (this.isPrintableInput(data) && !["g", "p", "d", "a", "n", "f"].includes(data)) {
|
|
605
1002
|
this.focusSearchWithOptionalInput(data);
|
|
606
1003
|
}
|
|
607
1004
|
}
|
|
@@ -629,6 +1026,35 @@ export class SkillsManagerModal implements Focusable {
|
|
|
629
1026
|
return rendered;
|
|
630
1027
|
}
|
|
631
1028
|
|
|
1029
|
+
private renderFilterPanel(width: number): string[] {
|
|
1030
|
+
const panelWidth = Math.max(34, Math.min(width - 10, 58));
|
|
1031
|
+
const top = this.theme.fg("borderAccent", `┌${"─".repeat(Math.max(1, panelWidth - 2))}┐`);
|
|
1032
|
+
const bottom = this.theme.fg("borderAccent", `└${"─".repeat(Math.max(1, panelWidth - 2))}┘`);
|
|
1033
|
+
const lines: string[] = [top];
|
|
1034
|
+
|
|
1035
|
+
lines.push(this.renderFramedLine(this.theme.fg("accent", this.theme.bold("Filters")), panelWidth));
|
|
1036
|
+
lines.push(this.renderFramedLine(this.theme.fg("dim", "Space toggle · Enter apply · Esc cancel"), panelWidth));
|
|
1037
|
+
lines.push(this.renderFramedLine("", panelWidth));
|
|
1038
|
+
|
|
1039
|
+
const draft = this.pendingFilters ?? this.activeFilters;
|
|
1040
|
+
const options = this.getFilterOptions();
|
|
1041
|
+
for (let i = 0; i < options.length; i++) {
|
|
1042
|
+
const option = options[i]!;
|
|
1043
|
+
const checked = draft[option.key] ? "[x]" : "[ ]";
|
|
1044
|
+
const cursor = i === this.filterCursor ? this.theme.fg("accent", "›") : " ";
|
|
1045
|
+
const text = `${cursor} ${checked} ${option.label}`;
|
|
1046
|
+
const rendered = i === this.filterCursor
|
|
1047
|
+
? this.theme.bg("selectedBg", truncateToWidth(text, Math.max(10, panelWidth - 4), ""))
|
|
1048
|
+
: truncateToWidth(text, Math.max(10, panelWidth - 4), "");
|
|
1049
|
+
lines.push(this.renderFramedLine(rendered, panelWidth));
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
lines.push(this.renderFramedLine("", panelWidth));
|
|
1053
|
+
lines.push(this.renderFramedLine(this.theme.fg("dim", `Draft: ${filtersLabel(draft)}`), panelWidth));
|
|
1054
|
+
lines.push(bottom);
|
|
1055
|
+
return lines;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
632
1058
|
render(width: number): string[] {
|
|
633
1059
|
const safeWidth = Math.max(60, width);
|
|
634
1060
|
const top = this.theme.fg("borderAccent", `┌${"─".repeat(Math.max(1, safeWidth - 2))}┐`);
|
|
@@ -655,10 +1081,11 @@ export class SkillsManagerModal implements Focusable {
|
|
|
655
1081
|
safeWidth,
|
|
656
1082
|
));
|
|
657
1083
|
|
|
1084
|
+
lines.push(this.renderFramedLine(this.theme.fg("dim", `Legend: [G] global · [P] project · [E] external (read-only) · filters: ${filtersLabel(this.activeFilters)}`), safeWidth));
|
|
658
1085
|
lines.push(this.renderFramedLine("", safeWidth));
|
|
659
1086
|
|
|
660
1087
|
if (filteredRows.length === 0) {
|
|
661
|
-
const emptyMessage = this.rows.length === 0 ? "No skills found yet." : "No skills match the current search.";
|
|
1088
|
+
const emptyMessage = this.rows.length === 0 ? "No skills found yet." : "No skills match the current filters/search.";
|
|
662
1089
|
lines.push(this.renderFramedLine(this.theme.fg("warning", emptyMessage), safeWidth));
|
|
663
1090
|
lines.push(this.renderFramedLine("", safeWidth));
|
|
664
1091
|
} else {
|
|
@@ -672,10 +1099,13 @@ export class SkillsManagerModal implements Focusable {
|
|
|
672
1099
|
const absoluteIndex = start + i;
|
|
673
1100
|
const cursor = absoluteIndex === this.selectedIndex ? this.theme.fg("accent", "›") : " ";
|
|
674
1101
|
const check = row.selected ? this.theme.fg("accent", "[x]") : this.theme.fg("dim", "[ ]");
|
|
675
|
-
const
|
|
1102
|
+
const category = row.category === "G"
|
|
676
1103
|
? this.theme.fg("accent", "[G]")
|
|
677
|
-
:
|
|
678
|
-
|
|
1104
|
+
: row.category === "P"
|
|
1105
|
+
? this.theme.fg("warning", "[P]")
|
|
1106
|
+
: this.theme.fg("dim", "[E]");
|
|
1107
|
+
|
|
1108
|
+
const baseText = `${cursor} ${check} ${category} ${row.displayName} (${row.displayPath})`;
|
|
679
1109
|
const lineText = absoluteIndex === this.selectedIndex
|
|
680
1110
|
? this.theme.bg("selectedBg", truncateToWidth(baseText, Math.max(10, safeWidth - 4), ""))
|
|
681
1111
|
: truncateToWidth(baseText, Math.max(10, safeWidth - 4), "");
|
|
@@ -689,10 +1119,16 @@ export class SkillsManagerModal implements Focusable {
|
|
|
689
1119
|
lines.push(this.renderFramedLine("", safeWidth));
|
|
690
1120
|
const currentRow = this.getCurrentRow();
|
|
691
1121
|
if (currentRow) {
|
|
692
|
-
|
|
1122
|
+
const scopeLabel = currentRow.category === "E"
|
|
1123
|
+
? "external (read-only)"
|
|
1124
|
+
: currentRow.scope === "project"
|
|
1125
|
+
? "project"
|
|
1126
|
+
: "global";
|
|
1127
|
+
lines.push(this.renderFramedLine(this.theme.fg("accent", `Focused: ${currentRow.displayName} · ${scopeLabel}`), safeWidth));
|
|
693
1128
|
lines.push(...this.renderWrappedSection([
|
|
694
1129
|
currentRow.description || "(no description)",
|
|
695
1130
|
this.theme.fg("dim", currentRow.skillId),
|
|
1131
|
+
this.theme.fg("dim", currentRow.displayPath),
|
|
696
1132
|
], safeWidth));
|
|
697
1133
|
}
|
|
698
1134
|
}
|
|
@@ -705,9 +1141,17 @@ export class SkillsManagerModal implements Focusable {
|
|
|
705
1141
|
const help = this.pendingDeleteConfirm
|
|
706
1142
|
? "Confirm delete: y yes · n no · esc cancel"
|
|
707
1143
|
: this.callbacks.projectName
|
|
708
|
-
? "↑↓ move · space select · / search · tab switch · g global · p project · d delete · a all · n none · esc close"
|
|
709
|
-
: "↑↓ move · space select · / search · tab switch · g global · p project (disabled) · d delete · a all · n none · esc close";
|
|
1144
|
+
? "↑↓ move · space select · / search · f filters · tab switch · g global · p project · d delete · a all · n none · esc close"
|
|
1145
|
+
: "↑↓ move · space select · / search · f filters · tab switch · g global · p project (disabled) · d delete · a all · n none · esc close";
|
|
710
1146
|
lines.push(this.renderFramedLine(this.theme.fg("dim", help), safeWidth));
|
|
1147
|
+
|
|
1148
|
+
if (this.focusArea === "filters") {
|
|
1149
|
+
lines.push(this.renderFramedLine("", safeWidth));
|
|
1150
|
+
for (const panelLine of this.renderFilterPanel(Math.min(64, safeWidth - 6))) {
|
|
1151
|
+
lines.push(this.renderFramedLine(panelLine, safeWidth));
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
|
|
711
1155
|
lines.push(bottom);
|
|
712
1156
|
return lines;
|
|
713
1157
|
}
|
|
@@ -715,18 +1159,35 @@ export class SkillsManagerModal implements Focusable {
|
|
|
715
1159
|
|
|
716
1160
|
export function registerSkillsCommand(pi: ExtensionAPI, store: SkillStore): void {
|
|
717
1161
|
pi.registerCommand("memory-skills", {
|
|
718
|
-
description: "Manage global
|
|
1162
|
+
description: "Manage global, active-project, and loaded external procedural skills",
|
|
719
1163
|
handler: async (_args, ctx: ExtensionCommandContext) => {
|
|
720
|
-
const
|
|
1164
|
+
const getSkillCommands = (): SkillCommandInfo[] => {
|
|
1165
|
+
const readCommands = (owner: unknown): SkillCommandInfo[] | null => {
|
|
1166
|
+
try {
|
|
1167
|
+
const getter = (owner as { getCommands?: () => unknown })?.getCommands;
|
|
1168
|
+
if (typeof getter !== "function") return null;
|
|
1169
|
+
const commands = getter.call(owner);
|
|
1170
|
+
return Array.isArray(commands) ? commands as SkillCommandInfo[] : [];
|
|
1171
|
+
} catch {
|
|
1172
|
+
return null;
|
|
1173
|
+
}
|
|
1174
|
+
};
|
|
1175
|
+
|
|
1176
|
+
return readCommands(pi)
|
|
1177
|
+
?? readCommands(ctx)
|
|
1178
|
+
?? [];
|
|
1179
|
+
};
|
|
1180
|
+
|
|
1181
|
+
const managedSkills = await store.loadIndex();
|
|
1182
|
+
const loadedSkills = collectLoadedSkillsFromCommands(getSkillCommands());
|
|
1183
|
+
const initialRows = buildUnifiedSkillRows(managedSkills, loadedSkills);
|
|
721
1184
|
const projectName = store.getProjectName();
|
|
722
1185
|
|
|
723
1186
|
if (!ctx.hasUI || typeof ctx.ui.custom !== "function") {
|
|
724
|
-
ctx.ui.notify(formatSkillsList(
|
|
1187
|
+
ctx.ui.notify(formatSkillsList(initialRows, projectName), "info");
|
|
725
1188
|
return;
|
|
726
1189
|
}
|
|
727
1190
|
|
|
728
|
-
const initialRows = buildSkillRows(skills);
|
|
729
|
-
|
|
730
1191
|
try {
|
|
731
1192
|
await ctx.ui.custom<void>(
|
|
732
1193
|
(tui, theme, _keybindings, done) => new SkillsManagerModal(
|
|
@@ -739,25 +1200,33 @@ export function registerSkillsCommand(pi: ExtensionAPI, store: SkillStore): void
|
|
|
739
1200
|
close: () => done(undefined),
|
|
740
1201
|
projectName,
|
|
741
1202
|
},
|
|
1203
|
+
{
|
|
1204
|
+
managedSkills,
|
|
1205
|
+
loadedSkills,
|
|
1206
|
+
},
|
|
742
1207
|
),
|
|
743
1208
|
{
|
|
744
1209
|
overlay: true,
|
|
745
1210
|
overlayOptions: {
|
|
746
1211
|
anchor: "center",
|
|
747
|
-
width: "
|
|
748
|
-
minWidth:
|
|
749
|
-
maxHeight: "
|
|
1212
|
+
width: "92%",
|
|
1213
|
+
minWidth: 76,
|
|
1214
|
+
maxHeight: "88%",
|
|
750
1215
|
margin: 1,
|
|
751
1216
|
},
|
|
752
1217
|
},
|
|
753
1218
|
);
|
|
754
1219
|
} catch {
|
|
755
|
-
const
|
|
1220
|
+
const latestManagedSkills = await store.loadIndex();
|
|
1221
|
+
const latestRows = buildUnifiedSkillRows(
|
|
1222
|
+
latestManagedSkills,
|
|
1223
|
+
collectLoadedSkillsFromCommands(getSkillCommands()),
|
|
1224
|
+
);
|
|
756
1225
|
ctx.ui.notify(
|
|
757
1226
|
"Interactive skills manager unavailable in this runtime; showing read-only list fallback.",
|
|
758
1227
|
"warning",
|
|
759
1228
|
);
|
|
760
|
-
ctx.ui.notify(formatSkillsList(
|
|
1229
|
+
ctx.ui.notify(formatSkillsList(latestRows, projectName), "info");
|
|
761
1230
|
}
|
|
762
1231
|
},
|
|
763
1232
|
});
|