@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.
- package/extensions/.dm-extensions.json +64 -4
- package/extensions/dm-context/package.json +1 -1
- package/extensions/dm-context/skills/context-management/SKILL.md +173 -223
- package/extensions/dm-context/skills/context-management/references/development-and-troubleshooting.md +75 -0
- package/extensions/dm-context/skills/context-management/references/interleaved-async-work.md +143 -0
- package/extensions/dm-context/skills/context-management/references/planning-and-execution.md +81 -0
- package/extensions/dm-context/skills/context-management/references/repeated-items-and-batch-work.md +69 -0
- package/extensions/dm-context/skills/context-management/references/retry-branch-and-pivot.md +80 -0
- package/extensions/dm-context/skills/context-management/references/search-research-and-reading.md +103 -0
- package/extensions/dm-context/skills/context-management/references/task-switching-and-cleanup.md +73 -0
- package/extensions/dm-context/src/context.js +3 -2
- package/extensions/dm-context/src/index.js +168 -84
- package/extensions/dm-skills-manager/THIRD_PARTY_NOTICES.md +27 -0
- package/extensions/dm-skills-manager/extensions/skills-manager/components.js +265 -0
- package/extensions/dm-skills-manager/extensions/skills-manager/constants.js +31 -0
- package/extensions/dm-skills-manager/extensions/skills-manager/creation-fallback.js +20 -0
- package/extensions/dm-skills-manager/extensions/skills-manager/creation.js +145 -0
- package/extensions/dm-skills-manager/extensions/skills-manager/dialog.js +738 -0
- package/extensions/dm-skills-manager/extensions/skills-manager/dm-ai-compat.js +15 -0
- package/extensions/dm-skills-manager/extensions/skills-manager/format.js +125 -0
- package/extensions/dm-skills-manager/extensions/skills-manager/glyphs.js +142 -0
- package/extensions/dm-skills-manager/extensions/skills-manager/layout.js +93 -0
- package/extensions/dm-skills-manager/extensions/skills-manager/paths.js +76 -0
- package/extensions/dm-skills-manager/extensions/skills-manager/registry.js +95 -0
- package/extensions/dm-skills-manager/extensions/skills-manager/settings.js +93 -0
- package/extensions/dm-skills-manager/extensions/skills-manager/startup.js +32 -0
- package/extensions/dm-skills-manager/extensions/skills-manager/toggle.js +54 -0
- package/extensions/dm-skills-manager/extensions/skills-manager/types.js +14 -0
- package/extensions/dm-skills-manager/extensions/skills-manager/ui.js +121 -0
- package/extensions/dm-skills-manager/extensions/skills-manager.js +111 -0
- package/extensions/dm-skills-manager/package.json +121 -0
- package/package.json +1 -1
|
@@ -1,9 +1,36 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
Type
|
|
3
|
+
} from "@duckmind/dm-ai";
|
|
2
4
|
import { formatTokens } from "./utils.js";
|
|
3
|
-
const InternalTools = ["
|
|
5
|
+
const InternalTools = ["context_checkpoint", "context_timeline", "context_compact"];
|
|
6
|
+
const DmContextCustomMessageType = "dm-context";
|
|
7
|
+
const AcmEnableFollowUp = "Agentic context management is now enabled";
|
|
4
8
|
let CommandCtx = null;
|
|
5
|
-
let
|
|
9
|
+
let CompactParams = null;
|
|
6
10
|
const isInternal = (name) => InternalTools.includes(name);
|
|
11
|
+
const formatContextUsage = (usage, includeTokens = false) => {
|
|
12
|
+
if (usage?.percent == null)
|
|
13
|
+
return "Unknown";
|
|
14
|
+
const percent = `${usage.percent.toFixed(1)}%`;
|
|
15
|
+
if (!includeTokens || usage.tokens == null)
|
|
16
|
+
return percent;
|
|
17
|
+
return `${percent} (${formatTokens(usage.tokens)}/${formatTokens(usage.contextWindow)})`;
|
|
18
|
+
};
|
|
19
|
+
const PassiveCompactionEntryTypes = new Set([
|
|
20
|
+
"custom",
|
|
21
|
+
"label",
|
|
22
|
+
"session_info",
|
|
23
|
+
"model_change",
|
|
24
|
+
"thinking_level_change"
|
|
25
|
+
]);
|
|
26
|
+
export const didConversationAdvance = (branch, compactTurnLeaf) => {
|
|
27
|
+
if (!compactTurnLeaf)
|
|
28
|
+
return true;
|
|
29
|
+
const compactTurnIndex = branch.findIndex((entry) => entry.id === compactTurnLeaf);
|
|
30
|
+
if (compactTurnIndex === -1)
|
|
31
|
+
return true;
|
|
32
|
+
return branch.slice(compactTurnIndex + 1).some((entry) => !PassiveCompactionEntryTypes.has(entry.type));
|
|
33
|
+
};
|
|
7
34
|
const resolveTargetId = (sm, target) => {
|
|
8
35
|
if (target.toLowerCase() === "root") {
|
|
9
36
|
const tree = sm.getTree();
|
|
@@ -21,18 +48,21 @@ const resolveTargetId = (sm, target) => {
|
|
|
21
48
|
}
|
|
22
49
|
return target;
|
|
23
50
|
};
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
51
|
+
const ContextTimelineDescription = "Inspect the active conversation path as a structural map: checkpoints, summaries/compactions, branch points, user turns, and current position. Use when orientation or compact target selection depends on the shape of history.";
|
|
52
|
+
const ContextTimelineParams = Type.Object({
|
|
53
|
+
limit: Type.Optional(Type.Number({ description: "Maximum visible timeline entries (default: 50)." })),
|
|
54
|
+
verbose: Type.Optional(Type.Boolean({ description: "If true, show all messages including internal context-tool traffic. If false (default), collapse to structural milestones." }))
|
|
27
55
|
});
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
56
|
+
const ContextCompactDescription = "Create a summarized continuation branch from an earlier checkpoint or history node. The selected target is the branch point; the summary must restore the useful state from the compacted path after that target. This changes conversation history only; it does not modify or roll back disk files or external systems.";
|
|
57
|
+
const ContextCompactParams = Type.Object({
|
|
58
|
+
target: Type.String({ description: "Checkpoint name, history node ID, or root to use as the branch point for the summarized continuation." }),
|
|
59
|
+
summary: Type.String({ description: "Handoff summary injected into the new continuation branch. Restore current task/state, decisions/constraints, important external side effects (changed files, processes, browser/tickets/remote state), validation status, source anchors/evidence/open questions likely needed soon, and explicit next step. Do not rely on backupCheckpoint for details needed in the next phase." }),
|
|
60
|
+
backupCheckpoint: Type.Optional(Type.String({ description: "Optional checkpoint name to label the current conversation state before branching. This is only a recovery pointer; the summary must still contain the state needed to continue." }))
|
|
32
61
|
});
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
62
|
+
const ContextCheckpointDescription = "Create a named anchor by labeling a conversation history node. This does not branch, summarize, or affect external state; it only makes the point easy to find later in timeline or compact target selection.";
|
|
63
|
+
const ContextCheckpointParams = Type.Object({
|
|
64
|
+
name: Type.String({ description: "Unique semantic anchor name that encodes the task and phase/purpose, e.g. parser-fix-start or timeout-investigation-search. Avoid generic names like start, checkpoint-1, or retry." }),
|
|
65
|
+
target: Type.Optional(Type.String({ description: "Optional history node ID or checkpoint name to label. Defaults to the current meaningful position near the conversation head." }))
|
|
36
66
|
});
|
|
37
67
|
export default function src_default(pi) {
|
|
38
68
|
pi.registerCommand("acm", {
|
|
@@ -40,41 +70,38 @@ export default function src_default(pi) {
|
|
|
40
70
|
handler: async (args, ctx) => {
|
|
41
71
|
CommandCtx = ctx;
|
|
42
72
|
ctx.ui.notify("Agentic Context Management enabled.", "info");
|
|
43
|
-
pi.sendMessage({
|
|
44
|
-
customType: "dm-context",
|
|
45
|
-
content: "use context-management skill",
|
|
46
|
-
display: false
|
|
47
|
-
}, {
|
|
48
|
-
deliverAs: "followUp"
|
|
49
|
-
});
|
|
50
73
|
if (args) {
|
|
51
|
-
pi.sendUserMessage(args);
|
|
74
|
+
pi.sendUserMessage(args, { deliverAs: "followUp" });
|
|
52
75
|
}
|
|
53
76
|
}
|
|
54
77
|
});
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
78
|
+
const findCheckpointInTree = (sm, nodes, checkpointName) => {
|
|
79
|
+
const stack = [...nodes].reverse();
|
|
80
|
+
while (stack.length > 0) {
|
|
81
|
+
const n = stack.pop();
|
|
82
|
+
if (sm.getLabel(n.entry.id) === checkpointName)
|
|
58
83
|
return n.entry.id;
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
84
|
+
if (n.children?.length) {
|
|
85
|
+
for (let i = n.children.length - 1;i >= 0; i--) {
|
|
86
|
+
stack.push(n.children[i]);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
62
89
|
}
|
|
63
90
|
return null;
|
|
64
91
|
};
|
|
65
92
|
pi.registerTool({
|
|
66
|
-
name: "
|
|
67
|
-
label: "Context
|
|
68
|
-
description:
|
|
69
|
-
parameters:
|
|
93
|
+
name: "context_checkpoint",
|
|
94
|
+
label: "Context Checkpoint",
|
|
95
|
+
description: ContextCheckpointDescription,
|
|
96
|
+
parameters: ContextCheckpointParams,
|
|
70
97
|
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
71
98
|
const sm = ctx.sessionManager;
|
|
72
|
-
const
|
|
73
|
-
if (
|
|
99
|
+
const existingCheckpointId = findCheckpointInTree(sm, sm.getTree(), params.name);
|
|
100
|
+
if (existingCheckpointId) {
|
|
74
101
|
return {
|
|
75
102
|
content: [{
|
|
76
103
|
type: "text",
|
|
77
|
-
text: `Error:
|
|
104
|
+
text: `Error: Checkpoint '${params.name}' already exists at ${existingCheckpointId}. Checkpoint names must be unique. Use a different name or remove the existing one first.`
|
|
78
105
|
}],
|
|
79
106
|
details: {}
|
|
80
107
|
};
|
|
@@ -106,14 +133,20 @@ export default function src_default(pi) {
|
|
|
106
133
|
id = sm.getLeafId() ?? "";
|
|
107
134
|
}
|
|
108
135
|
pi.setLabel(id, params.name);
|
|
109
|
-
return {
|
|
136
|
+
return {
|
|
137
|
+
content: [{
|
|
138
|
+
type: "text",
|
|
139
|
+
text: `Created checkpoint '${params.name}' at ${id}.`
|
|
140
|
+
}],
|
|
141
|
+
details: {}
|
|
142
|
+
};
|
|
110
143
|
}
|
|
111
144
|
});
|
|
112
145
|
pi.registerTool({
|
|
113
|
-
name: "
|
|
114
|
-
label: "Context
|
|
115
|
-
description:
|
|
116
|
-
parameters:
|
|
146
|
+
name: "context_timeline",
|
|
147
|
+
label: "Context Timeline",
|
|
148
|
+
description: ContextTimelineDescription,
|
|
149
|
+
parameters: ContextTimelineParams,
|
|
117
150
|
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
118
151
|
const sm = ctx.sessionManager;
|
|
119
152
|
const branch = sm.getBranch();
|
|
@@ -137,7 +170,7 @@ export default function src_default(pi) {
|
|
|
137
170
|
return e.summary || "[No summary provided]";
|
|
138
171
|
}
|
|
139
172
|
if (entry.type === "label") {
|
|
140
|
-
return `
|
|
173
|
+
return `checkpoint: ${entry.label}`;
|
|
141
174
|
}
|
|
142
175
|
if (entry.type === "message") {
|
|
143
176
|
const msg = entry.message;
|
|
@@ -234,7 +267,7 @@ export default function src_default(pi) {
|
|
|
234
267
|
}
|
|
235
268
|
const id = entry.id;
|
|
236
269
|
const isRoot = branch.length > 0 && entry.id === branch[0].id;
|
|
237
|
-
const meta = [isRoot ? "ROOT" : null, isHead ? "HEAD" : null, label ? `
|
|
270
|
+
const meta = [isRoot ? "ROOT" : null, isHead ? "HEAD" : null, label ? `checkpoint: ${label}` : null].filter(Boolean).join(", ");
|
|
238
271
|
const body = content.length > 100 ? content.slice(0, 100) + "..." : content;
|
|
239
272
|
const marker = isHead ? "*" : role === "USER" ? "•" : "|";
|
|
240
273
|
lines.push(`${marker} ${id}${meta ? ` (${meta})` : ""} [${role}] ${body}`);
|
|
@@ -242,26 +275,24 @@ export default function src_default(pi) {
|
|
|
242
275
|
if (hiddenCount > 0) {
|
|
243
276
|
lines.push(` : ... (${hiddenCount} hidden messages) ...`);
|
|
244
277
|
}
|
|
245
|
-
const
|
|
246
|
-
let
|
|
247
|
-
|
|
248
|
-
usageStr = `${usage.percent.toFixed(1)}% (${formatTokens(usage.tokens)}/${formatTokens(usage.contextWindow)})`;
|
|
249
|
-
}
|
|
250
|
-
let stepsSinceTag = 0;
|
|
251
|
-
let nearestTagName = "None";
|
|
278
|
+
const usageStr = formatContextUsage(ctx.getContextUsage(), true);
|
|
279
|
+
let stepsSinceCheckpoint = 0;
|
|
280
|
+
let nearestCheckpointName = "None";
|
|
252
281
|
for (let i = branch.length - 1;i >= 0; i--) {
|
|
253
282
|
const id = branch[i].id;
|
|
254
283
|
const label = sm.getLabel(id);
|
|
255
284
|
if (label) {
|
|
256
|
-
|
|
285
|
+
nearestCheckpointName = label;
|
|
257
286
|
break;
|
|
258
287
|
}
|
|
259
|
-
|
|
288
|
+
stepsSinceCheckpoint++;
|
|
260
289
|
}
|
|
290
|
+
const compactCue = nearestCheckpointName === "None" ? "create a checkpoint before the next noisy phase" : `if this segment has produced a stable result and another phase remains, compact to '${nearestCheckpointName}' with a handoff summary before continuing`;
|
|
261
291
|
const hud = [
|
|
262
292
|
`[Context Dashboard]`,
|
|
263
293
|
`• Context Usage: ${usageStr}`,
|
|
264
|
-
`• Segment Size: ${
|
|
294
|
+
`• Segment Size: ${stepsSinceCheckpoint} steps since last checkpoint '${nearestCheckpointName}'`,
|
|
295
|
+
`• Compact Cue: ${compactCue}`,
|
|
265
296
|
`---------------------------------------------------`
|
|
266
297
|
].join(`
|
|
267
298
|
`);
|
|
@@ -271,13 +302,16 @@ export default function src_default(pi) {
|
|
|
271
302
|
}
|
|
272
303
|
});
|
|
273
304
|
pi.registerTool({
|
|
274
|
-
name: "
|
|
275
|
-
label: "Context
|
|
276
|
-
description:
|
|
277
|
-
parameters:
|
|
305
|
+
name: "context_compact",
|
|
306
|
+
label: "Context Compact",
|
|
307
|
+
description: ContextCompactDescription,
|
|
308
|
+
parameters: ContextCompactParams,
|
|
278
309
|
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
279
310
|
if (!CommandCtx) {
|
|
280
|
-
|
|
311
|
+
const editorText = ctx.ui.getEditorText();
|
|
312
|
+
const followUp = editorText ? `${AcmEnableFollowUp}
|
|
313
|
+
${editorText}` : AcmEnableFollowUp;
|
|
314
|
+
ctx.ui.setEditorText(`/acm ${followUp}`);
|
|
281
315
|
return {
|
|
282
316
|
content: [{
|
|
283
317
|
type: "text",
|
|
@@ -287,52 +321,102 @@ export default function src_default(pi) {
|
|
|
287
321
|
};
|
|
288
322
|
}
|
|
289
323
|
const sm = ctx.sessionManager;
|
|
324
|
+
const usageBeforeText = formatContextUsage(ctx.getContextUsage());
|
|
290
325
|
const tid = resolveTargetId(sm, params.target);
|
|
291
326
|
const currentLeaf = sm.getLeafId();
|
|
292
327
|
if (currentLeaf === tid) {
|
|
293
328
|
return { content: [{ type: "text", text: `Already at target ${tid}` }], details: {} };
|
|
294
329
|
}
|
|
295
|
-
if (params.
|
|
296
|
-
pi.setLabel(currentLeaf, params.
|
|
330
|
+
if (params.backupCheckpoint && currentLeaf) {
|
|
331
|
+
pi.setLabel(currentLeaf, params.backupCheckpoint);
|
|
297
332
|
}
|
|
298
333
|
const currentLabel = currentLeaf ? sm.getLabel(currentLeaf) : undefined;
|
|
299
|
-
const origin = currentLabel ? `
|
|
300
|
-
const enrichedMessage = `(summary from ${origin})
|
|
301
|
-
${params.
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
return { content: [{ type: "text", text: "checkout start" }], details: {} };
|
|
334
|
+
const origin = currentLabel ? `checkpoint: ${currentLabel}` : currentLeaf || "unknown";
|
|
335
|
+
const enrichedMessage = `(handoff summary from ${origin})
|
|
336
|
+
${params.summary}`;
|
|
337
|
+
CompactParams = params;
|
|
338
|
+
CompactParams.tid = tid;
|
|
339
|
+
CompactParams.enrichedMessage = enrichedMessage;
|
|
340
|
+
CompactParams.usageBeforeText = usageBeforeText;
|
|
341
|
+
return { content: [{ type: "text", text: "compact start" }], details: {} };
|
|
308
342
|
}
|
|
309
343
|
});
|
|
310
|
-
pi.on("turn_end", async (
|
|
311
|
-
if (!
|
|
344
|
+
pi.on("turn_end", async (_event, ctx) => {
|
|
345
|
+
if (!CompactParams) {
|
|
312
346
|
return;
|
|
313
347
|
}
|
|
314
348
|
ctx.abort();
|
|
315
349
|
});
|
|
316
350
|
pi.on("agent_end", async (_event, ctx) => {
|
|
317
|
-
if (!
|
|
351
|
+
if (!CompactParams) {
|
|
318
352
|
return;
|
|
319
353
|
}
|
|
320
354
|
if (!CommandCtx) {
|
|
321
355
|
return;
|
|
322
356
|
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
357
|
+
const sm = ctx.sessionManager;
|
|
358
|
+
const compactParams = CompactParams;
|
|
359
|
+
const commandCtx = CommandCtx;
|
|
360
|
+
CompactParams = null;
|
|
361
|
+
const compactTurnLeaf = sm.getLeafId();
|
|
362
|
+
setTimeout(async () => {
|
|
363
|
+
try {
|
|
364
|
+
await commandCtx.waitForIdle();
|
|
365
|
+
const branch = sm.getBranch();
|
|
366
|
+
if (didConversationAdvance(branch, compactTurnLeaf)) {
|
|
367
|
+
commandCtx.ui.notify("context_compact cancelled: conversation advanced before compaction completed.", "warning");
|
|
368
|
+
pi.sendMessage({
|
|
369
|
+
customType: DmContextCustomMessageType,
|
|
370
|
+
content: [
|
|
371
|
+
"context_compact cancelled: conversation advanced before the summary branch was created.",
|
|
372
|
+
"No compaction was applied; continue from the current path. If still useful, inspect timeline and retry with an updated summary."
|
|
373
|
+
].join(`
|
|
374
|
+
`),
|
|
375
|
+
display: false
|
|
376
|
+
}, {
|
|
377
|
+
triggerTurn: true,
|
|
378
|
+
deliverAs: "followUp"
|
|
379
|
+
});
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
const nid = sm.branchWithSummary(compactParams.tid, compactParams.enrichedMessage);
|
|
383
|
+
compactParams.nid = nid;
|
|
384
|
+
sm.branch(compactParams.tid);
|
|
385
|
+
await commandCtx.navigateTree(compactParams.nid, {
|
|
386
|
+
summarize: false
|
|
387
|
+
});
|
|
388
|
+
const usageAfter = commandCtx.getContextUsage();
|
|
389
|
+
commandCtx.ui.notify([
|
|
390
|
+
`Compacted to ${compactParams.target}${compactParams.target === compactParams.tid ? "" : `(${compactParams.tid})`}`,
|
|
391
|
+
`Context Usage: ${compactParams.usageBeforeText} -> ${formatContextUsage(usageAfter)}`,
|
|
392
|
+
`Backup checkpoint created: ${compactParams.backupCheckpoint || "none"}`,
|
|
393
|
+
`Summary: ${compactParams.enrichedMessage}`
|
|
394
|
+
].join(`
|
|
395
|
+
`), "info");
|
|
396
|
+
pi.sendMessage({
|
|
397
|
+
customType: DmContextCustomMessageType,
|
|
398
|
+
content: "context_compact complete. A handoff summary of your previous conversation path was injected above. Read it to understand your new state. Execute the Next Step from the summary",
|
|
399
|
+
display: false
|
|
400
|
+
}, {
|
|
401
|
+
triggerTurn: true,
|
|
402
|
+
deliverAs: "followUp"
|
|
403
|
+
});
|
|
404
|
+
} catch (err) {
|
|
405
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
406
|
+
commandCtx.ui.notify(`context_compact failed: ${message}`, "error");
|
|
407
|
+
pi.sendMessage({
|
|
408
|
+
customType: DmContextCustomMessageType,
|
|
409
|
+
content: [
|
|
410
|
+
`context_compact failed: ${message}`,
|
|
411
|
+
"No compaction was applied; continue from the current path. Retry only with a fresh timeline/summary."
|
|
412
|
+
].join(`
|
|
413
|
+
`),
|
|
414
|
+
display: false
|
|
415
|
+
}, {
|
|
416
|
+
triggerTurn: true,
|
|
417
|
+
deliverAs: "followUp"
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
}, 0);
|
|
337
421
|
});
|
|
338
422
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Third-party notices
|
|
2
|
+
|
|
3
|
+
This package is based on ideas and portions of [`@kmiyh/pi-skills-menu`](https://github.com/Kmiyh/pi-skills-menu), which is licensed under the MIT License.
|
|
4
|
+
|
|
5
|
+
```text
|
|
6
|
+
MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2025 Kmiyh
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
```
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Container,
|
|
3
|
+
Editor,
|
|
4
|
+
Key,
|
|
5
|
+
Markdown,
|
|
6
|
+
matchesKey,
|
|
7
|
+
Spacer,
|
|
8
|
+
Text,
|
|
9
|
+
truncateToWidth,
|
|
10
|
+
visibleWidth
|
|
11
|
+
} from "@duckmind/dm-tui";
|
|
12
|
+
import { getMarkdownTheme } from "@duckmind/dm-coding-agent";
|
|
13
|
+
import { buildFrontmatterBlock } from "./format.js";
|
|
14
|
+
import { glyphs } from "./glyphs.js";
|
|
15
|
+
import { isDeletableSkill } from "./registry.js";
|
|
16
|
+
import {
|
|
17
|
+
getEditorTheme,
|
|
18
|
+
inlineLine,
|
|
19
|
+
packageLabel,
|
|
20
|
+
padAnsi,
|
|
21
|
+
scopeLabel,
|
|
22
|
+
skillEntityTitle,
|
|
23
|
+
skillKeyHints,
|
|
24
|
+
skillLocation,
|
|
25
|
+
skillSelectedLine,
|
|
26
|
+
toneText,
|
|
27
|
+
renderFrame
|
|
28
|
+
} from "./ui.js";
|
|
29
|
+
|
|
30
|
+
export class SingleLineText {
|
|
31
|
+
text;
|
|
32
|
+
ellipsis;
|
|
33
|
+
constructor(text, ellipsis = "...") {
|
|
34
|
+
this.text = text;
|
|
35
|
+
this.ellipsis = ellipsis;
|
|
36
|
+
}
|
|
37
|
+
render(width) {
|
|
38
|
+
return [truncateToWidth(inlineLine(this.text), width, this.ellipsis)];
|
|
39
|
+
}
|
|
40
|
+
invalidate() {}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export class ListLineText {
|
|
44
|
+
text;
|
|
45
|
+
selected;
|
|
46
|
+
theme;
|
|
47
|
+
ellipsis;
|
|
48
|
+
constructor(text, selected, theme, ellipsis = "...") {
|
|
49
|
+
this.text = text;
|
|
50
|
+
this.selected = selected;
|
|
51
|
+
this.theme = theme;
|
|
52
|
+
this.ellipsis = ellipsis;
|
|
53
|
+
}
|
|
54
|
+
render(width) {
|
|
55
|
+
const line = truncateToWidth(inlineLine(this.text), width, this.ellipsis);
|
|
56
|
+
return [this.selected ? skillSelectedLine(this.theme, line, width) : line];
|
|
57
|
+
}
|
|
58
|
+
invalidate() {}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export class PrefixedEditor {
|
|
62
|
+
editor;
|
|
63
|
+
prefix;
|
|
64
|
+
constructor(editor, prefix = "> ") {
|
|
65
|
+
this.editor = editor;
|
|
66
|
+
this.prefix = prefix;
|
|
67
|
+
}
|
|
68
|
+
render(width) {
|
|
69
|
+
const rendered = this.editor.render(Math.max(1, width - this.prefix.length));
|
|
70
|
+
const lines = rendered.length >= 2 ? rendered.slice(1, -1) : rendered;
|
|
71
|
+
return lines.length === 0 ? [this.prefix] : lines.map((line, index) => `${index === 0 ? this.prefix : " "}${line}`);
|
|
72
|
+
}
|
|
73
|
+
invalidate() {
|
|
74
|
+
this.editor.invalidate();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export class SearchInputLine {
|
|
79
|
+
input;
|
|
80
|
+
theme;
|
|
81
|
+
prefix;
|
|
82
|
+
constructor(input, theme, prefix = " ") {
|
|
83
|
+
this.input = input;
|
|
84
|
+
this.theme = theme;
|
|
85
|
+
this.prefix = prefix;
|
|
86
|
+
}
|
|
87
|
+
render(width) {
|
|
88
|
+
const inputWidth = Math.max(1, width - visibleWidth(this.prefix));
|
|
89
|
+
const line = truncateToWidth(`${this.prefix}${this.input.render(inputWidth)[0] ?? ""}`, width, "");
|
|
90
|
+
return [this.theme.bg("toolPendingBg", padAnsi(line, width))];
|
|
91
|
+
}
|
|
92
|
+
invalidate() {
|
|
93
|
+
this.input.invalidate();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export class ScrollableSkillPreview {
|
|
98
|
+
scrollOffset = 0;
|
|
99
|
+
lastInnerWidth = 1;
|
|
100
|
+
lastContentLines = [];
|
|
101
|
+
skill;
|
|
102
|
+
theme;
|
|
103
|
+
getTerminalRows;
|
|
104
|
+
constructor(skill, theme, getTerminalRows) {
|
|
105
|
+
this.skill = skill;
|
|
106
|
+
this.theme = theme;
|
|
107
|
+
this.getTerminalRows = getTerminalRows;
|
|
108
|
+
}
|
|
109
|
+
setSkill(skill) {
|
|
110
|
+
this.skill = skill;
|
|
111
|
+
this.scrollOffset = 0;
|
|
112
|
+
this.lastContentLines = [];
|
|
113
|
+
}
|
|
114
|
+
invalidate() {
|
|
115
|
+
this.lastContentLines = [];
|
|
116
|
+
}
|
|
117
|
+
maxHeight() {
|
|
118
|
+
return Math.max(10, Math.floor(this.getTerminalRows() * 0.78));
|
|
119
|
+
}
|
|
120
|
+
buildContentLines(innerWidth) {
|
|
121
|
+
const content = new Container;
|
|
122
|
+
const status = this.skill.enabled ? this.theme.fg("success", "enabled") : this.theme.fg("warning", "disabled");
|
|
123
|
+
const source = packageLabel(this.skill) ? `${packageLabel(this.skill)}` : this.skill.path;
|
|
124
|
+
content.addChild(new Text(skillEntityTitle(this.theme, this.skill.name), 0, 0));
|
|
125
|
+
content.addChild(new Text(`${this.theme.fg("muted", scopeLabel(this.skill))}${this.theme.fg("dim", ` ${glyphs().bullet.trim()} `)}${this.theme.fg("muted", source)}${this.theme.fg("dim", ` ${glyphs().bullet.trim()} `)}${status}`, 0, 0));
|
|
126
|
+
content.addChild(new Spacer(1));
|
|
127
|
+
content.addChild(new Text(this.theme.fg("muted", this.theme.bold("Description")), 0, 0));
|
|
128
|
+
content.addChild(new Text(this.skill.description, 0, 0));
|
|
129
|
+
content.addChild(new Spacer(1));
|
|
130
|
+
content.addChild(new Text(this.theme.fg("muted", this.theme.bold("Metadata")), 0, 0));
|
|
131
|
+
content.addChild(new Text(this.theme.fg("dim", buildFrontmatterBlock(this.skill)), 0, 0));
|
|
132
|
+
content.addChild(new Spacer(1));
|
|
133
|
+
content.addChild(new Text(this.theme.fg("muted", this.theme.bold("Content")), 0, 0));
|
|
134
|
+
content.addChild(new Spacer(1));
|
|
135
|
+
content.addChild(new Markdown(this.skill.content || this.theme.fg("dim", "(empty skill body)"), 0, 0, getMarkdownTheme()));
|
|
136
|
+
const lines = content.render(innerWidth);
|
|
137
|
+
this.lastInnerWidth = innerWidth;
|
|
138
|
+
this.lastContentLines = lines;
|
|
139
|
+
return lines;
|
|
140
|
+
}
|
|
141
|
+
footer(innerWidth, visibleHeight, totalLines) {
|
|
142
|
+
const maxScroll = Math.max(0, totalLines - visibleHeight);
|
|
143
|
+
const scroll = maxScroll > 0 ? this.theme.fg("dim", ` ${glyphs().bullet.trim()} ${this.scrollOffset + 1}-${Math.min(totalLines, this.scrollOffset + visibleHeight)}/${totalLines}`) : "";
|
|
144
|
+
const hints = [["-/=", "page"]];
|
|
145
|
+
hints.push(["ctrl+x", "enable/disable"]);
|
|
146
|
+
if (isDeletableSkill(this.skill))
|
|
147
|
+
hints.push(["alt+e", "edit"], ["alt+r", "rename"], ["backspace", "delete"]);
|
|
148
|
+
return truncateToWidth(`${skillKeyHints(this.theme, hints)}${scroll}`, innerWidth, this.theme.fg("dim", "..."));
|
|
149
|
+
}
|
|
150
|
+
render(width) {
|
|
151
|
+
if (width < 8)
|
|
152
|
+
return [];
|
|
153
|
+
const innerWidth = Math.max(1, width - 4);
|
|
154
|
+
const visibleHeight = Math.max(1, this.maxHeight() - 3);
|
|
155
|
+
const contentLines = this.buildContentLines(innerWidth);
|
|
156
|
+
const maxScroll = Math.max(0, contentLines.length - visibleHeight);
|
|
157
|
+
this.scrollOffset = Math.max(0, Math.min(this.scrollOffset, maxScroll));
|
|
158
|
+
const visible = contentLines.slice(this.scrollOffset, this.scrollOffset + visibleHeight);
|
|
159
|
+
return renderFrame(this.theme, width, [...visible, this.footer(innerWidth, visibleHeight, contentLines.length)]);
|
|
160
|
+
}
|
|
161
|
+
handleInput(data) {
|
|
162
|
+
const visibleHeight = Math.max(1, this.maxHeight() - 3);
|
|
163
|
+
const total = this.lastContentLines.length || this.buildContentLines(this.lastInnerWidth).length;
|
|
164
|
+
const maxScroll = Math.max(0, total - visibleHeight);
|
|
165
|
+
if (matchesKey(data, Key.up))
|
|
166
|
+
this.scrollOffset = Math.max(0, this.scrollOffset - 1);
|
|
167
|
+
else if (matchesKey(data, Key.down))
|
|
168
|
+
this.scrollOffset = Math.min(maxScroll, this.scrollOffset + 1);
|
|
169
|
+
else if (matchesKey(data, "-") || matchesKey(data, Key.pageUp))
|
|
170
|
+
this.scrollOffset = Math.max(0, this.scrollOffset - visibleHeight);
|
|
171
|
+
else if (matchesKey(data, "=") || matchesKey(data, Key.pageDown))
|
|
172
|
+
this.scrollOffset = Math.min(maxScroll, this.scrollOffset + visibleHeight);
|
|
173
|
+
else if (matchesKey(data, Key.home))
|
|
174
|
+
this.scrollOffset = 0;
|
|
175
|
+
else if (matchesKey(data, Key.end))
|
|
176
|
+
this.scrollOffset = maxScroll;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export class SkillEditorView {
|
|
181
|
+
editor;
|
|
182
|
+
initialText;
|
|
183
|
+
proxyTui;
|
|
184
|
+
skill;
|
|
185
|
+
theme;
|
|
186
|
+
realTui;
|
|
187
|
+
onSave;
|
|
188
|
+
onCancel;
|
|
189
|
+
virtualRows = 24;
|
|
190
|
+
_focused = false;
|
|
191
|
+
message;
|
|
192
|
+
get focused() {
|
|
193
|
+
return this._focused;
|
|
194
|
+
}
|
|
195
|
+
set focused(value) {
|
|
196
|
+
this._focused = value;
|
|
197
|
+
this.editor.focused = value;
|
|
198
|
+
}
|
|
199
|
+
constructor(skill, theme, realTui, initialText, onSave, onCancel) {
|
|
200
|
+
this.skill = skill;
|
|
201
|
+
this.theme = theme;
|
|
202
|
+
this.realTui = realTui;
|
|
203
|
+
this.onSave = onSave;
|
|
204
|
+
this.onCancel = onCancel;
|
|
205
|
+
this.initialText = initialText;
|
|
206
|
+
const self = this;
|
|
207
|
+
this.proxyTui = { requestRender: () => realTui.requestRender(), get terminal() {
|
|
208
|
+
return { ...realTui.terminal, rows: Math.max(1, self.virtualRows) };
|
|
209
|
+
} };
|
|
210
|
+
this.editor = new Editor(this.proxyTui, getEditorTheme(theme), { autocompleteMaxVisible: 6 });
|
|
211
|
+
this.editor.setText(initialText);
|
|
212
|
+
}
|
|
213
|
+
setSkill(skill) {
|
|
214
|
+
this.skill = skill;
|
|
215
|
+
}
|
|
216
|
+
setMessage(text, tone) {
|
|
217
|
+
this.message = { text, tone };
|
|
218
|
+
}
|
|
219
|
+
isDirty() {
|
|
220
|
+
return this.editor.getText() !== this.initialText;
|
|
221
|
+
}
|
|
222
|
+
invalidate() {
|
|
223
|
+
this.editor.invalidate();
|
|
224
|
+
}
|
|
225
|
+
targetHeight() {
|
|
226
|
+
return Math.max(10, Math.floor(this.realTui.terminal.rows * 0.78));
|
|
227
|
+
}
|
|
228
|
+
rowsForVisibleEditorLines(targetVisibleLines) {
|
|
229
|
+
let rows = 5;
|
|
230
|
+
while (Math.max(5, Math.floor(rows * 0.3)) < targetVisibleLines && rows < 1000)
|
|
231
|
+
rows += 1;
|
|
232
|
+
return rows;
|
|
233
|
+
}
|
|
234
|
+
render(width) {
|
|
235
|
+
const innerWidth = Math.max(1, width - 4);
|
|
236
|
+
const lines = [
|
|
237
|
+
skillEntityTitle(this.theme, `Edit ${this.skill.name}`),
|
|
238
|
+
this.theme.fg("muted", skillLocation(this.skill)),
|
|
239
|
+
this.theme.fg("dim", `Name is immutable here: ${this.skill.name}`)
|
|
240
|
+
];
|
|
241
|
+
if (this.message)
|
|
242
|
+
lines.push("", toneText(this.theme, this.message.tone, this.message.text));
|
|
243
|
+
const targetInnerLines = Math.max(1, this.targetHeight() - 2);
|
|
244
|
+
const staticLineCount = lines.length + 3;
|
|
245
|
+
const editorBlockLines = Math.max(7, targetInnerLines - staticLineCount);
|
|
246
|
+
this.virtualRows = this.rowsForVisibleEditorLines(Math.max(5, editorBlockLines - 2));
|
|
247
|
+
lines.push("", ...this.editor.render(innerWidth), "", truncateToWidth(skillKeyHints(this.theme, [["alt+s", "save"]]), innerWidth, this.theme.fg("dim", "...")));
|
|
248
|
+
while (lines.length < targetInnerLines)
|
|
249
|
+
lines.splice(Math.max(0, lines.length - 1), 0, "");
|
|
250
|
+
return renderFrame(this.theme, width, lines.slice(0, targetInnerLines));
|
|
251
|
+
}
|
|
252
|
+
handleInput(data) {
|
|
253
|
+
if (matchesKey(data, Key.escape)) {
|
|
254
|
+
this.onCancel();
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
if (matchesKey(data, Key.alt("s")) || matchesKey(data, Key.ctrl("s"))) {
|
|
258
|
+
this.onSave(this.editor.getText());
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
if (this.message?.tone === "error")
|
|
262
|
+
this.message = undefined;
|
|
263
|
+
this.editor.handleInput(data);
|
|
264
|
+
}
|
|
265
|
+
}
|