@duckmind/dm-windows-x64 0.63.4 → 0.63.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/extensions/.dm-extensions.json +64 -4
  2. package/extensions/dm-context/package.json +1 -1
  3. package/extensions/dm-context/skills/context-management/SKILL.md +173 -223
  4. package/extensions/dm-context/skills/context-management/references/development-and-troubleshooting.md +75 -0
  5. package/extensions/dm-context/skills/context-management/references/interleaved-async-work.md +143 -0
  6. package/extensions/dm-context/skills/context-management/references/planning-and-execution.md +81 -0
  7. package/extensions/dm-context/skills/context-management/references/repeated-items-and-batch-work.md +69 -0
  8. package/extensions/dm-context/skills/context-management/references/retry-branch-and-pivot.md +80 -0
  9. package/extensions/dm-context/skills/context-management/references/search-research-and-reading.md +103 -0
  10. package/extensions/dm-context/skills/context-management/references/task-switching-and-cleanup.md +73 -0
  11. package/extensions/dm-context/src/context.js +3 -2
  12. package/extensions/dm-context/src/index.js +168 -84
  13. package/extensions/dm-skills-manager/THIRD_PARTY_NOTICES.md +27 -0
  14. package/extensions/dm-skills-manager/extensions/skills-manager/components.js +265 -0
  15. package/extensions/dm-skills-manager/extensions/skills-manager/constants.js +31 -0
  16. package/extensions/dm-skills-manager/extensions/skills-manager/creation-fallback.js +20 -0
  17. package/extensions/dm-skills-manager/extensions/skills-manager/creation.js +145 -0
  18. package/extensions/dm-skills-manager/extensions/skills-manager/dialog.js +738 -0
  19. package/extensions/dm-skills-manager/extensions/skills-manager/dm-ai-compat.js +15 -0
  20. package/extensions/dm-skills-manager/extensions/skills-manager/format.js +125 -0
  21. package/extensions/dm-skills-manager/extensions/skills-manager/glyphs.js +142 -0
  22. package/extensions/dm-skills-manager/extensions/skills-manager/layout.js +93 -0
  23. package/extensions/dm-skills-manager/extensions/skills-manager/paths.js +76 -0
  24. package/extensions/dm-skills-manager/extensions/skills-manager/registry.js +95 -0
  25. package/extensions/dm-skills-manager/extensions/skills-manager/settings.js +93 -0
  26. package/extensions/dm-skills-manager/extensions/skills-manager/startup.js +32 -0
  27. package/extensions/dm-skills-manager/extensions/skills-manager/toggle.js +54 -0
  28. package/extensions/dm-skills-manager/extensions/skills-manager/types.js +14 -0
  29. package/extensions/dm-skills-manager/extensions/skills-manager/ui.js +121 -0
  30. package/extensions/dm-skills-manager/extensions/skills-manager.js +111 -0
  31. package/extensions/dm-skills-manager/package.json +121 -0
  32. package/package.json +1 -1
@@ -0,0 +1,738 @@
1
+ import { writeFileSync } from "node:fs";
2
+ import {
3
+ Container,
4
+ Editor,
5
+ Input,
6
+ Key,
7
+ matchesKey,
8
+ Spacer,
9
+ Text,
10
+ wrapTextWithAnsi
11
+ } from "@duckmind/dm-tui";
12
+ import {
13
+ ListLineText,
14
+ PrefixedEditor,
15
+ ScrollableSkillPreview,
16
+ SearchInputLine,
17
+ SingleLineText,
18
+ SkillEditorView
19
+ } from "./components.js";
20
+ import { DEFAULT_LIST_ROWS, DEFAULT_POPUP_MAX_HEIGHT, DEFAULT_POPUP_WIDTH } from "./constants.js";
21
+ import { renameSkillEntry } from "./creation.js";
22
+ import {
23
+ buildEditableSkillDocument,
24
+ normalizeSkillName,
25
+ parseEditableSkillDocument,
26
+ readSkillDocument,
27
+ toUpdatedSkill
28
+ } from "./format.js";
29
+ import { normalizeListRows, responsiveBrowsePageSelection, responsiveBrowseWindow, sanitizePopupMaxHeight } from "./layout.js";
30
+ import { isDeletableSkill, skillStorageTarget } from "./registry.js";
31
+ import { settingNumber, settingOverlaySize, settingString } from "./settings.js";
32
+ import {
33
+ acquirekendexModalLock,
34
+ getEditorTheme,
35
+ packageLabel,
36
+ renderCenteredDialog,
37
+ renderFrame,
38
+ scopeLabel,
39
+ skillEntityTitle,
40
+ skillKeyHints,
41
+ skillSectionTitle,
42
+ toneText
43
+ } from "./ui.js";
44
+ import {
45
+ CREATE_STEPS
46
+ } from "./types.js";
47
+
48
+ class SkillsManagerDialog {
49
+ ctx;
50
+ theme;
51
+ tui;
52
+ done;
53
+ options;
54
+ requestRender;
55
+ mode = "browse";
56
+ _focused = false;
57
+ registry;
58
+ filteredSkills = [];
59
+ selectedIndex;
60
+ browseQuery;
61
+ browseInput = new Input;
62
+ descriptionEditor;
63
+ renameInput = new Input;
64
+ createStepIndex = 0;
65
+ createValues = { name: "", description: "" };
66
+ createLocation;
67
+ submittedDescriptionValue;
68
+ createError;
69
+ previewSkillPath;
70
+ preview;
71
+ editorView;
72
+ renameError;
73
+ deleteSkillPath;
74
+ deleteReturnMode = "browse";
75
+ generationAbortController;
76
+ generationRunId = 0;
77
+ configuredListRows;
78
+ popupMaxHeight;
79
+ constructor(ctx, registry, theme, tui, done, options, requestRender, initialSelectedIndex = 0, initialQuery = "") {
80
+ this.ctx = ctx;
81
+ this.theme = theme;
82
+ this.tui = tui;
83
+ this.done = done;
84
+ this.options = options;
85
+ this.requestRender = requestRender;
86
+ this.registry = registry;
87
+ this.selectedIndex = Math.max(0, initialSelectedIndex);
88
+ this.browseQuery = initialQuery;
89
+ this.browseInput.setValue(initialQuery);
90
+ this.createLocation = settingString("defaultCreateLocation", "project", ctx.cwd) === "global" ? "global" : "project";
91
+ this.configuredListRows = normalizeListRows(settingNumber("listRows", DEFAULT_LIST_ROWS, ctx.cwd));
92
+ this.popupMaxHeight = sanitizePopupMaxHeight(settingOverlaySize("popupMaxHeight", DEFAULT_POPUP_MAX_HEIGHT, ctx.cwd));
93
+ this.descriptionEditor = new Editor(tui, { borderColor: (text) => " ".repeat(text.length), selectList: getEditorTheme(theme).selectList });
94
+ this.descriptionEditor.onSubmit = (text) => {
95
+ this.submittedDescriptionValue = text;
96
+ this.advanceCreate();
97
+ };
98
+ this.renameInput.onSubmit = (value) => {
99
+ this.submitRename(value);
100
+ };
101
+ this.refreshBrowseList();
102
+ }
103
+ get focused() {
104
+ return this._focused;
105
+ }
106
+ set focused(value) {
107
+ this._focused = value;
108
+ this.syncFocus();
109
+ }
110
+ invalidate() {
111
+ this.browseInput.invalidate();
112
+ this.descriptionEditor.invalidate();
113
+ this.renameInput.invalidate();
114
+ this.preview?.invalidate();
115
+ this.editorView?.invalidate();
116
+ }
117
+ syncFocus() {
118
+ this.browseInput.focused = this._focused && (this.mode === "browse" || this.mode === "create" && this.currentCreateStep.id === "name");
119
+ this.descriptionEditor.focused = this._focused && this.mode === "create" && this.currentCreateStep.id === "description";
120
+ this.renameInput.focused = this._focused && this.mode === "rename";
121
+ if (this.editorView)
122
+ this.editorView.focused = this._focused && this.mode === "edit";
123
+ }
124
+ searchableText(skill) {
125
+ return [skill.name, skill.description, scopeLabel(skill), skill.origin, skill.source, skill.path, skill.baseDir ?? ""].join(" ").toLowerCase();
126
+ }
127
+ filterSkills(query) {
128
+ const trimmed = query.trim().toLowerCase();
129
+ if (!trimmed)
130
+ return this.registry.allSkills;
131
+ const tokens = trimmed.split(/\s+/).filter(Boolean);
132
+ return this.registry.allSkills.filter((skill) => tokens.every((token) => this.searchableText(skill).includes(token)));
133
+ }
134
+ orderBrowseSkills(skills) {
135
+ const own = skills.filter((skill) => isDeletableSkill(skill));
136
+ const library = skills.filter((skill) => !isDeletableSkill(skill));
137
+ return [...own, ...library];
138
+ }
139
+ refreshBrowseList(preferredPath) {
140
+ const currentPath = preferredPath ?? this.getSelectedSkill()?.path;
141
+ this.filteredSkills = this.orderBrowseSkills(this.filterSkills(this.browseQuery));
142
+ if (currentPath) {
143
+ const nextIndex = this.filteredSkills.findIndex((skill) => skill.path === currentPath);
144
+ if (nextIndex >= 0) {
145
+ this.selectedIndex = nextIndex + 1;
146
+ return;
147
+ }
148
+ }
149
+ this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredSkills.length));
150
+ }
151
+ getSelectedSkill() {
152
+ return this.selectedIndex === 0 ? undefined : this.filteredSkills[this.selectedIndex - 1];
153
+ }
154
+ getCurrentSkill() {
155
+ return this.previewSkillPath ? this.registry.allSkills.find((skill) => skill.path === this.previewSkillPath) : undefined;
156
+ }
157
+ get currentCreateStep() {
158
+ return CREATE_STEPS[this.createStepIndex];
159
+ }
160
+ enterCreateMode() {
161
+ this.mode = "create";
162
+ this.createStepIndex = 0;
163
+ this.createError = undefined;
164
+ this.syncCreateInput();
165
+ this.syncFocus();
166
+ this.requestRender();
167
+ }
168
+ exitToBrowse(preferredPath) {
169
+ this.mode = "browse";
170
+ this.createError = undefined;
171
+ this.renameError = undefined;
172
+ this.previewSkillPath = undefined;
173
+ this.preview = undefined;
174
+ this.editorView = undefined;
175
+ this.deleteSkillPath = undefined;
176
+ this.browseInput.setValue(this.browseQuery);
177
+ this.refreshBrowseList(preferredPath);
178
+ this.syncFocus();
179
+ this.requestRender();
180
+ }
181
+ openPreview(skill) {
182
+ this.previewSkillPath = skill.path;
183
+ this.preview = new ScrollableSkillPreview(skill, this.theme, () => this.tui.terminal.rows);
184
+ this.mode = "preview";
185
+ this.syncFocus();
186
+ this.requestRender();
187
+ }
188
+ openDeleteConfirm(skill, returnMode) {
189
+ this.deleteSkillPath = skill.path;
190
+ this.deleteReturnMode = returnMode;
191
+ this.mode = "delete-confirm";
192
+ this.syncFocus();
193
+ this.requestRender();
194
+ }
195
+ openEditor() {
196
+ const skill = this.getCurrentSkill();
197
+ if (!skill || !isDeletableSkill(skill))
198
+ return;
199
+ this.editorView = new SkillEditorView(skill, this.theme, this.tui, buildEditableSkillDocument(skill, readSkillDocument(skill)), (value) => {
200
+ this.saveEditedSkill(value);
201
+ }, () => this.closeEditor());
202
+ this.mode = "edit";
203
+ this.syncFocus();
204
+ this.requestRender();
205
+ }
206
+ closeEditor() {
207
+ this.editorView = undefined;
208
+ this.mode = "preview";
209
+ this.syncFocus();
210
+ this.requestRender();
211
+ }
212
+ openRenameDialog() {
213
+ const skill = this.getCurrentSkill();
214
+ if (!skill || !isDeletableSkill(skill))
215
+ return;
216
+ this.renameError = undefined;
217
+ this.renameInput.setValue(skill.name);
218
+ this.mode = "rename";
219
+ this.syncFocus();
220
+ this.requestRender();
221
+ }
222
+ closeRenameDialog() {
223
+ this.renameError = undefined;
224
+ this.mode = "preview";
225
+ this.syncFocus();
226
+ this.requestRender();
227
+ }
228
+ syncCreateInput() {
229
+ const step = this.currentCreateStep;
230
+ if (step.id === "name")
231
+ this.browseInput.setValue(this.createValues.name);
232
+ if (step.id === "description") {
233
+ this.submittedDescriptionValue = undefined;
234
+ this.descriptionEditor.setText(this.createValues.description);
235
+ }
236
+ }
237
+ persistCreateInput() {
238
+ const step = this.currentCreateStep;
239
+ if (step.id === "name")
240
+ this.createValues.name = this.browseInput.getValue();
241
+ else if (step.id === "description") {
242
+ this.createValues.description = this.submittedDescriptionValue !== undefined ? this.submittedDescriptionValue : this.descriptionEditor.getText();
243
+ this.submittedDescriptionValue = undefined;
244
+ }
245
+ }
246
+ validateCreateStep() {
247
+ this.persistCreateInput();
248
+ const step = this.currentCreateStep;
249
+ if (step.kind === "text" && !step.optional) {
250
+ const value = this.createValues[step.id].trim();
251
+ if (!value) {
252
+ this.createError = `${step.title} is required.`;
253
+ return false;
254
+ }
255
+ if (step.id === "name" && !normalizeSkillName(value)) {
256
+ this.createError = "Name must contain letters, numbers, or hyphens.";
257
+ return false;
258
+ }
259
+ }
260
+ this.createError = undefined;
261
+ return true;
262
+ }
263
+ goToPreviousCreateStep() {
264
+ this.persistCreateInput();
265
+ if (this.createStepIndex > 0) {
266
+ this.createError = undefined;
267
+ this.createStepIndex -= 1;
268
+ this.syncCreateInput();
269
+ this.syncFocus();
270
+ }
271
+ }
272
+ async advanceCreate() {
273
+ if (!this.validateCreateStep())
274
+ return;
275
+ if (this.createStepIndex >= CREATE_STEPS.length - 1)
276
+ await this.submitCreate();
277
+ else {
278
+ this.createStepIndex += 1;
279
+ this.syncCreateInput();
280
+ this.syncFocus();
281
+ }
282
+ this.requestRender();
283
+ }
284
+ async submitCreate() {
285
+ const name = normalizeSkillName(this.createValues.name);
286
+ if (!name) {
287
+ this.createStepIndex = 0;
288
+ this.syncCreateInput();
289
+ this.createError = "Name is required.";
290
+ return;
291
+ }
292
+ if (!this.createValues.description.trim()) {
293
+ this.createStepIndex = 1;
294
+ this.syncCreateInput();
295
+ this.createError = "Description is required.";
296
+ return;
297
+ }
298
+ this.mode = "generating";
299
+ const runId = ++this.generationRunId;
300
+ const abortController = new AbortController;
301
+ this.generationAbortController = abortController;
302
+ this.syncFocus();
303
+ this.requestRender();
304
+ const created = await this.options.onCreate({ name, description: this.createValues.description.trim(), allowedTools: [], location: this.createLocation }, abortController.signal);
305
+ if (this.generationRunId !== runId)
306
+ return;
307
+ this.generationAbortController = undefined;
308
+ if (abortController.signal.aborted || !created) {
309
+ this.mode = "create";
310
+ this.syncFocus();
311
+ this.requestRender();
312
+ return;
313
+ }
314
+ await this.refreshRegistry(created.path);
315
+ this.openPreview(this.registry.allSkills.find((skill) => skill.path === created.path) ?? created);
316
+ }
317
+ async refreshRegistry(preferredPath) {
318
+ this.registry = await this.options.onRefresh();
319
+ this.refreshBrowseList(preferredPath);
320
+ if (this.previewSkillPath) {
321
+ const current = this.registry.allSkills.find((skill) => skill.path === this.previewSkillPath);
322
+ if (!current) {
323
+ this.exitToBrowse(preferredPath);
324
+ return;
325
+ }
326
+ this.preview?.setSkill(current);
327
+ this.editorView?.setSkill(current);
328
+ }
329
+ }
330
+ async toggleSkill(skill) {
331
+ const nextEnabled = !skill.enabled;
332
+ try {
333
+ await this.options.onToggle(skill, nextEnabled);
334
+ await this.refreshRegistry(skill.path);
335
+ this.ctx.ui.notify(`${nextEnabled ? "Enabled" : "Disabled"} ${skill.name}. Run /reload to fully apply the change.`, "info");
336
+ } catch (error) {
337
+ this.ctx.ui.notify(error instanceof Error ? error.message : "Failed to update skill visibility", "error");
338
+ }
339
+ this.requestRender();
340
+ }
341
+ async confirmDelete() {
342
+ const skill = this.deleteSkillPath ? this.registry.allSkills.find((entry) => entry.path === this.deleteSkillPath) : undefined;
343
+ if (!skill) {
344
+ this.exitToBrowse();
345
+ return;
346
+ }
347
+ const deleted = await this.options.onDelete(skill);
348
+ if (!deleted) {
349
+ this.mode = this.deleteReturnMode === "preview" ? "preview" : "browse";
350
+ this.syncFocus();
351
+ this.requestRender();
352
+ return;
353
+ }
354
+ this.deleteSkillPath = undefined;
355
+ this.previewSkillPath = undefined;
356
+ this.preview = undefined;
357
+ await this.refreshRegistry();
358
+ this.exitToBrowse();
359
+ }
360
+ async submitRename(value) {
361
+ const skill = this.getCurrentSkill();
362
+ if (!skill) {
363
+ this.exitToBrowse();
364
+ return;
365
+ }
366
+ try {
367
+ const renamed = await renameSkillEntry(this.ctx, skill, value);
368
+ if (!renamed) {
369
+ this.closeRenameDialog();
370
+ return;
371
+ }
372
+ this.previewSkillPath = renamed.path;
373
+ await this.refreshRegistry(renamed.path);
374
+ this.closeRenameDialog();
375
+ } catch (error) {
376
+ this.renameError = error instanceof Error ? error.message : "Failed to rename skill";
377
+ this.requestRender();
378
+ }
379
+ }
380
+ async saveEditedSkill(raw) {
381
+ const skill = this.getCurrentSkill();
382
+ if (!skill) {
383
+ this.exitToBrowse();
384
+ return;
385
+ }
386
+ try {
387
+ const parsed = parseEditableSkillDocument(raw, skill.name);
388
+ writeFileSync(skill.path, parsed.raw, "utf8");
389
+ await this.refreshRegistry(skill.path);
390
+ this.preview?.setSkill(this.registry.allSkills.find((entry) => entry.path === skill.path) ?? toUpdatedSkill(skill, parsed));
391
+ this.ctx.ui.notify(`Updated skill: ${skill.name}`, "info");
392
+ this.closeEditor();
393
+ } catch (error) {
394
+ this.editorView?.setMessage(error instanceof Error ? error.message : "Failed to save skill", "error");
395
+ this.requestRender();
396
+ }
397
+ }
398
+ render(width) {
399
+ if (this.mode === "preview")
400
+ return this.preview?.render(width) ?? [];
401
+ if (this.mode === "edit")
402
+ return this.editorView?.render(width) ?? [];
403
+ if (this.mode === "rename")
404
+ return this.renderRenameDialog(width);
405
+ if (this.mode === "delete-confirm")
406
+ return this.renderDeleteDialog(width);
407
+ if (this.mode === "generating")
408
+ return this.renderGeneratingDialog(width);
409
+ return this.mode === "create" ? this.renderCreate(width) : this.renderBrowse(width);
410
+ }
411
+ renderBrowse(width) {
412
+ const innerWidth = Math.max(1, width - 4);
413
+ const root = new Container;
414
+ const enabledCount = this.registry.allSkills.filter((skill) => skill.enabled).length;
415
+ const totalCount = this.registry.allSkills.length;
416
+ root.addChild(new SearchInputLine(this.browseInput, this.theme));
417
+ root.addChild(new Spacer(1));
418
+ const list = new Container;
419
+ const entries = [{ kind: "create" }];
420
+ const own = this.filteredSkills.filter((skill) => isDeletableSkill(skill));
421
+ const library = this.filteredSkills.filter((skill) => !isDeletableSkill(skill));
422
+ if (own.length > 0)
423
+ entries.push({ kind: "header", label: "Your Skills" }, ...own.map((skill) => ({ kind: "skill", skill })));
424
+ if (library.length > 0)
425
+ entries.push({ kind: "header", label: "Library Skills" }, ...library.map((skill) => ({ kind: "skill", skill })));
426
+ let selectedDisplayIndex = 0;
427
+ let selectableIndex = 0;
428
+ for (let i = 0;i < entries.length; i++) {
429
+ const entry = entries[i];
430
+ if (entry.kind === "create" || entry.kind === "skill") {
431
+ if (selectableIndex === this.selectedIndex) {
432
+ selectedDisplayIndex = i;
433
+ break;
434
+ }
435
+ selectableIndex += 1;
436
+ }
437
+ }
438
+ const { startIndex, endIndex } = responsiveBrowseWindow(this.configuredListRows, this.tui.terminal.rows, entries.length, selectedDisplayIndex, this.popupMaxHeight);
439
+ selectableIndex = 0;
440
+ const ellipsis = this.theme.fg("dim", "...");
441
+ for (let i = 0;i < endIndex; i++) {
442
+ const entry = entries[i];
443
+ const isSelectable = entry.kind === "create" || entry.kind === "skill";
444
+ const isSelected = isSelectable && selectableIndex === this.selectedIndex;
445
+ if (i >= startIndex) {
446
+ if (entry.kind === "header") {
447
+ list.addChild(new Spacer(1));
448
+ list.addChild(new SingleLineText(skillSectionTitle(this.theme, entry.label), ellipsis));
449
+ } else if (entry.kind === "create") {
450
+ const prefix = " ";
451
+ const label = "Create new skill";
452
+ list.addChild(new ListLineText(`${prefix}${label}${this.theme.fg("dim", " — generate and save a new skill")}`, isSelected, this.theme, ellipsis));
453
+ } else {
454
+ const skill = entry.skill;
455
+ const prefix = " ";
456
+ const name = skill.enabled ? skill.name : this.theme.fg("muted", skill.name);
457
+ const status = skill.enabled ? "" : this.theme.fg("warning", " [disabled]");
458
+ const scope = this.theme.fg("muted", ` [${scopeLabel(skill)}]`);
459
+ const source = packageLabel(skill) ? this.theme.fg("muted", ` [${packageLabel(skill)}]`) : "";
460
+ const description = this.theme.fg("dim", ` — ${skill.description}`);
461
+ list.addChild(new ListLineText(`${prefix}${name}${status}${scope}${source}${description}`, isSelected, this.theme, ellipsis));
462
+ }
463
+ }
464
+ if (isSelectable)
465
+ selectableIndex += 1;
466
+ }
467
+ if (entries.length === 1 && this.filteredSkills.length === 0)
468
+ list.addChild(new Text(this.theme.fg("dim", "No skills match your search."), 1, 0));
469
+ root.addChild(list);
470
+ root.addChild(new Spacer(1));
471
+ const selected = this.getSelectedSkill();
472
+ const actions = [["-/=", "page"]];
473
+ if (selected) {
474
+ actions.push(["tab", "preview"], ["ctrl+x", "enable/disable"]);
475
+ if (!this.browseQuery && isDeletableSkill(selected))
476
+ actions.push(["backspace", "delete"]);
477
+ }
478
+ root.addChild(new Text(skillKeyHints(this.theme, actions), 1, 0));
479
+ return renderFrame(this.theme, width, root.render(innerWidth), undefined, "Skills Manager", `${enabledCount}/${totalCount} enabled`);
480
+ }
481
+ renderCreate(width) {
482
+ const innerWidth = Math.max(1, width - 4);
483
+ const step = this.currentCreateStep;
484
+ const root = new Container;
485
+ root.addChild(new Text(skillEntityTitle(this.theme, `${step.title} (${this.createStepIndex + 1}/${CREATE_STEPS.length})`), 1, 0));
486
+ root.addChild(new Spacer(1));
487
+ if (step.id === "name") {
488
+ root.addChild(this.browseInput);
489
+ root.addChild(new Spacer(1));
490
+ root.addChild(new Text(this.theme.fg("dim", step.hint), 1, 0));
491
+ } else if (step.id === "description") {
492
+ root.addChild(new PrefixedEditor(this.descriptionEditor));
493
+ root.addChild(new Spacer(1));
494
+ root.addChild(new Text(this.theme.fg("dim", step.hint), 1, 0));
495
+ } else if ("options" in step) {
496
+ for (const option of step.options) {
497
+ const selected = option.value === this.createLocation;
498
+ root.addChild(new ListLineText(` ${option.label}${this.theme.fg(selected ? "text" : "dim", ` — ${option.description}`)}`, selected, this.theme));
499
+ }
500
+ root.addChild(new Spacer(1));
501
+ root.addChild(new Text(this.theme.fg("dim", step.hint), 1, 0));
502
+ }
503
+ if (this.createError) {
504
+ root.addChild(new Spacer(1));
505
+ root.addChild(new Text(this.theme.fg("error", this.createError), 1, 0));
506
+ }
507
+ root.addChild(new Spacer(1));
508
+ const footer = step.id === "description" ? skillKeyHints(this.theme, [["alt+←", "back"], ["alt+→", "next"]]) : step.id === "location" ? skillKeyHints(this.theme, [["alt+←", "back"]]) : skillKeyHints(this.theme, [["alt+←", "back"], ["alt+→", "next"]]);
509
+ root.addChild(new Text(footer, 1, 0));
510
+ return renderFrame(this.theme, width, root.render(innerWidth));
511
+ }
512
+ renderRenameDialog(width) {
513
+ const lines = [skillEntityTitle(this.theme, "Rename skill"), "", this.theme.fg("dim", "enter new skill name (lowercase letters, numbers, hyphens)"), "", ...this.renameInput.render(Math.max(1, Math.min(width - 4, 64)))];
514
+ if (this.renameError)
515
+ lines.push("", toneText(this.theme, "error", this.renameError));
516
+ return renderCenteredDialog(this.theme, width, lines);
517
+ }
518
+ renderDeleteDialog(width) {
519
+ const skill = this.deleteSkillPath ? this.registry.allSkills.find((entry) => entry.path === this.deleteSkillPath) : undefined;
520
+ const innerWidth = Math.max(1, Math.min(width - 4, 64));
521
+ const message = skill ? `Delete ${skill.name}? This removes ${skillStorageTarget(skill)} and cannot be undone.` : "Delete this skill?";
522
+ return renderCenteredDialog(this.theme, width, [skillEntityTitle(this.theme, "Delete skill"), "", ...wrapTextWithAnsi(message, innerWidth), ""]);
523
+ }
524
+ renderGeneratingDialog(width) {
525
+ const modelLabel = this.ctx.model?.id ?? "fallback template";
526
+ return renderCenteredDialog(this.theme, width, [skillEntityTitle(this.theme, "Generating skill"), "", this.theme.fg("dim", `Using ${modelLabel} to draft SKILL.md.`), this.theme.fg("dim", "The preview opens when generation finishes.")]);
527
+ }
528
+ handleInput(data) {
529
+ if (this.mode === "generating") {
530
+ if (matchesKey(data, Key.escape)) {
531
+ this.generationAbortController?.abort();
532
+ this.generationAbortController = undefined;
533
+ this.generationRunId += 1;
534
+ this.mode = "create";
535
+ this.syncFocus();
536
+ this.requestRender();
537
+ }
538
+ return;
539
+ }
540
+ if (this.mode === "rename") {
541
+ if (matchesKey(data, Key.escape)) {
542
+ this.closeRenameDialog();
543
+ return;
544
+ }
545
+ if (this.renameError)
546
+ this.renameError = undefined;
547
+ this.renameInput.handleInput(data);
548
+ return;
549
+ }
550
+ if (this.mode === "delete-confirm") {
551
+ if (matchesKey(data, Key.escape)) {
552
+ this.mode = this.deleteReturnMode === "preview" ? "preview" : "browse";
553
+ this.syncFocus();
554
+ return;
555
+ }
556
+ if (matchesKey(data, Key.enter))
557
+ this.confirmDelete();
558
+ return;
559
+ }
560
+ if (this.mode === "edit") {
561
+ this.editorView?.handleInput(data);
562
+ return;
563
+ }
564
+ if (this.mode === "preview") {
565
+ const skill = this.getCurrentSkill();
566
+ if (!skill) {
567
+ this.exitToBrowse();
568
+ return;
569
+ }
570
+ if (matchesKey(data, Key.escape) || matchesKey(data, Key.tab)) {
571
+ this.exitToBrowse(skill.path);
572
+ return;
573
+ }
574
+ if (matchesKey(data, Key.enter)) {
575
+ if (!skill.enabled)
576
+ this.ctx.ui.notify("Enable this skill first with ctrl+x", "info");
577
+ else
578
+ this.done(skill);
579
+ return;
580
+ }
581
+ if (matchesKey(data, Key.alt("x")) || matchesKey(data, Key.ctrl("x"))) {
582
+ this.toggleSkill(skill);
583
+ return;
584
+ }
585
+ if (isDeletableSkill(skill) && (matchesKey(data, Key.alt("e")) || matchesKey(data, Key.ctrl("e")))) {
586
+ this.openEditor();
587
+ return;
588
+ }
589
+ if (isDeletableSkill(skill) && (matchesKey(data, Key.alt("r")) || matchesKey(data, Key.ctrl("r")))) {
590
+ this.openRenameDialog();
591
+ return;
592
+ }
593
+ if (isDeletableSkill(skill) && (matchesKey(data, Key.backspace) || matchesKey(data, "delete"))) {
594
+ this.openDeleteConfirm(skill, "preview");
595
+ return;
596
+ }
597
+ this.preview?.handleInput(data);
598
+ return;
599
+ }
600
+ if (this.mode === "create") {
601
+ this.handleCreateInput(data);
602
+ return;
603
+ }
604
+ this.handleBrowseInput(data);
605
+ }
606
+ handleBrowseInput(data) {
607
+ if (matchesKey(data, Key.up)) {
608
+ this.selectedIndex = this.selectedIndex === 0 ? this.filteredSkills.length : this.selectedIndex - 1;
609
+ return;
610
+ }
611
+ if (matchesKey(data, Key.down)) {
612
+ this.selectedIndex = this.selectedIndex === this.filteredSkills.length ? 0 : this.selectedIndex + 1;
613
+ return;
614
+ }
615
+ if (matchesKey(data, "-") || matchesKey(data, Key.pageUp)) {
616
+ this.selectedIndex = responsiveBrowsePageSelection(this.configuredListRows, this.tui.terminal.rows, this.selectedIndex, this.filteredSkills.length, -1, this.popupMaxHeight);
617
+ return;
618
+ }
619
+ if (matchesKey(data, "=") || matchesKey(data, Key.pageDown)) {
620
+ this.selectedIndex = responsiveBrowsePageSelection(this.configuredListRows, this.tui.terminal.rows, this.selectedIndex, this.filteredSkills.length, 1, this.popupMaxHeight);
621
+ return;
622
+ }
623
+ if (matchesKey(data, Key.enter)) {
624
+ if (this.selectedIndex === 0) {
625
+ this.enterCreateMode();
626
+ return;
627
+ }
628
+ const skill = this.getSelectedSkill();
629
+ if (!skill)
630
+ return;
631
+ if (!skill.enabled)
632
+ this.ctx.ui.notify("Enable this skill first with ctrl+x", "info");
633
+ else
634
+ this.done(skill);
635
+ return;
636
+ }
637
+ if (matchesKey(data, Key.tab)) {
638
+ const skill = this.getSelectedSkill();
639
+ if (skill)
640
+ this.openPreview(skill);
641
+ return;
642
+ }
643
+ if (matchesKey(data, Key.alt("x")) || matchesKey(data, Key.ctrl("x"))) {
644
+ const skill = this.getSelectedSkill();
645
+ if (skill)
646
+ this.toggleSkill(skill);
647
+ return;
648
+ }
649
+ if (matchesKey(data, Key.backspace) && !this.browseInput.getValue()) {
650
+ const skill = this.getSelectedSkill();
651
+ if (skill && isDeletableSkill(skill))
652
+ this.openDeleteConfirm(skill, "browse");
653
+ return;
654
+ }
655
+ if (matchesKey(data, Key.escape)) {
656
+ if (this.browseInput.getValue()) {
657
+ this.browseQuery = "";
658
+ this.browseInput.setValue("");
659
+ this.refreshBrowseList();
660
+ } else
661
+ this.done(null);
662
+ return;
663
+ }
664
+ this.browseInput.handleInput(data);
665
+ this.browseQuery = this.browseInput.getValue();
666
+ this.refreshBrowseList();
667
+ }
668
+ handleCreateInput(data) {
669
+ if (matchesKey(data, Key.escape)) {
670
+ this.exitToBrowse();
671
+ return;
672
+ }
673
+ if (matchesKey(data, Key.alt("left"))) {
674
+ this.goToPreviousCreateStep();
675
+ return;
676
+ }
677
+ if (matchesKey(data, Key.alt("right"))) {
678
+ this.advanceCreate();
679
+ return;
680
+ }
681
+ if (matchesKey(data, Key.enter) && this.currentCreateStep.id !== "description") {
682
+ this.advanceCreate();
683
+ return;
684
+ }
685
+ this.createError = undefined;
686
+ const step = this.currentCreateStep;
687
+ if (step.id === "name") {
688
+ this.browseInput.handleInput(data);
689
+ this.createValues.name = this.browseInput.getValue();
690
+ return;
691
+ }
692
+ if (step.id === "location") {
693
+ if (matchesKey(data, Key.up))
694
+ this.createLocation = this.createLocation === "project" ? "global" : "project";
695
+ else if (matchesKey(data, Key.down))
696
+ this.createLocation = this.createLocation === "project" ? "global" : "project";
697
+ return;
698
+ }
699
+ this.descriptionEditor.handleInput(data);
700
+ if (!matchesKey(data, Key.enter))
701
+ this.createValues.description = this.descriptionEditor.getText();
702
+ }
703
+ }
704
+ export async function showSkillsManager(ctx, registry, options) {
705
+ const releaseModalLock = acquirekendexModalLock();
706
+ try {
707
+ return await ctx.ui.custom((tui, theme, _kb, done) => {
708
+ const dialog = new SkillsManagerDialog(ctx, registry, theme, tui, done, options, () => tui.requestRender());
709
+ return {
710
+ get focused() {
711
+ return dialog.focused;
712
+ },
713
+ set focused(value) {
714
+ dialog.focused = value;
715
+ },
716
+ render(width) {
717
+ return dialog.render(width);
718
+ },
719
+ invalidate() {
720
+ dialog.invalidate();
721
+ },
722
+ handleInput(data) {
723
+ dialog.handleInput(data);
724
+ tui.requestRender();
725
+ }
726
+ };
727
+ }, {
728
+ overlay: true,
729
+ overlayOptions: {
730
+ anchor: "center",
731
+ width: settingOverlaySize("popupWidth", DEFAULT_POPUP_WIDTH, ctx.cwd),
732
+ maxHeight: sanitizePopupMaxHeight(settingOverlaySize("popupMaxHeight", DEFAULT_POPUP_MAX_HEIGHT, ctx.cwd))
733
+ }
734
+ });
735
+ } finally {
736
+ releaseModalLock();
737
+ }
738
+ }