@oh-my-pi/pi-coding-agent 4.6.0 → 4.7.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,23 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [4.7.0] - 2026-01-12
6
+ ### Added
7
+
8
+ - Add `omp config` subcommand for managing settings (`list`, `get`, `set`, `reset`, `path`)
9
+ - Add `todoCompletion` setting to warn agent when it stops with incomplete todos (up to 3 reminders)
10
+ - Add multi-part questions support to `ask` tool via `questions` array parameter
11
+
12
+ ### Changed
13
+
14
+ - Updated multi-select cursor behavior in `ask` tool to stay on the toggled option instead of jumping to top
15
+ - Single-file reads now render inline (e.g., `Read AGENTS.md:23`) instead of tree structure
16
+
17
+ ### Fixed
18
+
19
+ - Subagent model resolution now respects explicit provider prefix (e.g., `zai/glm-4.7` no longer matches `cerebras/zai-glm-4.7`)
20
+ - Auto-compaction now skips to next model candidate when retry delay exceeds 30 seconds
21
+
5
22
  ## [4.6.0] - 2026-01-12
6
23
 
7
24
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-coding-agent",
3
- "version": "4.6.0",
3
+ "version": "4.7.0",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "ompConfig": {
@@ -39,10 +39,10 @@
39
39
  "prepublishOnly": "bun run generate-template && bun run clean && bun run build"
40
40
  },
41
41
  "dependencies": {
42
- "@oh-my-pi/pi-ai": "4.6.0",
43
- "@oh-my-pi/pi-agent-core": "4.6.0",
44
- "@oh-my-pi/pi-git-tool": "4.6.0",
45
- "@oh-my-pi/pi-tui": "4.6.0",
42
+ "@oh-my-pi/pi-ai": "4.7.0",
43
+ "@oh-my-pi/pi-agent-core": "4.7.0",
44
+ "@oh-my-pi/pi-git-tool": "4.7.0",
45
+ "@oh-my-pi/pi-tui": "4.7.0",
46
46
  "@openai/agents": "^0.3.7",
47
47
  "@sinclair/typebox": "^0.34.46",
48
48
  "ajv": "^8.17.1",
@@ -0,0 +1,344 @@
1
+ /**
2
+ * Config CLI command handlers.
3
+ *
4
+ * Handles `omp config <command>` subcommands for managing settings.
5
+ * Uses SETTINGS_DEFS as the source of truth for available settings.
6
+ */
7
+
8
+ import chalk from "chalk";
9
+ import { APP_NAME, getAgentDir } from "../config";
10
+ import { SettingsManager } from "../core/settings-manager";
11
+ import { SETTINGS_DEFS, type SettingDef } from "../modes/interactive/components/settings-defs";
12
+ import { theme } from "../modes/interactive/theme/theme";
13
+
14
+ // =============================================================================
15
+ // Types
16
+ // =============================================================================
17
+
18
+ export type ConfigAction = "list" | "get" | "set" | "reset" | "path";
19
+
20
+ export interface ConfigCommandArgs {
21
+ action: ConfigAction;
22
+ key?: string;
23
+ value?: string;
24
+ flags: {
25
+ json?: boolean;
26
+ };
27
+ }
28
+
29
+ // =============================================================================
30
+ // Setting Filtering
31
+ // =============================================================================
32
+
33
+ /** Find setting definition by ID */
34
+ function findSettingDef(id: string): SettingDef | undefined {
35
+ return SETTINGS_DEFS.find((def) => def.id === id);
36
+ }
37
+
38
+ /** Get available values for a setting */
39
+ function getSettingValues(def: SettingDef, sm: SettingsManager): readonly string[] | undefined {
40
+ if (def.type === "enum") {
41
+ return def.values;
42
+ }
43
+ if (def.type === "submenu") {
44
+ const options = def.getOptions(sm);
45
+ if (options.length > 0) {
46
+ return options.map((o) => o.value);
47
+ }
48
+ }
49
+ return undefined;
50
+ }
51
+
52
+ // =============================================================================
53
+ // Argument Parser
54
+ // =============================================================================
55
+
56
+ const VALID_ACTIONS: ConfigAction[] = ["list", "get", "set", "reset", "path"];
57
+
58
+ /**
59
+ * Parse config subcommand arguments.
60
+ * Returns undefined if not a config command.
61
+ */
62
+ export function parseConfigArgs(args: string[]): ConfigCommandArgs | undefined {
63
+ if (args.length === 0 || args[0] !== "config") {
64
+ return undefined;
65
+ }
66
+
67
+ if (args.length < 2 || args[1] === "--help" || args[1] === "-h") {
68
+ return { action: "list", flags: {} };
69
+ }
70
+
71
+ const action = args[1];
72
+ if (!VALID_ACTIONS.includes(action as ConfigAction)) {
73
+ console.error(chalk.red(`Unknown config command: ${action}`));
74
+ console.error(`Valid commands: ${VALID_ACTIONS.join(", ")}`);
75
+ process.exit(1);
76
+ }
77
+
78
+ const result: ConfigCommandArgs = {
79
+ action: action as ConfigAction,
80
+ flags: {},
81
+ };
82
+
83
+ const positionalArgs: string[] = [];
84
+ for (let i = 2; i < args.length; i++) {
85
+ const arg = args[i];
86
+ if (arg === "--json") {
87
+ result.flags.json = true;
88
+ } else if (!arg.startsWith("-")) {
89
+ positionalArgs.push(arg);
90
+ }
91
+ }
92
+
93
+ if (positionalArgs.length > 0) {
94
+ result.key = positionalArgs[0];
95
+ }
96
+ if (positionalArgs.length > 1) {
97
+ result.value = positionalArgs.slice(1).join(" ");
98
+ }
99
+
100
+ return result;
101
+ }
102
+
103
+ // =============================================================================
104
+ // Value Parsing
105
+ // =============================================================================
106
+
107
+ function parseValue(value: string, def: SettingDef, sm: SettingsManager): unknown {
108
+ if (def.type === "boolean") {
109
+ const lower = value.toLowerCase();
110
+ if (lower === "true" || lower === "1" || lower === "yes" || lower === "on") {
111
+ return true;
112
+ }
113
+ if (lower === "false" || lower === "0" || lower === "no" || lower === "off") {
114
+ return false;
115
+ }
116
+ throw new Error(`Invalid boolean value: ${value}. Use true/false, yes/no, on/off, or 1/0`);
117
+ }
118
+
119
+ const validValues = getSettingValues(def, sm);
120
+ if (validValues && validValues.length > 0 && !validValues.includes(value)) {
121
+ throw new Error(`Invalid value: ${value}. Valid values: ${validValues.join(", ")}`);
122
+ }
123
+
124
+ return value;
125
+ }
126
+
127
+ function formatValue(value: unknown): string {
128
+ if (value === undefined || value === null) {
129
+ return chalk.dim("(not set)");
130
+ }
131
+ if (typeof value === "boolean") {
132
+ return value ? chalk.green("true") : chalk.red("false");
133
+ }
134
+ if (typeof value === "number") {
135
+ return chalk.cyan(String(value));
136
+ }
137
+ return chalk.yellow(String(value));
138
+ }
139
+
140
+ function getTypeDisplay(def: SettingDef, sm: SettingsManager): string {
141
+ if (def.type === "boolean") {
142
+ return "(boolean)";
143
+ }
144
+ const values = getSettingValues(def, sm);
145
+ if (values && values.length > 0) {
146
+ return `(${values.join("|")})`;
147
+ }
148
+ return "(string)";
149
+ }
150
+
151
+ // =============================================================================
152
+ // Command Handlers
153
+ // =============================================================================
154
+
155
+ export async function runConfigCommand(cmd: ConfigCommandArgs): Promise<void> {
156
+ const settingsManager = await SettingsManager.create();
157
+
158
+ switch (cmd.action) {
159
+ case "list":
160
+ handleList(settingsManager, cmd.flags);
161
+ break;
162
+ case "get":
163
+ handleGet(settingsManager, cmd.key, cmd.flags);
164
+ break;
165
+ case "set":
166
+ await handleSet(settingsManager, cmd.key, cmd.value, cmd.flags);
167
+ break;
168
+ case "reset":
169
+ await handleReset(settingsManager, cmd.key, cmd.flags);
170
+ break;
171
+ case "path":
172
+ handlePath();
173
+ break;
174
+ }
175
+ }
176
+
177
+ function handleList(settingsManager: SettingsManager, flags: { json?: boolean }): void {
178
+ if (flags.json) {
179
+ const result: Record<string, { value: unknown; type: string; description: string }> = {};
180
+ for (const def of SETTINGS_DEFS) {
181
+ result[def.id] = {
182
+ value: def.get(settingsManager),
183
+ type: def.type,
184
+ description: def.description,
185
+ };
186
+ }
187
+ console.log(JSON.stringify(result, null, 2));
188
+ return;
189
+ }
190
+
191
+ console.log(chalk.bold("Settings:\n"));
192
+
193
+ // Group by tab
194
+ const groups: Record<string, SettingDef[]> = {};
195
+ for (const def of SETTINGS_DEFS) {
196
+ if (!groups[def.tab]) {
197
+ groups[def.tab] = [];
198
+ }
199
+ groups[def.tab].push(def);
200
+ }
201
+
202
+ const sortedGroups = Object.keys(groups).sort((a, b) => {
203
+ if (a === "config") return -1;
204
+ if (b === "config") return 1;
205
+ return a.localeCompare(b);
206
+ });
207
+
208
+ for (const group of sortedGroups) {
209
+ console.log(chalk.bold.blue(`[${group}]`));
210
+ for (const def of groups[group]) {
211
+ const value = def.get(settingsManager);
212
+ const valueStr = formatValue(value);
213
+ const typeStr = getTypeDisplay(def, settingsManager);
214
+ console.log(` ${chalk.white(def.id)} = ${valueStr} ${chalk.dim(typeStr)}`);
215
+ }
216
+ console.log("");
217
+ }
218
+ }
219
+
220
+ function handleGet(settingsManager: SettingsManager, key: string | undefined, flags: { json?: boolean }): void {
221
+ if (!key) {
222
+ console.error(chalk.red(`Usage: ${APP_NAME} config get <key>`));
223
+ console.error(chalk.dim(`\nRun '${APP_NAME} config list' to see available keys`));
224
+ process.exit(1);
225
+ }
226
+
227
+ const def = findSettingDef(key);
228
+ if (!def) {
229
+ console.error(chalk.red(`Unknown setting: ${key}`));
230
+ console.error(chalk.dim(`\nRun '${APP_NAME} config list' to see available keys`));
231
+ process.exit(1);
232
+ }
233
+
234
+ const value = def.get(settingsManager);
235
+
236
+ if (flags.json) {
237
+ console.log(JSON.stringify({ key: def.id, value, type: def.type, description: def.description }, null, 2));
238
+ return;
239
+ }
240
+
241
+ console.log(formatValue(value));
242
+ }
243
+
244
+ async function handleSet(
245
+ settingsManager: SettingsManager,
246
+ key: string | undefined,
247
+ value: string | undefined,
248
+ flags: { json?: boolean },
249
+ ): Promise<void> {
250
+ if (!key || value === undefined) {
251
+ console.error(chalk.red(`Usage: ${APP_NAME} config set <key> <value>`));
252
+ console.error(chalk.dim(`\nRun '${APP_NAME} config list' to see available keys`));
253
+ process.exit(1);
254
+ }
255
+
256
+ const def = findSettingDef(key);
257
+ if (!def) {
258
+ console.error(chalk.red(`Unknown setting: ${key}`));
259
+ console.error(chalk.dim(`\nRun '${APP_NAME} config list' to see available keys`));
260
+ process.exit(1);
261
+ }
262
+
263
+ let parsedValue: unknown;
264
+ try {
265
+ parsedValue = parseValue(value, def, settingsManager);
266
+ } catch (err) {
267
+ console.error(chalk.red(String(err)));
268
+ process.exit(1);
269
+ }
270
+
271
+ def.set(settingsManager, parsedValue as never);
272
+
273
+ if (flags.json) {
274
+ console.log(JSON.stringify({ key: def.id, value: parsedValue }));
275
+ } else {
276
+ console.log(chalk.green(`${theme.status.success} Set ${def.id} = ${formatValue(parsedValue)}`));
277
+ }
278
+ }
279
+
280
+ async function handleReset(
281
+ settingsManager: SettingsManager,
282
+ key: string | undefined,
283
+ flags: { json?: boolean },
284
+ ): Promise<void> {
285
+ if (!key) {
286
+ console.error(chalk.red(`Usage: ${APP_NAME} config reset <key>`));
287
+ console.error(chalk.dim(`\nRun '${APP_NAME} config list' to see available keys`));
288
+ process.exit(1);
289
+ }
290
+
291
+ const def = findSettingDef(key);
292
+ if (!def) {
293
+ console.error(chalk.red(`Unknown setting: ${key}`));
294
+ console.error(chalk.dim(`\nRun '${APP_NAME} config list' to see available keys`));
295
+ process.exit(1);
296
+ }
297
+
298
+ // Get default value from a fresh in-memory settings manager
299
+ const defaults = SettingsManager.inMemory();
300
+ const defaultValue = def.get(defaults);
301
+
302
+ def.set(settingsManager, defaultValue as never);
303
+
304
+ if (flags.json) {
305
+ console.log(JSON.stringify({ key: def.id, value: defaultValue }));
306
+ } else {
307
+ console.log(chalk.green(`${theme.status.success} Reset ${def.id} to ${formatValue(defaultValue)}`));
308
+ }
309
+ }
310
+
311
+ function handlePath(): void {
312
+ console.log(getAgentDir());
313
+ }
314
+
315
+ // =============================================================================
316
+ // Help
317
+ // =============================================================================
318
+
319
+ export function printConfigHelp(): void {
320
+ console.log(`${chalk.bold(`${APP_NAME} config`)} - Manage settings
321
+
322
+ ${chalk.bold("Commands:")}
323
+ list List all settings with current values
324
+ get <key> Get a specific setting value
325
+ set <key> <value> Set a setting value
326
+ reset <key> Reset a setting to its default value
327
+ path Print the config directory path
328
+
329
+ ${chalk.bold("Options:")}
330
+ --json Output as JSON
331
+
332
+ ${chalk.bold("Examples:")}
333
+ ${APP_NAME} config list
334
+ ${APP_NAME} config get theme
335
+ ${APP_NAME} config set theme catppuccin-mocha
336
+ ${APP_NAME} config set autoCompact false
337
+ ${APP_NAME} config set thinkingLevel medium
338
+ ${APP_NAME} config reset steeringMode
339
+ ${APP_NAME} config list --json
340
+
341
+ ${chalk.bold("Boolean Values:")}
342
+ true, false, yes, no, on, off, 1, 0
343
+ `);
344
+ }
@@ -57,6 +57,8 @@ import { expandSlashCommand, type FileSlashCommand } from "./slash-commands";
57
57
  import { closeAllConnections } from "./ssh/connection-manager";
58
58
  import { unmountAll } from "./ssh/sshfs-mount";
59
59
  import type { BashOperations } from "./tools/bash";
60
+ import { getArtifactsDir } from "./tools/task/artifacts";
61
+ import type { TodoItem } from "./tools/todo-write";
60
62
  import type { TtsrManager } from "./ttsr";
61
63
 
62
64
  /** Session-specific events that extend the core AgentEvent */
@@ -66,7 +68,8 @@ export type AgentSessionEvent =
66
68
  | { type: "auto_compaction_end"; result: CompactionResult | undefined; aborted: boolean; willRetry: boolean }
67
69
  | { type: "auto_retry_start"; attempt: number; maxAttempts: number; delayMs: number; errorMessage: string }
68
70
  | { type: "auto_retry_end"; success: boolean; attempt: number; finalError?: string }
69
- | { type: "ttsr_triggered"; rules: Rule[] };
71
+ | { type: "ttsr_triggered"; rules: Rule[] }
72
+ | { type: "todo_reminder"; todos: TodoItem[]; attempt: number; maxAttempts: number };
70
73
 
71
74
  /** Listener function for agent session events */
72
75
  export type AgentSessionEventListener = (event: AgentSessionEvent) => void;
@@ -228,6 +231,9 @@ export class AgentSession {
228
231
  private _retryPromise: Promise<void> | undefined = undefined;
229
232
  private _retryResolve: (() => void) | undefined = undefined;
230
233
 
234
+ // Todo completion reminder state
235
+ private _todoReminderCount = 0;
236
+
231
237
  // Bash execution state
232
238
  private _bashAbortController: AbortController | undefined = undefined;
233
239
  private _pendingBashMessages: BashExecutionMessage[] = [];
@@ -442,6 +448,11 @@ export class AgentSession {
442
448
  }
443
449
 
444
450
  await this._checkCompaction(msg);
451
+
452
+ // Check for incomplete todos (unless there was an error or abort)
453
+ if (msg.stopReason !== "error" && msg.stopReason !== "aborted") {
454
+ await this._checkTodoCompletion();
455
+ }
445
456
  }
446
457
  };
447
458
 
@@ -757,6 +768,9 @@ export class AgentSession {
757
768
  // Flush any pending bash messages before the new prompt
758
769
  this._flushPendingBashMessages();
759
770
 
771
+ // Reset todo reminder count on new user prompt
772
+ this._todoReminderCount = 0;
773
+
760
774
  // Validate model
761
775
  if (!this.model) {
762
776
  throw new Error(
@@ -1218,6 +1232,7 @@ export class AgentSession {
1218
1232
  this._steeringMessages = [];
1219
1233
  this._followUpMessages = [];
1220
1234
  this._pendingNextTurnMessages = [];
1235
+ this._todoReminderCount = 0;
1221
1236
  this._reconnectToAgent();
1222
1237
 
1223
1238
  // Emit session_switch event with reason "new" to hooks
@@ -1702,6 +1717,83 @@ export class AgentSession {
1702
1717
  }
1703
1718
  }
1704
1719
 
1720
+ /**
1721
+ * Check if agent stopped with incomplete todos and prompt to continue.
1722
+ */
1723
+ private async _checkTodoCompletion(): Promise<void> {
1724
+ const settings = this.settingsManager.getTodoCompletionSettings();
1725
+ if (!settings.enabled) {
1726
+ this._todoReminderCount = 0;
1727
+ return;
1728
+ }
1729
+
1730
+ const maxReminders = settings.maxReminders ?? 3;
1731
+ if (this._todoReminderCount >= maxReminders) {
1732
+ logger.debug("Todo completion: max reminders reached", { count: this._todoReminderCount });
1733
+ return;
1734
+ }
1735
+
1736
+ // Load current todos from artifacts
1737
+ const sessionFile = this.sessionManager.getSessionFile();
1738
+ if (!sessionFile) return;
1739
+
1740
+ const artifactsDir = getArtifactsDir(sessionFile);
1741
+ if (!artifactsDir) return;
1742
+
1743
+ const todoPath = `${artifactsDir}/todos.json`;
1744
+ const file = Bun.file(todoPath);
1745
+ if (!(await file.exists())) {
1746
+ this._todoReminderCount = 0;
1747
+ return;
1748
+ }
1749
+
1750
+ let todos: TodoItem[];
1751
+ try {
1752
+ const data = await file.json();
1753
+ todos = data?.todos ?? [];
1754
+ } catch {
1755
+ return;
1756
+ }
1757
+
1758
+ // Check for incomplete todos
1759
+ const incomplete = todos.filter((t) => t.status !== "completed");
1760
+ if (incomplete.length === 0) {
1761
+ this._todoReminderCount = 0;
1762
+ return;
1763
+ }
1764
+
1765
+ // Build reminder message
1766
+ this._todoReminderCount++;
1767
+ const todoList = incomplete.map((t) => `- ${t.content}`).join("\n");
1768
+ const reminder =
1769
+ `<system_reminder>\n` +
1770
+ `You stopped with ${incomplete.length} incomplete todo item(s):\n${todoList}\n\n` +
1771
+ `Please continue working on these tasks or mark them complete if finished.\n` +
1772
+ `(Reminder ${this._todoReminderCount}/${maxReminders})\n` +
1773
+ `</system_reminder>`;
1774
+
1775
+ logger.debug("Todo completion: sending reminder", {
1776
+ incomplete: incomplete.length,
1777
+ attempt: this._todoReminderCount,
1778
+ });
1779
+
1780
+ // Emit event for UI to render notification
1781
+ this._emit({
1782
+ type: "todo_reminder",
1783
+ todos: incomplete,
1784
+ attempt: this._todoReminderCount,
1785
+ maxAttempts: maxReminders,
1786
+ });
1787
+
1788
+ // Inject reminder and continue the conversation
1789
+ this.agent.appendMessage({
1790
+ role: "user",
1791
+ content: [{ type: "text", text: reminder }],
1792
+ timestamp: Date.now(),
1793
+ });
1794
+ this.agent.continue().catch(() => {});
1795
+ }
1796
+
1705
1797
  private _getModelKey(model: Model<any>): string {
1706
1798
  return `${model.provider}/${model.id}`;
1707
1799
  }
@@ -1862,6 +1954,24 @@ export class AgentSession {
1862
1954
 
1863
1955
  const baseDelayMs = retrySettings.baseDelayMs * 2 ** attempt;
1864
1956
  const delayMs = retryAfterMs !== undefined ? Math.max(baseDelayMs, retryAfterMs) : baseDelayMs;
1957
+
1958
+ // If retry delay is too long (>30s), try next candidate instead of waiting
1959
+ const maxAcceptableDelayMs = 30_000;
1960
+ if (delayMs > maxAcceptableDelayMs) {
1961
+ const hasMoreCandidates = candidates.indexOf(candidate) < candidates.length - 1;
1962
+ if (hasMoreCandidates) {
1963
+ logger.warn("Auto-compaction retry delay too long, trying next model", {
1964
+ delayMs,
1965
+ retryAfterMs,
1966
+ error: message,
1967
+ model: `${candidate.provider}/${candidate.id}`,
1968
+ });
1969
+ lastError = error;
1970
+ break; // Exit retry loop, continue to next candidate
1971
+ }
1972
+ // No more candidates - we have to wait
1973
+ }
1974
+
1865
1975
  attempt++;
1866
1976
  logger.warn("Auto-compaction failed, retrying", {
1867
1977
  attempt,
@@ -1869,6 +1979,7 @@ export class AgentSession {
1869
1979
  delayMs,
1870
1980
  retryAfterMs,
1871
1981
  error: message,
1982
+ model: `${candidate.provider}/${candidate.id}`,
1872
1983
  });
1873
1984
  await new Promise((resolve) => setTimeout(resolve, delayMs));
1874
1985
  }
@@ -46,6 +46,8 @@ export type { AppAction, KeybindingsManager } from "../keybindings";
46
46
  export interface ExtensionUIDialogOptions {
47
47
  signal?: AbortSignal;
48
48
  timeout?: number;
49
+ /** Initial cursor position for select dialogs (0-indexed) */
50
+ initialIndex?: number;
49
51
  }
50
52
 
51
53
  /**
@@ -124,6 +124,11 @@ export interface TtsrSettings {
124
124
  repeatGap?: number; // default: 10
125
125
  }
126
126
 
127
+ export interface TodoCompletionSettings {
128
+ enabled?: boolean; // default: false - warn agent when it stops with incomplete todos
129
+ maxReminders?: number; // default: 3 - maximum reminders before giving up
130
+ }
131
+
127
132
  export interface VoiceSettings {
128
133
  enabled?: boolean; // default: false
129
134
  transcriptionModel?: string; // default: "whisper-1"
@@ -207,6 +212,7 @@ export interface Settings {
207
212
  lsp?: LspSettings;
208
213
  edit?: EditSettings;
209
214
  ttsr?: TtsrSettings;
215
+ todoCompletion?: TodoCompletionSettings;
210
216
  voice?: VoiceSettings;
211
217
  providers?: ProviderSettings;
212
218
  disabledProviders?: string[]; // Discovery provider IDs that are disabled
@@ -767,6 +773,37 @@ export class SettingsManager {
767
773
  };
768
774
  }
769
775
 
776
+ getTodoCompletionSettings(): { enabled: boolean; maxReminders: number } {
777
+ return {
778
+ enabled: this.settings.todoCompletion?.enabled ?? false,
779
+ maxReminders: this.settings.todoCompletion?.maxReminders ?? 3,
780
+ };
781
+ }
782
+
783
+ getTodoCompletionEnabled(): boolean {
784
+ return this.settings.todoCompletion?.enabled ?? false;
785
+ }
786
+
787
+ async setTodoCompletionEnabled(enabled: boolean): Promise<void> {
788
+ if (!this.globalSettings.todoCompletion) {
789
+ this.globalSettings.todoCompletion = {};
790
+ }
791
+ this.globalSettings.todoCompletion.enabled = enabled;
792
+ await this.save();
793
+ }
794
+
795
+ getTodoCompletionMaxReminders(): number {
796
+ return this.settings.todoCompletion?.maxReminders ?? 3;
797
+ }
798
+
799
+ async setTodoCompletionMaxReminders(maxReminders: number): Promise<void> {
800
+ if (!this.globalSettings.todoCompletion) {
801
+ this.globalSettings.todoCompletion = {};
802
+ }
803
+ this.globalSettings.todoCompletion.maxReminders = maxReminders;
804
+ await this.save();
805
+ }
806
+
770
807
  getThinkingBudgets(): ThinkingBudgetsSettings | undefined {
771
808
  return this.settings.thinkingBudgets;
772
809
  }