@gotgenes/pi-subagents 17.5.0 → 18.0.0

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.
@@ -1,233 +0,0 @@
1
- /**
2
- * agent-creation-wizard.ts — AI-generation and manual-form agent creation flows.
3
- *
4
- * Extracted from agent-menu.ts to give each concern a single responsibility.
5
- * Receives dependencies via injection — no direct `node:fs` imports.
6
- */
7
-
8
- import { join } from "node:path";
9
-
10
- import { BUILTIN_TOOL_NAMES } from "#src/config/agent-types";
11
- import type { ParentSnapshot } from "#src/lifecycle/parent-snapshot";
12
- import type { Subagent } from "#src/types";
13
- import type { AgentFileOps } from "#src/ui/agent-file-ops";
14
- import { writeAgentFile } from "#src/ui/agent-file-writer";
15
- import type { MenuUI } from "#src/ui/agent-menu";
16
-
17
- // ---- Deps interface ----
18
-
19
- /** Narrow manager interface for agent spawning (generate wizard). */
20
- export interface WizardManager {
21
- spawnAndWait: (
22
- parentSnapshot: ParentSnapshot,
23
- type: string,
24
- prompt: string,
25
- opts: { description: string; maxTurns: number },
26
- ) => Promise<Subagent>;
27
- }
28
-
29
- /** Narrow registry interface for reloading after creation. */
30
- export interface WizardRegistry {
31
- reload(): void;
32
- }
33
-
34
- // ---- Class ----
35
-
36
- export class AgentCreationWizard {
37
- constructor(
38
- private readonly fileOps: AgentFileOps,
39
- private readonly manager: WizardManager,
40
- private readonly registry: WizardRegistry,
41
- private readonly personalAgentsDir: string,
42
- private readonly projectAgentsDir: string,
43
- ) {}
44
-
45
- async showCreateWizard(ui: MenuUI, parentSnapshot: ParentSnapshot): Promise<void> {
46
- const location = await ui.select("Choose location", [
47
- "Project (.pi/agents/)",
48
- `Personal (${this.personalAgentsDir})`,
49
- ]);
50
- if (!location) return;
51
-
52
- const targetDir = location.startsWith("Project")
53
- ? this.projectAgentsDir
54
- : this.personalAgentsDir;
55
-
56
- const method = await ui.select("Creation method", [
57
- "Generate with Claude (recommended)",
58
- "Manual configuration",
59
- ]);
60
- if (!method) return;
61
-
62
- if (method.startsWith("Generate")) {
63
- await this.showGenerateWizard(ui, parentSnapshot, targetDir);
64
- } else {
65
- await this.showManualWizard(ui, targetDir);
66
- }
67
- }
68
-
69
- private async showGenerateWizard(
70
- ui: MenuUI,
71
- parentSnapshot: ParentSnapshot,
72
- targetDir: string,
73
- ): Promise<void> {
74
- const description = await ui.input("Describe what this agent should do");
75
- if (!description) return;
76
-
77
- const name = await ui.input("Agent name (filename, no spaces)");
78
- if (!name) return;
79
-
80
- this.fileOps.ensureDir(targetDir);
81
-
82
- const targetPath = join(targetDir, `${name}.md`);
83
- if (this.fileOps.exists(targetPath)) {
84
- const overwrite = await ui.confirm(
85
- "Overwrite",
86
- `${targetPath} already exists. Overwrite?`,
87
- );
88
- if (!overwrite) return;
89
- }
90
-
91
- ui.notify("Generating agent definition...", "info");
92
-
93
- const generatePrompt = `Create a custom pi sub-agent definition file based on this description: "${description}"
94
-
95
- Write a markdown file to: ${targetPath}
96
-
97
- The file format is a markdown file with YAML frontmatter and a system prompt body:
98
-
99
- \`\`\`markdown
100
- ---
101
- description: <one-line description shown in UI>
102
- tools: <comma-separated built-in tools: read, bash, edit, write, grep, find, ls. Use "none" for no tools. Omit for all tools>
103
- model: <optional model as "provider/modelId", e.g. "anthropic/claude-haiku-4-5-20251001". Omit to inherit parent model>
104
- thinking: <optional thinking level: off, minimal, low, medium, high, xhigh. Omit to inherit>
105
- max_turns: <optional max agentic turns. 0 or omit for unlimited (default)>
106
- prompt_mode: <"replace" (body IS the full system prompt) or "append" (body is appended to default prompt). Default: append>
107
- inherit_context: <true to fork parent conversation into agent so it sees chat history. Default: false>
108
- run_in_background: <true to run in background by default. Default: false>
109
- ---
110
-
111
- <system prompt body — instructions for the agent>
112
- \`\`\`
113
-
114
- Guidelines for choosing settings:
115
- - For read-only tasks (review, analysis): tools: read, bash, grep, find, ls
116
- - For code modification tasks: include edit, write
117
- - Use prompt_mode: append if the agent should keep the default system prompt and add specialization on top
118
- - Use prompt_mode: replace for fully custom agents with their own personality/instructions
119
- - Set inherit_context: true if the agent needs to know what was discussed in the parent conversation
120
- - Only include frontmatter fields that differ from defaults — omit fields where the default is fine
121
-
122
- Write the file using the write tool. Only write the file, nothing else.`;
123
-
124
- const record = await this.manager.spawnAndWait(
125
- parentSnapshot,
126
- "general-purpose",
127
- generatePrompt,
128
- {
129
- description: `Generate ${name} agent`,
130
- maxTurns: 5,
131
- },
132
- );
133
-
134
- if (record.status === "error") {
135
- ui.notify(`Generation failed: ${record.error}`, "warning");
136
- return;
137
- }
138
-
139
- this.registry.reload();
140
-
141
- if (this.fileOps.exists(targetPath)) {
142
- ui.notify(`Created ${targetPath}`, "info");
143
- } else {
144
- ui.notify(
145
- "Agent generation completed but file was not created. Check the agent output.",
146
- "warning",
147
- );
148
- }
149
- }
150
-
151
- private async showManualWizard(ui: MenuUI, targetDir: string): Promise<void> {
152
- const name = await ui.input("Agent name (filename, no spaces)");
153
- if (!name) return;
154
-
155
- const description = await ui.input("Description (one line)");
156
- if (!description) return;
157
-
158
- const toolChoice = await ui.select("Tools", [
159
- "all",
160
- "none",
161
- "read-only (read, bash, grep, find, ls)",
162
- "custom...",
163
- ]);
164
- if (!toolChoice) return;
165
-
166
- let tools: string;
167
- if (toolChoice === "all") {
168
- tools = BUILTIN_TOOL_NAMES.join(", ");
169
- } else if (toolChoice === "none") {
170
- tools = "none";
171
- } else if (toolChoice.startsWith("read-only")) {
172
- tools = "read, bash, grep, find, ls";
173
- } else {
174
- const customTools = await ui.input(
175
- "Tools (comma-separated)",
176
- BUILTIN_TOOL_NAMES.join(", "),
177
- );
178
- if (!customTools) return;
179
- tools = customTools;
180
- }
181
-
182
- const modelChoice = await ui.select("Model", [
183
- "inherit (parent model)",
184
- "haiku",
185
- "sonnet",
186
- "opus",
187
- "custom...",
188
- ]);
189
- if (!modelChoice) return;
190
-
191
- let modelLine = "";
192
- if (modelChoice === "haiku")
193
- modelLine = "\nmodel: anthropic/claude-haiku-4-5-20251001";
194
- else if (modelChoice === "sonnet")
195
- modelLine = "\nmodel: anthropic/claude-sonnet-4-6";
196
- else if (modelChoice === "opus")
197
- modelLine = "\nmodel: anthropic/claude-opus-4-6";
198
- else if (modelChoice === "custom...") {
199
- const customModel = await ui.input("Model (provider/modelId)");
200
- if (customModel) modelLine = `\nmodel: ${customModel}`;
201
- }
202
-
203
- const thinkingChoice = await ui.select("Thinking level", [
204
- "inherit",
205
- "off",
206
- "minimal",
207
- "low",
208
- "medium",
209
- "high",
210
- "xhigh",
211
- ]);
212
- if (!thinkingChoice) return;
213
-
214
- let thinkingLine = "";
215
- if (thinkingChoice !== "inherit") thinkingLine = `\nthinking: ${thinkingChoice}`;
216
-
217
- const systemPrompt = await ui.editor("System prompt", "");
218
- if (systemPrompt === undefined) return;
219
-
220
- const content = `---
221
- description: ${description}
222
- tools: ${tools}${modelLine}${thinkingLine}
223
- prompt_mode: replace
224
- ---
225
-
226
- ${systemPrompt}
227
- `;
228
-
229
- const targetPath = join(targetDir, `${name}.md`);
230
-
231
- await writeAgentFile(this.fileOps, ui, this.registry, targetPath, content, "Created");
232
- }
233
- }
@@ -1,59 +0,0 @@
1
- /**
2
- * agent-file-ops.ts — Filesystem abstraction for agent .md file operations.
3
- *
4
- * Decouples menu sub-modules from direct `node:fs` imports, making them
5
- * testable via plain stub objects without `vi.mock()`.
6
- */
7
-
8
- import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
9
- import { dirname, join } from "node:path";
10
-
11
- // ---- Interface ----
12
-
13
- /** Filesystem operations for agent `.md` files. */
14
- export interface AgentFileOps {
15
- exists(filePath: string): boolean;
16
- read(filePath: string): string | undefined;
17
- write(filePath: string, content: string): void;
18
- remove(filePath: string): void;
19
- ensureDir(dirPath: string): void;
20
- findAgentFile(name: string, dirs: string[]): string | undefined;
21
- }
22
-
23
- // ---- Production implementation ----
24
-
25
- /** Production implementation wrapping `node:fs` synchronous APIs. */
26
- export class FsAgentFileOps implements AgentFileOps {
27
- exists(filePath: string): boolean {
28
- return existsSync(filePath);
29
- }
30
-
31
- read(filePath: string): string | undefined {
32
- try {
33
- return readFileSync(filePath, "utf-8");
34
- } catch {
35
- return undefined;
36
- }
37
- }
38
-
39
- write(filePath: string, content: string): void {
40
- mkdirSync(dirname(filePath), { recursive: true });
41
- writeFileSync(filePath, content, "utf-8");
42
- }
43
-
44
- remove(filePath: string): void {
45
- unlinkSync(filePath);
46
- }
47
-
48
- ensureDir(dirPath: string): void {
49
- mkdirSync(dirPath, { recursive: true });
50
- }
51
-
52
- findAgentFile(name: string, dirs: string[]): string | undefined {
53
- for (const dir of dirs) {
54
- const p = join(dir, `${name}.md`);
55
- if (existsSync(p)) return p;
56
- }
57
- return undefined;
58
- }
59
- }
@@ -1,55 +0,0 @@
1
- /**
2
- * agent-file-writer.ts — Shared overwrite-guard + write + reload + notify helper.
3
- *
4
- * Extracted from AgentConfigEditor.ejectAgent and AgentCreationWizard.showManualWizard
5
- * to eliminate the duplicated 20-line pattern. Uses narrow interfaces (ISP) so callers
6
- * are not forced to depend on the full AgentFileOps or MenuUI shapes.
7
- */
8
-
9
- // ---- Narrow interfaces ----
10
-
11
- /** Minimal file operations needed by the overwrite-guard-and-write pattern. */
12
- export interface FileWriter {
13
- exists(filePath: string): boolean;
14
- write(filePath: string, content: string): void;
15
- }
16
-
17
- /** Minimal UI needed by the overwrite-guard-and-write pattern. */
18
- export interface WriterUI {
19
- confirm(title: string, message: string): Promise<boolean>;
20
- notify(message: string, level: "info" | "warning" | "error"): void;
21
- }
22
-
23
- /** Registry that can be reloaded after file changes. */
24
- export interface Reloadable {
25
- reload(): void;
26
- }
27
-
28
- // ---- Function ----
29
-
30
- /**
31
- * Write an agent `.md` file with an overwrite guard.
32
- *
33
- * If `targetPath` already exists, prompts the user for confirmation before writing.
34
- * On write: reloads the registry and notifies the user as `"${label} ${targetPath}"`.
35
- *
36
- * Returns `true` if the file was written, `false` if the user declined to overwrite.
37
- */
38
- export async function writeAgentFile(
39
- fileOps: FileWriter,
40
- ui: WriterUI,
41
- registry: Reloadable,
42
- targetPath: string,
43
- content: string,
44
- label: string,
45
- ): Promise<boolean> {
46
- if (fileOps.exists(targetPath)) {
47
- const overwrite = await ui.confirm("Overwrite", `${targetPath} already exists. Overwrite?`);
48
- if (!overwrite) return false;
49
- }
50
-
51
- fileOps.write(targetPath, content);
52
- registry.reload();
53
- ui.notify(`${label} ${targetPath}`, "info");
54
- return true;
55
- }
@@ -1,331 +0,0 @@
1
- /* eslint-disable @typescript-eslint/no-unsafe-assignment -- Pi SDK types are not fully exported; see upstream Pi SDK for type improvements */
2
- import { wrapTextWithAnsi } from "@earendil-works/pi-tui";
3
- import { AgentTypeRegistry } from "#src/config/agent-types";
4
- import type { ParentSnapshot } from "#src/lifecycle/parent-snapshot";
5
- import { type ModelRegistry, resolveModel } from "#src/session/model-resolver";
6
- import { getModelLabelFromConfig } from "#src/tools/helpers";
7
- import type { AgentConfig, Subagent } from "#src/types";
8
- import { AgentConfigEditor } from "#src/ui/agent-config-editor";
9
- import { AgentCreationWizard } from "#src/ui/agent-creation-wizard";
10
- import type { AgentFileOps } from "#src/ui/agent-file-ops";
11
- import { formatDuration, getDisplayName } from "#src/ui/display";
12
-
13
- // ---- Narrow interfaces ----
14
-
15
- /** Narrow manager interface for menu operations. */
16
- export interface AgentMenuManager {
17
- listAgents: () => Subagent[];
18
- getRecord: (id: string) => Subagent | undefined;
19
- /** Used by generate wizard to spawn an agent that writes the .md file. */
20
- spawnAndWait: (
21
- parentSnapshot: ParentSnapshot,
22
- type: string,
23
- prompt: string,
24
- opts: { description: string; maxTurns: number },
25
- ) => Promise<Subagent>;
26
- }
27
-
28
- /** Narrow settings interface required by the agent menu. */
29
- export interface AgentMenuSettings {
30
- readonly maxConcurrent: number;
31
- readonly defaultMaxTurns: number | undefined;
32
- readonly graceTurns: number;
33
- applyMaxConcurrent(n: number): { message: string; level: "info" | "warning" };
34
- applyDefaultMaxTurns(n: number): { message: string; level: "info" | "warning" };
35
- applyGraceTurns(n: number): { message: string; level: "info" | "warning" };
36
- }
37
-
38
- // ---- Narrow UI context types ----
39
-
40
- /** Narrow UI interface — only the ctx.ui methods menu handlers actually call. */
41
- export interface MenuUI {
42
- select(title: string, options: string[]): Promise<string | undefined>;
43
- confirm(title: string, message: string): Promise<boolean>;
44
- input(title: string, defaultValue?: string): Promise<string | undefined>;
45
- notify(message: string, level: "info" | "warning" | "error"): void;
46
- editor(title: string, content: string): Promise<string | undefined>;
47
- custom<R>(component: any, options?: any): Promise<R>;
48
- }
49
-
50
- // ---- Class ----
51
-
52
- /**
53
- * Handler for the `/agents` slash command.
54
- *
55
- * Call `handle(ctx)` from the Pi command registration to open the interactive menu.
56
- */
57
- export class AgentsMenuHandler {
58
- private readonly editor: AgentConfigEditor;
59
- private readonly wizard: AgentCreationWizard;
60
-
61
- constructor(
62
- private readonly manager: AgentMenuManager,
63
- private readonly registry: AgentTypeRegistry,
64
- private readonly settings: AgentMenuSettings,
65
- fileOps: AgentFileOps,
66
- personalAgentsDir: string,
67
- projectAgentsDir: string,
68
- ) {
69
- this.editor = new AgentConfigEditor(
70
- fileOps,
71
- registry,
72
- personalAgentsDir,
73
- projectAgentsDir,
74
- );
75
- this.wizard = new AgentCreationWizard(
76
- fileOps,
77
- manager,
78
- registry,
79
- personalAgentsDir,
80
- projectAgentsDir,
81
- );
82
- }
83
-
84
- async handle({
85
- ui,
86
- modelRegistry,
87
- parentSnapshot,
88
- }: {
89
- ui: MenuUI;
90
- modelRegistry: ModelRegistry;
91
- parentSnapshot: ParentSnapshot;
92
- }): Promise<void> {
93
- await this.showAgentsMenu(ui, modelRegistry, parentSnapshot);
94
- }
95
-
96
- private getModelLabel(type: string, modelRegistry?: ModelRegistry): string {
97
- const cfg = this.registry.resolveAgentConfig(type);
98
- if (!cfg.model) return "inherit";
99
- if (modelRegistry) {
100
- const resolved = resolveModel(cfg.model, modelRegistry);
101
- if (typeof resolved === "string") return "inherit";
102
- }
103
- return getModelLabelFromConfig(cfg.model);
104
- }
105
-
106
- private async showAgentsMenu(
107
- ui: MenuUI,
108
- modelRegistry: ModelRegistry,
109
- parentSnapshot: ParentSnapshot,
110
- ): Promise<void> {
111
- this.registry.reload();
112
- const allNames = this.registry.getAllTypes();
113
-
114
- const options: string[] = [];
115
-
116
- const agents = this.manager.listAgents();
117
- if (agents.length > 0) {
118
- const running = agents.filter(
119
- (a) => a.status === "running" || a.status === "queued",
120
- ).length;
121
- const done = agents.filter(
122
- (a) => a.status === "completed" || a.status === "steered",
123
- ).length;
124
- options.push(
125
- `Running agents (${agents.length}) — ${running} running, ${done} done`,
126
- );
127
- }
128
-
129
- if (allNames.length > 0) {
130
- options.push(`Agent types (${allNames.length})`);
131
- }
132
-
133
- options.push("Create new agent");
134
- options.push("Settings");
135
-
136
- const noAgentsMsg =
137
- allNames.length === 0 && agents.length === 0
138
- ? "No agents found. Create specialized subagents that can be delegated to.\n\n" +
139
- "Each subagent has its own context window, custom system prompt, and specific tools.\n\n" +
140
- "Try creating: Code Reviewer, Security Auditor, Test Writer, or Documentation Writer.\n\n"
141
- : "";
142
-
143
- if (noAgentsMsg) {
144
- ui.notify(noAgentsMsg, "info");
145
- }
146
-
147
- const choice = await ui.select("Agents", options);
148
- if (!choice) return;
149
-
150
- if (choice.startsWith("Running agents (")) {
151
- await this.showRunningAgents(ui);
152
- await this.showAgentsMenu(ui, modelRegistry, parentSnapshot);
153
- } else if (choice.startsWith("Agent types (")) {
154
- await this.showAllAgentsList(ui, modelRegistry);
155
- await this.showAgentsMenu(ui, modelRegistry, parentSnapshot);
156
- } else if (choice === "Create new agent") {
157
- await this.wizard.showCreateWizard(ui, parentSnapshot);
158
- } else if (choice === "Settings") {
159
- await this.showSettings(ui);
160
- await this.showAgentsMenu(ui, modelRegistry, parentSnapshot);
161
- }
162
- }
163
-
164
- private async showAllAgentsList(ui: MenuUI, modelRegistry: ModelRegistry): Promise<void> {
165
- const allNames = this.registry.getAllTypes();
166
- if (allNames.length === 0) {
167
- ui.notify("No agents.", "info");
168
- return;
169
- }
170
-
171
- const sourceIndicator = (cfg: AgentConfig | undefined) => {
172
- const disabled = cfg?.enabled === false;
173
- if (cfg?.source === "project") return disabled ? "✕• " : "• ";
174
- if (cfg?.source === "global") return disabled ? "✕◦ " : "◦ ";
175
- if (disabled) return "✕ ";
176
- return " ";
177
- };
178
-
179
- const entries = allNames.map((name) => {
180
- const cfg = this.registry.resolveAgentConfig(name);
181
- const disabled = cfg.enabled === false;
182
- const model = this.getModelLabel(name, modelRegistry);
183
- const indicator = sourceIndicator(cfg);
184
- const prefix = `${indicator}${name} · ${model}`;
185
- const desc = disabled ? "(disabled)" : cfg.description;
186
- return { name, prefix, desc };
187
- });
188
- const maxPrefix = Math.max(...entries.map((e) => e.prefix.length));
189
-
190
- const hasCustom = allNames.some((n) => {
191
- const c = this.registry.resolveAgentConfig(n);
192
- return !c.isDefault && c.enabled !== false;
193
- });
194
- const hasDisabled = allNames.some(
195
- (n) => this.registry.resolveAgentConfig(n).enabled === false,
196
- );
197
- const legendParts: string[] = [];
198
- if (hasCustom) legendParts.push("• = project ◦ = global");
199
- if (hasDisabled) legendParts.push("✕ = disabled");
200
- const legend = legendParts.length ? "\n" + legendParts.join(" ") : "";
201
-
202
- const options = entries.map(
203
- ({ prefix, desc }) => `${prefix.padEnd(maxPrefix)} — ${desc}`,
204
- );
205
- if (legend) options.push(legend);
206
-
207
- const choice = await ui.select("Agent types", options);
208
- if (!choice) return;
209
-
210
- const agentName = choice
211
- .split(" · ")[0]
212
- .replace(/^[•◦✕\s]+/, "")
213
- .trim();
214
- if (this.registry.resolveType(agentName) != null) {
215
- await this.editor.showAgentDetail(ui, agentName);
216
- await this.showAllAgentsList(ui, modelRegistry);
217
- }
218
- }
219
-
220
- private async showRunningAgents(ui: MenuUI): Promise<void> {
221
- const agents = this.manager.listAgents();
222
- if (agents.length === 0) {
223
- ui.notify("No agents.", "info");
224
- return;
225
- }
226
-
227
- const options = agents.map((a) => {
228
- const dn = getDisplayName(a.type, this.registry);
229
- const dur = formatDuration(a.startedAt, a.completedAt);
230
- return `${dn} (${a.description}) · ${a.toolUses} tools · ${a.status} · ${dur}`;
231
- });
232
-
233
- const choice = await ui.select("Running agents", options);
234
- if (!choice) return;
235
-
236
- const idx = options.indexOf(choice);
237
- if (idx < 0) return;
238
- const record = agents[idx];
239
-
240
- await this.viewAgentConversation(ui, record);
241
- await this.showRunningAgents(ui);
242
- }
243
-
244
- private async viewAgentConversation(ui: MenuUI, record: Subagent): Promise<void> {
245
- if (!record.isSessionReady()) {
246
- ui.notify(
247
- `Agent is ${record.status === "queued" ? "queued" : "expired"} — no session available.`,
248
- "info",
249
- );
250
- return;
251
- }
252
-
253
- const { ConversationViewer, VIEWPORT_HEIGHT_PCT } = await import(
254
- "./conversation-viewer"
255
- );
256
-
257
- await ui.custom<undefined>(
258
- (tui: any, theme: any, _keybindings: any, done: any) => {
259
- return new ConversationViewer({
260
- tui,
261
- record,
262
- theme,
263
- done,
264
- registry: this.registry,
265
- wrapText: wrapTextWithAnsi,
266
- });
267
- },
268
- {
269
- overlay: true,
270
- overlayOptions: {
271
- anchor: "center",
272
- width: "90%",
273
- maxHeight: `${VIEWPORT_HEIGHT_PCT}%`,
274
- },
275
- },
276
- );
277
- }
278
-
279
- private async showSettings(ui: MenuUI): Promise<void> {
280
- const choice = await ui.select("Settings", [
281
- `Max concurrency (current: ${this.settings.maxConcurrent})`,
282
- `Default max turns (current: ${this.settings.defaultMaxTurns ?? "unlimited"})`,
283
- `Grace turns (current: ${this.settings.graceTurns})`,
284
- ]);
285
- if (!choice) return;
286
-
287
- if (choice.startsWith("Max concurrency")) {
288
- const val = await ui.input(
289
- "Max concurrent background agents",
290
- String(this.settings.maxConcurrent),
291
- );
292
- if (val) {
293
- const n = parseInt(val, 10);
294
- if (n >= 1) {
295
- const toast = this.settings.applyMaxConcurrent(n);
296
- ui.notify(toast.message, toast.level);
297
- } else {
298
- ui.notify("Must be a positive integer.", "warning");
299
- }
300
- }
301
- } else if (choice.startsWith("Default max turns")) {
302
- const val = await ui.input(
303
- "Default max turns before wrap-up (0 = unlimited)",
304
- String(this.settings.defaultMaxTurns ?? 0),
305
- );
306
- if (val) {
307
- const n = parseInt(val, 10);
308
- if (n >= 0) {
309
- const toast = this.settings.applyDefaultMaxTurns(n);
310
- ui.notify(toast.message, toast.level);
311
- } else {
312
- ui.notify("Must be 0 (unlimited) or a positive integer.", "warning");
313
- }
314
- }
315
- } else if (choice.startsWith("Grace turns")) {
316
- const val = await ui.input(
317
- "Grace turns after wrap-up steer",
318
- String(this.settings.graceTurns),
319
- );
320
- if (val) {
321
- const n = parseInt(val, 10);
322
- if (n >= 1) {
323
- const toast = this.settings.applyGraceTurns(n);
324
- ui.notify(toast.message, toast.level);
325
- } else {
326
- ui.notify("Must be a positive integer.", "warning");
327
- }
328
- }
329
- }
330
- }
331
- }