@oh-my-pi/pi-coding-agent 6.7.670 → 6.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 +28 -0
- package/package.json +6 -7
- package/src/cli/session-picker.ts +27 -28
- package/src/cli/setup-cli.ts +7 -16
- package/src/cli/update-cli.ts +1 -1
- package/src/config.ts +1 -1
- package/src/core/agent-session.ts +202 -37
- package/src/core/agent-storage.ts +1 -1
- package/src/core/auth-storage.ts +15 -25
- package/src/core/bash-executor.ts +63 -105
- package/src/core/custom-commands/loader.ts +1 -1
- package/src/core/custom-tools/loader.ts +1 -1
- package/src/core/custom-tools/types.ts +1 -2
- package/src/core/exec.ts +16 -100
- package/src/core/extensions/index.ts +1 -7
- package/src/core/extensions/loader.ts +1 -1
- package/src/core/extensions/runner.ts +1 -1
- package/src/core/extensions/types.ts +2 -2
- package/src/core/extensions/wrapper.ts +15 -20
- package/src/core/frontmatter.ts +1 -1
- package/src/core/history-storage.ts +3 -6
- package/src/core/hooks/index.ts +2 -2
- package/src/core/hooks/loader.ts +1 -1
- package/src/core/hooks/tool-wrapper.ts +14 -26
- package/src/core/hooks/types.ts +1 -2
- package/src/core/keybindings.ts +1 -1
- package/src/core/mcp/client.ts +13 -13
- package/src/core/mcp/json-rpc.ts +1 -1
- package/src/core/mcp/loader.ts +1 -1
- package/src/core/mcp/manager.ts +2 -2
- package/src/core/mcp/tool-cache.ts +1 -1
- package/src/core/mcp/transports/http.ts +32 -70
- package/src/core/model-registry.ts +1 -1
- package/src/core/plugins/installer.ts +13 -11
- package/src/core/prompt-templates.ts +4 -9
- package/src/core/python-executor.ts +23 -18
- package/src/core/python-gateway-coordinator.ts +29 -28
- package/src/core/python-kernel.ts +230 -211
- package/src/core/sdk.ts +10 -13
- package/src/core/session-manager.ts +1 -1
- package/src/core/settings-manager.ts +22 -9
- package/src/core/skills.ts +1 -1
- package/src/core/ssh/connection-manager.ts +19 -33
- package/src/core/ssh/ssh-executor.ts +39 -35
- package/src/core/ssh/sshfs-mount.ts +14 -33
- package/src/core/storage-migration.ts +1 -1
- package/src/core/streaming-output.ts +183 -127
- package/src/core/system-prompt.ts +119 -79
- package/src/core/title-generator.ts +1 -1
- package/src/core/tools/ask.ts +2 -2
- package/src/core/tools/bash.ts +3 -3
- package/src/core/tools/calculator.ts +1 -1
- package/src/core/tools/exa/mcp-client.ts +1 -1
- package/src/core/tools/exa/render.ts +1 -1
- package/src/core/tools/find.ts +39 -71
- package/src/core/tools/gemini-image.ts +1 -1
- package/src/core/tools/grep.ts +88 -100
- package/src/core/tools/index.ts +1 -1
- package/src/core/tools/ls.ts +1 -1
- package/src/core/tools/lsp/client.ts +50 -50
- package/src/core/tools/lsp/clients/lsp-linter-client.ts +1 -1
- package/src/core/tools/lsp/config.ts +1 -1
- package/src/core/tools/lsp/index.ts +2 -4
- package/src/core/tools/lsp/lspmux.ts +1 -1
- package/src/core/tools/lsp/rust-analyzer.ts +2 -2
- package/src/core/tools/lsp/utils.ts +0 -14
- package/src/core/tools/notebook.ts +1 -1
- package/src/core/tools/patch/shared.ts +3 -4
- package/src/core/tools/python.ts +3 -3
- package/src/core/tools/read.ts +29 -68
- package/src/core/tools/render-utils.ts +0 -5
- package/src/core/tools/ssh.ts +3 -3
- package/src/core/tools/task/model-resolver.ts +7 -9
- package/src/core/tools/task/worker.ts +144 -139
- package/src/core/tools/todo-write.ts +1 -1
- package/src/core/tools/truncate.ts +2 -2
- package/src/core/tools/web-fetch.ts +13 -15
- package/src/core/tools/web-scrapers/types.ts +1 -3
- package/src/core/tools/web-scrapers/utils.ts +14 -13
- package/src/core/tools/web-scrapers/youtube.ts +39 -12
- package/src/core/tools/web-search/auth.ts +1 -1
- package/src/core/tools/write.ts +1 -1
- package/src/core/ttsr.ts +1 -1
- package/src/core/utils.ts +1 -187
- package/src/core/voice-controller.ts +1 -1
- package/src/core/voice-supervisor.ts +11 -38
- package/src/core/voice.ts +1 -8
- package/src/discovery/codex.ts +1 -1
- package/src/index.ts +4 -4
- package/src/main.ts +5 -10
- package/src/migrations.ts +1 -1
- package/src/modes/index.ts +7 -40
- package/src/modes/interactive/components/extensions/state-manager.ts +1 -1
- package/src/modes/interactive/components/hook-editor.ts +12 -9
- package/src/modes/interactive/components/login-dialog.ts +24 -11
- package/src/modes/interactive/components/settings-defs.ts +9 -0
- package/src/modes/interactive/components/status-line.ts +36 -35
- package/src/modes/interactive/components/todo-display.ts +1 -1
- package/src/modes/interactive/components/tool-execution.ts +1 -1
- package/src/modes/interactive/controllers/command-controller.ts +50 -84
- package/src/modes/interactive/controllers/extension-ui-controller.ts +76 -76
- package/src/modes/interactive/controllers/input-controller.ts +12 -11
- package/src/modes/interactive/interactive-mode.ts +10 -11
- package/src/modes/interactive/theme/theme.ts +1 -1
- package/src/modes/interactive/types.ts +1 -1
- package/src/modes/rpc/rpc-client.ts +91 -121
- package/src/modes/rpc/rpc-mode.ts +71 -79
- package/src/prompts/system/ttsr-interrupt.md +7 -0
- package/src/utils/clipboard.ts +57 -141
- package/src/utils/shell-snapshot.ts +12 -60
- package/src/utils/shell.ts +35 -56
- package/src/utils/tools-manager.ts +42 -71
- package/src/core/logger.ts +0 -111
- package/src/modes/cleanup.ts +0 -23
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,34 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [6.8.0] - 2026-01-20
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added streaming abort setting to control edit tool behavior when patch preview fails
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Replaced internal logger with @oh-my-pi/pi-utils logger across all modules
|
|
14
|
+
- Updated process spawning to use cspawn and ptree utilities from pi-utils
|
|
15
|
+
- Migrated file operations to use async fs/promises and Bun file APIs
|
|
16
|
+
- Refactored promise handling to use Promise.withResolvers and utility functions
|
|
17
|
+
- Updated timeout and abort handling to use standardized utility functions
|
|
18
|
+
- Refactored authentication login method to use OAuthController interface instead of individual callbacks
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- Fixed Python package installation to handle async operations properly
|
|
23
|
+
- Fixed streaming output truncation to use consistent column limits
|
|
24
|
+
- Fixed shell command execution to properly handle process cleanup and timeouts
|
|
25
|
+
- Fixed SSH connection management to properly await async operations
|
|
26
|
+
- Fixed voice supervisor process cleanup to use proper async handling
|
|
27
|
+
- Added automatic regex pattern validation in grep tool to handle invalid patterns by switching to literal mode
|
|
28
|
+
|
|
29
|
+
### Security
|
|
30
|
+
|
|
31
|
+
- Updated temporary file cleanup to use secure async removal methods
|
|
32
|
+
|
|
5
33
|
## [6.7.67] - 2026-01-19
|
|
6
34
|
### Added
|
|
7
35
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.8.0",
|
|
4
4
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"ompConfig": {
|
|
@@ -40,10 +40,11 @@
|
|
|
40
40
|
"prepublishOnly": "bun run generate-template && bun run clean && bun run build"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@oh-my-pi/pi-agent-core": "6.
|
|
44
|
-
"@oh-my-pi/pi-ai": "6.
|
|
45
|
-
"@oh-my-pi/pi-git-tool": "6.
|
|
46
|
-
"@oh-my-pi/pi-tui": "6.
|
|
43
|
+
"@oh-my-pi/pi-agent-core": "6.8.0",
|
|
44
|
+
"@oh-my-pi/pi-ai": "6.8.0",
|
|
45
|
+
"@oh-my-pi/pi-git-tool": "6.8.0",
|
|
46
|
+
"@oh-my-pi/pi-tui": "6.8.0",
|
|
47
|
+
"@oh-my-pi/pi-utils": "6.8.0",
|
|
47
48
|
"@openai/agents": "^0.3.7",
|
|
48
49
|
"@sinclair/typebox": "^0.34.46",
|
|
49
50
|
"ajv": "^8.17.1",
|
|
@@ -61,8 +62,6 @@
|
|
|
61
62
|
"node-html-parser": "^6.1.13",
|
|
62
63
|
"smol-toml": "^1.6.0",
|
|
63
64
|
"strip-ansi": "^7.1.2",
|
|
64
|
-
"winston": "^3.17.0",
|
|
65
|
-
"winston-daily-rotate-file": "^5.0.0",
|
|
66
65
|
"zod": "^4.3.5"
|
|
67
66
|
},
|
|
68
67
|
"devDependencies": {
|
|
@@ -8,34 +8,33 @@ import { SessionSelectorComponent } from "../modes/interactive/components/sessio
|
|
|
8
8
|
|
|
9
9
|
/** Show TUI session selector and return selected session path or null if cancelled */
|
|
10
10
|
export async function selectSession(sessions: SessionInfo[]): Promise<string | null> {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
(
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
()
|
|
25
|
-
|
|
26
|
-
resolved = true;
|
|
27
|
-
ui.stop();
|
|
28
|
-
resolve(null);
|
|
29
|
-
}
|
|
30
|
-
},
|
|
31
|
-
() => {
|
|
11
|
+
const { promise, resolve } = Promise.withResolvers<string | null>();
|
|
12
|
+
const ui = new TUI(new ProcessTerminal());
|
|
13
|
+
let resolved = false;
|
|
14
|
+
const selector = new SessionSelectorComponent(
|
|
15
|
+
sessions,
|
|
16
|
+
(path: string) => {
|
|
17
|
+
if (!resolved) {
|
|
18
|
+
resolved = true;
|
|
19
|
+
ui.stop();
|
|
20
|
+
resolve(path);
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
() => {
|
|
24
|
+
if (!resolved) {
|
|
25
|
+
resolved = true;
|
|
32
26
|
ui.stop();
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
|
|
27
|
+
resolve(null);
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
() => {
|
|
31
|
+
ui.stop();
|
|
32
|
+
process.exit(0);
|
|
33
|
+
},
|
|
34
|
+
);
|
|
36
35
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
ui.addChild(selector);
|
|
37
|
+
ui.setFocus(selector.getSessionList());
|
|
38
|
+
ui.start();
|
|
39
|
+
return promise;
|
|
41
40
|
}
|
package/src/cli/setup-cli.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Handles `omp setup <component>` to install dependencies for optional features.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { $ } from "bun";
|
|
7
8
|
import chalk from "chalk";
|
|
8
9
|
import { APP_NAME } from "../config";
|
|
9
10
|
import { theme } from "../modes/interactive/theme/theme";
|
|
@@ -89,10 +90,8 @@ async function checkPythonSetup(): Promise<PythonCheckResult> {
|
|
|
89
90
|
|
|
90
91
|
for (const pkg of PYTHON_PACKAGES) {
|
|
91
92
|
const moduleName = pkg === "jupyter_kernel_gateway" ? "kernel_gateway" : pkg;
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
{ stdin: "ignore", stdout: "pipe", stderr: "pipe" },
|
|
95
|
-
);
|
|
93
|
+
const script = `import importlib.util; raise SystemExit(0 if importlib.util.find_spec('${moduleName}') else 1)`;
|
|
94
|
+
const check = await $`${pythonPath} -c ${script}`.quiet().nothrow();
|
|
96
95
|
if (check.exitCode === 0) {
|
|
97
96
|
result.installedPackages.push(pkg);
|
|
98
97
|
} else {
|
|
@@ -107,24 +106,16 @@ async function checkPythonSetup(): Promise<PythonCheckResult> {
|
|
|
107
106
|
/**
|
|
108
107
|
* Install Python packages using uv (preferred) or pip.
|
|
109
108
|
*/
|
|
110
|
-
function installPythonPackages(packages: string[], uvPath?: string, pipPath?: string): boolean {
|
|
109
|
+
async function installPythonPackages(packages: string[], uvPath?: string, pipPath?: string): Promise<boolean> {
|
|
111
110
|
if (uvPath) {
|
|
112
111
|
console.log(chalk.dim(`Installing via uv: ${packages.join(" ")}`));
|
|
113
|
-
const result =
|
|
114
|
-
stdin: "ignore",
|
|
115
|
-
stdout: "inherit",
|
|
116
|
-
stderr: "inherit",
|
|
117
|
-
});
|
|
112
|
+
const result = await $`${uvPath} pip install ${packages}`.nothrow();
|
|
118
113
|
return result.exitCode === 0;
|
|
119
114
|
}
|
|
120
115
|
|
|
121
116
|
if (pipPath) {
|
|
122
117
|
console.log(chalk.dim(`Installing via pip: ${packages.join(" ")}`));
|
|
123
|
-
const result =
|
|
124
|
-
stdin: "ignore",
|
|
125
|
-
stdout: "inherit",
|
|
126
|
-
stderr: "inherit",
|
|
127
|
-
});
|
|
118
|
+
const result = await $`${pipPath} install ${packages}`.nothrow();
|
|
128
119
|
return result.exitCode === 0;
|
|
129
120
|
}
|
|
130
121
|
|
|
@@ -188,7 +179,7 @@ async function handlePythonSetup(flags: { json?: boolean; check?: boolean }): Pr
|
|
|
188
179
|
}
|
|
189
180
|
|
|
190
181
|
console.log("");
|
|
191
|
-
const success = installPythonPackages(check.missingPackages, check.uvPath, check.pipPath);
|
|
182
|
+
const success = await installPythonPackages(check.missingPackages, check.uvPath, check.pipPath);
|
|
192
183
|
|
|
193
184
|
if (!success) {
|
|
194
185
|
console.error(chalk.red(`\n${theme.status.error} Installation failed`));
|
package/src/cli/update-cli.ts
CHANGED
|
@@ -142,7 +142,7 @@ async function updateViaBun(): Promise<void> {
|
|
|
142
142
|
console.log(chalk.dim("Updating via bun..."));
|
|
143
143
|
|
|
144
144
|
try {
|
|
145
|
-
execSync(`bun update -g ${PACKAGE}`, { stdio: "inherit" });
|
|
145
|
+
execSync(`bun update --latest -g ${PACKAGE}`, { stdio: "inherit" });
|
|
146
146
|
console.log(chalk.green(`\n${theme.status.success} Update complete`));
|
|
147
147
|
} catch (error) {
|
|
148
148
|
throw new Error("bun update failed", { cause: error });
|
package/src/config.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import { logger } from "@oh-my-pi/pi-utils";
|
|
4
5
|
// Embed package.json at build time for config
|
|
5
6
|
import packageJson from "../package.json" with { type: "json" };
|
|
6
|
-
import { logger } from "./core/logger";
|
|
7
7
|
|
|
8
8
|
// =============================================================================
|
|
9
9
|
// App Config (from embedded package.json)
|
|
@@ -13,12 +13,15 @@
|
|
|
13
13
|
* Modes use this class and add their own I/O layer on top.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
16
17
|
import type { Agent, AgentEvent, AgentMessage, AgentState, AgentTool, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
17
|
-
import type { AssistantMessage, ImageContent, Message, Model, TextContent, Usage } from "@oh-my-pi/pi-ai";
|
|
18
|
+
import type { AssistantMessage, ImageContent, Message, Model, TextContent, ToolCall, Usage } from "@oh-my-pi/pi-ai";
|
|
18
19
|
import { isContextOverflow, modelsAreEqual, supportsXhigh } from "@oh-my-pi/pi-ai";
|
|
20
|
+
import { abortableSleep, logger } from "@oh-my-pi/pi-utils";
|
|
19
21
|
import type { Rule } from "../capability/rule";
|
|
20
22
|
import { getAgentDbPath } from "../config";
|
|
21
23
|
import { theme } from "../modes/interactive/theme/theme";
|
|
24
|
+
import ttsrInterruptTemplate from "../prompts/system/ttsr-interrupt.md" with { type: "text" };
|
|
22
25
|
import { type BashResult, executeBash as executeBashCommand, executeBashWithOperations } from "./bash-executor";
|
|
23
26
|
import {
|
|
24
27
|
type CompactionResult,
|
|
@@ -47,11 +50,10 @@ import type {
|
|
|
47
50
|
import type { CompactOptions, ContextUsage } from "./extensions/types";
|
|
48
51
|
import { extractFileMentions, generateFileMentionMessages } from "./file-mentions";
|
|
49
52
|
import type { HookCommandContext } from "./hooks/types";
|
|
50
|
-
import { logger } from "./logger";
|
|
51
53
|
import type { BashExecutionMessage, CustomMessage } from "./messages";
|
|
52
54
|
import type { ModelRegistry } from "./model-registry";
|
|
53
55
|
import { parseModelString } from "./model-resolver";
|
|
54
|
-
import { expandPromptTemplate, type PromptTemplate, parseCommandArgs } from "./prompt-templates";
|
|
56
|
+
import { expandPromptTemplate, type PromptTemplate, parseCommandArgs, renderPromptTemplate } from "./prompt-templates";
|
|
55
57
|
import type { BranchSummaryEntry, CompactionEntry, NewSessionOptions, SessionManager } from "./session-manager";
|
|
56
58
|
import type { SettingsManager, SkillsSettings } from "./settings-manager";
|
|
57
59
|
import type { Skill, SkillWarning } from "./skills";
|
|
@@ -59,6 +61,8 @@ import { expandSlashCommand, type FileSlashCommand } from "./slash-commands";
|
|
|
59
61
|
import { closeAllConnections } from "./ssh/connection-manager";
|
|
60
62
|
import { unmountAll } from "./ssh/sshfs-mount";
|
|
61
63
|
import type { BashOperations } from "./tools/bash";
|
|
64
|
+
import { normalizeDiff, normalizeToLF, ParseError, previewPatch, stripBom } from "./tools/patch";
|
|
65
|
+
import { resolveToCwd } from "./tools/path-utils";
|
|
62
66
|
import { getArtifactsDir } from "./tools/task/artifacts";
|
|
63
67
|
import type { TodoItem } from "./tools/todo-write";
|
|
64
68
|
import type { TtsrManager } from "./ttsr";
|
|
@@ -271,6 +275,10 @@ export class AgentSession {
|
|
|
271
275
|
private _pendingTtsrInjections: Rule[] = [];
|
|
272
276
|
private _ttsrAbortPending = false;
|
|
273
277
|
|
|
278
|
+
private _streamingEditAbortTriggered = false;
|
|
279
|
+
private _streamingEditCheckedLineCounts = new Map<string, number>();
|
|
280
|
+
private _streamingEditFileCache = new Map<string, string>();
|
|
281
|
+
|
|
274
282
|
constructor(config: AgentSessionConfig) {
|
|
275
283
|
this.agent = config.agent;
|
|
276
284
|
this.sessionManager = config.sessionManager;
|
|
@@ -352,9 +360,10 @@ export class AgentSession {
|
|
|
352
360
|
// Notify all listeners
|
|
353
361
|
this._emit(event);
|
|
354
362
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
363
|
+
if (event.type === "turn_start") {
|
|
364
|
+
this._resetStreamingEditState();
|
|
365
|
+
// TTSR: Reset buffer on turn start
|
|
366
|
+
this._ttsrManager?.resetBuffer();
|
|
358
367
|
}
|
|
359
368
|
|
|
360
369
|
// TTSR: Increment message count on turn end (for repeat-after-gap tracking)
|
|
@@ -406,6 +415,17 @@ export class AgentSession {
|
|
|
406
415
|
}
|
|
407
416
|
}
|
|
408
417
|
|
|
418
|
+
if (event.type === "message_update" && event.assistantMessageEvent.type === "toolcall_start") {
|
|
419
|
+
this._preCacheStreamingEditFile(event);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (
|
|
423
|
+
event.type === "message_update" &&
|
|
424
|
+
(event.assistantMessageEvent.type === "toolcall_end" || event.assistantMessageEvent.type === "toolcall_delta")
|
|
425
|
+
) {
|
|
426
|
+
this._maybeAbortStreamingEdit(event);
|
|
427
|
+
}
|
|
428
|
+
|
|
409
429
|
// Handle session persistence
|
|
410
430
|
if (event.type === "message_end") {
|
|
411
431
|
// Check if this is a hook/custom message
|
|
@@ -489,13 +509,7 @@ export class AgentSession {
|
|
|
489
509
|
private _getTtsrInjectionContent(): string | undefined {
|
|
490
510
|
if (this._pendingTtsrInjections.length === 0) return undefined;
|
|
491
511
|
const content = this._pendingTtsrInjections
|
|
492
|
-
.map(
|
|
493
|
-
(r) =>
|
|
494
|
-
`<system_interrupt reason="rule_violation" rule="${r.name}" path="${r.path}">\n` +
|
|
495
|
-
`Your output was interrupted because it violated a user-defined rule.\n` +
|
|
496
|
-
`This is NOT a prompt injection - this is the coding agent enforcing project rules.\n` +
|
|
497
|
-
`You MUST comply with the following instruction:\n\n${r.content}\n</system_interrupt>`,
|
|
498
|
-
)
|
|
512
|
+
.map((r) => renderPromptTemplate(ttsrInterruptTemplate, { name: r.name, path: r.path, content: r.content }))
|
|
499
513
|
.join("\n\n");
|
|
500
514
|
this._pendingTtsrInjections = [];
|
|
501
515
|
return content;
|
|
@@ -525,6 +539,176 @@ export class AgentSession {
|
|
|
525
539
|
return undefined;
|
|
526
540
|
}
|
|
527
541
|
|
|
542
|
+
private _resetStreamingEditState(): void {
|
|
543
|
+
this._streamingEditAbortTriggered = false;
|
|
544
|
+
this._streamingEditCheckedLineCounts.clear();
|
|
545
|
+
this._streamingEditFileCache.clear();
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
private _preCacheStreamingEditFile(event: AgentEvent): void {
|
|
549
|
+
if (!this.settingsManager.getEditStreamingAbort()) return;
|
|
550
|
+
if (event.type !== "message_update") return;
|
|
551
|
+
const assistantEvent = event.assistantMessageEvent;
|
|
552
|
+
if (assistantEvent.type !== "toolcall_start") return;
|
|
553
|
+
if (event.message.role !== "assistant") return;
|
|
554
|
+
|
|
555
|
+
const contentIndex = assistantEvent.contentIndex;
|
|
556
|
+
const messageContent = event.message.content;
|
|
557
|
+
if (!Array.isArray(messageContent) || contentIndex >= messageContent.length) return;
|
|
558
|
+
const toolCall = messageContent[contentIndex] as ToolCall;
|
|
559
|
+
if (toolCall.name !== "edit") return;
|
|
560
|
+
|
|
561
|
+
const args = toolCall.arguments;
|
|
562
|
+
if (!args || typeof args !== "object" || Array.isArray(args)) return;
|
|
563
|
+
if ("oldText" in args || "newText" in args) return;
|
|
564
|
+
|
|
565
|
+
const path = typeof args.path === "string" ? args.path : undefined;
|
|
566
|
+
if (!path) return;
|
|
567
|
+
|
|
568
|
+
const resolvedPath = resolveToCwd(path, this.sessionManager.getCwd());
|
|
569
|
+
this._ensureFileCache(resolvedPath);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
private _ensureFileCache(resolvedPath: string): void {
|
|
573
|
+
if (this._streamingEditFileCache.has(resolvedPath)) return;
|
|
574
|
+
|
|
575
|
+
try {
|
|
576
|
+
if (existsSync(resolvedPath)) {
|
|
577
|
+
const rawText = readFileSync(resolvedPath, "utf8");
|
|
578
|
+
const { text } = stripBom(rawText);
|
|
579
|
+
this._streamingEditFileCache.set(resolvedPath, normalizeToLF(text));
|
|
580
|
+
}
|
|
581
|
+
} catch {
|
|
582
|
+
// Ignore errors - mark as empty string so we don't retry
|
|
583
|
+
this._streamingEditFileCache.set(resolvedPath, "");
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
private _maybeAbortStreamingEdit(event: AgentEvent): void {
|
|
588
|
+
if (!this.settingsManager.getEditStreamingAbort()) return;
|
|
589
|
+
if (this._streamingEditAbortTriggered) return;
|
|
590
|
+
if (event.type !== "message_update") return;
|
|
591
|
+
const assistantEvent = event.assistantMessageEvent;
|
|
592
|
+
if (assistantEvent.type !== "toolcall_end" && assistantEvent.type !== "toolcall_delta") return;
|
|
593
|
+
if (event.message.role !== "assistant") return;
|
|
594
|
+
|
|
595
|
+
const contentIndex = assistantEvent.contentIndex;
|
|
596
|
+
const messageContent = event.message.content;
|
|
597
|
+
if (!Array.isArray(messageContent) || contentIndex >= messageContent.length) return;
|
|
598
|
+
const toolCall = messageContent[contentIndex] as ToolCall;
|
|
599
|
+
if (toolCall.name !== "edit" || !toolCall.id) return;
|
|
600
|
+
|
|
601
|
+
const args = toolCall.arguments;
|
|
602
|
+
if (!args || typeof args !== "object" || Array.isArray(args)) return;
|
|
603
|
+
if ("oldText" in args || "newText" in args) return;
|
|
604
|
+
|
|
605
|
+
const path = typeof args.path === "string" ? args.path : undefined;
|
|
606
|
+
const diff = typeof args.diff === "string" ? args.diff : undefined;
|
|
607
|
+
const op = typeof args.op === "string" ? args.op : undefined;
|
|
608
|
+
if (!path || !diff) return;
|
|
609
|
+
if (op && op !== "update") return;
|
|
610
|
+
|
|
611
|
+
if (!diff.includes("\n")) return;
|
|
612
|
+
const lastNewlineIndex = diff.lastIndexOf("\n");
|
|
613
|
+
if (lastNewlineIndex < 0) return;
|
|
614
|
+
const diffForCheck = diff.endsWith("\n") ? diff : diff.slice(0, lastNewlineIndex + 1);
|
|
615
|
+
if (diffForCheck.trim().length === 0) return;
|
|
616
|
+
|
|
617
|
+
const normalizedDiff = normalizeDiff(diffForCheck.replace(/\r/g, ""));
|
|
618
|
+
if (!normalizedDiff) return;
|
|
619
|
+
const lines = normalizedDiff.split("\n");
|
|
620
|
+
const hasChangeLine = lines.some((line) => line.startsWith("+") || line.startsWith("-"));
|
|
621
|
+
if (!hasChangeLine) return;
|
|
622
|
+
|
|
623
|
+
const lineCount = lines.length;
|
|
624
|
+
const lastChecked = this._streamingEditCheckedLineCounts.get(toolCall.id);
|
|
625
|
+
if (lastChecked !== undefined && lineCount <= lastChecked) return;
|
|
626
|
+
this._streamingEditCheckedLineCounts.set(toolCall.id, lineCount);
|
|
627
|
+
|
|
628
|
+
const rename = typeof args.rename === "string" ? args.rename : undefined;
|
|
629
|
+
|
|
630
|
+
const removedLines = lines
|
|
631
|
+
.filter((line) => line.startsWith("-") && !line.startsWith("--- "))
|
|
632
|
+
.map((line) => line.slice(1));
|
|
633
|
+
if (removedLines.length > 0) {
|
|
634
|
+
const resolvedPath = resolveToCwd(path, this.sessionManager.getCwd());
|
|
635
|
+
const cachedContent = this._streamingEditFileCache.get(resolvedPath);
|
|
636
|
+
if (cachedContent !== undefined) {
|
|
637
|
+
const missing = removedLines.find((line) => !cachedContent.includes(normalizeToLF(line)));
|
|
638
|
+
if (missing) {
|
|
639
|
+
this._streamingEditAbortTriggered = true;
|
|
640
|
+
logger.warn("Streaming edit aborted due to patch preview failure", {
|
|
641
|
+
toolCallId: toolCall.id,
|
|
642
|
+
path,
|
|
643
|
+
error: `Failed to find expected lines in ${path}:\n${missing}`,
|
|
644
|
+
});
|
|
645
|
+
this.agent.abort();
|
|
646
|
+
}
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
if (assistantEvent.type === "toolcall_delta") return;
|
|
650
|
+
void this._checkRemovedLinesAsync(toolCall.id, path, resolvedPath, removedLines);
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
if (assistantEvent.type === "toolcall_delta") return;
|
|
655
|
+
void this._checkPreviewPatchAsync(toolCall.id, path, rename, normalizedDiff);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
private async _checkRemovedLinesAsync(
|
|
659
|
+
toolCallId: string,
|
|
660
|
+
path: string,
|
|
661
|
+
resolvedPath: string,
|
|
662
|
+
removedLines: string[],
|
|
663
|
+
): Promise<void> {
|
|
664
|
+
if (this._streamingEditAbortTriggered) return;
|
|
665
|
+
try {
|
|
666
|
+
if (!(await Bun.file(resolvedPath).exists())) return;
|
|
667
|
+
const { text } = stripBom(await Bun.file(resolvedPath).text());
|
|
668
|
+
const normalizedContent = normalizeToLF(text);
|
|
669
|
+
const missing = removedLines.find((line) => !normalizedContent.includes(normalizeToLF(line)));
|
|
670
|
+
if (missing) {
|
|
671
|
+
this._streamingEditAbortTriggered = true;
|
|
672
|
+
logger.warn("Streaming edit aborted due to patch preview failure", {
|
|
673
|
+
toolCallId,
|
|
674
|
+
path,
|
|
675
|
+
error: `Failed to find expected lines in ${path}:\n${missing}`,
|
|
676
|
+
});
|
|
677
|
+
this.agent.abort();
|
|
678
|
+
}
|
|
679
|
+
} catch {
|
|
680
|
+
// Ignore errors during async fallback
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
private async _checkPreviewPatchAsync(
|
|
685
|
+
toolCallId: string,
|
|
686
|
+
path: string,
|
|
687
|
+
rename: string | undefined,
|
|
688
|
+
normalizedDiff: string,
|
|
689
|
+
): Promise<void> {
|
|
690
|
+
if (this._streamingEditAbortTriggered) return;
|
|
691
|
+
try {
|
|
692
|
+
await previewPatch(
|
|
693
|
+
{ path, op: "update", rename, diff: normalizedDiff },
|
|
694
|
+
{
|
|
695
|
+
cwd: this.sessionManager.getCwd(),
|
|
696
|
+
allowFuzzy: this.settingsManager.getEditFuzzyMatch(),
|
|
697
|
+
fuzzyThreshold: this.settingsManager.getEditFuzzyThreshold(),
|
|
698
|
+
},
|
|
699
|
+
);
|
|
700
|
+
} catch (error) {
|
|
701
|
+
if (error instanceof ParseError) return;
|
|
702
|
+
this._streamingEditAbortTriggered = true;
|
|
703
|
+
logger.warn("Streaming edit aborted due to patch preview failure", {
|
|
704
|
+
toolCallId,
|
|
705
|
+
path,
|
|
706
|
+
error: error instanceof Error ? error.message : String(error),
|
|
707
|
+
});
|
|
708
|
+
this.agent.abort();
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
528
712
|
/** Rewrite tool call arguments in agent state and persisted session history. */
|
|
529
713
|
private async _rewriteToolCallArgs(toolCallId: string, args: Record<string, unknown>): Promise<void> {
|
|
530
714
|
let updated = false;
|
|
@@ -2042,7 +2226,7 @@ export class AgentSession {
|
|
|
2042
2226
|
error: message,
|
|
2043
2227
|
model: `${candidate.provider}/${candidate.id}`,
|
|
2044
2228
|
});
|
|
2045
|
-
await
|
|
2229
|
+
await Bun.sleep(delayMs);
|
|
2046
2230
|
}
|
|
2047
2231
|
}
|
|
2048
2232
|
|
|
@@ -2223,9 +2407,9 @@ export class AgentSession {
|
|
|
2223
2407
|
// Create retry promise on first attempt so waitForRetry() can await it
|
|
2224
2408
|
// Ensure only one promise exists (avoid orphaned promises from concurrent calls)
|
|
2225
2409
|
if (!this._retryPromise) {
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2410
|
+
const { promise, resolve } = Promise.withResolvers<void>();
|
|
2411
|
+
this._retryPromise = promise;
|
|
2412
|
+
this._retryResolve = resolve;
|
|
2229
2413
|
}
|
|
2230
2414
|
|
|
2231
2415
|
if (this._retryAttempt > settings.maxRetries) {
|
|
@@ -2280,7 +2464,7 @@ export class AgentSession {
|
|
|
2280
2464
|
}
|
|
2281
2465
|
this._retryAbortController = new AbortController();
|
|
2282
2466
|
try {
|
|
2283
|
-
await
|
|
2467
|
+
await abortableSleep(delayMs, this._retryAbortController.signal);
|
|
2284
2468
|
} catch {
|
|
2285
2469
|
// Aborted during sleep - emit end event so UI can clean up
|
|
2286
2470
|
const attempt = this._retryAttempt;
|
|
@@ -2307,25 +2491,6 @@ export class AgentSession {
|
|
|
2307
2491
|
return true;
|
|
2308
2492
|
}
|
|
2309
2493
|
|
|
2310
|
-
/**
|
|
2311
|
-
* Sleep helper that respects abort signal.
|
|
2312
|
-
*/
|
|
2313
|
-
private _sleep(ms: number, signal?: AbortSignal): Promise<void> {
|
|
2314
|
-
return new Promise((resolve, reject) => {
|
|
2315
|
-
if (signal?.aborted) {
|
|
2316
|
-
reject(new Error("Aborted"));
|
|
2317
|
-
return;
|
|
2318
|
-
}
|
|
2319
|
-
|
|
2320
|
-
const timeout = setTimeout(resolve, ms);
|
|
2321
|
-
|
|
2322
|
-
signal?.addEventListener("abort", () => {
|
|
2323
|
-
clearTimeout(timeout);
|
|
2324
|
-
reject(new Error("Aborted"));
|
|
2325
|
-
});
|
|
2326
|
-
});
|
|
2327
|
-
}
|
|
2328
|
-
|
|
2329
2494
|
/**
|
|
2330
2495
|
* Cancel in-progress retry.
|
|
2331
2496
|
*/
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Database } from "bun:sqlite";
|
|
2
2
|
import { chmodSync, existsSync, mkdirSync } from "node:fs";
|
|
3
3
|
import { dirname } from "node:path";
|
|
4
|
+
import { logger } from "@oh-my-pi/pi-utils";
|
|
4
5
|
import { getAgentDbPath } from "../config";
|
|
5
6
|
import type { AuthCredential } from "./auth-storage";
|
|
6
|
-
import { logger } from "./logger";
|
|
7
7
|
import type { Settings } from "./settings-manager";
|
|
8
8
|
|
|
9
9
|
/** Prepared SQLite statement type from bun:sqlite */
|
package/src/core/auth-storage.ts
CHANGED
|
@@ -13,12 +13,13 @@ import {
|
|
|
13
13
|
loginGeminiCli,
|
|
14
14
|
loginGitHubCopilot,
|
|
15
15
|
loginOpenAICodex,
|
|
16
|
+
type OAuthController,
|
|
16
17
|
type OAuthCredentials,
|
|
17
18
|
type OAuthProvider,
|
|
18
19
|
} from "@oh-my-pi/pi-ai";
|
|
20
|
+
import { logger } from "@oh-my-pi/pi-utils";
|
|
19
21
|
import { getAgentDbPath, getAuthPath } from "../config";
|
|
20
22
|
import { AgentStorage } from "./agent-storage";
|
|
21
|
-
import { logger } from "./logger";
|
|
22
23
|
import { migrateJsonStorage } from "./storage-migration";
|
|
23
24
|
|
|
24
25
|
export type ApiKeyCredential = {
|
|
@@ -545,51 +546,40 @@ export class AuthStorage {
|
|
|
545
546
|
*/
|
|
546
547
|
async login(
|
|
547
548
|
provider: OAuthProvider,
|
|
548
|
-
|
|
549
|
+
ctrl: OAuthController & {
|
|
550
|
+
/** onAuth is required by auth-storage but optional in OAuthController */
|
|
549
551
|
onAuth: (info: { url: string; instructions?: string }) => void;
|
|
552
|
+
/** onPrompt is required for some providers (github-copilot, openai-codex) */
|
|
550
553
|
onPrompt: (prompt: { message: string; placeholder?: string }) => Promise<string>;
|
|
551
|
-
onProgress?: (message: string) => void;
|
|
552
|
-
/** For providers with local callback servers (e.g., openai-codex), races with browser callback */
|
|
553
|
-
onManualCodeInput?: () => Promise<string>;
|
|
554
|
-
/** For cancellation support (e.g., github-copilot polling) */
|
|
555
|
-
signal?: AbortSignal;
|
|
556
554
|
},
|
|
557
555
|
): Promise<void> {
|
|
558
556
|
let credentials: OAuthCredentials;
|
|
559
557
|
|
|
560
558
|
switch (provider) {
|
|
561
559
|
case "anthropic":
|
|
562
|
-
credentials = await loginAnthropic(
|
|
563
|
-
(url) => callbacks.onAuth({ url }),
|
|
564
|
-
() => callbacks.onPrompt({ message: "Paste the authorization code:" }),
|
|
565
|
-
);
|
|
560
|
+
credentials = await loginAnthropic(ctrl);
|
|
566
561
|
break;
|
|
567
562
|
case "github-copilot":
|
|
568
563
|
credentials = await loginGitHubCopilot({
|
|
569
|
-
onAuth: (url, instructions) =>
|
|
570
|
-
onPrompt:
|
|
571
|
-
onProgress:
|
|
572
|
-
signal:
|
|
564
|
+
onAuth: (url, instructions) => ctrl.onAuth({ url, instructions }),
|
|
565
|
+
onPrompt: ctrl.onPrompt,
|
|
566
|
+
onProgress: ctrl.onProgress,
|
|
567
|
+
signal: ctrl.signal,
|
|
573
568
|
});
|
|
574
569
|
break;
|
|
575
570
|
case "google-gemini-cli":
|
|
576
|
-
credentials = await loginGeminiCli(
|
|
571
|
+
credentials = await loginGeminiCli(ctrl);
|
|
577
572
|
break;
|
|
578
573
|
case "google-antigravity":
|
|
579
|
-
credentials = await loginAntigravity(
|
|
574
|
+
credentials = await loginAntigravity(ctrl);
|
|
580
575
|
break;
|
|
581
576
|
case "openai-codex":
|
|
582
|
-
credentials = await loginOpenAICodex(
|
|
583
|
-
onAuth: callbacks.onAuth,
|
|
584
|
-
onPrompt: callbacks.onPrompt,
|
|
585
|
-
onProgress: callbacks.onProgress,
|
|
586
|
-
onManualCodeInput: callbacks.onManualCodeInput,
|
|
587
|
-
});
|
|
577
|
+
credentials = await loginOpenAICodex(ctrl);
|
|
588
578
|
break;
|
|
589
579
|
case "cursor":
|
|
590
580
|
credentials = await loginCursor(
|
|
591
|
-
(url) =>
|
|
592
|
-
|
|
581
|
+
(url) => ctrl.onAuth({ url }),
|
|
582
|
+
ctrl.onProgress ? () => ctrl.onProgress?.("Waiting for browser authentication...") : undefined,
|
|
593
583
|
);
|
|
594
584
|
break;
|
|
595
585
|
default:
|