@duckmind/dm-darwin-arm64 0.13.6 → 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.
Files changed (78) hide show
  1. package/dm +0 -0
  2. package/extensions/.dm-extensions.json +26 -2
  3. package/extensions/dm-phone/README.md +23 -0
  4. package/extensions/dm-phone/index.ts +12 -0
  5. package/extensions/dm-phone/node_modules/.package-lock.json +29 -0
  6. package/extensions/dm-phone/node_modules/ws/LICENSE +20 -0
  7. package/extensions/dm-phone/node_modules/ws/README.md +548 -0
  8. package/extensions/dm-phone/node_modules/ws/browser.js +8 -0
  9. package/extensions/dm-phone/node_modules/ws/index.js +22 -0
  10. package/extensions/dm-phone/node_modules/ws/lib/buffer-util.js +131 -0
  11. package/extensions/dm-phone/node_modules/ws/lib/constants.js +19 -0
  12. package/extensions/dm-phone/node_modules/ws/lib/event-target.js +292 -0
  13. package/extensions/dm-phone/node_modules/ws/lib/extension.js +203 -0
  14. package/extensions/dm-phone/node_modules/ws/lib/limiter.js +55 -0
  15. package/extensions/dm-phone/node_modules/ws/lib/permessage-deflate.js +528 -0
  16. package/extensions/dm-phone/node_modules/ws/lib/receiver.js +706 -0
  17. package/extensions/dm-phone/node_modules/ws/lib/sender.js +602 -0
  18. package/extensions/dm-phone/node_modules/ws/lib/stream.js +161 -0
  19. package/extensions/dm-phone/node_modules/ws/lib/subprotocol.js +62 -0
  20. package/extensions/dm-phone/node_modules/ws/lib/validation.js +152 -0
  21. package/extensions/dm-phone/node_modules/ws/lib/websocket-server.js +554 -0
  22. package/extensions/dm-phone/node_modules/ws/lib/websocket.js +1393 -0
  23. package/extensions/dm-phone/node_modules/ws/package.json +70 -0
  24. package/extensions/dm-phone/node_modules/ws/wrapper.mjs +21 -0
  25. package/extensions/dm-phone/package-lock.json +66 -0
  26. package/extensions/dm-phone/package.json +35 -0
  27. package/extensions/dm-phone/phone-session-pool.ts +8 -0
  28. package/extensions/dm-phone/public/app/attachments.js +233 -0
  29. package/extensions/dm-phone/public/app/autocomplete-controller.js +81 -0
  30. package/extensions/dm-phone/public/app/autocomplete.js +135 -0
  31. package/extensions/dm-phone/public/app/bindings.js +178 -0
  32. package/extensions/dm-phone/public/app/command-catalog.js +76 -0
  33. package/extensions/dm-phone/public/app/commands.js +370 -0
  34. package/extensions/dm-phone/public/app/constants.js +60 -0
  35. package/extensions/dm-phone/public/app/formatters.js +131 -0
  36. package/extensions/dm-phone/public/app/handlers.js +442 -0
  37. package/extensions/dm-phone/public/app/main.js +6 -0
  38. package/extensions/dm-phone/public/app/markdown.js +105 -0
  39. package/extensions/dm-phone/public/app/messages.js +418 -0
  40. package/extensions/dm-phone/public/app/sheet-actions.js +113 -0
  41. package/extensions/dm-phone/public/app/sheet-navigation.js +19 -0
  42. package/extensions/dm-phone/public/app/sheets-view.js +272 -0
  43. package/extensions/dm-phone/public/app/state.js +95 -0
  44. package/extensions/dm-phone/public/app/tool-rendering.js +562 -0
  45. package/extensions/dm-phone/public/app/transport.js +176 -0
  46. package/extensions/dm-phone/public/app/ui.js +409 -0
  47. package/extensions/dm-phone/public/app.js +1 -0
  48. package/extensions/dm-phone/public/icon.svg +15 -0
  49. package/extensions/dm-phone/public/index.html +147 -0
  50. package/extensions/dm-phone/public/manifest.webmanifest +17 -0
  51. package/extensions/dm-phone/public/styles.css +1139 -0
  52. package/extensions/dm-phone/public/sw.js +78 -0
  53. package/extensions/dm-phone/src/extension/phone-args.ts +121 -0
  54. package/extensions/dm-phone/src/extension/phone-paths.ts +250 -0
  55. package/extensions/dm-phone/src/extension/phone-quota.ts +188 -0
  56. package/extensions/dm-phone/src/extension/phone-runtime.ts +154 -0
  57. package/extensions/dm-phone/src/extension/phone-server-runtime.ts +1217 -0
  58. package/extensions/dm-phone/src/extension/phone-sessions.ts +139 -0
  59. package/extensions/dm-phone/src/extension/phone-static.ts +30 -0
  60. package/extensions/dm-phone/src/extension/phone-tailscale.ts +148 -0
  61. package/extensions/dm-phone/src/extension/phone-theme.ts +85 -0
  62. package/extensions/dm-phone/src/extension/register-phone-child-extension.ts +112 -0
  63. package/extensions/dm-phone/src/extension/register-phone-extension.ts +106 -0
  64. package/extensions/dm-phone/src/extension/types.ts +73 -0
  65. package/extensions/dm-phone/src/session-pool/parent-session-worker.ts +881 -0
  66. package/extensions/dm-phone/src/session-pool/session-pool.ts +470 -0
  67. package/extensions/dm-phone/src/session-pool/session-worker.ts +734 -0
  68. package/extensions/dm-phone/src/session-pool/types.ts +105 -0
  69. package/extensions/dm-phone/src/session-pool/utils.ts +23 -0
  70. package/extensions/dm-subagents/agent-management.ts +15 -6
  71. package/extensions/dm-subagents/agent-manager-detail.ts +12 -2
  72. package/extensions/dm-subagents/agent-manager-edit.ts +75 -23
  73. package/extensions/dm-subagents/agent-manager-list.ts +9 -2
  74. package/extensions/dm-subagents/agent-manager.ts +199 -11
  75. package/extensions/dm-subagents/agents.ts +315 -20
  76. package/extensions/dm-ultrathink/README.md +5 -0
  77. package/extensions/dm-ultrathink/src/naming.ts +75 -3
  78. package/package.json +1 -1
@@ -0,0 +1,105 @@
1
+ import type { WebSocket } from "ws";
2
+
3
+ export type SessionKind = "parent" | "parallel";
4
+
5
+ export type SessionSummary = {
6
+ id: string;
7
+ kind: SessionKind;
8
+ sessionId: string | null;
9
+ sessionFile: string | null;
10
+ sessionName: string | null;
11
+ label: string;
12
+ secondaryLabel: string;
13
+ firstUserPreview: string | null;
14
+ lastUserPreview: string | null;
15
+ model: { id: string; name: string; provider: string } | null;
16
+ isRunning: boolean;
17
+ isStreaming: boolean;
18
+ isCompacting: boolean;
19
+ messageCount: number;
20
+ pendingMessageCount: number;
21
+ hasPendingUiRequest: boolean;
22
+ lastError: string;
23
+ lastActivityAt: number;
24
+ childPid: number | null;
25
+ cwd?: string | null;
26
+ mirrorsCli?: boolean;
27
+ };
28
+
29
+ export type PendingRequest = {
30
+ resolve: (value: any) => void;
31
+ reject: (error: Error) => void;
32
+ timer: NodeJS.Timeout;
33
+ };
34
+
35
+ export type PendingClientResponse = {
36
+ ws: WebSocket;
37
+ responseCommand?: string;
38
+ responseData?: Record<string, unknown>;
39
+ onSuccess?: (payload: any) => void;
40
+ onError?: (payload: any) => void;
41
+ };
42
+
43
+ export type SessionSnapshot = {
44
+ state: any;
45
+ messages: any[];
46
+ commands: any[];
47
+ liveAssistantMessage: any;
48
+ liveTools: any[];
49
+ };
50
+
51
+ export type ClientState = {
52
+ activeSessionId: string | null;
53
+ };
54
+
55
+ export type SessionWorkerOptions<TWorker> = {
56
+ cwd: string;
57
+ send: (ws: WebSocket, payload: unknown) => void;
58
+ onActivity: () => void;
59
+ onStateChange: () => void;
60
+ onEnvelope: (worker: TWorker, envelope: any) => void;
61
+ shouldAutoRestart: (worker: TWorker) => boolean;
62
+ };
63
+
64
+ export type SessionStatus = {
65
+ childRunning: boolean;
66
+ cwd: string;
67
+ previousCwd: string | null;
68
+ isStreaming: boolean;
69
+ isCompacting: boolean;
70
+ lastError: string;
71
+ childPid: number | null;
72
+ sessionWorkerId: string;
73
+ sessionKind: SessionKind;
74
+ };
75
+
76
+ export interface SessionController {
77
+ id: string;
78
+ kind: SessionKind;
79
+ cwd: string;
80
+ previousCwd: string | null;
81
+ currentSessionFile: string | null;
82
+ lastError: string;
83
+ lastActivityAt: number;
84
+ pendingUiRequest: any;
85
+ ensureStarted(startOptions?: { sessionFile?: string | null }): Promise<void>;
86
+ request(command: Record<string, unknown>, timeoutMs?: number): Promise<any>;
87
+ refreshCachedSnapshot(timeoutMs?: number): Promise<SessionSnapshot>;
88
+ getSnapshot(): Promise<SessionSnapshot>;
89
+ sendClientCommand(command: Record<string, unknown>, meta?: PendingClientResponse): Promise<string | undefined>;
90
+ reload(): Promise<void>;
91
+ dispose(): Promise<void>;
92
+ getStatus(): SessionStatus;
93
+ getSummary(): SessionSummary;
94
+ getCachedSnapshot(): SessionSnapshot;
95
+ setTrackedCwd?(cwd: string, previousCwd?: string | null): void;
96
+ }
97
+
98
+ export type PhoneSessionPoolOptions = {
99
+ cwd: string;
100
+ send: (ws: WebSocket, payload: unknown) => void;
101
+ onActivity: () => void;
102
+ buildStatusMeta: () => Record<string, unknown>;
103
+ createDefaultSession: () => SessionController;
104
+ createParallelSession: (sessionFile?: string | null) => SessionController;
105
+ };
@@ -0,0 +1,23 @@
1
+ export function contentToPreviewText(content: unknown): string {
2
+ if (typeof content === "string") {
3
+ return content.replace(/\s+/g, " ").trim();
4
+ }
5
+
6
+ if (!Array.isArray(content)) {
7
+ return "";
8
+ }
9
+
10
+ return content
11
+ .map((part: any) => {
12
+ if (part?.type === "text") return part.text || "";
13
+ if (part?.type === "image") return "[image]";
14
+ return "";
15
+ })
16
+ .join(" ")
17
+ .replace(/\s+/g, " ")
18
+ .trim();
19
+ }
20
+
21
+ export function shortId(value: unknown): string {
22
+ return String(value || "").trim().slice(0, 8);
23
+ }
@@ -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