@oh-my-pi/pi-coding-agent 6.8.5 → 6.9.69
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 +51 -0
- package/package.json +6 -6
- package/src/cli/stats-cli.ts +191 -0
- package/src/core/agent-session.ts +103 -1
- package/src/core/extensions/index.ts +2 -0
- package/src/core/extensions/runner.ts +31 -0
- package/src/core/extensions/types.ts +24 -0
- package/src/core/messages.ts +48 -0
- package/src/core/sdk.ts +0 -2
- package/src/core/session-manager.ts +10 -1
- package/src/core/settings-manager.ts +0 -105
- package/src/core/tools/bash.ts +5 -7
- package/src/core/tools/index.ts +1 -5
- package/src/core/tools/patch/applicator.ts +115 -17
- package/src/core/tools/patch/index.ts +1 -1
- package/src/core/tools/patch/normalize.ts +185 -10
- package/src/core/tools/python.ts +444 -86
- package/src/core/tools/task/executor.ts +2 -6
- package/src/core/tools/task/index.ts +30 -12
- package/src/core/tools/task/render.ts +163 -30
- package/src/core/tools/task/template.ts +37 -0
- package/src/core/tools/task/types.ts +6 -2
- package/src/core/tools/task/worker.ts +1 -1
- package/src/index.ts +2 -2
- package/src/main.ts +12 -0
- package/src/modes/interactive/components/python-execution.ts +180 -0
- package/src/modes/interactive/components/settings-defs.ts +0 -70
- package/src/modes/interactive/components/settings-selector.ts +0 -1
- package/src/modes/interactive/components/welcome.ts +1 -0
- package/src/modes/interactive/controllers/command-controller.ts +46 -0
- package/src/modes/interactive/controllers/event-controller.ts +0 -11
- package/src/modes/interactive/controllers/input-controller.ts +28 -1
- package/src/modes/interactive/controllers/selector-controller.ts +0 -9
- package/src/modes/interactive/interactive-mode.ts +10 -58
- package/src/modes/interactive/theme/dark.json +2 -9
- package/src/modes/interactive/theme/defaults/alabaster.json +2 -8
- package/src/modes/interactive/theme/defaults/amethyst.json +2 -9
- package/src/modes/interactive/theme/defaults/anthracite.json +2 -9
- package/src/modes/interactive/theme/defaults/basalt.json +89 -88
- package/src/modes/interactive/theme/defaults/birch.json +2 -8
- package/src/modes/interactive/theme/defaults/dark-abyss.json +2 -8
- package/src/modes/interactive/theme/defaults/dark-arctic.json +2 -9
- package/src/modes/interactive/theme/defaults/dark-aurora.json +3 -2
- package/src/modes/interactive/theme/defaults/dark-catppuccin.json +2 -1
- package/src/modes/interactive/theme/defaults/dark-cavern.json +2 -8
- package/src/modes/interactive/theme/defaults/dark-copper.json +3 -2
- package/src/modes/interactive/theme/defaults/dark-cosmos.json +2 -8
- package/src/modes/interactive/theme/defaults/dark-cyberpunk.json +2 -9
- package/src/modes/interactive/theme/defaults/dark-dracula.json +2 -9
- package/src/modes/interactive/theme/defaults/dark-eclipse.json +2 -8
- package/src/modes/interactive/theme/defaults/dark-ember.json +3 -2
- package/src/modes/interactive/theme/defaults/dark-equinox.json +2 -8
- package/src/modes/interactive/theme/defaults/dark-forest.json +2 -9
- package/src/modes/interactive/theme/defaults/dark-github.json +2 -9
- package/src/modes/interactive/theme/defaults/dark-gruvbox.json +2 -9
- package/src/modes/interactive/theme/defaults/dark-lavender.json +3 -2
- package/src/modes/interactive/theme/defaults/dark-lunar.json +2 -8
- package/src/modes/interactive/theme/defaults/dark-midnight.json +3 -2
- package/src/modes/interactive/theme/defaults/dark-monochrome.json +2 -9
- package/src/modes/interactive/theme/defaults/dark-monokai.json +2 -9
- package/src/modes/interactive/theme/defaults/dark-nebula.json +2 -8
- package/src/modes/interactive/theme/defaults/dark-nord.json +2 -9
- package/src/modes/interactive/theme/defaults/dark-ocean.json +2 -9
- package/src/modes/interactive/theme/defaults/dark-one.json +2 -9
- package/src/modes/interactive/theme/defaults/dark-rainforest.json +2 -8
- package/src/modes/interactive/theme/defaults/dark-reef.json +2 -8
- package/src/modes/interactive/theme/defaults/dark-retro.json +2 -9
- package/src/modes/interactive/theme/defaults/dark-rose-pine.json +2 -1
- package/src/modes/interactive/theme/defaults/dark-sakura.json +3 -2
- package/src/modes/interactive/theme/defaults/dark-slate.json +3 -2
- package/src/modes/interactive/theme/defaults/dark-solarized.json +2 -1
- package/src/modes/interactive/theme/defaults/dark-solstice.json +2 -8
- package/src/modes/interactive/theme/defaults/dark-starfall.json +2 -8
- package/src/modes/interactive/theme/defaults/dark-sunset.json +2 -9
- package/src/modes/interactive/theme/defaults/dark-swamp.json +2 -8
- package/src/modes/interactive/theme/defaults/dark-synthwave.json +2 -1
- package/src/modes/interactive/theme/defaults/dark-taiga.json +2 -8
- package/src/modes/interactive/theme/defaults/dark-terminal.json +3 -2
- package/src/modes/interactive/theme/defaults/dark-tokyo-night.json +2 -9
- package/src/modes/interactive/theme/defaults/dark-tundra.json +2 -8
- package/src/modes/interactive/theme/defaults/dark-twilight.json +2 -8
- package/src/modes/interactive/theme/defaults/dark-volcanic.json +2 -8
- package/src/modes/interactive/theme/defaults/graphite.json +2 -9
- package/src/modes/interactive/theme/defaults/light-arctic.json +2 -1
- package/src/modes/interactive/theme/defaults/light-aurora-day.json +2 -8
- package/src/modes/interactive/theme/defaults/light-canyon.json +2 -8
- package/src/modes/interactive/theme/defaults/light-catppuccin.json +2 -1
- package/src/modes/interactive/theme/defaults/light-cirrus.json +2 -8
- package/src/modes/interactive/theme/defaults/light-coral.json +3 -2
- package/src/modes/interactive/theme/defaults/light-cyberpunk.json +2 -9
- package/src/modes/interactive/theme/defaults/light-dawn.json +2 -8
- package/src/modes/interactive/theme/defaults/light-dunes.json +2 -8
- package/src/modes/interactive/theme/defaults/light-eucalyptus.json +3 -2
- package/src/modes/interactive/theme/defaults/light-forest.json +2 -9
- package/src/modes/interactive/theme/defaults/light-frost.json +3 -2
- package/src/modes/interactive/theme/defaults/light-github.json +2 -1
- package/src/modes/interactive/theme/defaults/light-glacier.json +2 -8
- package/src/modes/interactive/theme/defaults/light-gruvbox.json +2 -9
- package/src/modes/interactive/theme/defaults/light-haze.json +2 -8
- package/src/modes/interactive/theme/defaults/light-honeycomb.json +3 -2
- package/src/modes/interactive/theme/defaults/light-lagoon.json +2 -8
- package/src/modes/interactive/theme/defaults/light-lavender.json +3 -2
- package/src/modes/interactive/theme/defaults/light-meadow.json +2 -8
- package/src/modes/interactive/theme/defaults/light-mint.json +3 -2
- package/src/modes/interactive/theme/defaults/light-monochrome.json +2 -1
- package/src/modes/interactive/theme/defaults/light-ocean.json +2 -9
- package/src/modes/interactive/theme/defaults/light-one.json +2 -8
- package/src/modes/interactive/theme/defaults/light-opal.json +2 -8
- package/src/modes/interactive/theme/defaults/light-orchard.json +2 -8
- package/src/modes/interactive/theme/defaults/light-paper.json +3 -2
- package/src/modes/interactive/theme/defaults/light-prism.json +2 -8
- package/src/modes/interactive/theme/defaults/light-retro.json +2 -9
- package/src/modes/interactive/theme/defaults/light-sand.json +3 -2
- package/src/modes/interactive/theme/defaults/light-savanna.json +2 -8
- package/src/modes/interactive/theme/defaults/light-solarized.json +2 -1
- package/src/modes/interactive/theme/defaults/light-soleil.json +2 -8
- package/src/modes/interactive/theme/defaults/light-sunset.json +2 -9
- package/src/modes/interactive/theme/defaults/light-synthwave.json +2 -9
- package/src/modes/interactive/theme/defaults/light-tokyo-night.json +2 -9
- package/src/modes/interactive/theme/defaults/light-wetland.json +2 -8
- package/src/modes/interactive/theme/defaults/light-zenith.json +2 -8
- package/src/modes/interactive/theme/defaults/limestone.json +2 -8
- package/src/modes/interactive/theme/defaults/mahogany.json +2 -9
- package/src/modes/interactive/theme/defaults/marble.json +2 -8
- package/src/modes/interactive/theme/defaults/obsidian.json +89 -88
- package/src/modes/interactive/theme/defaults/onyx.json +89 -88
- package/src/modes/interactive/theme/defaults/pearl.json +2 -8
- package/src/modes/interactive/theme/defaults/porcelain.json +89 -88
- package/src/modes/interactive/theme/defaults/quartz.json +2 -8
- package/src/modes/interactive/theme/defaults/sandstone.json +2 -8
- package/src/modes/interactive/theme/defaults/titanium.json +88 -87
- package/src/modes/interactive/theme/light.json +2 -8
- package/src/modes/interactive/theme/theme-schema.json +5 -0
- package/src/modes/interactive/theme/theme.ts +7 -0
- package/src/modes/interactive/types.ts +5 -15
- package/src/modes/interactive/utils/ui-helpers.ts +20 -0
- package/src/prompts/system/system-prompt.md +8 -0
- package/src/prompts/tools/python.md +40 -2
- package/src/prompts/tools/task.md +8 -13
- package/src/core/custom-commands/bundled/wt/index.ts +0 -435
- package/src/core/tools/git.ts +0 -213
- package/src/core/voice-controller.ts +0 -135
- package/src/core/voice-supervisor.ts +0 -976
- package/src/core/voice.ts +0 -314
- package/src/lib/worktree/collapse.ts +0 -180
- package/src/lib/worktree/constants.ts +0 -14
- package/src/lib/worktree/errors.ts +0 -23
- package/src/lib/worktree/git.ts +0 -60
- package/src/lib/worktree/index.ts +0 -15
- package/src/lib/worktree/operations.ts +0 -216
- package/src/lib/worktree/session.ts +0 -114
- package/src/lib/worktree/stats.ts +0 -67
- package/src/modes/interactive/utils/voice-manager.ts +0 -96
- package/src/prompts/tools/git.md +0 -9
- package/src/prompts/voice-summary.md +0 -12
|
@@ -98,10 +98,6 @@ export interface BashInterceptorSettings {
|
|
|
98
98
|
patterns?: BashInterceptorRule[]; // default: built-in rules
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
export interface GitSettings {
|
|
102
|
-
enabled?: boolean; // default: false (structured git tool; use bash for git commands when disabled)
|
|
103
|
-
}
|
|
104
|
-
|
|
105
101
|
export interface MCPSettings {
|
|
106
102
|
enableProjectConfig?: boolean; // default: true (load .mcp.json from project root)
|
|
107
103
|
}
|
|
@@ -145,15 +141,6 @@ export interface TodoCompletionSettings {
|
|
|
145
141
|
maxReminders?: number; // default: 3 - maximum reminders before giving up
|
|
146
142
|
}
|
|
147
143
|
|
|
148
|
-
export interface VoiceSettings {
|
|
149
|
-
enabled?: boolean; // default: false
|
|
150
|
-
transcriptionModel?: string; // default: "whisper-1"
|
|
151
|
-
transcriptionLanguage?: string; // optional language hint (e.g., "en")
|
|
152
|
-
ttsModel?: string; // default: "gpt-4o-mini-tts"
|
|
153
|
-
ttsVoice?: string; // default: "alloy"
|
|
154
|
-
ttsFormat?: "wav" | "mp3" | "opus" | "aac" | "flac"; // default: "wav"
|
|
155
|
-
}
|
|
156
|
-
|
|
157
144
|
export type StatusLineSegmentId =
|
|
158
145
|
| "pi"
|
|
159
146
|
| "model"
|
|
@@ -224,14 +211,12 @@ export interface Settings {
|
|
|
224
211
|
enabledModels?: string[]; // Model patterns for cycling (same format as --models CLI flag)
|
|
225
212
|
exa?: ExaSettings;
|
|
226
213
|
bashInterceptor?: BashInterceptorSettings;
|
|
227
|
-
git?: GitSettings;
|
|
228
214
|
mcp?: MCPSettings;
|
|
229
215
|
lsp?: LspSettings;
|
|
230
216
|
python?: PythonSettings;
|
|
231
217
|
edit?: EditSettings;
|
|
232
218
|
ttsr?: TtsrSettings;
|
|
233
219
|
todoCompletion?: TodoCompletionSettings;
|
|
234
|
-
voice?: VoiceSettings;
|
|
235
220
|
providers?: ProviderSettings;
|
|
236
221
|
disabledProviders?: string[]; // Discovery provider IDs that are disabled
|
|
237
222
|
disabledExtensions?: string[]; // Individual extension IDs that are disabled (e.g., "skill:commit")
|
|
@@ -252,12 +237,6 @@ export const DEFAULT_BASH_INTERCEPTOR_RULES: BashInterceptorRule[] = [
|
|
|
252
237
|
tool: "grep",
|
|
253
238
|
message: "Use the `grep` tool instead of grep/rg. It respects .gitignore and provides structured output.",
|
|
254
239
|
},
|
|
255
|
-
{
|
|
256
|
-
pattern: "^\\s*git(\\s+|$)",
|
|
257
|
-
tool: "git",
|
|
258
|
-
message:
|
|
259
|
-
"Use the `git` tool instead of running git in bash. It provides structured output and safety confirmations.",
|
|
260
|
-
},
|
|
261
240
|
{
|
|
262
241
|
pattern: "^\\s*(find|fd|locate)\\s+.*(-name|-iname|-type|--type|-glob)",
|
|
263
242
|
tool: "find",
|
|
@@ -319,19 +298,11 @@ const DEFAULT_SETTINGS: Settings = {
|
|
|
319
298
|
enableWebsets: false,
|
|
320
299
|
},
|
|
321
300
|
bashInterceptor: DEFAULT_BASH_INTERCEPTOR_SETTINGS,
|
|
322
|
-
git: { enabled: false },
|
|
323
301
|
mcp: { enableProjectConfig: true },
|
|
324
302
|
lsp: { formatOnWrite: false, diagnosticsOnWrite: true, diagnosticsOnEdit: false },
|
|
325
303
|
python: { toolMode: "both", kernelMode: "session", sharedGateway: true },
|
|
326
304
|
edit: { fuzzyMatch: true, fuzzyThreshold: 0.95, streamingAbort: false },
|
|
327
305
|
ttsr: { enabled: true, contextMode: "discard", repeatMode: "once", repeatGap: 10 },
|
|
328
|
-
voice: {
|
|
329
|
-
enabled: false,
|
|
330
|
-
transcriptionModel: "whisper-1",
|
|
331
|
-
ttsModel: "gpt-4o-mini-tts",
|
|
332
|
-
ttsVoice: "alloy",
|
|
333
|
-
ttsFormat: "wav",
|
|
334
|
-
},
|
|
335
306
|
providers: { webSearch: "auto", image: "auto" },
|
|
336
307
|
} satisfies Settings;
|
|
337
308
|
|
|
@@ -1170,10 +1141,6 @@ export class SettingsManager {
|
|
|
1170
1141
|
await this.save();
|
|
1171
1142
|
}
|
|
1172
1143
|
|
|
1173
|
-
getGitToolEnabled(): boolean {
|
|
1174
|
-
return this.settings.git?.enabled ?? false;
|
|
1175
|
-
}
|
|
1176
|
-
|
|
1177
1144
|
getPythonToolMode(): PythonToolMode {
|
|
1178
1145
|
return this.settings.python?.toolMode ?? "both";
|
|
1179
1146
|
}
|
|
@@ -1210,14 +1177,6 @@ export class SettingsManager {
|
|
|
1210
1177
|
await this.save();
|
|
1211
1178
|
}
|
|
1212
1179
|
|
|
1213
|
-
async setGitToolEnabled(enabled: boolean): Promise<void> {
|
|
1214
|
-
if (!this.globalSettings.git) {
|
|
1215
|
-
this.globalSettings.git = {};
|
|
1216
|
-
}
|
|
1217
|
-
this.globalSettings.git.enabled = enabled;
|
|
1218
|
-
await this.save();
|
|
1219
|
-
}
|
|
1220
|
-
|
|
1221
1180
|
getMCPProjectConfigEnabled(): boolean {
|
|
1222
1181
|
return this.settings.mcp?.enableProjectConfig ?? true;
|
|
1223
1182
|
}
|
|
@@ -1430,70 +1389,6 @@ export class SettingsManager {
|
|
|
1430
1389
|
await this.save();
|
|
1431
1390
|
}
|
|
1432
1391
|
|
|
1433
|
-
getVoiceSettings(): Required<VoiceSettings> {
|
|
1434
|
-
return {
|
|
1435
|
-
enabled: this.settings.voice?.enabled ?? false,
|
|
1436
|
-
transcriptionModel: this.settings.voice?.transcriptionModel ?? "whisper-1",
|
|
1437
|
-
transcriptionLanguage: this.settings.voice?.transcriptionLanguage ?? "",
|
|
1438
|
-
ttsModel: this.settings.voice?.ttsModel ?? "tts-1",
|
|
1439
|
-
ttsVoice: this.settings.voice?.ttsVoice ?? "alloy",
|
|
1440
|
-
ttsFormat: this.settings.voice?.ttsFormat ?? "wav",
|
|
1441
|
-
};
|
|
1442
|
-
}
|
|
1443
|
-
|
|
1444
|
-
async setVoiceSettings(settings: VoiceSettings): Promise<void> {
|
|
1445
|
-
this.globalSettings.voice = { ...this.globalSettings.voice, ...settings };
|
|
1446
|
-
await this.save();
|
|
1447
|
-
}
|
|
1448
|
-
|
|
1449
|
-
getVoiceEnabled(): boolean {
|
|
1450
|
-
return this.settings.voice?.enabled ?? false;
|
|
1451
|
-
}
|
|
1452
|
-
|
|
1453
|
-
async setVoiceEnabled(enabled: boolean): Promise<void> {
|
|
1454
|
-
if (!this.globalSettings.voice) {
|
|
1455
|
-
this.globalSettings.voice = {};
|
|
1456
|
-
}
|
|
1457
|
-
this.globalSettings.voice.enabled = enabled;
|
|
1458
|
-
await this.save();
|
|
1459
|
-
}
|
|
1460
|
-
|
|
1461
|
-
getVoiceTtsModel(): string {
|
|
1462
|
-
return this.settings.voice?.ttsModel ?? "gpt-4o-mini-tts";
|
|
1463
|
-
}
|
|
1464
|
-
|
|
1465
|
-
async setVoiceTtsModel(model: string): Promise<void> {
|
|
1466
|
-
if (!this.globalSettings.voice) {
|
|
1467
|
-
this.globalSettings.voice = {};
|
|
1468
|
-
}
|
|
1469
|
-
this.globalSettings.voice.ttsModel = model;
|
|
1470
|
-
await this.save();
|
|
1471
|
-
}
|
|
1472
|
-
|
|
1473
|
-
getVoiceTtsVoice(): string {
|
|
1474
|
-
return this.settings.voice?.ttsVoice ?? "alloy";
|
|
1475
|
-
}
|
|
1476
|
-
|
|
1477
|
-
async setVoiceTtsVoice(voice: string): Promise<void> {
|
|
1478
|
-
if (!this.globalSettings.voice) {
|
|
1479
|
-
this.globalSettings.voice = {};
|
|
1480
|
-
}
|
|
1481
|
-
this.globalSettings.voice.ttsVoice = voice;
|
|
1482
|
-
await this.save();
|
|
1483
|
-
}
|
|
1484
|
-
|
|
1485
|
-
getVoiceTtsFormat(): "wav" | "mp3" | "opus" | "aac" | "flac" {
|
|
1486
|
-
return this.settings.voice?.ttsFormat ?? "wav";
|
|
1487
|
-
}
|
|
1488
|
-
|
|
1489
|
-
async setVoiceTtsFormat(format: "wav" | "mp3" | "opus" | "aac" | "flac"): Promise<void> {
|
|
1490
|
-
if (!this.globalSettings.voice) {
|
|
1491
|
-
this.globalSettings.voice = {};
|
|
1492
|
-
}
|
|
1493
|
-
this.globalSettings.voice.ttsFormat = format;
|
|
1494
|
-
await this.save();
|
|
1495
|
-
}
|
|
1496
|
-
|
|
1497
1392
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
1498
1393
|
// Status Line Settings
|
|
1499
1394
|
// ═══════════════════════════════════════════════════════════════════════════
|
package/src/core/tools/bash.ts
CHANGED
|
@@ -20,9 +20,7 @@ export const BASH_DEFAULT_PREVIEW_LINES = 10;
|
|
|
20
20
|
const bashSchema = Type.Object({
|
|
21
21
|
command: Type.String({ description: "Bash command to execute" }),
|
|
22
22
|
timeout: Type.Optional(Type.Number({ description: "Timeout in seconds (optional, no default timeout)" })),
|
|
23
|
-
|
|
24
|
-
Type.String({ description: "Working directory for the command (default: current directory)" }),
|
|
25
|
-
),
|
|
23
|
+
cwd: Type.Optional(Type.String({ description: "Working directory for the command (default: current directory)" })),
|
|
26
24
|
});
|
|
27
25
|
|
|
28
26
|
export interface BashToolDetails {
|
|
@@ -53,7 +51,7 @@ export class BashTool implements AgentTool<typeof bashSchema, BashToolDetails> {
|
|
|
53
51
|
|
|
54
52
|
public async execute(
|
|
55
53
|
_toolCallId: string,
|
|
56
|
-
{ command, timeout,
|
|
54
|
+
{ command, timeout, cwd }: { command: string; timeout?: number; cwd?: string },
|
|
57
55
|
signal?: AbortSignal,
|
|
58
56
|
onUpdate?: AgentToolUpdateCallback<BashToolDetails>,
|
|
59
57
|
ctx?: AgentToolContext,
|
|
@@ -73,7 +71,7 @@ export class BashTool implements AgentTool<typeof bashSchema, BashToolDetails> {
|
|
|
73
71
|
}
|
|
74
72
|
}
|
|
75
73
|
|
|
76
|
-
const commandCwd =
|
|
74
|
+
const commandCwd = cwd ? resolveToCwd(cwd, this.session.cwd) : this.session.cwd;
|
|
77
75
|
let cwdStat: Awaited<ReturnType<Bun.BunFile["stat"]>>;
|
|
78
76
|
try {
|
|
79
77
|
cwdStat = await Bun.file(commandCwd).stat();
|
|
@@ -143,7 +141,7 @@ export class BashTool implements AgentTool<typeof bashSchema, BashToolDetails> {
|
|
|
143
141
|
interface BashRenderArgs {
|
|
144
142
|
command?: string;
|
|
145
143
|
timeout?: number;
|
|
146
|
-
|
|
144
|
+
cwd?: string;
|
|
147
145
|
}
|
|
148
146
|
|
|
149
147
|
interface BashRenderContext {
|
|
@@ -166,7 +164,7 @@ export const bashToolRenderer = {
|
|
|
166
164
|
const command = args.command || uiTheme.format.ellipsis;
|
|
167
165
|
const prompt = uiTheme.fg("accent", "$");
|
|
168
166
|
const cwd = process.cwd();
|
|
169
|
-
let displayWorkdir = args.
|
|
167
|
+
let displayWorkdir = args.cwd;
|
|
170
168
|
|
|
171
169
|
if (displayWorkdir) {
|
|
172
170
|
const resolvedCwd = resolve(cwd);
|
package/src/core/tools/index.ts
CHANGED
|
@@ -7,7 +7,6 @@ export { exaTools } from "./exa/index";
|
|
|
7
7
|
export type { ExaRenderDetails, ExaSearchResponse, ExaSearchResult } from "./exa/types";
|
|
8
8
|
export { type FindOperations, FindTool, type FindToolDetails, type FindToolOptions } from "./find";
|
|
9
9
|
export { setPreferredImageProvider } from "./gemini-image";
|
|
10
|
-
export { GitTool, type GitToolDetails } from "./git";
|
|
11
10
|
export { type GrepOperations, GrepTool, type GrepToolDetails, type GrepToolOptions } from "./grep";
|
|
12
11
|
export { type LsOperations, LsTool, type LsToolDetails, type LsToolOptions } from "./ls";
|
|
13
12
|
export {
|
|
@@ -72,7 +71,6 @@ import { BashTool } from "./bash";
|
|
|
72
71
|
import { CalculatorTool } from "./calculator";
|
|
73
72
|
import { CompleteTool } from "./complete";
|
|
74
73
|
import { FindTool } from "./find";
|
|
75
|
-
import { GitTool } from "./git";
|
|
76
74
|
import { GrepTool } from "./grep";
|
|
77
75
|
import { LsTool } from "./ls";
|
|
78
76
|
import { LspTool } from "./lsp/index";
|
|
@@ -132,7 +130,6 @@ export interface ToolSession {
|
|
|
132
130
|
getEditFuzzyMatch(): boolean;
|
|
133
131
|
getEditFuzzyThreshold?(): number;
|
|
134
132
|
getEditPatchMode?(): boolean;
|
|
135
|
-
getGitToolEnabled(): boolean;
|
|
136
133
|
getBashInterceptorEnabled(): boolean;
|
|
137
134
|
getBashInterceptorSimpleLsEnabled(): boolean;
|
|
138
135
|
getBashInterceptorRules(): BashInterceptorRule[];
|
|
@@ -152,7 +149,6 @@ export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
|
|
|
152
149
|
ssh: loadSshTool,
|
|
153
150
|
edit: (s) => new EditTool(s),
|
|
154
151
|
find: (s) => new FindTool(s),
|
|
155
|
-
git: GitTool.createIf,
|
|
156
152
|
grep: (s) => new GrepTool(s),
|
|
157
153
|
ls: (s) => new LsTool(s),
|
|
158
154
|
lsp: LspTool.createIf,
|
|
@@ -225,7 +221,7 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
225
221
|
});
|
|
226
222
|
} else if (!isTestEnv && getPreludeDocs().length === 0) {
|
|
227
223
|
const sessionFile = session.getSessionFile?.() ?? undefined;
|
|
228
|
-
const warmSessionId = sessionFile ? `session:${sessionFile}:
|
|
224
|
+
const warmSessionId = sessionFile ? `session:${sessionFile}:cwd:${session.cwd}` : `cwd:${session.cwd}`;
|
|
229
225
|
void warmPythonEnvironment(session.cwd, warmSessionId, session.settings?.getPythonSharedGateway?.()).catch(
|
|
230
226
|
(err) => {
|
|
231
227
|
logger.warn("Failed to warm Python environment", {
|
|
@@ -11,6 +11,7 @@ import { resolveToCwd } from "../path-utils";
|
|
|
11
11
|
import { DEFAULT_FUZZY_THRESHOLD, findContextLine, findMatch, seekSequence } from "./fuzzy";
|
|
12
12
|
import {
|
|
13
13
|
adjustIndentation,
|
|
14
|
+
convertLeadingTabsToSpaces,
|
|
14
15
|
countLeadingWhitespace,
|
|
15
16
|
detectLineEnding,
|
|
16
17
|
getLeadingWhitespace,
|
|
@@ -82,6 +83,34 @@ function adjustLinesIndentation(patternLines: string[], actualLines: string[], n
|
|
|
82
83
|
return newLines;
|
|
83
84
|
}
|
|
84
85
|
|
|
86
|
+
// If pattern already matches actual exactly (including indentation), preserve agent's intended changes
|
|
87
|
+
if (patternLines.length === actualLines.length) {
|
|
88
|
+
let exactMatch = true;
|
|
89
|
+
for (let i = 0; i < patternLines.length; i++) {
|
|
90
|
+
if (patternLines[i] !== actualLines[i]) {
|
|
91
|
+
exactMatch = false;
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (exactMatch) {
|
|
96
|
+
return newLines;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// If the patch is purely an indentation change (same trimmed content), apply exactly as specified
|
|
101
|
+
if (patternLines.length === newLines.length) {
|
|
102
|
+
let indentationOnly = true;
|
|
103
|
+
for (let i = 0; i < patternLines.length; i++) {
|
|
104
|
+
if (patternLines[i].trim() !== newLines[i].trim()) {
|
|
105
|
+
indentationOnly = false;
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (indentationOnly) {
|
|
110
|
+
return newLines;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
85
114
|
// Detect indent character from actual content
|
|
86
115
|
let indentChar = " ";
|
|
87
116
|
for (const line of actualLines) {
|
|
@@ -92,6 +121,55 @@ function adjustLinesIndentation(patternLines: string[], actualLines: string[], n
|
|
|
92
121
|
}
|
|
93
122
|
}
|
|
94
123
|
|
|
124
|
+
let patternTabOnly = true;
|
|
125
|
+
let actualSpaceOnly = true;
|
|
126
|
+
let patternMixed = false;
|
|
127
|
+
let actualMixed = false;
|
|
128
|
+
|
|
129
|
+
for (const line of patternLines) {
|
|
130
|
+
if (line.trim().length === 0) continue;
|
|
131
|
+
const ws = getLeadingWhitespace(line);
|
|
132
|
+
if (ws.includes(" ")) patternTabOnly = false;
|
|
133
|
+
if (ws.includes(" ") && ws.includes("\t")) patternMixed = true;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
for (const line of actualLines) {
|
|
137
|
+
if (line.trim().length === 0) continue;
|
|
138
|
+
const ws = getLeadingWhitespace(line);
|
|
139
|
+
if (ws.includes("\t")) actualSpaceOnly = false;
|
|
140
|
+
if (ws.includes(" ") && ws.includes("\t")) actualMixed = true;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (!patternMixed && !actualMixed && patternTabOnly && actualSpaceOnly) {
|
|
144
|
+
let ratio: number | undefined;
|
|
145
|
+
const lineCount = Math.min(patternLines.length, actualLines.length);
|
|
146
|
+
let consistent = true;
|
|
147
|
+
for (let i = 0; i < lineCount; i++) {
|
|
148
|
+
const patternLine = patternLines[i];
|
|
149
|
+
const actualLine = actualLines[i];
|
|
150
|
+
if (patternLine.trim().length === 0 || actualLine.trim().length === 0) continue;
|
|
151
|
+
const patternIndent = countLeadingWhitespace(patternLine);
|
|
152
|
+
const actualIndent = countLeadingWhitespace(actualLine);
|
|
153
|
+
if (patternIndent === 0) continue;
|
|
154
|
+
if (actualIndent % patternIndent !== 0) {
|
|
155
|
+
consistent = false;
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
const nextRatio = actualIndent / patternIndent;
|
|
159
|
+
if (!ratio) {
|
|
160
|
+
ratio = nextRatio;
|
|
161
|
+
} else if (ratio !== nextRatio) {
|
|
162
|
+
consistent = false;
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (consistent && ratio) {
|
|
168
|
+
const converted = convertLeadingTabsToSpaces(newLines.join("\n"), ratio).split("\n");
|
|
169
|
+
return converted;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
95
173
|
// Build a map from trimmed content to actual lines (by content, not position)
|
|
96
174
|
// This handles fuzzy matches where pattern and actual may not be positionally aligned
|
|
97
175
|
const contentToActualLines = new Map<string, string[]>();
|
|
@@ -106,18 +184,29 @@ function adjustLinesIndentation(patternLines: string[], actualLines: string[], n
|
|
|
106
184
|
}
|
|
107
185
|
}
|
|
108
186
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
187
|
+
let patternMin = Infinity;
|
|
188
|
+
for (const line of patternLines) {
|
|
189
|
+
if (line.trim().length === 0) continue;
|
|
190
|
+
patternMin = Math.min(patternMin, countLeadingWhitespace(line));
|
|
191
|
+
}
|
|
192
|
+
if (patternMin === Infinity) {
|
|
193
|
+
patternMin = 0;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
let delta: number | undefined;
|
|
197
|
+
const deltas: number[] = [];
|
|
112
198
|
for (let i = 0; i < Math.min(patternLines.length, actualLines.length); i++) {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
199
|
+
const patternLine = patternLines[i];
|
|
200
|
+
const actualLine = actualLines[i];
|
|
201
|
+
if (patternLine.trim().length === 0 || actualLine.trim().length === 0) continue;
|
|
202
|
+
const pIndent = countLeadingWhitespace(patternLine);
|
|
203
|
+
const aIndent = countLeadingWhitespace(actualLine);
|
|
204
|
+
deltas.push(aIndent - pIndent);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (deltas.length > 0 && deltas.every((value) => value === deltas[0])) {
|
|
208
|
+
delta = deltas[0];
|
|
119
209
|
}
|
|
120
|
-
const avgDelta = deltaCount > 0 ? Math.round(totalDelta / deltaCount) : 0;
|
|
121
210
|
|
|
122
211
|
// Track which actual lines we've used to handle duplicate content correctly
|
|
123
212
|
const usedActualLines = new Map<string, number>(); // trimmed content -> count used
|
|
@@ -132,6 +221,12 @@ function adjustLinesIndentation(patternLines: string[], actualLines: string[], n
|
|
|
132
221
|
|
|
133
222
|
// Check if this is a context line (same trimmed content exists in actual)
|
|
134
223
|
if (matchingActualLines && matchingActualLines.length > 0) {
|
|
224
|
+
if (matchingActualLines.length === 1) {
|
|
225
|
+
return matchingActualLines[0];
|
|
226
|
+
}
|
|
227
|
+
if (matchingActualLines.includes(newLine)) {
|
|
228
|
+
return newLine;
|
|
229
|
+
}
|
|
135
230
|
const usedCount = usedActualLines.get(trimmed) ?? 0;
|
|
136
231
|
if (usedCount < matchingActualLines.length) {
|
|
137
232
|
usedActualLines.set(trimmed, usedCount + 1);
|
|
@@ -140,13 +235,16 @@ function adjustLinesIndentation(patternLines: string[], actualLines: string[], n
|
|
|
140
235
|
}
|
|
141
236
|
}
|
|
142
237
|
|
|
143
|
-
// This is a new/added line - apply
|
|
144
|
-
if (
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
238
|
+
// This is a new/added line - apply consistent delta if safe
|
|
239
|
+
if (delta && delta !== 0) {
|
|
240
|
+
const newIndent = countLeadingWhitespace(newLine);
|
|
241
|
+
if (newIndent === patternMin) {
|
|
242
|
+
if (delta > 0) {
|
|
243
|
+
return indentChar.repeat(delta) + newLine;
|
|
244
|
+
}
|
|
245
|
+
const toRemove = Math.min(-delta, newIndent);
|
|
246
|
+
return newLine.slice(toRemove);
|
|
247
|
+
}
|
|
150
248
|
}
|
|
151
249
|
return newLine;
|
|
152
250
|
});
|
|
@@ -92,6 +92,114 @@ export function detectIndentChar(text: string): string {
|
|
|
92
92
|
return " ";
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
function gcd(a: number, b: number): number {
|
|
96
|
+
let x = Math.abs(a);
|
|
97
|
+
let y = Math.abs(b);
|
|
98
|
+
while (y !== 0) {
|
|
99
|
+
const temp = y;
|
|
100
|
+
y = x % y;
|
|
101
|
+
x = temp;
|
|
102
|
+
}
|
|
103
|
+
return x;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
interface IndentProfile {
|
|
107
|
+
lines: string[];
|
|
108
|
+
indentStrings: string[];
|
|
109
|
+
indentCounts: number[];
|
|
110
|
+
min: number;
|
|
111
|
+
char: " " | "\t" | undefined;
|
|
112
|
+
spaceOnly: boolean;
|
|
113
|
+
tabOnly: boolean;
|
|
114
|
+
mixed: boolean;
|
|
115
|
+
unit: number;
|
|
116
|
+
nonEmptyCount: number;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function buildIndentProfile(text: string): IndentProfile {
|
|
120
|
+
const lines = text.split("\n");
|
|
121
|
+
const indentStrings: string[] = [];
|
|
122
|
+
const indentCounts: number[] = [];
|
|
123
|
+
let min = Infinity;
|
|
124
|
+
let char: " " | "\t" | undefined;
|
|
125
|
+
let spaceOnly = true;
|
|
126
|
+
let tabOnly = true;
|
|
127
|
+
let mixed = false;
|
|
128
|
+
let nonEmptyCount = 0;
|
|
129
|
+
let unit = 0;
|
|
130
|
+
|
|
131
|
+
for (const line of lines) {
|
|
132
|
+
if (line.trim().length === 0) continue;
|
|
133
|
+
nonEmptyCount++;
|
|
134
|
+
const indent = getLeadingWhitespace(line);
|
|
135
|
+
indentStrings.push(indent);
|
|
136
|
+
indentCounts.push(indent.length);
|
|
137
|
+
min = Math.min(min, indent.length);
|
|
138
|
+
if (indent.includes(" ")) {
|
|
139
|
+
tabOnly = false;
|
|
140
|
+
}
|
|
141
|
+
if (indent.includes("\t")) {
|
|
142
|
+
spaceOnly = false;
|
|
143
|
+
}
|
|
144
|
+
if (indent.includes(" ") && indent.includes("\t")) {
|
|
145
|
+
mixed = true;
|
|
146
|
+
}
|
|
147
|
+
if (indent.length > 0) {
|
|
148
|
+
const currentChar = indent[0] as " " | "\t";
|
|
149
|
+
if (!char) {
|
|
150
|
+
char = currentChar;
|
|
151
|
+
} else if (char !== currentChar) {
|
|
152
|
+
mixed = true;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (min === Infinity) {
|
|
158
|
+
min = 0;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (spaceOnly && nonEmptyCount > 0) {
|
|
162
|
+
let current = 0;
|
|
163
|
+
for (const count of indentCounts) {
|
|
164
|
+
if (count === 0) continue;
|
|
165
|
+
current = current === 0 ? count : gcd(current, count);
|
|
166
|
+
}
|
|
167
|
+
unit = current;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (tabOnly && nonEmptyCount > 0) {
|
|
171
|
+
unit = 1;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
lines,
|
|
176
|
+
indentStrings,
|
|
177
|
+
indentCounts,
|
|
178
|
+
min,
|
|
179
|
+
char,
|
|
180
|
+
spaceOnly,
|
|
181
|
+
tabOnly,
|
|
182
|
+
mixed,
|
|
183
|
+
unit,
|
|
184
|
+
nonEmptyCount,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function convertLeadingTabsToSpaces(text: string, spacesPerTab: number): string {
|
|
189
|
+
if (spacesPerTab <= 0) return text;
|
|
190
|
+
return text
|
|
191
|
+
.split("\n")
|
|
192
|
+
.map((line) => {
|
|
193
|
+
const trimmed = line.trimStart();
|
|
194
|
+
if (trimmed.length === 0) return line;
|
|
195
|
+
const leading = getLeadingWhitespace(line);
|
|
196
|
+
if (!leading.includes("\t") || leading.includes(" ")) return line;
|
|
197
|
+
const converted = " ".repeat(leading.length * spacesPerTab);
|
|
198
|
+
return converted + trimmed;
|
|
199
|
+
})
|
|
200
|
+
.join("\n");
|
|
201
|
+
}
|
|
202
|
+
|
|
95
203
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
96
204
|
// Unicode Normalization
|
|
97
205
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -191,27 +299,94 @@ export function normalizeForFuzzy(line: string): string {
|
|
|
191
299
|
* to each line in newText.
|
|
192
300
|
*/
|
|
193
301
|
export function adjustIndentation(oldText: string, actualText: string, newText: string): string {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
302
|
+
// If old text already matches actual text exactly, preserve agent's intended indentation
|
|
303
|
+
if (oldText === actualText) {
|
|
304
|
+
return newText;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// If the patch is purely an indentation change (same trimmed content), apply exactly as specified
|
|
308
|
+
const oldLines = oldText.split("\n");
|
|
309
|
+
const newLines = newText.split("\n");
|
|
310
|
+
if (oldLines.length === newLines.length) {
|
|
311
|
+
let indentationOnly = true;
|
|
312
|
+
for (let i = 0; i < oldLines.length; i++) {
|
|
313
|
+
if (oldLines[i].trim() !== newLines[i].trim()) {
|
|
314
|
+
indentationOnly = false;
|
|
315
|
+
break;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if (indentationOnly) {
|
|
319
|
+
return newText;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const oldProfile = buildIndentProfile(oldText);
|
|
324
|
+
const actualProfile = buildIndentProfile(actualText);
|
|
325
|
+
const newProfile = buildIndentProfile(newText);
|
|
326
|
+
|
|
327
|
+
if (newProfile.nonEmptyCount === 0 || oldProfile.nonEmptyCount === 0 || actualProfile.nonEmptyCount === 0) {
|
|
328
|
+
return newText;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (oldProfile.mixed || actualProfile.mixed || newProfile.mixed) {
|
|
332
|
+
return newText;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (oldProfile.char && actualProfile.char && oldProfile.char !== actualProfile.char) {
|
|
336
|
+
if (actualProfile.spaceOnly && oldProfile.tabOnly && newProfile.tabOnly && actualProfile.unit > 0) {
|
|
337
|
+
let consistent = true;
|
|
338
|
+
const lineCount = Math.min(oldProfile.lines.length, actualProfile.lines.length);
|
|
339
|
+
for (let i = 0; i < lineCount; i++) {
|
|
340
|
+
const oldLine = oldProfile.lines[i];
|
|
341
|
+
const actualLine = actualProfile.lines[i];
|
|
342
|
+
if (oldLine.trim().length === 0 || actualLine.trim().length === 0) continue;
|
|
343
|
+
const oldIndent = getLeadingWhitespace(oldLine);
|
|
344
|
+
const actualIndent = getLeadingWhitespace(actualLine);
|
|
345
|
+
if (oldIndent.length === 0) continue;
|
|
346
|
+
if (actualIndent.length !== oldIndent.length * actualProfile.unit) {
|
|
347
|
+
consistent = false;
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return consistent ? convertLeadingTabsToSpaces(newText, actualProfile.unit) : newText;
|
|
352
|
+
}
|
|
353
|
+
return newText;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const lineCount = Math.min(oldProfile.lines.length, actualProfile.lines.length);
|
|
357
|
+
const deltas: number[] = [];
|
|
358
|
+
for (let i = 0; i < lineCount; i++) {
|
|
359
|
+
const oldLine = oldProfile.lines[i];
|
|
360
|
+
const actualLine = actualProfile.lines[i];
|
|
361
|
+
if (oldLine.trim().length === 0 || actualLine.trim().length === 0) continue;
|
|
362
|
+
deltas.push(countLeadingWhitespace(actualLine) - countLeadingWhitespace(oldLine));
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (deltas.length === 0) {
|
|
366
|
+
return newText;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const delta = deltas[0];
|
|
370
|
+
if (!deltas.every((value) => value === delta)) {
|
|
371
|
+
return newText;
|
|
372
|
+
}
|
|
197
373
|
|
|
198
374
|
if (delta === 0) {
|
|
199
375
|
return newText;
|
|
200
376
|
}
|
|
201
377
|
|
|
202
|
-
|
|
203
|
-
|
|
378
|
+
if (newProfile.char && actualProfile.char && newProfile.char !== actualProfile.char) {
|
|
379
|
+
return newText;
|
|
380
|
+
}
|
|
204
381
|
|
|
205
|
-
const
|
|
382
|
+
const indentChar = actualProfile.char ?? oldProfile.char ?? detectIndentChar(actualText);
|
|
383
|
+
const adjusted = newText.split("\n").map((line) => {
|
|
206
384
|
if (line.trim().length === 0) {
|
|
207
|
-
return line;
|
|
385
|
+
return line;
|
|
208
386
|
}
|
|
209
|
-
|
|
210
387
|
if (delta > 0) {
|
|
211
388
|
return indentChar.repeat(delta) + line;
|
|
212
389
|
}
|
|
213
|
-
|
|
214
|
-
// Remove indentation (delta < 0)
|
|
215
390
|
const toRemove = Math.min(-delta, countLeadingWhitespace(line));
|
|
216
391
|
return line.slice(toRemove);
|
|
217
392
|
});
|