@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.
Files changed (35) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/examples/extensions/plan-mode.ts +8 -8
  3. package/examples/extensions/tools.ts +7 -7
  4. package/package.json +6 -6
  5. package/src/cli/session-picker.ts +5 -2
  6. package/src/core/agent-session.ts +18 -5
  7. package/src/core/auth-storage.ts +13 -1
  8. package/src/core/bash-executor.ts +5 -4
  9. package/src/core/exec.ts +4 -2
  10. package/src/core/extensions/types.ts +1 -1
  11. package/src/core/hooks/types.ts +4 -3
  12. package/src/core/mcp/transports/http.ts +35 -27
  13. package/src/core/prompt-templates.ts +1 -1
  14. package/src/core/python-gateway-coordinator.ts +5 -4
  15. package/src/core/ssh/ssh-executor.ts +1 -1
  16. package/src/core/tools/lsp/client.ts +1 -1
  17. package/src/core/tools/patch/applicator.ts +38 -24
  18. package/src/core/tools/patch/diff.ts +7 -3
  19. package/src/core/tools/patch/fuzzy.ts +19 -1
  20. package/src/core/tools/patch/index.ts +4 -1
  21. package/src/core/tools/patch/types.ts +4 -0
  22. package/src/core/tools/python.ts +1 -0
  23. package/src/core/tools/task/executor.ts +100 -64
  24. package/src/core/tools/task/worker.ts +44 -14
  25. package/src/core/tools/web-fetch.ts +47 -7
  26. package/src/core/tools/web-scrapers/youtube.ts +6 -49
  27. package/src/lib/worktree/collapse.ts +3 -3
  28. package/src/lib/worktree/git.ts +6 -40
  29. package/src/lib/worktree/index.ts +1 -1
  30. package/src/main.ts +7 -5
  31. package/src/modes/interactive/components/login-dialog.ts +6 -2
  32. package/src/modes/interactive/components/tool-execution.ts +4 -0
  33. package/src/modes/interactive/interactive-mode.ts +3 -0
  34. package/src/utils/clipboard.ts +3 -5
  35. package/src/core/tools/task/model-resolver.ts +0 -206
@@ -1,5 +1,5 @@
1
1
  import * as path from "node:path";
2
- import type { Subprocess } from "bun";
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 gitWithStdin(args: string[], stdin: string, cwd?: string): Promise<GitResult> {
54
- const proc: Subprocess = Bun.spawn(["git", ...args], {
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: "pipe",
57
- stdout: "pipe",
58
- stderr: "pipe",
30
+ stdin: Buffer.from(stdin),
59
31
  });
60
32
 
61
- await writeStdin(proc.stdin, stdin);
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.then((newVersion) => {
96
- if (newVersion) {
97
- mode.showNewVersionNotification(newVersion);
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.addChild(this.input);
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.addChild(this.input);
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);
@@ -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, timeout]);
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
- }