@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 CHANGED
Binary file
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "status": "ok",
3
- "prepared_at": "2026-04-14T04:48:34.917084+00:00",
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": "0165165b797692a6e1ade71f0cbcb34b4792ce9c",
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> | undefined {
38
+ function configObject(config: unknown): { value?: Record<string, unknown>; error?: string } {
39
39
  let val = config;
40
40
  if (typeof val === "string") {
41
- try { val = JSON.parse(val); } catch { return undefined; }
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 undefined;
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 cfg = configObject(params.config);
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 cfg = configObject(params.config);
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" ? "[builtin]" : agent.source === "project" ? "[proj]" : "[user]";
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
- ? " [l]aunch [v] raw/resolved [↑↓] scroll [esc] back "
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
- export function createEditState(draft: AgentConfig, isNew: boolean, models: ModelInfo[], skills: SkillInfo[]): EditState {
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 = FIELD_ORDER.indexOf("model"); state.fieldMode = "model"; state.modelSearchQuery = ""; state.filteredModels = [...models];
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 = FIELD_ORDER.indexOf("thinking"); state.fieldMode = "thinking";
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 = FIELD_ORDER.indexOf("skills"); state.fieldMode = "skills"; state.skillSearchQuery = ""; state.filteredSkills = [...skills]; state.skillSelected = new Set(state.draft.skills ?? []); state.skillCursor = 0;
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(FIELD_ORDER.length - 1, state.fieldIndex + 1); return; }
188
- const field = FIELD_ORDER[state.fieldIndex]!;
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 = FIELD_ORDER[state.fieldIndex]!; applyFieldValue(field, state, state.fieldEditor.buffer); state.fieldMode = null; return { nextScreen: "edit" }; }
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 < FIELD_ORDER.length; i++) {
263
- const field = FIELD_ORDER[i]!; if (field === "prompt") break;
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 labelText = pad(`${field[0]!.toUpperCase()}${field.slice(1)}:`, labelWidth); let valueText = renderFieldValue(field, state);
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
- lines.push(row("", width, theme));
279
- const promptFocused = state.fieldIndex === FIELD_ORDER.indexOf("prompt");
280
- const promptPrefix = promptFocused ? theme.fg("accent", "▸ ") : " ";
281
- lines.push(row(` ${promptPrefix}${theme.fg("dim", "── System Prompt ──")}`, width, theme));
282
- const previewWidth = innerW - 2; const wrapped = wrapText(state.draft.systemPrompt ?? "", previewWidth); const previewLines = wrapped.lines.slice(0, 4);
283
- for (const line of previewLines) lines.push(row(` ${line}`, width, theme));
284
- for (let i = previewLines.length; i < 4; i++) lines.push(row("", width, theme));
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
- lines.push(renderFooter(" [ctrl+s] save [esc] back ", width, theme));
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 = 9;
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" ? "[chain]" : agent.source === "builtin" ? "[builtin]" : agent.source === "project" ? "[proj]" : "[user]";
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 type { AgentConfig, ChainConfig } from "./agents.ts";
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
- function cloneConfig(config: AgentConfig): AgentConfig { return { ...config, tools: config.tools ? [...config.tools] : undefined, mcpDirectTools: config.mcpDirectTools ? [...config.mcpDirectTools] : undefined, skills: config.skills ? [...config.skills] : undefined, defaultReads: config.defaultReads ? [...config.defaultReads] : undefined, extraFields: config.extraFields ? { ...config.extraFields } : undefined }; }
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") { if (entry.config.source === "builtin") { this.statusMessage = { text: "Builtin agents cannot be edited. Clone to user scope to override.", type: "error" }; this.screen = "list"; return; } this.enterEdit(entry); return; }
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
- let currentDir = cwd;
220
- while (true) {
221
- const candidateAlt = path.join(currentDir, ".agents");
222
- if (isDirectory(candidateAlt)) return candidateAlt;
223
-
224
- const candidate = path.join(currentDir, ".dm", "agents");
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 builtinAgents = loadAgentsFromDir(BUILTIN_AGENTS_DIR, "builtin");
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 builtin = loadAgentsFromDir(BUILTIN_AGENTS_DIR, "builtin");
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(ctx.modelRegistry.getAvailable(), config) ??
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
- : availableModels.map(buildDefaultNamingChoice);
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 DM models to use for branch and commit naming.");
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(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@duckmind/dm-darwin-arm64",
3
- "version": "0.13.7",
3
+ "version": "0.13.8",
4
4
  "description": "DuckMind (dm) binary payload for darwin arm64",
5
5
  "license": "MIT",
6
6
  "os": [