@oh-my-pi/pi-coding-agent 4.6.0 → 4.8.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 +23 -0
- package/package.json +8 -6
- package/src/cli/config-cli.ts +344 -0
- package/src/core/agent-session.ts +112 -1
- package/src/core/extensions/types.ts +2 -0
- package/src/core/settings-manager.ts +37 -0
- package/src/core/tools/ask.ts +272 -99
- package/src/core/tools/task/model-resolver.ts +28 -2
- package/src/main.ts +12 -0
- package/src/modes/interactive/components/hook-selector.ts +3 -1
- package/src/modes/interactive/components/index.ts +1 -0
- package/src/modes/interactive/components/read-tool-group.ts +12 -4
- package/src/modes/interactive/components/settings-defs.ts +9 -0
- package/src/modes/interactive/components/todo-display.ts +1 -1
- package/src/modes/interactive/components/todo-reminder.ts +42 -0
- package/src/modes/interactive/controllers/command-controller.ts +1 -0
- package/src/modes/interactive/controllers/event-controller.ts +8 -0
- package/src/modes/interactive/controllers/extension-ui-controller.ts +9 -2
- package/src/modes/interactive/controllers/input-controller.ts +1 -1
- package/src/modes/interactive/controllers/selector-controller.ts +2 -0
- package/src/modes/interactive/interactive-mode.ts +8 -3
- package/src/modes/interactive/types.ts +2 -1
- package/src/prompts/tools/ask.md +14 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [4.8.0] - 2026-01-12
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Move `sharp` to optional dependencies with all platform binaries to fix arm64 runtime errors
|
|
10
|
+
|
|
11
|
+
## [4.7.0] - 2026-01-12
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Add `omp config` subcommand for managing settings (`list`, `get`, `set`, `reset`, `path`)
|
|
15
|
+
- Add `todoCompletion` setting to warn agent when it stops with incomplete todos (up to 3 reminders)
|
|
16
|
+
- Add multi-part questions support to `ask` tool via `questions` array parameter
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- Updated multi-select cursor behavior in `ask` tool to stay on the toggled option instead of jumping to top
|
|
21
|
+
- Single-file reads now render inline (e.g., `Read AGENTS.md:23`) instead of tree structure
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- Subagent model resolution now respects explicit provider prefix (e.g., `zai/glm-4.7` no longer matches `cerebras/zai-glm-4.7`)
|
|
26
|
+
- Auto-compaction now skips to next model candidate when retry delay exceeds 30 seconds
|
|
27
|
+
|
|
5
28
|
## [4.6.0] - 2026-01-12
|
|
6
29
|
|
|
7
30
|
### Added
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.8.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.
|
|
43
|
-
"@oh-my-pi/pi-agent-core": "4.
|
|
44
|
-
"@oh-my-pi/pi-git-tool": "4.
|
|
45
|
-
"@oh-my-pi/pi-tui": "4.
|
|
42
|
+
"@oh-my-pi/pi-ai": "4.8.0",
|
|
43
|
+
"@oh-my-pi/pi-agent-core": "4.8.0",
|
|
44
|
+
"@oh-my-pi/pi-git-tool": "4.8.0",
|
|
45
|
+
"@oh-my-pi/pi-tui": "4.8.0",
|
|
46
46
|
"@openai/agents": "^0.3.7",
|
|
47
47
|
"@sinclair/typebox": "^0.34.46",
|
|
48
48
|
"ajv": "^8.17.1",
|
|
@@ -58,7 +58,6 @@
|
|
|
58
58
|
"nanoid": "^5.1.6",
|
|
59
59
|
"ndjson": "^2.0.0",
|
|
60
60
|
"node-html-parser": "^6.1.13",
|
|
61
|
-
"sharp": "^0.34.2",
|
|
62
61
|
"smol-toml": "^1.6.0",
|
|
63
62
|
"strip-ansi": "^7.1.2",
|
|
64
63
|
"winston": "^3.17.0",
|
|
@@ -72,6 +71,9 @@
|
|
|
72
71
|
"@types/node": "^24.3.0",
|
|
73
72
|
"ms": "^2.1.3"
|
|
74
73
|
},
|
|
74
|
+
"optionalDependencies": {
|
|
75
|
+
"sharp": "^0.34.2"
|
|
76
|
+
},
|
|
75
77
|
"keywords": [
|
|
76
78
|
"coding-agent",
|
|
77
79
|
"ai",
|
|
@@ -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
|
}
|