@duckmind/dm-darwin-arm64 0.13.7 → 0.13.8
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/dm +0 -0
- package/extensions/.dm-extensions.json +2 -2
- package/extensions/dm-subagents/agent-management.ts +15 -6
- package/extensions/dm-subagents/agent-manager-detail.ts +12 -2
- package/extensions/dm-subagents/agent-manager-edit.ts +75 -23
- package/extensions/dm-subagents/agent-manager-list.ts +9 -2
- package/extensions/dm-subagents/agent-manager.ts +199 -11
- package/extensions/dm-subagents/agents.ts +315 -20
- package/extensions/dm-ultrathink/README.md +5 -0
- package/extensions/dm-ultrathink/src/naming.ts +75 -3
- package/package.json +1 -1
package/dm
CHANGED
|
Binary file
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"status": "ok",
|
|
3
|
-
"prepared_at": "2026-04-
|
|
3
|
+
"prepared_at": "2026-04-14T05:04:55.747359+00:00",
|
|
4
4
|
"managed_entries": [
|
|
5
5
|
{
|
|
6
6
|
"id": "dm-context",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"id": "dm-subagents",
|
|
23
23
|
"upstream_name": "pi-subagents",
|
|
24
24
|
"source_url": "https://github.com/nicobailon/pi-subagents",
|
|
25
|
-
"upstream_revision": "
|
|
25
|
+
"upstream_revision": "670f9999a4452b01fb6510c897a31818c89e3f40",
|
|
26
26
|
"target_dir": "extensions/dm-subagents",
|
|
27
27
|
"bundle_mode": "source-package",
|
|
28
28
|
"copied_paths": [
|
|
@@ -35,13 +35,18 @@ function parseCsv(value: string): string[] {
|
|
|
35
35
|
return [...new Set(value.split(",").map((v) => v.trim()).filter(Boolean))];
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
function configObject(config: unknown): Record<string, unknown
|
|
38
|
+
function configObject(config: unknown): { value?: Record<string, unknown>; error?: string } {
|
|
39
39
|
let val = config;
|
|
40
40
|
if (typeof val === "string") {
|
|
41
|
-
try {
|
|
41
|
+
try {
|
|
42
|
+
val = JSON.parse(val);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
45
|
+
return { error: `config must be valid JSON: ${message}` };
|
|
46
|
+
}
|
|
42
47
|
}
|
|
43
|
-
if (!val || typeof val !== "object" || Array.isArray(val)) return
|
|
44
|
-
return val as Record<string, unknown
|
|
48
|
+
if (!val || typeof val !== "object" || Array.isArray(val)) return {};
|
|
49
|
+
return { value: val as Record<string, unknown> };
|
|
45
50
|
}
|
|
46
51
|
|
|
47
52
|
function hasKey(obj: Record<string, unknown>, key: string): boolean {
|
|
@@ -383,7 +388,9 @@ export function handleGet(params: ManagementParams, ctx: ManagementContext): Age
|
|
|
383
388
|
}
|
|
384
389
|
|
|
385
390
|
export function handleCreate(params: ManagementParams, ctx: ManagementContext): AgentToolResult<Details> {
|
|
386
|
-
const
|
|
391
|
+
const parsedConfig = configObject(params.config);
|
|
392
|
+
if (parsedConfig.error) return result(parsedConfig.error, true);
|
|
393
|
+
const cfg = parsedConfig.value;
|
|
387
394
|
if (!cfg) return result("config required for create.", true);
|
|
388
395
|
if (typeof cfg.name !== "string" || !cfg.name.trim()) return result("config.name is required and must be a non-empty string.", true);
|
|
389
396
|
if (typeof cfg.description !== "string" || !cfg.description.trim()) return result("config.description is required and must be a non-empty string.", true);
|
|
@@ -427,7 +434,9 @@ export function handleCreate(params: ManagementParams, ctx: ManagementContext):
|
|
|
427
434
|
export function handleUpdate(params: ManagementParams, ctx: ManagementContext): AgentToolResult<Details> {
|
|
428
435
|
if (!params.agent && !params.chainName) return result("Specify 'agent' or 'chainName' for update.", true);
|
|
429
436
|
if (params.agent && params.chainName) return result("Specify either 'agent' or 'chainName', not both.", true);
|
|
430
|
-
const
|
|
437
|
+
const parsedConfig = configObject(params.config);
|
|
438
|
+
if (parsedConfig.error) return result(parsedConfig.error, true);
|
|
439
|
+
const cfg = parsedConfig.value;
|
|
431
440
|
if (!cfg) return result("config required for update.", true);
|
|
432
441
|
const warnings: string[] = [];
|
|
433
442
|
if (params.agent) {
|
|
@@ -61,6 +61,10 @@ function buildDetailLines(
|
|
|
61
61
|
const maxSubagentDepth = agent.maxSubagentDepth !== undefined ? String(agent.maxSubagentDepth) : "(default)";
|
|
62
62
|
|
|
63
63
|
lines.push(renderFieldLine("Model:", agent.model ?? "default", contentWidth, theme));
|
|
64
|
+
if (agent.override) {
|
|
65
|
+
const overrideLabel = `${agent.override.scope} · ${formatPath(agent.override.path)}`;
|
|
66
|
+
lines.push(renderFieldLine("Override:", overrideLabel, contentWidth, theme));
|
|
67
|
+
}
|
|
64
68
|
lines.push(renderFieldLine("Thinking:", agent.thinking ?? "off", contentWidth, theme));
|
|
65
69
|
lines.push(renderFieldLine("Tools:", tools, contentWidth, theme));
|
|
66
70
|
lines.push(renderFieldLine("MCP:", mcp, contentWidth, theme));
|
|
@@ -131,7 +135,11 @@ export function renderDetail(
|
|
|
131
135
|
theme: Theme,
|
|
132
136
|
): string[] {
|
|
133
137
|
const lines: string[] = [];
|
|
134
|
-
const scopeBadge = agent.source === "builtin"
|
|
138
|
+
const scopeBadge = agent.source === "builtin"
|
|
139
|
+
? (agent.override ? `[builtin+${agent.override.scope}]` : "[builtin]")
|
|
140
|
+
: agent.source === "project"
|
|
141
|
+
? "[proj]"
|
|
142
|
+
: "[user]";
|
|
135
143
|
const headerText = ` ${agent.name} ${scopeBadge} ${formatPath(agent.filePath)} `;
|
|
136
144
|
lines.push(renderHeader(headerText, width, theme));
|
|
137
145
|
lines.push(row("", width, theme));
|
|
@@ -152,7 +160,9 @@ export function renderDetail(
|
|
|
152
160
|
lines.push(row(scrollInfo ? ` ${theme.fg("dim", scrollInfo)}` : "", width, theme));
|
|
153
161
|
|
|
154
162
|
const footer = agent.source === "builtin"
|
|
155
|
-
?
|
|
163
|
+
? agent.override
|
|
164
|
+
? " [l]aunch [e]dit override [v] raw/resolved [↑↓] scroll [esc] back "
|
|
165
|
+
: " [l]aunch [e]create override [v] raw/resolved [↑↓] scroll [esc] back "
|
|
156
166
|
: " [l]aunch [e]dit [v] raw/resolved [↑↓] scroll [esc] back ";
|
|
157
167
|
lines.push(renderFooter(footer, width, theme));
|
|
158
168
|
return lines;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Theme } from "@mariozechner/pi-coding-agent";
|
|
2
2
|
import { matchesKey, truncateToWidth } from "@mariozechner/pi-tui";
|
|
3
|
-
import type { AgentConfig } from "./agents.ts";
|
|
3
|
+
import type { AgentConfig, BuiltinAgentOverrideBase } from "./agents.ts";
|
|
4
4
|
import { createEditorState, ensureCursorVisible, getCursorDisplayPos, handleEditorInput, renderEditor, wrapText } from "./text-editor.ts";
|
|
5
5
|
import type { TextEditorState } from "./text-editor.ts";
|
|
6
6
|
import { pad, row, renderHeader, renderFooter, formatScrollInfo } from "./render-helpers.ts";
|
|
@@ -8,30 +8,70 @@ import { pad, row, renderHeader, renderFooter, formatScrollInfo } from "./render
|
|
|
8
8
|
export interface ModelInfo { provider: string; id: string; fullId: string; }
|
|
9
9
|
export interface SkillInfo { name: string; source: string; description?: string; }
|
|
10
10
|
export type EditScreen = "edit" | "edit-field" | "edit-prompt";
|
|
11
|
+
export type EditField = typeof FIELD_ORDER[number];
|
|
12
|
+
|
|
11
13
|
export interface EditState {
|
|
12
14
|
draft: AgentConfig; isNew: boolean; fieldIndex: number; fieldMode: "text" | "model" | "thinking" | "skills" | null;
|
|
13
15
|
fieldEditor: TextEditorState; promptEditor: TextEditorState; modelSearchQuery: string; modelCursor: number; filteredModels: ModelInfo[];
|
|
14
16
|
thinkingCursor: number; skillSearchQuery: string; skillCursor: number; filteredSkills: SkillInfo[]; skillSelected: Set<string>; error?: string;
|
|
17
|
+
fields: EditField[];
|
|
18
|
+
title?: string;
|
|
19
|
+
overrideBase?: BuiltinAgentOverrideBase;
|
|
20
|
+
}
|
|
21
|
+
export interface EditInputResult { action?: "save" | "discard" | "delete"; nextScreen?: EditScreen; }
|
|
22
|
+
export interface CreateEditStateOptions {
|
|
23
|
+
fields?: EditField[];
|
|
24
|
+
title?: string;
|
|
25
|
+
overrideBase?: BuiltinAgentOverrideBase;
|
|
15
26
|
}
|
|
16
|
-
export interface EditInputResult { action?: "save" | "discard"; nextScreen?: EditScreen; }
|
|
17
27
|
|
|
18
28
|
const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"] as const;
|
|
19
29
|
const FIELD_ORDER = ["name", "description", "model", "fallbackModels", "thinking", "tools", "extensions", "skills", "output", "reads", "progress", "interactive", "prompt"] as const;
|
|
20
|
-
type EditField = typeof FIELD_ORDER[number];
|
|
21
30
|
type ThinkingLevel = typeof THINKING_LEVELS[number];
|
|
22
31
|
const PROMPT_VIEWPORT_HEIGHT = 16;
|
|
23
32
|
const MODEL_SELECTOR_HEIGHT = 10;
|
|
24
33
|
const SKILL_SELECTOR_HEIGHT = 10;
|
|
25
34
|
|
|
26
|
-
function formatTools(draft: AgentConfig): string { const tools = [...(draft.tools ?? []), ...(draft.mcpDirectTools ?? []).map((tool) => `mcp:${tool}`)]; return tools.length > 0 ? tools.join(", ") : ""; }
|
|
35
|
+
function formatTools(draft: Pick<AgentConfig, "tools" | "mcpDirectTools">): string { const tools = [...(draft.tools ?? []), ...(draft.mcpDirectTools ?? []).map((tool) => `mcp:${tool}`)]; return tools.length > 0 ? tools.join(", ") : ""; }
|
|
36
|
+
function toolList(draft: Pick<AgentConfig, "tools" | "mcpDirectTools">): string[] | undefined { const tools = [...(draft.tools ?? []), ...(draft.mcpDirectTools ?? []).map((tool) => `mcp:${tool}`)]; return tools.length > 0 ? tools : undefined; }
|
|
27
37
|
function parseTools(value: string): { tools?: string[]; mcp?: string[] } { const items = value.split(",").map((item) => item.trim()).filter((item) => item.length > 0); const tools: string[] = []; const mcp: string[] = []; for (const item of items) { if (item.startsWith("mcp:")) { const name = item.slice(4).trim(); if (name) mcp.push(name); } else { tools.push(item); } } return { tools: tools.length > 0 ? tools : undefined, mcp: mcp.length > 0 ? mcp : undefined }; }
|
|
28
38
|
function parseCommaList(value: string): string[] | undefined { const items = value.split(",").map((item) => item.trim()).filter((item) => item.length > 0); return items.length > 0 ? items : undefined; }
|
|
39
|
+
function arraysEqual(a: string[] | undefined, b: string[] | undefined): boolean { if (!a && !b) return true; if (!a || !b || a.length !== b.length) return false; for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false; return true; }
|
|
40
|
+
|
|
41
|
+
function fieldValueMatchesBase(field: EditField, state: EditState): boolean {
|
|
42
|
+
const base = state.overrideBase;
|
|
43
|
+
if (!base) return false;
|
|
44
|
+
switch (field) {
|
|
45
|
+
case "model": return state.draft.model === base.model;
|
|
46
|
+
case "fallbackModels": return arraysEqual(state.draft.fallbackModels, base.fallbackModels);
|
|
47
|
+
case "thinking": return state.draft.thinking === base.thinking;
|
|
48
|
+
case "tools": return arraysEqual(toolList(state.draft), toolList(base));
|
|
49
|
+
case "skills": return arraysEqual(state.draft.skills, base.skills);
|
|
50
|
+
case "prompt": return state.draft.systemPrompt === base.systemPrompt;
|
|
51
|
+
default: return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
29
54
|
|
|
30
|
-
|
|
55
|
+
function resetFieldToBase(field: EditField, state: EditState): void {
|
|
56
|
+
const base = state.overrideBase;
|
|
57
|
+
if (!base) return;
|
|
58
|
+
switch (field) {
|
|
59
|
+
case "model": state.draft.model = base.model; break;
|
|
60
|
+
case "fallbackModels": state.draft.fallbackModels = base.fallbackModels ? [...base.fallbackModels] : undefined; break;
|
|
61
|
+
case "thinking": state.draft.thinking = base.thinking; break;
|
|
62
|
+
case "tools": state.draft.tools = base.tools ? [...base.tools] : undefined; state.draft.mcpDirectTools = base.mcpDirectTools ? [...base.mcpDirectTools] : undefined; break;
|
|
63
|
+
case "skills": state.draft.skills = base.skills ? [...base.skills] : undefined; break;
|
|
64
|
+
case "prompt": state.draft.systemPrompt = base.systemPrompt; state.promptEditor = createEditorState(base.systemPrompt); break;
|
|
65
|
+
default: break;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function createEditState(draft: AgentConfig, isNew: boolean, models: ModelInfo[], skills: SkillInfo[], options: CreateEditStateOptions = {}): EditState {
|
|
31
70
|
return {
|
|
32
71
|
draft: { ...draft, tools: draft.tools ? [...draft.tools] : undefined, mcpDirectTools: draft.mcpDirectTools ? [...draft.mcpDirectTools] : undefined, skills: draft.skills ? [...draft.skills] : undefined, fallbackModels: draft.fallbackModels ? [...draft.fallbackModels] : undefined, extensions: draft.extensions ? [...draft.extensions] : draft.extensions, defaultReads: draft.defaultReads ? [...draft.defaultReads] : undefined, extraFields: draft.extraFields ? { ...draft.extraFields } : undefined },
|
|
33
72
|
isNew, fieldIndex: 0, fieldMode: null, fieldEditor: createEditorState(), promptEditor: createEditorState(draft.systemPrompt ?? ""),
|
|
34
73
|
modelSearchQuery: "", modelCursor: 0, filteredModels: [...models], thinkingCursor: 0, skillSearchQuery: "", skillCursor: 0, filteredSkills: [...skills], skillSelected: new Set(draft.skills ?? []),
|
|
74
|
+
fields: options.fields ?? [...FIELD_ORDER], title: options.title, overrideBase: options.overrideBase,
|
|
35
75
|
};
|
|
36
76
|
}
|
|
37
77
|
|
|
@@ -71,15 +111,15 @@ function applyFieldValue(field: EditField, state: EditState, value: string): voi
|
|
|
71
111
|
}
|
|
72
112
|
|
|
73
113
|
function openModelPicker(state: EditState, models: ModelInfo[]): void {
|
|
74
|
-
state.fieldIndex =
|
|
114
|
+
state.fieldIndex = state.fields.indexOf("model"); state.fieldMode = "model"; state.modelSearchQuery = ""; state.filteredModels = [...models];
|
|
75
115
|
const idx = state.filteredModels.findIndex((m) => m.fullId === state.draft.model || m.id === state.draft.model); state.modelCursor = idx >= 0 ? idx : 0;
|
|
76
116
|
}
|
|
77
117
|
function openThinkingPicker(state: EditState): void {
|
|
78
|
-
state.fieldIndex =
|
|
118
|
+
state.fieldIndex = state.fields.indexOf("thinking"); state.fieldMode = "thinking";
|
|
79
119
|
const idx = THINKING_LEVELS.indexOf((state.draft.thinking ?? "off") as ThinkingLevel); state.thinkingCursor = idx >= 0 ? idx : 0;
|
|
80
120
|
}
|
|
81
121
|
function openSkillPicker(state: EditState, skills: SkillInfo[]): void {
|
|
82
|
-
state.fieldIndex =
|
|
122
|
+
state.fieldIndex = state.fields.indexOf("skills"); state.fieldMode = "skills"; state.skillSearchQuery = ""; state.filteredSkills = [...skills]; state.skillSelected = new Set(state.draft.skills ?? []); state.skillCursor = 0;
|
|
83
123
|
}
|
|
84
124
|
|
|
85
125
|
function renderModelPicker(state: EditState, width: number, theme: Theme): string[] {
|
|
@@ -183,9 +223,11 @@ export function handleEditInput(screen: EditScreen, state: EditState, data: stri
|
|
|
183
223
|
if (screen === "edit") {
|
|
184
224
|
if (matchesKey(data, "ctrl+s")) return { action: "save" };
|
|
185
225
|
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) return { action: "discard" };
|
|
226
|
+
if (data === "D" && state.overrideBase) return { action: "delete" };
|
|
186
227
|
if (matchesKey(data, "up")) { state.fieldIndex = Math.max(0, state.fieldIndex - 1); return; }
|
|
187
|
-
if (matchesKey(data, "down")) { state.fieldIndex = Math.min(
|
|
188
|
-
const field =
|
|
228
|
+
if (matchesKey(data, "down")) { state.fieldIndex = Math.min(state.fields.length - 1, state.fieldIndex + 1); return; }
|
|
229
|
+
const field = state.fields[state.fieldIndex]!;
|
|
230
|
+
if (data === "r" && state.overrideBase) { resetFieldToBase(field, state); return; }
|
|
189
231
|
if (data === "m") { openModelPicker(state, models); return { nextScreen: "edit-field" }; }
|
|
190
232
|
if (data === "t") { openThinkingPicker(state); return { nextScreen: "edit-field" }; }
|
|
191
233
|
if (data === "s") { openSkillPicker(state, skills); return { nextScreen: "edit-field" }; }
|
|
@@ -234,7 +276,7 @@ export function handleEditInput(screen: EditScreen, state: EditState, data: stri
|
|
|
234
276
|
return;
|
|
235
277
|
}
|
|
236
278
|
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) { state.fieldMode = null; return { nextScreen: "edit" }; }
|
|
237
|
-
if (matchesKey(data, "return")) { const field =
|
|
279
|
+
if (matchesKey(data, "return")) { const field = state.fields[state.fieldIndex]!; applyFieldValue(field, state, state.fieldEditor.buffer); state.fieldMode = null; return { nextScreen: "edit" }; }
|
|
238
280
|
if (matchesKey(data, "tab")) return;
|
|
239
281
|
const innerW = width - 2; const labelWidth = 12; const textWidth = Math.max(10, innerW - labelWidth - 6);
|
|
240
282
|
const nextState = handleEditorInput(state.fieldEditor, data, textWidth); if (nextState) state.fieldEditor = nextState; return;
|
|
@@ -256,13 +298,14 @@ export function renderEdit(screen: EditScreen, state: EditState, width: number,
|
|
|
256
298
|
if (screen === "edit-prompt") return renderPromptEditor(state, width, theme);
|
|
257
299
|
const lines: string[] = [];
|
|
258
300
|
const scopeBadge = state.draft.source === "user" ? "[user]" : "[proj]"; const label = state.isNew ? " [new]" : "";
|
|
259
|
-
lines.push(renderHeader(` Editing: ${state.draft.name} ${scopeBadge}${label} `, width, theme));
|
|
301
|
+
lines.push(renderHeader(` ${state.title ?? `Editing: ${state.draft.name} ${scopeBadge}${label}`} `, width, theme));
|
|
260
302
|
lines.push(row("", width, theme));
|
|
261
303
|
const innerW = width - 2; const labelWidth = 12; const valueWidth = Math.max(10, innerW - labelWidth - 6);
|
|
262
|
-
for (let i = 0; i <
|
|
263
|
-
const field =
|
|
304
|
+
for (let i = 0; i < state.fields.length; i++) {
|
|
305
|
+
const field = state.fields[i]!; if (field === "prompt") break;
|
|
264
306
|
const isFocused = i === state.fieldIndex; const prefix = isFocused ? theme.fg("accent", "▸ ") : " ";
|
|
265
|
-
const
|
|
307
|
+
const rawLabel = pad(`${field[0]!.toUpperCase()}${field.slice(1)}:`, labelWidth);
|
|
308
|
+
const labelText = state.overrideBase && !fieldValueMatchesBase(field, state) ? theme.fg("accent", rawLabel) : rawLabel; let valueText = renderFieldValue(field, state);
|
|
266
309
|
if (field === "progress") { const toggle = state.draft.defaultProgress ? theme.fg("success", "[x]") : "[ ]"; valueText = `${toggle} ${state.draft.defaultProgress ? "on" : "off"}`; lines.push(row(` ${prefix}${labelText} ${pad(truncateToWidth(valueText, valueWidth), valueWidth)}`, width, theme)); continue; }
|
|
267
310
|
if (field === "interactive") { const toggle = state.draft.interactive ? theme.fg("success", "[x]") : "[ ]"; valueText = `${toggle} ${state.draft.interactive ? "on" : "off"}`; lines.push(row(` ${prefix}${labelText} ${pad(truncateToWidth(valueText, valueWidth), valueWidth)}`, width, theme)); continue; }
|
|
268
311
|
let displayValue = truncateToWidth(valueText, valueWidth);
|
|
@@ -275,14 +318,23 @@ export function renderEdit(screen: EditScreen, state: EditState, width: number,
|
|
|
275
318
|
}
|
|
276
319
|
lines.push(row(` ${prefix}${labelText} [${displayValue}]`, width, theme));
|
|
277
320
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
321
|
+
if (state.fields.includes("prompt")) {
|
|
322
|
+
lines.push(row("", width, theme));
|
|
323
|
+
const promptIndex = state.fields.indexOf("prompt");
|
|
324
|
+
const promptFocused = state.fieldIndex === promptIndex;
|
|
325
|
+
const promptPrefix = promptFocused ? theme.fg("accent", "▸ ") : " ";
|
|
326
|
+
const promptTitle = state.overrideBase && !fieldValueMatchesBase("prompt", state)
|
|
327
|
+
? theme.fg("accent", "── System Prompt ──")
|
|
328
|
+
: theme.fg("dim", "── System Prompt ──");
|
|
329
|
+
lines.push(row(` ${promptPrefix}${promptTitle}`, width, theme));
|
|
330
|
+
const previewWidth = innerW - 2; const wrapped = wrapText(state.draft.systemPrompt ?? "", previewWidth); const previewLines = wrapped.lines.slice(0, 4);
|
|
331
|
+
for (const line of previewLines) lines.push(row(` ${line}`, width, theme));
|
|
332
|
+
for (let i = previewLines.length; i < 4; i++) lines.push(row("", width, theme));
|
|
333
|
+
}
|
|
285
334
|
if (state.error) lines.push(row(` ${theme.fg("error", state.error)}`, width, theme)); else lines.push(row("", width, theme));
|
|
286
|
-
|
|
335
|
+
const footer = state.overrideBase
|
|
336
|
+
? " [ctrl+s] save [r] reset field [D] remove override [esc] back "
|
|
337
|
+
: " [ctrl+s] save [esc] back ";
|
|
338
|
+
lines.push(renderFooter(footer, width, theme));
|
|
287
339
|
return lines;
|
|
288
340
|
}
|
|
@@ -9,6 +9,7 @@ export interface ListAgent {
|
|
|
9
9
|
description: string;
|
|
10
10
|
model?: string;
|
|
11
11
|
source: AgentSource;
|
|
12
|
+
overrideScope?: "user" | "project";
|
|
12
13
|
kind: "agent" | "chain";
|
|
13
14
|
stepCount?: number;
|
|
14
15
|
}
|
|
@@ -190,7 +191,7 @@ export function renderList(
|
|
|
190
191
|
const innerW = width - 2;
|
|
191
192
|
const nameWidth = 16;
|
|
192
193
|
const modelWidth = 12;
|
|
193
|
-
const scopeWidth =
|
|
194
|
+
const scopeWidth = 17;
|
|
194
195
|
|
|
195
196
|
for (let i = 0; i < visible.length; i++) {
|
|
196
197
|
const agent = visible[i]!;
|
|
@@ -208,7 +209,13 @@ export function renderList(
|
|
|
208
209
|
const modelDisplay = modelRaw.includes("/") ? modelRaw.split("/").pop() ?? modelRaw : modelRaw;
|
|
209
210
|
const nameText = isCursor ? theme.fg("accent", agent.name) : agent.name;
|
|
210
211
|
const modelText = theme.fg("dim", modelDisplay);
|
|
211
|
-
const scopeLabel = agent.kind === "chain"
|
|
212
|
+
const scopeLabel = agent.kind === "chain"
|
|
213
|
+
? "[chain]"
|
|
214
|
+
: agent.source === "builtin"
|
|
215
|
+
? (agent.overrideScope ? `[builtin+${agent.overrideScope}]` : "[builtin]")
|
|
216
|
+
: agent.source === "project"
|
|
217
|
+
? "[proj]"
|
|
218
|
+
: "[user]";
|
|
212
219
|
const scopeBadge = theme.fg("dim", scopeLabel);
|
|
213
220
|
const descText = theme.fg("dim", agent.description);
|
|
214
221
|
|
|
@@ -3,7 +3,15 @@ import * as path from "node:path";
|
|
|
3
3
|
import type { Theme } from "@mariozechner/pi-coding-agent";
|
|
4
4
|
import type { Component, TUI } from "@mariozechner/pi-tui";
|
|
5
5
|
import { matchesKey, truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
|
|
6
|
-
import
|
|
6
|
+
import {
|
|
7
|
+
buildBuiltinOverrideConfig,
|
|
8
|
+
discoverAgentsAll,
|
|
9
|
+
removeBuiltinAgentOverride,
|
|
10
|
+
saveBuiltinAgentOverride,
|
|
11
|
+
type AgentConfig,
|
|
12
|
+
type BuiltinAgentOverrideBase,
|
|
13
|
+
type ChainConfig,
|
|
14
|
+
} from "./agents.ts";
|
|
7
15
|
import { serializeAgent } from "./agent-serializer.ts";
|
|
8
16
|
import { TEMPLATE_ITEMS, type AgentTemplate, type TemplateItem } from "./agent-templates.ts";
|
|
9
17
|
import { parseChain, serializeChain } from "./chain-serializer.ts";
|
|
@@ -11,7 +19,7 @@ import { renderList, handleListInput, type ListAgent, type ListState, type ListA
|
|
|
11
19
|
import { createParallelState, handleParallelInput, renderParallel, formatParallelTitle, type ParallelState, type AgentOption } from "./agent-manager-parallel.ts";
|
|
12
20
|
import { renderDetail, handleDetailInput, renderTaskInput, type DetailState, type DetailAction } from "./agent-manager-detail.ts";
|
|
13
21
|
import { renderChainDetail, handleChainDetailInput, type ChainDetailAction, type ChainDetailState } from "./agent-manager-chain-detail.ts";
|
|
14
|
-
import { createEditState, handleEditInput, renderEdit, type EditScreen, type EditState, type ModelInfo, type SkillInfo } from "./agent-manager-edit.ts";
|
|
22
|
+
import { createEditState, handleEditInput, renderEdit, type EditField, type EditScreen, type EditState, type ModelInfo, type SkillInfo } from "./agent-manager-edit.ts";
|
|
15
23
|
import { createEditorState, ensureCursorVisible, getCursorDisplayPos, handleEditorInput, renderEditor, wrapText } from "./text-editor.ts";
|
|
16
24
|
import type { TextEditorState } from "./text-editor.ts";
|
|
17
25
|
import { loadRunsForAgent } from "./run-history.ts";
|
|
@@ -24,14 +32,39 @@ export type ManagerResult =
|
|
|
24
32
|
| { action: "launch-chain"; chain: ChainConfig; task: string; skipClarify?: boolean }
|
|
25
33
|
| undefined;
|
|
26
34
|
|
|
27
|
-
export interface AgentData { builtin: AgentConfig[]; user: AgentConfig[]; project: AgentConfig[]; chains: ChainConfig[]; userDir: string; projectDir: string | null; cwd: string; }
|
|
28
|
-
type ManagerScreen = "list" | "detail" | "chain-detail" | "edit" | "edit-field" | "edit-prompt" | "task-input" | "confirm-delete" | "name-input" | "chain-edit" | "template-select" | "parallel-builder";
|
|
35
|
+
export interface AgentData { builtin: AgentConfig[]; user: AgentConfig[]; project: AgentConfig[]; chains: ChainConfig[]; userDir: string; projectDir: string | null; userSettingsPath: string; projectSettingsPath: string | null; cwd: string; }
|
|
36
|
+
type ManagerScreen = "list" | "detail" | "chain-detail" | "edit" | "edit-field" | "edit-prompt" | "task-input" | "confirm-delete" | "name-input" | "chain-edit" | "template-select" | "parallel-builder" | "override-scope";
|
|
29
37
|
interface AgentEntry { id: string; kind: "agent"; config: AgentConfig; isNew: boolean; }
|
|
30
38
|
interface ChainEntry { id: string; kind: "chain"; config: ChainConfig; }
|
|
31
39
|
interface NameInputState { mode: "new-agent" | "clone-agent" | "clone-chain" | "new-chain"; editor: TextEditorState; scope: "user" | "project"; allowProject: boolean; sourceId?: string; template?: AgentTemplate; error?: string; }
|
|
32
40
|
interface StatusMessage { text: string; type: "error" | "info"; }
|
|
41
|
+
interface OverrideScopeState { selectedScope: "user" | "project"; allowProject: boolean; }
|
|
33
42
|
|
|
34
|
-
|
|
43
|
+
const BUILTIN_OVERRIDE_FIELDS: EditField[] = ["model", "fallbackModels", "thinking", "tools", "skills", "prompt"];
|
|
44
|
+
|
|
45
|
+
function cloneConfig(config: AgentConfig): AgentConfig {
|
|
46
|
+
return {
|
|
47
|
+
...config,
|
|
48
|
+
tools: config.tools ? [...config.tools] : undefined,
|
|
49
|
+
mcpDirectTools: config.mcpDirectTools ? [...config.mcpDirectTools] : undefined,
|
|
50
|
+
skills: config.skills ? [...config.skills] : undefined,
|
|
51
|
+
fallbackModels: config.fallbackModels ? [...config.fallbackModels] : undefined,
|
|
52
|
+
defaultReads: config.defaultReads ? [...config.defaultReads] : undefined,
|
|
53
|
+
extraFields: config.extraFields ? { ...config.extraFields } : undefined,
|
|
54
|
+
override: config.override
|
|
55
|
+
? {
|
|
56
|
+
...config.override,
|
|
57
|
+
base: {
|
|
58
|
+
...config.override.base,
|
|
59
|
+
fallbackModels: config.override.base.fallbackModels ? [...config.override.base.fallbackModels] : undefined,
|
|
60
|
+
skills: config.override.base.skills ? [...config.override.base.skills] : undefined,
|
|
61
|
+
tools: config.override.base.tools ? [...config.override.base.tools] : undefined,
|
|
62
|
+
mcpDirectTools: config.override.base.mcpDirectTools ? [...config.override.base.mcpDirectTools] : undefined,
|
|
63
|
+
},
|
|
64
|
+
}
|
|
65
|
+
: undefined,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
35
68
|
function cloneChainConfig(config: ChainConfig): ChainConfig { return { ...config, steps: config.steps.map((step) => ({ ...step, reads: Array.isArray(step.reads) ? [...step.reads] : step.reads, skills: Array.isArray(step.skills) ? [...step.skills] : step.skills })), extraFields: config.extraFields ? { ...config.extraFields } : undefined }; }
|
|
36
69
|
function slugTemplateName(name: string): string { return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, ""); }
|
|
37
70
|
function nextSelectableIndex(items: TemplateItem[], current: number, direction: 1 | -1): number { let next = current + direction; while (next >= 0 && next < items.length && items[next]!.type === "separator") next += direction; if (next < 0 || next >= items.length) return current; return next; }
|
|
@@ -60,6 +93,8 @@ export class AgentManagerComponent implements Component {
|
|
|
60
93
|
private taskBackScreen: ManagerScreen = "list";
|
|
61
94
|
private templateCursor = 0;
|
|
62
95
|
private statusMessage?: StatusMessage;
|
|
96
|
+
private overrideScopeState: OverrideScopeState | null = null;
|
|
97
|
+
private builtinOverrideScope: "user" | "project" | null = null;
|
|
63
98
|
private nextId = 1;
|
|
64
99
|
private tui: TUI;
|
|
65
100
|
private theme: Theme;
|
|
@@ -86,15 +121,57 @@ export class AgentManagerComponent implements Component {
|
|
|
86
121
|
|
|
87
122
|
private getAgentEntry(id: string | null): AgentEntry | undefined { if (!id) return undefined; return this.agents.find((entry) => entry.id === id); }
|
|
88
123
|
private getChainEntry(id: string | null): ChainEntry | undefined { if (!id) return undefined; return this.chains.find((entry) => entry.id === id); }
|
|
89
|
-
private listAgents(): ListAgent[] { const a = this.agents.map((entry) => ({ id: entry.id, name: entry.config.name, description: entry.config.description, model: entry.config.model, source: entry.config.source, kind: "agent" as const })); const c = this.chains.map((entry) => ({ id: entry.id, name: entry.config.name, description: entry.config.description, source: entry.config.source, kind: "chain" as const, stepCount: entry.config.steps.length })); return [...a, ...c]; }
|
|
124
|
+
private listAgents(): ListAgent[] { const a = this.agents.map((entry) => ({ id: entry.id, name: entry.config.name, description: entry.config.description, model: entry.config.model, source: entry.config.source, overrideScope: entry.config.override?.scope, kind: "agent" as const })); const c = this.chains.map((entry) => ({ id: entry.id, name: entry.config.name, description: entry.config.description, source: entry.config.source, kind: "chain" as const, stepCount: entry.config.steps.length })); return [...a, ...c]; }
|
|
90
125
|
private clearStatus(): void { this.statusMessage = undefined; }
|
|
91
126
|
|
|
127
|
+
private resolveBuiltinOverrideBase(entry: AgentEntry): BuiltinAgentOverrideBase {
|
|
128
|
+
if (entry.config.override) return entry.config.override.base;
|
|
129
|
+
return {
|
|
130
|
+
model: entry.config.model,
|
|
131
|
+
fallbackModels: entry.config.fallbackModels ? [...entry.config.fallbackModels] : undefined,
|
|
132
|
+
thinking: entry.config.thinking,
|
|
133
|
+
systemPrompt: entry.config.systemPrompt,
|
|
134
|
+
skills: entry.config.skills ? [...entry.config.skills] : undefined,
|
|
135
|
+
tools: entry.config.tools ? [...entry.config.tools] : undefined,
|
|
136
|
+
mcpDirectTools: entry.config.mcpDirectTools ? [...entry.config.mcpDirectTools] : undefined,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private refreshAgentData(agentName?: string, chainName?: string): void {
|
|
141
|
+
this.agentData = { ...discoverAgentsAll(this.agentData.cwd), cwd: this.agentData.cwd };
|
|
142
|
+
this.nextId = 1;
|
|
143
|
+
this.loadEntries();
|
|
144
|
+
if (agentName) {
|
|
145
|
+
const entry = this.agents.find((candidate) => candidate.config.name === agentName);
|
|
146
|
+
this.currentAgentId = entry?.id ?? null;
|
|
147
|
+
}
|
|
148
|
+
if (chainName) {
|
|
149
|
+
const entry = this.chains.find((candidate) => candidate.config.name === chainName);
|
|
150
|
+
this.currentChainId = entry?.id ?? null;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
92
154
|
private removeAgentEntry(entry: AgentEntry): void { this.agents = this.agents.filter((e) => e.id !== entry.id); this.listState.selected = this.listState.selected.filter((id) => id !== entry.id); }
|
|
93
155
|
private removeChainEntry(entry: ChainEntry): void { this.chains = this.chains.filter((e) => e.id !== entry.id); }
|
|
94
156
|
|
|
95
157
|
private enterDetail(entry: AgentEntry): void { this.currentAgentId = entry.id; this.detailState = { resolved: true, scrollOffset: 0, recentRuns: loadRunsForAgent(entry.config.name).slice(0, 5) }; this.screen = "detail"; }
|
|
96
158
|
private enterChainDetail(entry: ChainEntry): void { this.currentChainId = entry.id; this.chainDetailState = { scrollOffset: 0 }; this.screen = "chain-detail"; }
|
|
97
|
-
private enterEdit(entry: AgentEntry): void { this.currentAgentId = entry.id; this.editState = createEditState(entry.config, entry.isNew, this.models, this.skills); this.screen = "edit"; }
|
|
159
|
+
private enterEdit(entry: AgentEntry): void { this.currentAgentId = entry.id; this.builtinOverrideScope = null; this.editState = createEditState(entry.config, entry.isNew, this.models, this.skills); this.screen = "edit"; }
|
|
160
|
+
private enterBuiltinOverrideScope(entry: AgentEntry): void {
|
|
161
|
+
this.currentAgentId = entry.id;
|
|
162
|
+
this.overrideScopeState = { selectedScope: this.agentData.projectSettingsPath ? "project" : "user", allowProject: Boolean(this.agentData.projectSettingsPath) };
|
|
163
|
+
this.screen = "override-scope";
|
|
164
|
+
}
|
|
165
|
+
private enterBuiltinOverrideEdit(entry: AgentEntry, scope: "user" | "project"): void {
|
|
166
|
+
this.currentAgentId = entry.id;
|
|
167
|
+
this.builtinOverrideScope = scope;
|
|
168
|
+
this.editState = createEditState(entry.config, false, this.models, this.skills, {
|
|
169
|
+
fields: BUILTIN_OVERRIDE_FIELDS,
|
|
170
|
+
title: `Builtin Override: ${entry.config.name} [${scope}]`,
|
|
171
|
+
overrideBase: this.resolveBuiltinOverrideBase(entry),
|
|
172
|
+
});
|
|
173
|
+
this.screen = "edit";
|
|
174
|
+
}
|
|
98
175
|
private enterParallelBuilder(ids: string[]): void {
|
|
99
176
|
const names = ids.map((id) => this.getAgentEntry(id)?.config.name).filter((n): n is string => Boolean(n));
|
|
100
177
|
if (names.length === 0) return;
|
|
@@ -127,6 +204,27 @@ export class AgentManagerComponent implements Component {
|
|
|
127
204
|
|
|
128
205
|
private saveEdit(): boolean {
|
|
129
206
|
const edit = this.editState; if (!edit) return false; const entry = this.getAgentEntry(this.currentAgentId); if (!entry) return false;
|
|
207
|
+
if (entry.config.source === "builtin") {
|
|
208
|
+
const scope = entry.config.override?.scope ?? this.builtinOverrideScope;
|
|
209
|
+
if (!scope) { edit.error = "Choose where to store the override first."; return false; }
|
|
210
|
+
try {
|
|
211
|
+
const override = buildBuiltinOverrideConfig(this.resolveBuiltinOverrideBase(entry), edit.draft);
|
|
212
|
+
if (override) {
|
|
213
|
+
saveBuiltinAgentOverride(this.agentData.cwd, entry.config.name, scope, override);
|
|
214
|
+
} else {
|
|
215
|
+
removeBuiltinAgentOverride(this.agentData.cwd, entry.config.name, scope);
|
|
216
|
+
}
|
|
217
|
+
this.refreshAgentData(entry.config.name);
|
|
218
|
+
this.builtinOverrideScope = null;
|
|
219
|
+
this.editState = null;
|
|
220
|
+
const refreshed = this.getAgentEntry(this.currentAgentId);
|
|
221
|
+
if (refreshed) this.enterDetail(refreshed);
|
|
222
|
+
return true;
|
|
223
|
+
} catch (err) {
|
|
224
|
+
edit.error = err instanceof Error ? err.message : "Failed to save builtin override.";
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
130
228
|
if (!edit.draft.name || !edit.draft.description) { edit.error = "Name and description are required."; return false; }
|
|
131
229
|
let filePath = entry.config.filePath;
|
|
132
230
|
if (entry.isNew) {
|
|
@@ -140,6 +238,24 @@ export class AgentManagerComponent implements Component {
|
|
|
140
238
|
catch (err) { edit.error = err instanceof Error ? err.message : "Failed to save agent."; return false; }
|
|
141
239
|
}
|
|
142
240
|
|
|
241
|
+
private removeBuiltinOverride(): boolean {
|
|
242
|
+
const edit = this.editState; if (!edit) return false; const entry = this.getAgentEntry(this.currentAgentId); if (!entry || entry.config.source !== "builtin") return false;
|
|
243
|
+
const scope = entry.config.override?.scope ?? this.builtinOverrideScope;
|
|
244
|
+
if (!scope) { edit.error = "No builtin override to remove."; return false; }
|
|
245
|
+
try {
|
|
246
|
+
removeBuiltinAgentOverride(this.agentData.cwd, entry.config.name, scope);
|
|
247
|
+
this.refreshAgentData(entry.config.name);
|
|
248
|
+
this.builtinOverrideScope = null;
|
|
249
|
+
this.editState = null;
|
|
250
|
+
const refreshed = this.getAgentEntry(this.currentAgentId);
|
|
251
|
+
if (refreshed) this.enterDetail(refreshed);
|
|
252
|
+
return true;
|
|
253
|
+
} catch (err) {
|
|
254
|
+
edit.error = err instanceof Error ? err.message : "Failed to remove builtin override.";
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
143
259
|
private saveChainEdit(): boolean {
|
|
144
260
|
const state = this.chainEditState; const entry = this.getChainEntry(this.currentChainId); if (!state || !entry) return false;
|
|
145
261
|
try { const parsed = parseChain(state.editor.buffer, entry.config.source, entry.config.filePath); fs.writeFileSync(entry.config.filePath, serializeChain(parsed), "utf-8"); entry.config = parsed; state.error = undefined; return true; }
|
|
@@ -159,6 +275,46 @@ export class AgentManagerComponent implements Component {
|
|
|
159
275
|
}
|
|
160
276
|
}
|
|
161
277
|
|
|
278
|
+
private handleOverrideScopeInput(data: string): void {
|
|
279
|
+
const state = this.overrideScopeState;
|
|
280
|
+
const entry = this.getAgentEntry(this.currentAgentId);
|
|
281
|
+
if (!state || !entry) {
|
|
282
|
+
this.screen = "detail";
|
|
283
|
+
this.tui.requestRender();
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
|
|
288
|
+
this.overrideScopeState = null;
|
|
289
|
+
this.enterDetail(entry);
|
|
290
|
+
this.tui.requestRender();
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (matchesKey(data, "tab") || matchesKey(data, "up") || matchesKey(data, "down")) {
|
|
295
|
+
if (state.allowProject) state.selectedScope = state.selectedScope === "user" ? "project" : "user";
|
|
296
|
+
this.tui.requestRender();
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (data === "u") {
|
|
301
|
+
state.selectedScope = "user";
|
|
302
|
+
this.tui.requestRender();
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (data === "p" && state.allowProject) {
|
|
307
|
+
state.selectedScope = "project";
|
|
308
|
+
this.tui.requestRender();
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (!matchesKey(data, "return")) return;
|
|
313
|
+
this.overrideScopeState = null;
|
|
314
|
+
this.enterBuiltinOverrideEdit(entry, state.selectedScope);
|
|
315
|
+
this.tui.requestRender();
|
|
316
|
+
}
|
|
317
|
+
|
|
162
318
|
private handleNameInput(data: string): void {
|
|
163
319
|
const state = this.nameInputState; if (!state) return; state.error = undefined;
|
|
164
320
|
const canToggleScope = state.allowProject;
|
|
@@ -219,6 +375,27 @@ export class AgentManagerComponent implements Component {
|
|
|
219
375
|
lines.push(renderFooter(" [enter] continue [esc] cancel ", w, this.theme)); return lines;
|
|
220
376
|
}
|
|
221
377
|
|
|
378
|
+
private renderOverrideScope(w: number): string[] {
|
|
379
|
+
const state = this.overrideScopeState;
|
|
380
|
+
const entry = this.getAgentEntry(this.currentAgentId);
|
|
381
|
+
if (!state || !entry) return [];
|
|
382
|
+
const lines: string[] = [];
|
|
383
|
+
lines.push(renderHeader(` Create Override: ${entry.config.name} `, w, this.theme));
|
|
384
|
+
lines.push(row("", w, this.theme));
|
|
385
|
+
lines.push(row(` ${this.theme.fg("dim", "Where should this builtin override live?")}`, w, this.theme));
|
|
386
|
+
lines.push(row("", w, this.theme));
|
|
387
|
+
const userLine = state.selectedScope === "user" ? this.theme.fg("accent", "▸ user") : " user";
|
|
388
|
+
lines.push(row(` ${userLine}${this.theme.fg("dim", ` ${this.agentData.userSettingsPath}`)}`, w, this.theme));
|
|
389
|
+
if (state.allowProject) {
|
|
390
|
+
const projectPath = this.agentData.projectSettingsPath ?? ".dm/settings.json";
|
|
391
|
+
const projectLine = state.selectedScope === "project" ? this.theme.fg("accent", "▸ project") : " project";
|
|
392
|
+
lines.push(row(` ${projectLine}${this.theme.fg("dim", ` ${projectPath}`)}`, w, this.theme));
|
|
393
|
+
}
|
|
394
|
+
while (lines.length < 8) lines.push(row("", w, this.theme));
|
|
395
|
+
lines.push(renderFooter(" [enter] continue [↑↓/tab] choose [esc] cancel ", w, this.theme));
|
|
396
|
+
return lines;
|
|
397
|
+
}
|
|
398
|
+
|
|
222
399
|
private renderTemplateSelect(w: number): string[] {
|
|
223
400
|
const lines: string[] = []; lines.push(renderHeader(" Select Template ", w, this.theme)); lines.push(row("", w, this.theme));
|
|
224
401
|
const innerW = w - 2; const viewport = 12; const start = Math.max(0, Math.min(this.templateCursor - Math.floor(viewport / 2), Math.max(0, TEMPLATE_ITEMS.length - viewport))); const visible = TEMPLATE_ITEMS.slice(start, start + viewport);
|
|
@@ -261,6 +438,7 @@ export class AgentManagerComponent implements Component {
|
|
|
261
438
|
switch (this.screen) {
|
|
262
439
|
case "list": { const action = handleListInput(this.listState, this.listAgents(), data); if (action) this.handleListAction(action); this.tui.requestRender(); return; }
|
|
263
440
|
case "template-select": this.handleTemplateSelectInput(data); return;
|
|
441
|
+
case "override-scope": this.handleOverrideScopeInput(data); return;
|
|
264
442
|
case "detail": {
|
|
265
443
|
const entry = this.getAgentEntry(this.currentAgentId); if (!entry) { this.screen = "list"; this.tui.requestRender(); return; }
|
|
266
444
|
const action = handleDetailInput(this.detailState, data); if (action) this.handleDetailAction(action, entry); this.tui.requestRender(); return;
|
|
@@ -337,6 +515,7 @@ export class AgentManagerComponent implements Component {
|
|
|
337
515
|
if (!this.editState) { this.screen = "list"; this.tui.requestRender(); return; }
|
|
338
516
|
const result = handleEditInput(this.screen as EditScreen, this.editState, data, this.overlayWidth, this.models, this.skills);
|
|
339
517
|
if (result?.action === "discard") { this.handleEditDiscard(); return; }
|
|
518
|
+
if (result?.action === "delete") { this.removeBuiltinOverride(); this.tui.requestRender(); return; }
|
|
340
519
|
if (result?.action === "save") { const ok = this.saveEdit(); if (ok) { const entry = this.getAgentEntry(this.currentAgentId); if (entry) this.enterDetail(entry); } this.tui.requestRender(); return; }
|
|
341
520
|
if (result?.nextScreen) this.screen = result.nextScreen; this.tui.requestRender(); return;
|
|
342
521
|
}
|
|
@@ -344,9 +523,9 @@ export class AgentManagerComponent implements Component {
|
|
|
344
523
|
}
|
|
345
524
|
|
|
346
525
|
private handleEditDiscard(): void {
|
|
347
|
-
const entry = this.getAgentEntry(this.currentAgentId); if (!entry) { this.screen = "list"; this.editState = null; this.tui.requestRender(); return; }
|
|
348
|
-
if (entry.isNew) { this.removeAgentEntry(entry); this.editState = null; this.screen = "list"; this.tui.requestRender(); return; }
|
|
349
|
-
this.editState = null; this.enterDetail(entry); this.tui.requestRender();
|
|
526
|
+
const entry = this.getAgentEntry(this.currentAgentId); if (!entry) { this.screen = "list"; this.editState = null; this.builtinOverrideScope = null; this.tui.requestRender(); return; }
|
|
527
|
+
if (entry.isNew) { this.removeAgentEntry(entry); this.editState = null; this.builtinOverrideScope = null; this.screen = "list"; this.tui.requestRender(); return; }
|
|
528
|
+
this.editState = null; this.builtinOverrideScope = null; this.enterDetail(entry); this.tui.requestRender();
|
|
350
529
|
}
|
|
351
530
|
|
|
352
531
|
private isBuiltin(id: string): boolean { const a = this.getAgentEntry(id); return a?.config.source === "builtin"; }
|
|
@@ -365,7 +544,15 @@ export class AgentManagerComponent implements Component {
|
|
|
365
544
|
|
|
366
545
|
private handleDetailAction(action: DetailAction, entry: AgentEntry): void {
|
|
367
546
|
if (action.type === "back") { this.screen = "list"; return; }
|
|
368
|
-
if (action.type === "edit") {
|
|
547
|
+
if (action.type === "edit") {
|
|
548
|
+
if (entry.config.source === "builtin") {
|
|
549
|
+
if (entry.config.override) this.enterBuiltinOverrideEdit(entry, entry.config.override.scope);
|
|
550
|
+
else this.enterBuiltinOverrideScope(entry);
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
this.enterEdit(entry);
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
369
556
|
if (action.type === "launch") { this.enterTaskInput([entry.id], "detail"); return; }
|
|
370
557
|
}
|
|
371
558
|
|
|
@@ -380,6 +567,7 @@ export class AgentManagerComponent implements Component {
|
|
|
380
567
|
switch (this.screen) {
|
|
381
568
|
case "list": return renderList(this.listState, this.listAgents(), w, this.theme, this.statusMessage);
|
|
382
569
|
case "template-select": return this.renderTemplateSelect(w);
|
|
570
|
+
case "override-scope": return this.renderOverrideScope(w);
|
|
383
571
|
case "detail": { const entry = this.getAgentEntry(this.currentAgentId); if (!entry) return renderList(this.listState, this.listAgents(), w, this.theme, this.statusMessage); return renderDetail(this.detailState, entry.config, this.agentData.cwd, w, this.theme); }
|
|
384
572
|
case "chain-detail": { const entry = this.getChainEntry(this.currentChainId); if (!entry) return renderList(this.listState, this.listAgents(), w, this.theme, this.statusMessage); return renderChainDetail(this.chainDetailState, entry.config, w, this.theme); }
|
|
385
573
|
case "edit": case "edit-field": case "edit-prompt": return this.editState ? renderEdit(this.screen as EditScreen, this.editState, w, this.theme) : [];
|
|
@@ -15,6 +15,31 @@ export type AgentScope = "user" | "project" | "both";
|
|
|
15
15
|
|
|
16
16
|
export type AgentSource = "builtin" | "user" | "project";
|
|
17
17
|
|
|
18
|
+
export interface BuiltinAgentOverrideBase {
|
|
19
|
+
model?: string;
|
|
20
|
+
fallbackModels?: string[];
|
|
21
|
+
thinking?: string;
|
|
22
|
+
systemPrompt: string;
|
|
23
|
+
skills?: string[];
|
|
24
|
+
tools?: string[];
|
|
25
|
+
mcpDirectTools?: string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface BuiltinAgentOverrideConfig {
|
|
29
|
+
model?: string | false;
|
|
30
|
+
fallbackModels?: string[] | false;
|
|
31
|
+
thinking?: string | false;
|
|
32
|
+
systemPrompt?: string;
|
|
33
|
+
skills?: string[] | false;
|
|
34
|
+
tools?: string[] | false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface BuiltinAgentOverrideInfo {
|
|
38
|
+
scope: "user" | "project";
|
|
39
|
+
path: string;
|
|
40
|
+
base: BuiltinAgentOverrideBase;
|
|
41
|
+
}
|
|
42
|
+
|
|
18
43
|
export interface AgentConfig {
|
|
19
44
|
name: string;
|
|
20
45
|
description: string;
|
|
@@ -28,13 +53,13 @@ export interface AgentConfig {
|
|
|
28
53
|
filePath: string;
|
|
29
54
|
skills?: string[];
|
|
30
55
|
extensions?: string[];
|
|
31
|
-
// Chain behavior fields
|
|
32
56
|
output?: string;
|
|
33
57
|
defaultReads?: string[];
|
|
34
58
|
defaultProgress?: boolean;
|
|
35
59
|
interactive?: boolean;
|
|
36
60
|
maxSubagentDepth?: number;
|
|
37
61
|
extraFields?: Record<string, string>;
|
|
62
|
+
override?: BuiltinAgentOverrideInfo;
|
|
38
63
|
}
|
|
39
64
|
|
|
40
65
|
export interface ChainStepConfig {
|
|
@@ -61,6 +86,266 @@ export interface AgentDiscoveryResult {
|
|
|
61
86
|
projectAgentsDir: string | null;
|
|
62
87
|
}
|
|
63
88
|
|
|
89
|
+
function splitToolList(rawTools: string[] | undefined): { tools?: string[]; mcpDirectTools?: string[] } {
|
|
90
|
+
const mcpDirectTools: string[] = [];
|
|
91
|
+
const tools: string[] = [];
|
|
92
|
+
for (const tool of rawTools ?? []) {
|
|
93
|
+
if (tool.startsWith("mcp:")) {
|
|
94
|
+
mcpDirectTools.push(tool.slice(4));
|
|
95
|
+
} else {
|
|
96
|
+
tools.push(tool);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
...(tools.length > 0 ? { tools } : {}),
|
|
101
|
+
...(mcpDirectTools.length > 0 ? { mcpDirectTools } : {}),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function joinToolList(config: Pick<AgentConfig, "tools" | "mcpDirectTools">): string[] | undefined {
|
|
106
|
+
const joined = [
|
|
107
|
+
...(config.tools ?? []),
|
|
108
|
+
...(config.mcpDirectTools ?? []).map((tool) => `mcp:${tool}`),
|
|
109
|
+
];
|
|
110
|
+
return joined.length > 0 ? joined : undefined;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function arraysEqual(a: string[] | undefined, b: string[] | undefined): boolean {
|
|
114
|
+
if (!a && !b) return true;
|
|
115
|
+
if (!a || !b) return false;
|
|
116
|
+
if (a.length !== b.length) return false;
|
|
117
|
+
for (let i = 0; i < a.length; i++) {
|
|
118
|
+
if (a[i] !== b[i]) return false;
|
|
119
|
+
}
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function cloneOverrideBase(agent: AgentConfig): BuiltinAgentOverrideBase {
|
|
124
|
+
return {
|
|
125
|
+
model: agent.model,
|
|
126
|
+
fallbackModels: agent.fallbackModels ? [...agent.fallbackModels] : undefined,
|
|
127
|
+
thinking: agent.thinking,
|
|
128
|
+
systemPrompt: agent.systemPrompt,
|
|
129
|
+
skills: agent.skills ? [...agent.skills] : undefined,
|
|
130
|
+
tools: agent.tools ? [...agent.tools] : undefined,
|
|
131
|
+
mcpDirectTools: agent.mcpDirectTools ? [...agent.mcpDirectTools] : undefined,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function cloneOverrideValue(override: BuiltinAgentOverrideConfig): BuiltinAgentOverrideConfig {
|
|
136
|
+
return {
|
|
137
|
+
...(override.model !== undefined ? { model: override.model } : {}),
|
|
138
|
+
...(override.fallbackModels !== undefined
|
|
139
|
+
? { fallbackModels: override.fallbackModels === false ? false : [...override.fallbackModels] }
|
|
140
|
+
: {}),
|
|
141
|
+
...(override.thinking !== undefined ? { thinking: override.thinking } : {}),
|
|
142
|
+
...(override.systemPrompt !== undefined ? { systemPrompt: override.systemPrompt } : {}),
|
|
143
|
+
...(override.skills !== undefined ? { skills: override.skills === false ? false : [...override.skills] } : {}),
|
|
144
|
+
...(override.tools !== undefined ? { tools: override.tools === false ? false : [...override.tools] } : {}),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function findNearestProjectRoot(cwd: string): string | null {
|
|
149
|
+
let currentDir = cwd;
|
|
150
|
+
while (true) {
|
|
151
|
+
if (isDirectory(path.join(currentDir, ".dm")) || isDirectory(path.join(currentDir, ".agents"))) {
|
|
152
|
+
return currentDir;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const parentDir = path.dirname(currentDir);
|
|
156
|
+
if (parentDir === currentDir) return null;
|
|
157
|
+
currentDir = parentDir;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function getUserAgentSettingsPath(): string {
|
|
162
|
+
return path.join(os.homedir(), ".dm", "agent", "settings.json");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function getProjectAgentSettingsPath(cwd: string): string | null {
|
|
166
|
+
const projectRoot = findNearestProjectRoot(cwd);
|
|
167
|
+
return projectRoot ? path.join(projectRoot, ".dm", "settings.json") : null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function readSettingsFileStrict(filePath: string): Record<string, unknown> {
|
|
171
|
+
if (!fs.existsSync(filePath)) return {};
|
|
172
|
+
let parsed: unknown;
|
|
173
|
+
try {
|
|
174
|
+
parsed = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
175
|
+
} catch (error) {
|
|
176
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
177
|
+
throw new Error(`Failed to parse settings file '${filePath}': ${message}`, { cause: error });
|
|
178
|
+
}
|
|
179
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
180
|
+
throw new Error(`Settings file '${filePath}' must contain a JSON object.`);
|
|
181
|
+
}
|
|
182
|
+
return parsed as Record<string, unknown>;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function writeSettingsFile(filePath: string, settings: Record<string, unknown>): void {
|
|
186
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
187
|
+
fs.writeFileSync(filePath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function parseStringArrayOrFalse(value: unknown): string[] | false | undefined {
|
|
191
|
+
if (value === false) return false;
|
|
192
|
+
if (!Array.isArray(value)) return undefined;
|
|
193
|
+
const items = value.filter((item): item is string => typeof item === "string").map((item) => item.trim()).filter(Boolean);
|
|
194
|
+
return items;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function parseBuiltinOverrideEntry(value: unknown): BuiltinAgentOverrideConfig | undefined {
|
|
198
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
199
|
+
const input = value as Record<string, unknown>;
|
|
200
|
+
const override: BuiltinAgentOverrideConfig = {};
|
|
201
|
+
|
|
202
|
+
if (typeof input.model === "string" || input.model === false) override.model = input.model;
|
|
203
|
+
if (typeof input.thinking === "string" || input.thinking === false) override.thinking = input.thinking;
|
|
204
|
+
if (typeof input.systemPrompt === "string") override.systemPrompt = input.systemPrompt;
|
|
205
|
+
|
|
206
|
+
const fallbackModels = parseStringArrayOrFalse(input.fallbackModels);
|
|
207
|
+
if (fallbackModels !== undefined) override.fallbackModels = fallbackModels;
|
|
208
|
+
|
|
209
|
+
const skills = parseStringArrayOrFalse(input.skills);
|
|
210
|
+
if (skills !== undefined) override.skills = skills;
|
|
211
|
+
|
|
212
|
+
const tools = parseStringArrayOrFalse(input.tools);
|
|
213
|
+
if (tools !== undefined) override.tools = tools;
|
|
214
|
+
|
|
215
|
+
return Object.keys(override).length > 0 ? override : undefined;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function readBuiltinOverrides(filePath: string | null): Record<string, BuiltinAgentOverrideConfig> {
|
|
219
|
+
if (!filePath || !fs.existsSync(filePath)) return {};
|
|
220
|
+
const settings = readSettingsFileStrict(filePath);
|
|
221
|
+
const subagents = settings.subagents;
|
|
222
|
+
if (!subagents || typeof subagents !== "object" || Array.isArray(subagents)) return {};
|
|
223
|
+
const agentOverrides = (subagents as Record<string, unknown>).agentOverrides;
|
|
224
|
+
if (!agentOverrides || typeof agentOverrides !== "object" || Array.isArray(agentOverrides)) return {};
|
|
225
|
+
|
|
226
|
+
const parsed: Record<string, BuiltinAgentOverrideConfig> = {};
|
|
227
|
+
for (const [name, value] of Object.entries(agentOverrides)) {
|
|
228
|
+
const override = parseBuiltinOverrideEntry(value);
|
|
229
|
+
if (override) parsed[name] = override;
|
|
230
|
+
}
|
|
231
|
+
return parsed;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function applyBuiltinOverride(
|
|
235
|
+
agent: AgentConfig,
|
|
236
|
+
override: BuiltinAgentOverrideConfig,
|
|
237
|
+
meta: { scope: "user" | "project"; path: string },
|
|
238
|
+
): AgentConfig {
|
|
239
|
+
const next: AgentConfig = {
|
|
240
|
+
...agent,
|
|
241
|
+
override: { ...meta, base: cloneOverrideBase(agent) },
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
if (override.model !== undefined) next.model = override.model === false ? undefined : override.model;
|
|
245
|
+
if (override.fallbackModels !== undefined) {
|
|
246
|
+
next.fallbackModels = override.fallbackModels === false ? undefined : [...override.fallbackModels];
|
|
247
|
+
}
|
|
248
|
+
if (override.thinking !== undefined) next.thinking = override.thinking === false ? undefined : override.thinking;
|
|
249
|
+
if (override.systemPrompt !== undefined) next.systemPrompt = override.systemPrompt;
|
|
250
|
+
if (override.skills !== undefined) next.skills = override.skills === false ? undefined : [...override.skills];
|
|
251
|
+
if (override.tools !== undefined) {
|
|
252
|
+
const { tools, mcpDirectTools } = splitToolList(override.tools === false ? [] : override.tools);
|
|
253
|
+
next.tools = tools;
|
|
254
|
+
next.mcpDirectTools = mcpDirectTools;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return next;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function applyBuiltinOverrides(
|
|
261
|
+
builtinAgents: AgentConfig[],
|
|
262
|
+
userOverrides: Record<string, BuiltinAgentOverrideConfig>,
|
|
263
|
+
projectOverrides: Record<string, BuiltinAgentOverrideConfig>,
|
|
264
|
+
userSettingsPath: string,
|
|
265
|
+
projectSettingsPath: string | null,
|
|
266
|
+
): AgentConfig[] {
|
|
267
|
+
return builtinAgents.map((agent) => {
|
|
268
|
+
const projectOverride = projectOverrides[agent.name];
|
|
269
|
+
if (projectOverride && projectSettingsPath) {
|
|
270
|
+
return applyBuiltinOverride(agent, projectOverride, { scope: "project", path: projectSettingsPath });
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const userOverride = userOverrides[agent.name];
|
|
274
|
+
if (userOverride) {
|
|
275
|
+
return applyBuiltinOverride(agent, userOverride, { scope: "user", path: userSettingsPath });
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return agent;
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export function buildBuiltinOverrideConfig(
|
|
283
|
+
base: BuiltinAgentOverrideBase,
|
|
284
|
+
draft: Pick<AgentConfig, "model" | "fallbackModels" | "thinking" | "systemPrompt" | "skills" | "tools" | "mcpDirectTools">,
|
|
285
|
+
): BuiltinAgentOverrideConfig | undefined {
|
|
286
|
+
const override: BuiltinAgentOverrideConfig = {};
|
|
287
|
+
|
|
288
|
+
if (draft.model !== base.model) override.model = draft.model ?? false;
|
|
289
|
+
if (!arraysEqual(draft.fallbackModels, base.fallbackModels)) override.fallbackModels = draft.fallbackModels ? [...draft.fallbackModels] : false;
|
|
290
|
+
if (draft.thinking !== base.thinking) override.thinking = draft.thinking ?? false;
|
|
291
|
+
if (draft.systemPrompt !== base.systemPrompt) override.systemPrompt = draft.systemPrompt;
|
|
292
|
+
if (!arraysEqual(draft.skills, base.skills)) override.skills = draft.skills ? [...draft.skills] : false;
|
|
293
|
+
|
|
294
|
+
const baseTools = joinToolList(base);
|
|
295
|
+
const draftTools = joinToolList(draft);
|
|
296
|
+
if (!arraysEqual(draftTools, baseTools)) override.tools = draftTools ? [...draftTools] : false;
|
|
297
|
+
|
|
298
|
+
return Object.keys(override).length > 0 ? override : undefined;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export function saveBuiltinAgentOverride(
|
|
302
|
+
cwd: string,
|
|
303
|
+
name: string,
|
|
304
|
+
scope: "user" | "project",
|
|
305
|
+
override: BuiltinAgentOverrideConfig,
|
|
306
|
+
): string {
|
|
307
|
+
const filePath = scope === "project" ? getProjectAgentSettingsPath(cwd) : getUserAgentSettingsPath();
|
|
308
|
+
if (!filePath) throw new Error("Project override is not available here. No project config root was found.");
|
|
309
|
+
|
|
310
|
+
const settings = readSettingsFileStrict(filePath);
|
|
311
|
+
const subagents = settings.subagents && typeof settings.subagents === "object" && !Array.isArray(settings.subagents)
|
|
312
|
+
? { ...(settings.subagents as Record<string, unknown>) }
|
|
313
|
+
: {};
|
|
314
|
+
const agentOverrides = subagents.agentOverrides && typeof subagents.agentOverrides === "object" && !Array.isArray(subagents.agentOverrides)
|
|
315
|
+
? { ...(subagents.agentOverrides as Record<string, unknown>) }
|
|
316
|
+
: {};
|
|
317
|
+
|
|
318
|
+
agentOverrides[name] = cloneOverrideValue(override);
|
|
319
|
+
subagents.agentOverrides = agentOverrides;
|
|
320
|
+
settings.subagents = subagents;
|
|
321
|
+
writeSettingsFile(filePath, settings);
|
|
322
|
+
return filePath;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export function removeBuiltinAgentOverride(cwd: string, name: string, scope: "user" | "project"): string {
|
|
326
|
+
const filePath = scope === "project" ? getProjectAgentSettingsPath(cwd) : getUserAgentSettingsPath();
|
|
327
|
+
if (!filePath) throw new Error("Project override is not available here. No project config root was found.");
|
|
328
|
+
if (!fs.existsSync(filePath)) return filePath;
|
|
329
|
+
|
|
330
|
+
const settings = readSettingsFileStrict(filePath);
|
|
331
|
+
const subagents = settings.subagents;
|
|
332
|
+
if (!subagents || typeof subagents !== "object" || Array.isArray(subagents)) return filePath;
|
|
333
|
+
const nextSubagents = { ...(subagents as Record<string, unknown>) };
|
|
334
|
+
const agentOverrides = nextSubagents.agentOverrides;
|
|
335
|
+
if (!agentOverrides || typeof agentOverrides !== "object" || Array.isArray(agentOverrides)) return filePath;
|
|
336
|
+
|
|
337
|
+
const nextOverrides = { ...(agentOverrides as Record<string, unknown>) };
|
|
338
|
+
delete nextOverrides[name];
|
|
339
|
+
if (Object.keys(nextOverrides).length > 0) nextSubagents.agentOverrides = nextOverrides;
|
|
340
|
+
else delete nextSubagents.agentOverrides;
|
|
341
|
+
|
|
342
|
+
if (Object.keys(nextSubagents).length > 0) settings.subagents = nextSubagents;
|
|
343
|
+
else delete settings.subagents;
|
|
344
|
+
|
|
345
|
+
writeSettingsFile(filePath, settings);
|
|
346
|
+
return filePath;
|
|
347
|
+
}
|
|
348
|
+
|
|
64
349
|
function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
|
|
65
350
|
const agents: AgentConfig[] = [];
|
|
66
351
|
|
|
@@ -111,7 +396,6 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
|
|
|
111
396
|
}
|
|
112
397
|
}
|
|
113
398
|
|
|
114
|
-
// Parse defaultReads as comma-separated list (like tools)
|
|
115
399
|
const defaultReads = frontmatter.defaultReads
|
|
116
400
|
?.split(",")
|
|
117
401
|
.map((f) => f.trim())
|
|
@@ -155,7 +439,6 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
|
|
|
155
439
|
filePath,
|
|
156
440
|
skills: skills && skills.length > 0 ? skills : undefined,
|
|
157
441
|
extensions,
|
|
158
|
-
// Chain behavior fields
|
|
159
442
|
output: frontmatter.output,
|
|
160
443
|
defaultReads: defaultReads && defaultReads.length > 0 ? defaultReads : undefined,
|
|
161
444
|
defaultProgress: frontmatter.defaultProgress === "true",
|
|
@@ -216,18 +499,12 @@ function isDirectory(p: string): boolean {
|
|
|
216
499
|
}
|
|
217
500
|
|
|
218
501
|
function findNearestProjectAgentsDir(cwd: string): string | null {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
if (isDirectory(candidate)) return candidate;
|
|
226
|
-
|
|
227
|
-
const parentDir = path.dirname(currentDir);
|
|
228
|
-
if (parentDir === currentDir) return null;
|
|
229
|
-
currentDir = parentDir;
|
|
230
|
-
}
|
|
502
|
+
const projectRoot = findNearestProjectRoot(cwd);
|
|
503
|
+
if (!projectRoot) return null;
|
|
504
|
+
const candidateAlt = path.join(projectRoot, ".agents");
|
|
505
|
+
if (isDirectory(candidateAlt)) return candidateAlt;
|
|
506
|
+
const candidate = path.join(projectRoot, ".dm", "agents");
|
|
507
|
+
return isDirectory(candidate) ? candidate : null;
|
|
231
508
|
}
|
|
232
509
|
|
|
233
510
|
const BUILTIN_AGENTS_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), "agents");
|
|
@@ -236,8 +513,16 @@ export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryRe
|
|
|
236
513
|
const userDirOld = path.join(os.homedir(), ".dm", "agent", "agents");
|
|
237
514
|
const userDirNew = path.join(os.homedir(), ".agents");
|
|
238
515
|
const projectAgentsDir = findNearestProjectAgentsDir(cwd);
|
|
239
|
-
|
|
240
|
-
const
|
|
516
|
+
const userSettingsPath = getUserAgentSettingsPath();
|
|
517
|
+
const projectSettingsPath = getProjectAgentSettingsPath(cwd);
|
|
518
|
+
|
|
519
|
+
const builtinAgents = applyBuiltinOverrides(
|
|
520
|
+
loadAgentsFromDir(BUILTIN_AGENTS_DIR, "builtin"),
|
|
521
|
+
scope === "project" ? {} : readBuiltinOverrides(userSettingsPath),
|
|
522
|
+
scope === "user" ? {} : readBuiltinOverrides(projectSettingsPath),
|
|
523
|
+
userSettingsPath,
|
|
524
|
+
projectSettingsPath,
|
|
525
|
+
);
|
|
241
526
|
|
|
242
527
|
const userAgentsOld = scope === "project" ? [] : loadAgentsFromDir(userDirOld, "user");
|
|
243
528
|
const userAgentsNew = scope === "project" ? [] : loadAgentsFromDir(userDirNew, "user");
|
|
@@ -256,12 +541,22 @@ export function discoverAgentsAll(cwd: string): {
|
|
|
256
541
|
chains: ChainConfig[];
|
|
257
542
|
userDir: string;
|
|
258
543
|
projectDir: string | null;
|
|
544
|
+
userSettingsPath: string;
|
|
545
|
+
projectSettingsPath: string | null;
|
|
259
546
|
} {
|
|
260
547
|
const userDirOld = path.join(os.homedir(), ".dm", "agent", "agents");
|
|
261
548
|
const userDirNew = path.join(os.homedir(), ".agents");
|
|
262
549
|
const projectDir = findNearestProjectAgentsDir(cwd);
|
|
263
|
-
|
|
264
|
-
const
|
|
550
|
+
const userSettingsPath = getUserAgentSettingsPath();
|
|
551
|
+
const projectSettingsPath = getProjectAgentSettingsPath(cwd);
|
|
552
|
+
|
|
553
|
+
const builtin = applyBuiltinOverrides(
|
|
554
|
+
loadAgentsFromDir(BUILTIN_AGENTS_DIR, "builtin"),
|
|
555
|
+
readBuiltinOverrides(userSettingsPath),
|
|
556
|
+
readBuiltinOverrides(projectSettingsPath),
|
|
557
|
+
userSettingsPath,
|
|
558
|
+
projectSettingsPath,
|
|
559
|
+
);
|
|
265
560
|
const user = [
|
|
266
561
|
...loadAgentsFromDir(userDirOld, "user"),
|
|
267
562
|
...loadAgentsFromDir(userDirNew, "user"),
|
|
@@ -275,5 +570,5 @@ export function discoverAgentsAll(cwd: string): {
|
|
|
275
570
|
|
|
276
571
|
const userDir = fs.existsSync(userDirNew) ? userDirNew : userDirOld;
|
|
277
572
|
|
|
278
|
-
return { builtin, user, project, chains, userDir, projectDir };
|
|
573
|
+
return { builtin, user, project, chains, userDir, projectDir, userSettingsPath, projectSettingsPath };
|
|
279
574
|
}
|
|
@@ -13,6 +13,11 @@ Bundled DM extension for multi-pass review loops and oracle-assisted review sess
|
|
|
13
13
|
- Global config path: `~/.dm/ultrathink.json`
|
|
14
14
|
- Config env override: `DM_ULTRATHINK_CONFIG_PATH`
|
|
15
15
|
|
|
16
|
+
## Naming-model catalogs
|
|
17
|
+
|
|
18
|
+
- DuckMind/OpenRouter sessions show `duckmind/free`, `duckmind/auto`, `duckmind/lite`, `duckmind/smart`, `duckmind/deep`
|
|
19
|
+
- OpenAI Codex / dm-multicodex sessions show `openai-codex/gpt-5.1`, `openai-codex/gpt-5.1-codex-max`, `openai-codex/gpt-5.1-codex-mini`, `openai-codex/gpt-5.2`, `openai-codex/gpt-5.2-codex`
|
|
20
|
+
|
|
16
21
|
## Bundled with dm
|
|
17
22
|
|
|
18
23
|
No separate install step is required. The extension is loaded from DM's bundled extension catalog.
|
|
@@ -61,6 +61,7 @@ type DuckMindPreset = {
|
|
|
61
61
|
|
|
62
62
|
const DUCKMIND_PROVIDER = "duckmind";
|
|
63
63
|
const OPENROUTER_PROVIDER = "openrouter";
|
|
64
|
+
const OPENAI_CODEX_PROVIDER = "openai-codex";
|
|
64
65
|
const DUCKMIND_PRESETS: readonly DuckMindPreset[] = [
|
|
65
66
|
{
|
|
66
67
|
alias: "free",
|
|
@@ -94,6 +95,34 @@ const DUCKMIND_PRESETS: readonly DuckMindPreset[] = [
|
|
|
94
95
|
},
|
|
95
96
|
];
|
|
96
97
|
|
|
98
|
+
const OPENAI_CODEX_NAMING_MODELS = [
|
|
99
|
+
{
|
|
100
|
+
modelId: "gpt-5.1",
|
|
101
|
+
fallbackModelId: "gpt-5.1-codex-mini",
|
|
102
|
+
displayName: "GPT-5.1",
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
modelId: "gpt-5.1-codex-max",
|
|
106
|
+
fallbackModelId: "gpt-5.1-codex-mini",
|
|
107
|
+
displayName: "GPT-5.1 Codex Max",
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
modelId: "gpt-5.1-codex-mini",
|
|
111
|
+
fallbackModelId: "gpt-5.1-codex-mini",
|
|
112
|
+
displayName: "GPT-5.1 Codex Mini",
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
modelId: "gpt-5.2",
|
|
116
|
+
fallbackModelId: "gpt-5.1-codex-mini",
|
|
117
|
+
displayName: "GPT-5.2",
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
modelId: "gpt-5.2-codex",
|
|
121
|
+
fallbackModelId: "gpt-5.1-codex-mini",
|
|
122
|
+
displayName: "GPT-5.2 Codex",
|
|
123
|
+
},
|
|
124
|
+
] as const;
|
|
125
|
+
|
|
97
126
|
let testOverrides: NamingTestOverrides | undefined;
|
|
98
127
|
|
|
99
128
|
export function setNamingTestOverrides(overrides?: NamingTestOverrides): void {
|
|
@@ -208,6 +237,25 @@ function buildDuckMindNamingChoices(availableModels: Model<any>[]): NamingModelC
|
|
|
208
237
|
});
|
|
209
238
|
}
|
|
210
239
|
|
|
240
|
+
function buildOpenAICodexNamingChoices(availableModels: Model<any>[]): NamingModelChoice[] {
|
|
241
|
+
const codexModels = availableModels.filter((model) => model.provider === OPENAI_CODEX_PROVIDER);
|
|
242
|
+
return OPENAI_CODEX_NAMING_MODELS.flatMap((entry) => {
|
|
243
|
+
const model = pickOpenRouterModel(codexModels, entry.modelId, entry.fallbackModelId, entry.displayName);
|
|
244
|
+
if (!model) {
|
|
245
|
+
return [];
|
|
246
|
+
}
|
|
247
|
+
return [
|
|
248
|
+
{
|
|
249
|
+
label: `${OPENAI_CODEX_PROVIDER}/${entry.modelId}`,
|
|
250
|
+
config: {
|
|
251
|
+
provider: OPENAI_CODEX_PROVIDER,
|
|
252
|
+
modelId: entry.modelId,
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
];
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
211
259
|
function resolveDuckMindAliasModel(availableModels: Model<any>[], config: NamingModelConfig): Model<any> | undefined {
|
|
212
260
|
const normalizedProvider = config.provider.trim().toLowerCase();
|
|
213
261
|
const normalizedModelId = config.modelId.trim().toLowerCase();
|
|
@@ -231,9 +279,31 @@ function resolveDuckMindAliasModel(availableModels: Model<any>[], config: Naming
|
|
|
231
279
|
);
|
|
232
280
|
}
|
|
233
281
|
|
|
282
|
+
function resolveOpenAICodexAliasModel(availableModels: Model<any>[], config: NamingModelConfig): Model<any> | undefined {
|
|
283
|
+
const normalizedProvider = config.provider.trim().toLowerCase();
|
|
284
|
+
const normalizedModelId = config.modelId.trim().toLowerCase();
|
|
285
|
+
if (normalizedProvider !== OPENAI_CODEX_PROVIDER) {
|
|
286
|
+
return undefined;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const preset = OPENAI_CODEX_NAMING_MODELS.find((entry) => normalizedModelId === entry.modelId.toLowerCase());
|
|
290
|
+
if (!preset) {
|
|
291
|
+
return undefined;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return pickOpenRouterModel(
|
|
295
|
+
availableModels.filter((model) => model.provider === OPENAI_CODEX_PROVIDER),
|
|
296
|
+
preset.modelId,
|
|
297
|
+
preset.fallbackModelId,
|
|
298
|
+
preset.displayName,
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
234
302
|
async function resolveNamingModel(ctx: ExtensionContext, config: NamingModelConfig): Promise<{ model: Model<any>; apiKey: string }> {
|
|
303
|
+
const availableModels = ctx.modelRegistry.getAvailable();
|
|
235
304
|
const model =
|
|
236
|
-
resolveDuckMindAliasModel(
|
|
305
|
+
resolveDuckMindAliasModel(availableModels, config) ??
|
|
306
|
+
resolveOpenAICodexAliasModel(availableModels, config) ??
|
|
237
307
|
ctx.modelRegistry.find(config.provider, config.modelId);
|
|
238
308
|
if (!model) {
|
|
239
309
|
throw new Error(`Ultrathink naming model ${config.provider}/${config.modelId} is not available in DM.`);
|
|
@@ -320,10 +390,12 @@ export async function ensureNamingModel(
|
|
|
320
390
|
const choices =
|
|
321
391
|
ctx.model?.provider === OPENROUTER_PROVIDER || ctx.model?.provider === DUCKMIND_PROVIDER
|
|
322
392
|
? buildDuckMindNamingChoices(availableModels)
|
|
323
|
-
:
|
|
393
|
+
: ctx.model?.provider === OPENAI_CODEX_PROVIDER
|
|
394
|
+
? buildOpenAICodexNamingChoices(availableModels)
|
|
395
|
+
: availableModels.map(buildDefaultNamingChoice);
|
|
324
396
|
|
|
325
397
|
if (choices.length === 0) {
|
|
326
|
-
throw new Error("Ultrathink could not find any available
|
|
398
|
+
throw new Error("Ultrathink could not find any available naming models to use for branch and commit naming.");
|
|
327
399
|
}
|
|
328
400
|
|
|
329
401
|
const selection = await ctx.ui.select(
|