@oh-my-pi/pi-coding-agent 6.8.0 → 6.8.2
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/examples/extensions/plan-mode.ts +8 -8
- package/examples/extensions/tools.ts +7 -7
- package/package.json +6 -6
- package/src/cli/session-picker.ts +5 -2
- package/src/core/agent-session.ts +18 -5
- package/src/core/auth-storage.ts +13 -1
- package/src/core/bash-executor.ts +5 -4
- package/src/core/exec.ts +4 -2
- package/src/core/extensions/types.ts +1 -1
- package/src/core/hooks/types.ts +4 -3
- package/src/core/mcp/transports/http.ts +35 -27
- package/src/core/prompt-templates.ts +1 -1
- package/src/core/python-gateway-coordinator.ts +5 -4
- package/src/core/ssh/ssh-executor.ts +1 -1
- package/src/core/tools/lsp/client.ts +1 -1
- package/src/core/tools/patch/applicator.ts +38 -24
- package/src/core/tools/patch/diff.ts +7 -3
- package/src/core/tools/patch/fuzzy.ts +19 -1
- package/src/core/tools/patch/index.ts +4 -1
- package/src/core/tools/patch/types.ts +4 -0
- package/src/core/tools/python.ts +1 -0
- package/src/core/tools/task/executor.ts +100 -64
- package/src/core/tools/task/worker.ts +44 -14
- package/src/core/tools/web-fetch.ts +47 -7
- package/src/core/tools/web-scrapers/youtube.ts +6 -49
- package/src/lib/worktree/collapse.ts +3 -3
- package/src/lib/worktree/git.ts +6 -40
- package/src/lib/worktree/index.ts +1 -1
- package/src/main.ts +7 -5
- package/src/modes/interactive/components/login-dialog.ts +6 -2
- package/src/modes/interactive/components/tool-execution.ts +4 -0
- package/src/modes/interactive/interactive-mode.ts +3 -0
- package/src/utils/clipboard.ts +3 -5
- package/src/core/tools/task/model-resolver.ts +0 -206
package/src/lib/worktree/git.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
|
-
import
|
|
2
|
+
import { ptree } from "@oh-my-pi/pi-utils";
|
|
3
3
|
import { execCommand } from "../../core/exec";
|
|
4
4
|
import { WorktreeError, WorktreeErrorCode } from "./errors";
|
|
5
5
|
|
|
@@ -9,32 +9,6 @@ export interface GitResult {
|
|
|
9
9
|
stderr: string;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
type WritableLike = {
|
|
13
|
-
write: (chunk: string | Uint8Array) => unknown;
|
|
14
|
-
flush?: () => unknown;
|
|
15
|
-
end?: () => unknown;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
const textEncoder = new TextEncoder();
|
|
19
|
-
|
|
20
|
-
async function writeStdin(handle: unknown, stdin: string): Promise<void> {
|
|
21
|
-
if (!handle || typeof handle === "number") return;
|
|
22
|
-
if (typeof (handle as WritableStream<Uint8Array>).getWriter === "function") {
|
|
23
|
-
const writer = (handle as WritableStream<Uint8Array>).getWriter();
|
|
24
|
-
try {
|
|
25
|
-
await writer.write(textEncoder.encode(stdin));
|
|
26
|
-
} finally {
|
|
27
|
-
await writer.close();
|
|
28
|
-
}
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const sink = handle as WritableLike;
|
|
33
|
-
sink.write(stdin);
|
|
34
|
-
if (sink.flush) sink.flush();
|
|
35
|
-
if (sink.end) sink.end();
|
|
36
|
-
}
|
|
37
|
-
|
|
38
12
|
/**
|
|
39
13
|
* Execute a git command.
|
|
40
14
|
* @param args - Command arguments (excluding 'git')
|
|
@@ -50,23 +24,15 @@ export async function git(args: string[], cwd?: string): Promise<GitResult> {
|
|
|
50
24
|
* Execute git command with stdin input.
|
|
51
25
|
* Used for piping diffs to `git apply`.
|
|
52
26
|
*/
|
|
53
|
-
export async function
|
|
54
|
-
const proc
|
|
27
|
+
export async function gitWithInput(args: string[], stdin: string, cwd?: string): Promise<GitResult> {
|
|
28
|
+
const proc = ptree.cspawn(["git", ...args], {
|
|
55
29
|
cwd: cwd ?? process.cwd(),
|
|
56
|
-
stdin:
|
|
57
|
-
stdout: "pipe",
|
|
58
|
-
stderr: "pipe",
|
|
30
|
+
stdin: Buffer.from(stdin),
|
|
59
31
|
});
|
|
60
32
|
|
|
61
|
-
await
|
|
62
|
-
|
|
63
|
-
const [stdout, stderr, exitCode] = await Promise.all([
|
|
64
|
-
(proc.stdout as ReadableStream<Uint8Array>).text(),
|
|
65
|
-
(proc.stderr as ReadableStream<Uint8Array>).text(),
|
|
66
|
-
proc.exited,
|
|
67
|
-
]);
|
|
33
|
+
const [stdout, stderr] = await Promise.all([proc.stdout.text(), proc.stderr.text()]);
|
|
68
34
|
|
|
69
|
-
return { code: exitCode ?? 0, stdout, stderr };
|
|
35
|
+
return { code: proc.exitCode ?? 0, stdout, stderr };
|
|
70
36
|
}
|
|
71
37
|
|
|
72
38
|
/**
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { type CollapseOptions, type CollapseResult, type CollapseStrategy, collapse } from "./collapse";
|
|
2
2
|
export { WORKTREE_BASE } from "./constants";
|
|
3
3
|
export { WorktreeError, WorktreeErrorCode } from "./errors";
|
|
4
|
-
export { getRepoName, getRepoRoot, git, gitWithStdin } from "./git";
|
|
4
|
+
export { getRepoName, getRepoRoot, git, gitWithInput as gitWithStdin } from "./git";
|
|
5
5
|
export { create, find, list, prune, remove, type Worktree, which } from "./operations";
|
|
6
6
|
export {
|
|
7
7
|
cleanupSessions,
|
package/src/main.ts
CHANGED
|
@@ -92,11 +92,13 @@ async function runInteractiveMode(
|
|
|
92
92
|
|
|
93
93
|
await mode.init();
|
|
94
94
|
|
|
95
|
-
versionCheckPromise
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
95
|
+
versionCheckPromise
|
|
96
|
+
.then((newVersion) => {
|
|
97
|
+
if (newVersion) {
|
|
98
|
+
mode.showNewVersionNotification(newVersion);
|
|
99
|
+
}
|
|
100
|
+
})
|
|
101
|
+
.catch(() => {});
|
|
100
102
|
|
|
101
103
|
mode.renderInitialMessages();
|
|
102
104
|
|
|
@@ -109,7 +109,9 @@ export class LoginDialogComponent extends Container {
|
|
|
109
109
|
showManualInput(prompt: string): Promise<string> {
|
|
110
110
|
this.contentContainer.addChild(new Spacer(1));
|
|
111
111
|
this.contentContainer.addChild(new Text(theme.fg("dim", prompt), 1, 0));
|
|
112
|
-
this.contentContainer.
|
|
112
|
+
if (!this.contentContainer.children.includes(this.input)) {
|
|
113
|
+
this.contentContainer.addChild(this.input);
|
|
114
|
+
}
|
|
113
115
|
this.contentContainer.addChild(new Text(theme.fg("dim", "(Escape to cancel)"), 1, 0));
|
|
114
116
|
this.tui.requestRender();
|
|
115
117
|
|
|
@@ -129,7 +131,9 @@ export class LoginDialogComponent extends Container {
|
|
|
129
131
|
if (placeholder) {
|
|
130
132
|
this.contentContainer.addChild(new Text(theme.fg("dim", `e.g., ${placeholder}`), 1, 0));
|
|
131
133
|
}
|
|
132
|
-
this.contentContainer.
|
|
134
|
+
if (!this.contentContainer.children.includes(this.input)) {
|
|
135
|
+
this.contentContainer.addChild(this.input);
|
|
136
|
+
}
|
|
133
137
|
this.contentContainer.addChild(new Text(theme.fg("dim", "(Escape to cancel, Enter to submit)"), 1, 0));
|
|
134
138
|
|
|
135
139
|
this.input.setValue("");
|
|
@@ -260,6 +260,10 @@ export class ToolExecutionComponent extends Container {
|
|
|
260
260
|
): void {
|
|
261
261
|
this.result = result;
|
|
262
262
|
this.isPartial = isPartial;
|
|
263
|
+
// When tool is complete, ensure args are marked complete so spinner stops
|
|
264
|
+
if (!isPartial) {
|
|
265
|
+
this.argsComplete = true;
|
|
266
|
+
}
|
|
263
267
|
this.updateSpinnerAnimation();
|
|
264
268
|
this.updateDisplay();
|
|
265
269
|
// Convert non-PNG images to PNG for Kitty protocol (async)
|
|
@@ -168,6 +168,9 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
168
168
|
this.editor.onAutocompleteCancel = () => {
|
|
169
169
|
this.ui.requestRender(true);
|
|
170
170
|
};
|
|
171
|
+
this.editor.onAutocompleteUpdate = () => {
|
|
172
|
+
this.ui.requestRender(true);
|
|
173
|
+
};
|
|
171
174
|
try {
|
|
172
175
|
this.historyStorage = HistoryStorage.open();
|
|
173
176
|
this.editor.setHistoryStorage(this.historyStorage);
|
package/src/utils/clipboard.ts
CHANGED
|
@@ -32,8 +32,6 @@ function selectPreferredImageMimeType(mimeTypes: string[]): string | null {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
export async function copyToClipboard(text: string): Promise<void> {
|
|
35
|
-
const timeout = Bun.sleep(3000).then(() => Promise.reject(new Error("Clipboard operation timed out")));
|
|
36
|
-
|
|
37
35
|
let promise: Promise<void>;
|
|
38
36
|
try {
|
|
39
37
|
switch (platform()) {
|
|
@@ -56,11 +54,11 @@ export async function copyToClipboard(text: string): Promise<void> {
|
|
|
56
54
|
}
|
|
57
55
|
} catch (error) {
|
|
58
56
|
if (error instanceof Error) {
|
|
59
|
-
throw new Error(`Failed to copy to clipboard: ${error.message}
|
|
57
|
+
throw new Error(`Failed to copy to clipboard: ${error.message}`, { cause: error });
|
|
60
58
|
}
|
|
61
|
-
throw new Error(`Failed to copy to clipboard: ${String(error)}
|
|
59
|
+
throw new Error(`Failed to copy to clipboard: ${String(error)}`, { cause: error });
|
|
62
60
|
}
|
|
63
|
-
await Promise.race([promise,
|
|
61
|
+
await Promise.race([promise, Bun.sleep(3000)]);
|
|
64
62
|
}
|
|
65
63
|
|
|
66
64
|
export interface ClipboardImage {
|
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Model resolution with fuzzy pattern matching.
|
|
3
|
-
*
|
|
4
|
-
* Returns models in "provider/modelId" format for use with --model flag.
|
|
5
|
-
*
|
|
6
|
-
* Supports:
|
|
7
|
-
* - Exact match: "gpt-5.2" → "p-openai/gpt-5.2"
|
|
8
|
-
* - Fuzzy match: "opus" → "p-anthropic/claude-opus-4-5"
|
|
9
|
-
* - Comma fallback: "gpt, opus" → tries gpt first, then opus
|
|
10
|
-
* - "default" → undefined (use system default)
|
|
11
|
-
* - "omp/slow" or "pi/slow" → configured slow model from settings
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { $ } from "bun";
|
|
15
|
-
import { type Settings as SettingsFile, settingsCapability } from "../../../capability/settings";
|
|
16
|
-
import { loadCapability } from "../../../discovery";
|
|
17
|
-
import type { Settings as SettingsData } from "../../settings-manager";
|
|
18
|
-
import { resolveOmpCommand } from "./omp-command";
|
|
19
|
-
|
|
20
|
-
/** Cache for available models (provider/modelId format) */
|
|
21
|
-
let cachedModels: string[] | null = null;
|
|
22
|
-
|
|
23
|
-
/** Cache expiry time (5 minutes) */
|
|
24
|
-
let cacheExpiry = 0;
|
|
25
|
-
|
|
26
|
-
const CACHE_TTL_MS = 5 * 60 * 1000;
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Get available models from `omp --list-models`.
|
|
30
|
-
* Returns models in "provider/modelId" format.
|
|
31
|
-
* Caches the result for performance.
|
|
32
|
-
*/
|
|
33
|
-
export async function getAvailableModels(): Promise<string[]> {
|
|
34
|
-
const now = Date.now();
|
|
35
|
-
if (cachedModels !== null && now < cacheExpiry) {
|
|
36
|
-
return cachedModels;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
try {
|
|
40
|
-
const ompCommand = resolveOmpCommand();
|
|
41
|
-
const result = await $`${ompCommand.cmd} ${ompCommand.args} --list-models`.quiet().nothrow();
|
|
42
|
-
const stdout = result.stdout?.toString() ?? "";
|
|
43
|
-
|
|
44
|
-
if (result.exitCode !== 0 || !stdout.trim()) {
|
|
45
|
-
cachedModels = [];
|
|
46
|
-
cacheExpiry = now + CACHE_TTL_MS;
|
|
47
|
-
return cachedModels;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Parse output: skip header line, extract provider/model
|
|
51
|
-
const lines = stdout.trim().split("\n");
|
|
52
|
-
cachedModels = lines
|
|
53
|
-
.slice(1) // Skip header
|
|
54
|
-
.map((line) => {
|
|
55
|
-
const parts = line.trim().split(/\s+/);
|
|
56
|
-
// Format: provider/modelId
|
|
57
|
-
return parts[0] && parts[1] ? `${parts[0]}/${parts[1]}` : "";
|
|
58
|
-
})
|
|
59
|
-
.filter(Boolean);
|
|
60
|
-
|
|
61
|
-
cacheExpiry = now + CACHE_TTL_MS;
|
|
62
|
-
return cachedModels;
|
|
63
|
-
} catch {
|
|
64
|
-
cachedModels = [];
|
|
65
|
-
cacheExpiry = now + CACHE_TTL_MS;
|
|
66
|
-
return cachedModels;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Clear the model cache (for testing).
|
|
72
|
-
*/
|
|
73
|
-
export function clearModelCache(): void {
|
|
74
|
-
cachedModels = null;
|
|
75
|
-
cacheExpiry = 0;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Load model roles from settings files using capability API.
|
|
80
|
-
*/
|
|
81
|
-
async function loadModelRoles(): Promise<Record<string, string>> {
|
|
82
|
-
const result = await loadCapability<SettingsFile>(settingsCapability.id, { cwd: process.cwd() });
|
|
83
|
-
|
|
84
|
-
// Merge all settings, prioritizing first (highest priority)
|
|
85
|
-
let modelRoles: Record<string, string> = {};
|
|
86
|
-
for (const settings of result.items.reverse()) {
|
|
87
|
-
const roles = settings.data.modelRoles as Record<string, string> | undefined;
|
|
88
|
-
if (roles) {
|
|
89
|
-
modelRoles = { ...modelRoles, ...roles };
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return modelRoles;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Resolve an omp/<role> alias to a model string.
|
|
98
|
-
* Looks up the role in settings.modelRoles and returns the configured model.
|
|
99
|
-
* Returns undefined if the role isn't configured.
|
|
100
|
-
*/
|
|
101
|
-
async function resolveOmpAlias(
|
|
102
|
-
role: string,
|
|
103
|
-
availableModels: string[],
|
|
104
|
-
settings?: SettingsData,
|
|
105
|
-
): Promise<string | undefined> {
|
|
106
|
-
const roles = settings?.modelRoles ?? (await loadModelRoles());
|
|
107
|
-
|
|
108
|
-
// Look up role in settings (case-insensitive)
|
|
109
|
-
const configured = roles[role] || roles[role.toLowerCase()];
|
|
110
|
-
if (!configured) return undefined;
|
|
111
|
-
|
|
112
|
-
// configured is in "provider/modelId" format, find in available models
|
|
113
|
-
return availableModels.find((m) => m.toLowerCase() === configured.toLowerCase());
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Extract model ID from "provider/modelId" format.
|
|
118
|
-
*/
|
|
119
|
-
function getModelId(fullModel: string): string {
|
|
120
|
-
const slashIdx = fullModel.indexOf("/");
|
|
121
|
-
return slashIdx > 0 ? fullModel.slice(slashIdx + 1) : fullModel;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Extract provider from "provider/modelId" format.
|
|
126
|
-
* Returns undefined if no provider prefix.
|
|
127
|
-
*/
|
|
128
|
-
function getProvider(fullModel: string): string | undefined {
|
|
129
|
-
const slashIdx = fullModel.indexOf("/");
|
|
130
|
-
return slashIdx > 0 ? fullModel.slice(0, slashIdx) : undefined;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Resolve a fuzzy model pattern to "provider/modelId" format.
|
|
135
|
-
*
|
|
136
|
-
* Supports comma-separated patterns (e.g., "gpt, opus") - tries each in order.
|
|
137
|
-
* Returns undefined if pattern is "default", undefined, or no match found.
|
|
138
|
-
*
|
|
139
|
-
* @param pattern - Model pattern to resolve
|
|
140
|
-
* @param availableModels - Optional pre-fetched list of available models (in provider/modelId format)
|
|
141
|
-
* @param settings - Optional settings for role alias resolution (pi/..., omp/...)
|
|
142
|
-
*/
|
|
143
|
-
export async function resolveModelPattern(
|
|
144
|
-
pattern: string | undefined,
|
|
145
|
-
availableModels?: string[],
|
|
146
|
-
settings?: SettingsData,
|
|
147
|
-
): Promise<string | undefined> {
|
|
148
|
-
if (!pattern || pattern === "default") {
|
|
149
|
-
return undefined;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const models = availableModels ?? (await getAvailableModels());
|
|
153
|
-
if (models.length === 0) {
|
|
154
|
-
// Fallback: return pattern as-is if we can't get available models
|
|
155
|
-
return pattern;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// Split by comma, try each pattern in order
|
|
159
|
-
const patterns = pattern
|
|
160
|
-
.split(",")
|
|
161
|
-
.map((p) => p.trim())
|
|
162
|
-
.filter(Boolean);
|
|
163
|
-
|
|
164
|
-
for (const p of patterns) {
|
|
165
|
-
// Handle omp/<role> or pi/<role> aliases - looks up role in settings.modelRoles
|
|
166
|
-
const lower = p.toLowerCase();
|
|
167
|
-
if (lower.startsWith("omp/") || lower.startsWith("pi/")) {
|
|
168
|
-
const role = lower.startsWith("omp/") ? p.slice(4) : p.slice(3);
|
|
169
|
-
const resolved = await resolveOmpAlias(role, models, settings);
|
|
170
|
-
if (resolved) return resolved;
|
|
171
|
-
continue; // Role not configured, try next pattern
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// Try exact match on full provider/modelId
|
|
175
|
-
const exactFull = models.find((m) => m.toLowerCase() === p.toLowerCase());
|
|
176
|
-
if (exactFull) return exactFull;
|
|
177
|
-
|
|
178
|
-
// Try exact match on model ID only
|
|
179
|
-
const exactId = models.find((m) => getModelId(m).toLowerCase() === p.toLowerCase());
|
|
180
|
-
if (exactId) return exactId;
|
|
181
|
-
|
|
182
|
-
// Check if pattern has provider prefix (e.g., "zai/glm-4.7")
|
|
183
|
-
const patternProvider = getProvider(p);
|
|
184
|
-
const patternModelId = getModelId(p);
|
|
185
|
-
|
|
186
|
-
// If pattern has provider prefix, fuzzy match must stay within that provider
|
|
187
|
-
// (don't cross provider boundaries when user explicitly specifies provider)
|
|
188
|
-
if (patternProvider) {
|
|
189
|
-
const providerFuzzyMatch = models.find(
|
|
190
|
-
(m) =>
|
|
191
|
-
getProvider(m)?.toLowerCase() === patternProvider.toLowerCase() &&
|
|
192
|
-
getModelId(m).toLowerCase().includes(patternModelId.toLowerCase()),
|
|
193
|
-
);
|
|
194
|
-
if (providerFuzzyMatch) return providerFuzzyMatch;
|
|
195
|
-
// No match in specified provider - don't fall through to other providers
|
|
196
|
-
continue;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// No provider prefix - fall back to general fuzzy match on model ID (substring)
|
|
200
|
-
const fuzzyMatch = models.find((m) => getModelId(m).toLowerCase().includes(patternModelId.toLowerCase()));
|
|
201
|
-
if (fuzzyMatch) return fuzzyMatch;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// No match found - use default model
|
|
205
|
-
return undefined;
|
|
206
|
-
}
|