@oh-my-pi/pi-coding-agent 13.2.1 → 13.3.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 +29 -2
- package/package.json +7 -7
- package/scripts/generate-docs-index.ts +2 -2
- package/src/cli/args.ts +2 -1
- package/src/config/settings-schema.ts +36 -4
- package/src/config/settings.ts +10 -0
- package/src/discovery/claude.ts +24 -6
- package/src/ipy/runtime.ts +1 -0
- package/src/mcp/config.ts +1 -1
- package/src/modes/components/settings-defs.ts +17 -1
- package/src/modes/components/status-line.ts +7 -5
- package/src/modes/controllers/mcp-command-controller.ts +4 -3
- package/src/modes/controllers/selector-controller.ts +21 -0
- package/src/modes/interactive-mode.ts +9 -0
- package/src/modes/oauth-manual-input.ts +42 -0
- package/src/modes/types.ts +2 -0
- package/src/patch/hashline.ts +19 -1
- package/src/prompts/system/commit-message-system.md +2 -0
- package/src/prompts/system/subagent-submit-reminder.md +3 -3
- package/src/prompts/system/subagent-system-prompt.md +4 -4
- package/src/prompts/system/system-prompt.md +13 -0
- package/src/prompts/tools/hashline.md +45 -1
- package/src/prompts/tools/task-summary.md +4 -4
- package/src/prompts/tools/task.md +1 -1
- package/src/sdk.ts +3 -0
- package/src/slash-commands/builtin-registry.ts +26 -1
- package/src/system-prompt.ts +4 -0
- package/src/task/index.ts +211 -70
- package/src/task/render.ts +24 -8
- package/src/task/types.ts +6 -1
- package/src/task/worktree.ts +394 -31
- package/src/tools/submit-result.ts +22 -23
- package/src/utils/commit-message-generator.ts +132 -0
- package/src/web/search/providers/exa.ts +41 -4
- package/src/web/search/providers/perplexity.ts +20 -8
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [13.3.0] - 2026-02-26
|
|
6
|
+
|
|
7
|
+
### Breaking Changes
|
|
8
|
+
|
|
9
|
+
- Renamed `task.isolation.enabled` (boolean) setting to `task.isolation.mode` (enum: `none`, `worktree`, `fuse-overlay`). Existing `true`/`false` values are auto-migrated to `worktree`/`none`.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Added `PERPLEXITY_COOKIES` env var for Perplexity web search via session cookies extracted from desktop app
|
|
14
|
+
- Added `fuse-overlay` isolation mode for subagents using `fuse-overlayfs` (copy-on-write overlay, no baseline patch apply needed)
|
|
15
|
+
- Added `task.isolation.merge` setting (`patch` or `branch`) to control how isolated task changes are integrated back. `branch` mode commits each task to a temp branch and cherry-picks for clean commit history
|
|
16
|
+
- Added `task.isolation.commits` setting (`generic` or `ai`) for commit messages on isolated task branches and nested repos. `ai` mode uses a smol model to generate conventional commit messages from diffs
|
|
17
|
+
- Nested non-submodule git repos are now discovered and handled during task isolation (changes captured and applied independently from parent repo)
|
|
18
|
+
- Added `task.eager` setting to encourage the agent to delegate work to subagents by default
|
|
19
|
+
- Added manual OAuth login flow that lets users paste redirect URLs with /login for callback-server providers and prevents overlapping logins
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- Fixed nested repo changes being lost when tasks commit inside the isolation (baseline state is now committed before task runs, so delta correctly excludes it)
|
|
24
|
+
- Fixed nested repo patches conflicting when multiple tasks contribute to the same repo (baseline untracked files no longer leak into patches)
|
|
25
|
+
- Nested repo changes are now committed after patch application (previously left as untracked files)
|
|
26
|
+
- Failed tasks no longer create stale branches or capture garbage patches (gated on exit code)
|
|
27
|
+
- Merge failures (e.g. conflicting patches) are now non-fatal — agent output is preserved with `merge failed` status instead of `failed`
|
|
28
|
+
- Stale branches are cleaned up when `commitToBranch` fails
|
|
29
|
+
- Commit message generator filters lock files from diffs before AI summarization
|
|
30
|
+
|
|
5
31
|
## [13.2.1] - 2026-02-24
|
|
6
32
|
|
|
7
33
|
### Fixed
|
|
@@ -11,7 +37,6 @@
|
|
|
11
37
|
### Changed
|
|
12
38
|
|
|
13
39
|
- Extracted non-interactive environment config from `bash-interactive.ts` into shared `non-interactive-env.ts` module, applied consistently to all bash execution paths
|
|
14
|
-
|
|
15
40
|
## [13.2.0] - 2026-02-23
|
|
16
41
|
### Breaking Changes
|
|
17
42
|
|
|
@@ -34,12 +59,14 @@
|
|
|
34
59
|
- Removed unused SSH resource cleanup functions `closeAllConnections` and `unmountAll` from session imports
|
|
35
60
|
|
|
36
61
|
## [13.1.2] - 2026-02-23
|
|
37
|
-
### Breaking Changes
|
|
38
62
|
|
|
63
|
+
### Breaking Changes
|
|
39
64
|
- Removed `timeout` parameter from await tool—tool now waits indefinitely until jobs complete or the call is aborted
|
|
40
65
|
- Renamed `job_ids` parameter to `jobs` in await tool schema
|
|
41
66
|
- Removed `timedOut` field from await tool result details
|
|
42
67
|
|
|
68
|
+
### Changed
|
|
69
|
+
- Resolved docs index generation paths using path.resolve relative to the script directory
|
|
43
70
|
## [13.1.1] - 2026-02-23
|
|
44
71
|
|
|
45
72
|
### Fixed
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
4
|
-
"version": "13.
|
|
4
|
+
"version": "13.3.0",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://github.com/can1357/oh-my-pi",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -41,12 +41,12 @@
|
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@mozilla/readability": "^0.6",
|
|
44
|
-
"@oh-my-pi/omp-stats": "13.
|
|
45
|
-
"@oh-my-pi/pi-agent-core": "13.
|
|
46
|
-
"@oh-my-pi/pi-ai": "13.
|
|
47
|
-
"@oh-my-pi/pi-natives": "13.
|
|
48
|
-
"@oh-my-pi/pi-tui": "13.
|
|
49
|
-
"@oh-my-pi/pi-utils": "13.
|
|
44
|
+
"@oh-my-pi/omp-stats": "13.3.0",
|
|
45
|
+
"@oh-my-pi/pi-agent-core": "13.3.0",
|
|
46
|
+
"@oh-my-pi/pi-ai": "13.3.0",
|
|
47
|
+
"@oh-my-pi/pi-natives": "13.3.0",
|
|
48
|
+
"@oh-my-pi/pi-tui": "13.3.0",
|
|
49
|
+
"@oh-my-pi/pi-utils": "13.3.0",
|
|
50
50
|
"@sinclair/typebox": "^0.34",
|
|
51
51
|
"@xterm/headless": "^6.0",
|
|
52
52
|
"ajv": "^8.18",
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
import { Glob } from "bun";
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
|
|
6
|
-
const docsDir =
|
|
7
|
-
const outputPath =
|
|
6
|
+
const docsDir = path.resolve(import.meta.dir, "../../../docs");
|
|
7
|
+
const outputPath = path.resolve(import.meta.dir, "../src/internal-urls/docs-index.generated.ts");
|
|
8
8
|
|
|
9
9
|
const glob = new Glob("**/*.md");
|
|
10
10
|
const entries: string[] = [];
|
package/src/cli/args.ts
CHANGED
|
@@ -216,7 +216,8 @@ export function getExtraHelpText(): string {
|
|
|
216
216
|
${chalk.dim("# Search & Tools")}
|
|
217
217
|
EXA_API_KEY - Exa web search
|
|
218
218
|
BRAVE_API_KEY - Brave web search
|
|
219
|
-
PERPLEXITY_API_KEY - Perplexity web search
|
|
219
|
+
PERPLEXITY_API_KEY - Perplexity web search (API)
|
|
220
|
+
PERPLEXITY_COOKIES - Perplexity web search (session cookie)
|
|
220
221
|
ANTHROPIC_SEARCH_API_KEY - Anthropic search provider
|
|
221
222
|
|
|
222
223
|
${chalk.dim("# Configuration")}
|
|
@@ -544,16 +544,48 @@ export const SETTINGS_SCHEMA = {
|
|
|
544
544
|
// ─────────────────────────────────────────────────────────────────────────
|
|
545
545
|
// Task tool settings
|
|
546
546
|
// ─────────────────────────────────────────────────────────────────────────
|
|
547
|
-
"task.isolation.
|
|
548
|
-
type: "
|
|
549
|
-
|
|
547
|
+
"task.isolation.mode": {
|
|
548
|
+
type: "enum",
|
|
549
|
+
values: ["none", "worktree", "fuse-overlay"] as const,
|
|
550
|
+
default: "none",
|
|
550
551
|
ui: {
|
|
551
552
|
tab: "tools",
|
|
552
553
|
label: "Task isolation",
|
|
553
|
-
description: "
|
|
554
|
+
description: "Isolation mode for subagents (none, git worktree, or fuse-overlay)",
|
|
554
555
|
submenu: true,
|
|
555
556
|
},
|
|
556
557
|
},
|
|
558
|
+
"task.isolation.merge": {
|
|
559
|
+
type: "enum",
|
|
560
|
+
values: ["patch", "branch"] as const,
|
|
561
|
+
default: "patch",
|
|
562
|
+
ui: {
|
|
563
|
+
tab: "tools",
|
|
564
|
+
label: "Task isolation merge",
|
|
565
|
+
description: "How isolated task changes are integrated (patch apply or branch merge)",
|
|
566
|
+
submenu: true,
|
|
567
|
+
},
|
|
568
|
+
},
|
|
569
|
+
"task.isolation.commits": {
|
|
570
|
+
type: "enum",
|
|
571
|
+
values: ["generic", "ai"] as const,
|
|
572
|
+
default: "generic",
|
|
573
|
+
ui: {
|
|
574
|
+
tab: "tools",
|
|
575
|
+
label: "Task isolation commits",
|
|
576
|
+
description: "Commit message style for nested repo changes (generic or AI-generated)",
|
|
577
|
+
submenu: true,
|
|
578
|
+
},
|
|
579
|
+
},
|
|
580
|
+
"task.eager": {
|
|
581
|
+
type: "boolean",
|
|
582
|
+
default: false,
|
|
583
|
+
ui: {
|
|
584
|
+
tab: "tools",
|
|
585
|
+
label: "Eager task delegation",
|
|
586
|
+
description: "Encourage the agent to delegate work to subagents unless changes are trivial",
|
|
587
|
+
},
|
|
588
|
+
},
|
|
557
589
|
"task.maxConcurrency": {
|
|
558
590
|
type: "number",
|
|
559
591
|
default: 32,
|
package/src/config/settings.ts
CHANGED
|
@@ -553,6 +553,16 @@ export class Settings {
|
|
|
553
553
|
}
|
|
554
554
|
}
|
|
555
555
|
|
|
556
|
+
// task.isolation.enabled (boolean) -> task.isolation.mode (enum)
|
|
557
|
+
const taskObj = raw.task as Record<string, unknown> | undefined;
|
|
558
|
+
const isolationObj = taskObj?.isolation as Record<string, unknown> | undefined;
|
|
559
|
+
if (isolationObj && "enabled" in isolationObj) {
|
|
560
|
+
if (typeof isolationObj.enabled === "boolean") {
|
|
561
|
+
isolationObj.mode = isolationObj.enabled ? "worktree" : "none";
|
|
562
|
+
}
|
|
563
|
+
delete isolationObj.enabled;
|
|
564
|
+
}
|
|
565
|
+
|
|
556
566
|
return raw;
|
|
557
567
|
}
|
|
558
568
|
|
package/src/discovery/claude.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Priority: 80 (tool-specific, below builtin but above shared standards)
|
|
6
6
|
*/
|
|
7
7
|
import * as path from "node:path";
|
|
8
|
-
import { tryParseJson } from "@oh-my-pi/pi-utils";
|
|
8
|
+
import { hasFsCode, tryParseJson } from "@oh-my-pi/pi-utils";
|
|
9
9
|
import { registerProvider } from "../capability";
|
|
10
10
|
import { type ContextFile, contextFileCapability } from "../capability/context-file";
|
|
11
11
|
import { type ExtensionModule, extensionModuleCapability } from "../capability/extension-module";
|
|
@@ -47,6 +47,10 @@ function getProjectClaude(ctx: LoadContext): string {
|
|
|
47
47
|
return path.join(ctx.cwd, CONFIG_DIR);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
function isMissingDirectoryError(error: unknown): boolean {
|
|
51
|
+
return hasFsCode(error, "ENOENT") || hasFsCode(error, "ENOTDIR");
|
|
52
|
+
}
|
|
53
|
+
|
|
50
54
|
// =============================================================================
|
|
51
55
|
// MCP Servers
|
|
52
56
|
// =============================================================================
|
|
@@ -162,15 +166,29 @@ async function loadSkills(ctx: LoadContext): Promise<LoadResult<Skill>> {
|
|
|
162
166
|
const userSkillsDir = path.join(getUserClaude(ctx), "skills");
|
|
163
167
|
const projectSkillsDir = path.join(getProjectClaude(ctx), "skills");
|
|
164
168
|
|
|
165
|
-
const
|
|
169
|
+
const [userResult, projectResult] = await Promise.allSettled([
|
|
166
170
|
scanSkillsFromDir(ctx, { dir: userSkillsDir, providerId: PROVIDER_ID, level: "user" }),
|
|
167
171
|
scanSkillsFromDir(ctx, { dir: projectSkillsDir, providerId: PROVIDER_ID, level: "project" }),
|
|
168
172
|
]);
|
|
169
173
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
+
const items: Skill[] = [];
|
|
175
|
+
const warnings: string[] = [];
|
|
176
|
+
|
|
177
|
+
if (userResult.status === "fulfilled") {
|
|
178
|
+
items.push(...userResult.value.items);
|
|
179
|
+
warnings.push(...(userResult.value.warnings ?? []));
|
|
180
|
+
} else if (!isMissingDirectoryError(userResult.reason)) {
|
|
181
|
+
warnings.push(`Failed to scan Claude user skills in ${userSkillsDir}: ${String(userResult.reason)}`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (projectResult.status === "fulfilled") {
|
|
185
|
+
items.push(...projectResult.value.items);
|
|
186
|
+
warnings.push(...(projectResult.value.warnings ?? []));
|
|
187
|
+
} else if (!isMissingDirectoryError(projectResult.reason)) {
|
|
188
|
+
warnings.push(`Failed to scan Claude project skills in ${projectSkillsDir}: ${String(projectResult.reason)}`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return { items, warnings };
|
|
174
192
|
}
|
|
175
193
|
|
|
176
194
|
// =============================================================================
|
package/src/ipy/runtime.ts
CHANGED
package/src/mcp/config.ts
CHANGED
|
@@ -110,7 +110,7 @@ export async function loadAllMCPConfigs(cwd: string, options?: LoadMCPConfigsOpt
|
|
|
110
110
|
let sources: Record<string, SourceMeta> = {};
|
|
111
111
|
for (const server of servers) {
|
|
112
112
|
const config = convertToLegacyConfig(server);
|
|
113
|
-
if (config.enabled === false ||
|
|
113
|
+
if (config.enabled === false || disabledServers.has(server.name)) {
|
|
114
114
|
continue;
|
|
115
115
|
}
|
|
116
116
|
configs[server.name] = config;
|
|
@@ -93,6 +93,22 @@ const OPTION_PROVIDERS: Partial<Record<SettingPath, OptionProvider>> = {
|
|
|
93
93
|
{ value: "2", label: "Double" },
|
|
94
94
|
{ value: "3", label: "Triple" },
|
|
95
95
|
],
|
|
96
|
+
// Task isolation mode
|
|
97
|
+
"task.isolation.mode": [
|
|
98
|
+
{ value: "none", label: "None", description: "No isolation" },
|
|
99
|
+
{ value: "worktree", label: "Worktree", description: "Git worktree isolation" },
|
|
100
|
+
{ value: "fuse-overlay", label: "Fuse Overlay", description: "COW overlay via fuse-overlayfs" },
|
|
101
|
+
],
|
|
102
|
+
// Task isolation merge strategy
|
|
103
|
+
"task.isolation.merge": [
|
|
104
|
+
{ value: "patch", label: "Patch", description: "Combine diffs and git apply" },
|
|
105
|
+
{ value: "branch", label: "Branch", description: "Commit per task, merge with --no-ff" },
|
|
106
|
+
],
|
|
107
|
+
// Task isolation commit messages
|
|
108
|
+
"task.isolation.commits": [
|
|
109
|
+
{ value: "generic", label: "Generic", description: "Static commit message" },
|
|
110
|
+
{ value: "ai", label: "AI", description: "AI-generated commit message from diff" },
|
|
111
|
+
],
|
|
96
112
|
// Todo max reminders
|
|
97
113
|
"todo.reminders.max": [
|
|
98
114
|
{ value: "1", label: "1 reminder" },
|
|
@@ -166,7 +182,7 @@ const OPTION_PROVIDERS: Partial<Record<SettingPath, OptionProvider>> = {
|
|
|
166
182
|
{ value: "brave", label: "Brave", description: "Requires BRAVE_API_KEY" },
|
|
167
183
|
{ value: "jina", label: "Jina", description: "Requires JINA_API_KEY" },
|
|
168
184
|
{ value: "kimi", label: "Kimi", description: "Requires MOONSHOT_SEARCH_API_KEY or MOONSHOT_API_KEY" },
|
|
169
|
-
{ value: "perplexity", label: "Perplexity", description: "Requires PERPLEXITY_API_KEY" },
|
|
185
|
+
{ value: "perplexity", label: "Perplexity", description: "Requires PERPLEXITY_COOKIES or PERPLEXITY_API_KEY" },
|
|
170
186
|
{ value: "anthropic", label: "Anthropic", description: "Uses Anthropic web search" },
|
|
171
187
|
{ value: "zai", label: "Z.AI", description: "Calls Z.AI webSearchPrime MCP" },
|
|
172
188
|
{ value: "synthetic", label: "Synthetic", description: "Requires SYNTHETIC_API_KEY" },
|
|
@@ -50,6 +50,7 @@ export class StatusLineComponent implements Component {
|
|
|
50
50
|
// Git status caching (1s TTL)
|
|
51
51
|
#cachedGitStatus: { staged: number; unstaged: number; untracked: number } | null = null;
|
|
52
52
|
#gitStatusLastFetch = 0;
|
|
53
|
+
#gitStatusInFlight = false;
|
|
53
54
|
|
|
54
55
|
constructor(private readonly session: AgentSession) {
|
|
55
56
|
this.#settings = {
|
|
@@ -153,11 +154,12 @@ export class StatusLineComponent implements Component {
|
|
|
153
154
|
}
|
|
154
155
|
|
|
155
156
|
#getGitStatus(): { staged: number; unstaged: number; untracked: number } | null {
|
|
156
|
-
|
|
157
|
-
if (now - this.#gitStatusLastFetch < 1000) {
|
|
157
|
+
if (this.#gitStatusInFlight || Date.now() - this.#gitStatusLastFetch < 1000) {
|
|
158
158
|
return this.#cachedGitStatus;
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
+
this.#gitStatusInFlight = true;
|
|
162
|
+
|
|
161
163
|
// Fire async fetch, return cached value
|
|
162
164
|
(async () => {
|
|
163
165
|
try {
|
|
@@ -165,7 +167,6 @@ export class StatusLineComponent implements Component {
|
|
|
165
167
|
|
|
166
168
|
if (result.exitCode !== 0) {
|
|
167
169
|
this.#cachedGitStatus = null;
|
|
168
|
-
this.#gitStatusLastFetch = now;
|
|
169
170
|
return;
|
|
170
171
|
}
|
|
171
172
|
|
|
@@ -195,10 +196,11 @@ export class StatusLineComponent implements Component {
|
|
|
195
196
|
}
|
|
196
197
|
|
|
197
198
|
this.#cachedGitStatus = { staged, unstaged, untracked };
|
|
198
|
-
this.#gitStatusLastFetch = now;
|
|
199
199
|
} catch {
|
|
200
200
|
this.#cachedGitStatus = null;
|
|
201
|
-
|
|
201
|
+
} finally {
|
|
202
|
+
this.#gitStatusLastFetch = Date.now();
|
|
203
|
+
this.#gitStatusInFlight = false;
|
|
202
204
|
}
|
|
203
205
|
})();
|
|
204
206
|
|
|
@@ -739,10 +739,12 @@ export class MCPCommandController {
|
|
|
739
739
|
|
|
740
740
|
// Collect runtime-discovered servers not in config files
|
|
741
741
|
const configServerNames = new Set([...userServers, ...projectServers]);
|
|
742
|
+
const disabledServerNames = new Set(await readDisabledServers(userPath));
|
|
742
743
|
const discoveredServers: { name: string; source: SourceMeta }[] = [];
|
|
743
744
|
if (this.ctx.mcpManager) {
|
|
744
745
|
for (const name of this.ctx.mcpManager.getAllServerNames()) {
|
|
745
746
|
if (configServerNames.has(name)) continue;
|
|
747
|
+
if (disabledServerNames.has(name)) continue;
|
|
746
748
|
const source = this.ctx.mcpManager.getSource(name);
|
|
747
749
|
if (source) {
|
|
748
750
|
discoveredServers.push({ name, source });
|
|
@@ -754,7 +756,7 @@ export class MCPCommandController {
|
|
|
754
756
|
userServers.length === 0 &&
|
|
755
757
|
projectServers.length === 0 &&
|
|
756
758
|
discoveredServers.length === 0 &&
|
|
757
|
-
|
|
759
|
+
disabledServerNames.size === 0
|
|
758
760
|
) {
|
|
759
761
|
this.#showMessage(
|
|
760
762
|
[
|
|
@@ -851,8 +853,7 @@ export class MCPCommandController {
|
|
|
851
853
|
}
|
|
852
854
|
|
|
853
855
|
// Show servers disabled via /mcp disable (from third-party configs)
|
|
854
|
-
const
|
|
855
|
-
const relevantDisabled = disabledServers.filter(n => !configServerNames.has(n));
|
|
856
|
+
const relevantDisabled = [...disabledServerNames].filter(n => !configServerNames.has(n));
|
|
856
857
|
if (relevantDisabled.length > 0) {
|
|
857
858
|
lines.push(theme.fg("accent", "Disabled") + theme.fg("muted", " (discovered servers):"));
|
|
858
859
|
for (const name of relevantDisabled) {
|
|
@@ -31,6 +31,16 @@ import { ToolExecutionComponent } from "../components/tool-execution";
|
|
|
31
31
|
import { TreeSelectorComponent } from "../components/tree-selector";
|
|
32
32
|
import { UserMessageSelectorComponent } from "../components/user-message-selector";
|
|
33
33
|
|
|
34
|
+
const CALLBACK_SERVER_PROVIDERS = new Set<OAuthProvider>([
|
|
35
|
+
"anthropic",
|
|
36
|
+
"openai-codex",
|
|
37
|
+
"gitlab-duo",
|
|
38
|
+
"google-gemini-cli",
|
|
39
|
+
"google-antigravity",
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
const MANUAL_LOGIN_TIP = "Tip: You can complete pairing with /login <redirect URL>.";
|
|
43
|
+
|
|
34
44
|
export class SelectorController {
|
|
35
45
|
constructor(private ctx: InteractiveModeContext) {}
|
|
36
46
|
|
|
@@ -600,6 +610,8 @@ export class SelectorController {
|
|
|
600
610
|
done();
|
|
601
611
|
if (mode === "login") {
|
|
602
612
|
this.ctx.showStatus(`Logging in to ${providerId}…`);
|
|
613
|
+
const manualInput = this.ctx.oauthManualInput;
|
|
614
|
+
const useManualInput = CALLBACK_SERVER_PROVIDERS.has(providerId as OAuthProvider);
|
|
603
615
|
try {
|
|
604
616
|
await this.ctx.session.modelRegistry.authStorage.login(providerId as OAuthProvider, {
|
|
605
617
|
onAuth: (info: { url: string; instructions?: string }) => {
|
|
@@ -612,6 +624,10 @@ export class SelectorController {
|
|
|
612
624
|
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
613
625
|
this.ctx.chatContainer.addChild(new Text(theme.fg("warning", info.instructions), 1, 0));
|
|
614
626
|
}
|
|
627
|
+
if (useManualInput) {
|
|
628
|
+
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
629
|
+
this.ctx.chatContainer.addChild(new Text(theme.fg("dim", MANUAL_LOGIN_TIP), 1, 0));
|
|
630
|
+
}
|
|
615
631
|
this.ctx.ui.requestRender();
|
|
616
632
|
this.ctx.openInBrowser(info.url);
|
|
617
633
|
},
|
|
@@ -641,6 +657,7 @@ export class SelectorController {
|
|
|
641
657
|
this.ctx.chatContainer.addChild(new Text(theme.fg("dim", message), 1, 0));
|
|
642
658
|
this.ctx.ui.requestRender();
|
|
643
659
|
},
|
|
660
|
+
onManualCodeInput: useManualInput ? () => manualInput.waitForInput(providerId) : undefined,
|
|
644
661
|
});
|
|
645
662
|
// Refresh models to pick up new baseUrl (e.g., github-copilot)
|
|
646
663
|
await this.ctx.session.modelRegistry.refresh();
|
|
@@ -658,6 +675,10 @@ export class SelectorController {
|
|
|
658
675
|
this.ctx.ui.requestRender();
|
|
659
676
|
} catch (error: unknown) {
|
|
660
677
|
this.ctx.showError(`Login failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
678
|
+
} finally {
|
|
679
|
+
if (useManualInput) {
|
|
680
|
+
manualInput.clear(`Manual OAuth input cleared for ${providerId}`);
|
|
681
|
+
}
|
|
661
682
|
}
|
|
662
683
|
} else {
|
|
663
684
|
try {
|
|
@@ -51,6 +51,7 @@ import { InputController } from "./controllers/input-controller";
|
|
|
51
51
|
import { MCPCommandController } from "./controllers/mcp-command-controller";
|
|
52
52
|
import { SelectorController } from "./controllers/selector-controller";
|
|
53
53
|
import { SSHCommandController } from "./controllers/ssh-command-controller";
|
|
54
|
+
import { OAuthManualInputManager } from "./oauth-manual-input";
|
|
54
55
|
import { setMermaidRenderCallback } from "./theme/mermaid-cache";
|
|
55
56
|
import type { Theme } from "./theme/theme";
|
|
56
57
|
import { getEditorTheme, getMarkdownTheme, onThemeChange, theme } from "./theme/theme";
|
|
@@ -133,6 +134,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
133
134
|
lastStatusText: Text | undefined = undefined;
|
|
134
135
|
fileSlashCommands: Set<string> = new Set();
|
|
135
136
|
skillCommands: Map<string, string> = new Map();
|
|
137
|
+
oauthManualInput: OAuthManualInputManager = new OAuthManualInputManager();
|
|
136
138
|
|
|
137
139
|
#pendingSlashCommands: SlashCommand[] = [];
|
|
138
140
|
#cleanupUnsubscribe?: () => void;
|
|
@@ -674,6 +676,13 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
674
676
|
const previousTools = this.#planModePreviousTools ?? this.session.getActiveToolNames();
|
|
675
677
|
await this.#exitPlanMode({ silent: true, paused: false });
|
|
676
678
|
await this.handleClearCommand();
|
|
679
|
+
// The new session has a fresh local:// root — persist the approved plan there
|
|
680
|
+
// so `local://<title>.md` resolves correctly in the execution session.
|
|
681
|
+
const newLocalPath = resolveLocalUrlToPath(options.finalPlanFilePath, {
|
|
682
|
+
getArtifactsDir: () => this.sessionManager.getArtifactsDir(),
|
|
683
|
+
getSessionId: () => this.sessionManager.getSessionId(),
|
|
684
|
+
});
|
|
685
|
+
await Bun.write(newLocalPath, planContent);
|
|
677
686
|
if (previousTools.length > 0) {
|
|
678
687
|
await this.session.setActiveToolsByName(previousTools);
|
|
679
688
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
type PendingInput = {
|
|
2
|
+
providerId: string;
|
|
3
|
+
resolve: (value: string) => void;
|
|
4
|
+
reject: (error: Error) => void;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export class OAuthManualInputManager {
|
|
8
|
+
#pending?: PendingInput;
|
|
9
|
+
|
|
10
|
+
waitForInput(providerId: string): Promise<string> {
|
|
11
|
+
if (this.#pending) {
|
|
12
|
+
this.clear("Manual OAuth input superseded by a new login");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const { promise, resolve, reject } = Promise.withResolvers<string>();
|
|
16
|
+
this.#pending = { providerId, resolve, reject };
|
|
17
|
+
return promise;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
submit(input: string): boolean {
|
|
21
|
+
if (!this.#pending) return false;
|
|
22
|
+
const { resolve } = this.#pending;
|
|
23
|
+
this.#pending = undefined;
|
|
24
|
+
resolve(input);
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
clear(reason = "Manual OAuth input cleared"): void {
|
|
29
|
+
if (!this.#pending) return;
|
|
30
|
+
const { reject } = this.#pending;
|
|
31
|
+
this.#pending = undefined;
|
|
32
|
+
reject(new Error(reason));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
hasPending(): boolean {
|
|
36
|
+
return Boolean(this.#pending);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
get pendingProviderId(): string | undefined {
|
|
40
|
+
return this.#pending?.providerId;
|
|
41
|
+
}
|
|
42
|
+
}
|
package/src/modes/types.ts
CHANGED
|
@@ -19,6 +19,7 @@ import type { HookSelectorComponent } from "./components/hook-selector";
|
|
|
19
19
|
import type { PythonExecutionComponent } from "./components/python-execution";
|
|
20
20
|
import type { StatusLineComponent } from "./components/status-line";
|
|
21
21
|
import type { ToolExecutionHandle } from "./components/tool-execution";
|
|
22
|
+
import type { OAuthManualInputManager } from "./oauth-manual-input";
|
|
22
23
|
import type { Theme } from "./theme/theme";
|
|
23
24
|
|
|
24
25
|
export type CompactionQueuedMessage = {
|
|
@@ -97,6 +98,7 @@ export interface InteractiveModeContext {
|
|
|
97
98
|
lastStatusText: Text | undefined;
|
|
98
99
|
fileSlashCommands: Set<string>;
|
|
99
100
|
skillCommands: Map<string, string>;
|
|
101
|
+
oauthManualInput: OAuthManualInputManager;
|
|
100
102
|
todoPhases: TodoPhase[];
|
|
101
103
|
|
|
102
104
|
// Lifecycle
|
package/src/patch/hashline.ts
CHANGED
|
@@ -444,6 +444,7 @@ export function applyHashlineEdits(
|
|
|
444
444
|
const originalFileLines = [...fileLines];
|
|
445
445
|
let firstChangedLine: number | undefined;
|
|
446
446
|
const noopEdits: Array<{ editIndex: number; loc: string; current: string }> = [];
|
|
447
|
+
const warnings: string[] = [];
|
|
447
448
|
|
|
448
449
|
// Pre-validate: collect all hash mismatches before mutating
|
|
449
450
|
const mismatches: HashMismatch[] = [];
|
|
@@ -580,7 +581,23 @@ export function applyHashlineEdits(
|
|
|
580
581
|
trackFirstChanged(edit.pos.line);
|
|
581
582
|
} else {
|
|
582
583
|
const count = edit.end.line - edit.pos.line + 1;
|
|
583
|
-
const newLines = edit.lines;
|
|
584
|
+
const newLines = [...edit.lines];
|
|
585
|
+
const trailingReplacementLine = newLines[newLines.length - 1];
|
|
586
|
+
const nextSurvivingLine = fileLines[edit.end.line];
|
|
587
|
+
if (
|
|
588
|
+
trailingReplacementLine !== undefined &&
|
|
589
|
+
trailingReplacementLine.trim().length > 0 &&
|
|
590
|
+
nextSurvivingLine !== undefined &&
|
|
591
|
+
trailingReplacementLine.trim() === nextSurvivingLine.trim() &&
|
|
592
|
+
// Safety: only correct when end-line content differs from the duplicate.
|
|
593
|
+
// If end already points to the boundary, matching next line is coincidence.
|
|
594
|
+
fileLines[edit.end.line - 1].trim() !== trailingReplacementLine.trim()
|
|
595
|
+
) {
|
|
596
|
+
newLines.pop();
|
|
597
|
+
warnings.push(
|
|
598
|
+
`Auto-corrected range replace ${edit.pos.line}#${edit.pos.hash}-${edit.end.line}#${edit.end.hash}: removed trailing replacement line "${trailingReplacementLine.trim()}" that duplicated next surviving line`,
|
|
599
|
+
);
|
|
600
|
+
}
|
|
584
601
|
fileLines.splice(edit.pos.line - 1, count, ...newLines);
|
|
585
602
|
trackFirstChanged(edit.pos.line);
|
|
586
603
|
}
|
|
@@ -639,6 +656,7 @@ export function applyHashlineEdits(
|
|
|
639
656
|
return {
|
|
640
657
|
lines: fileLines.join("\n"),
|
|
641
658
|
firstChangedLine,
|
|
659
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
642
660
|
...(noopEdits.length > 0 ? { noopEdits } : {}),
|
|
643
661
|
};
|
|
644
662
|
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
Generate a concise git commit message from the provided diff. Use conventional commit format: `type(scope): description` where type is feat/fix/refactor/chore/test/docs and scope is optional. The description **MUST** be lowercase, imperative mood, no trailing period. Keep it under 72 characters.
|
|
2
|
+
You **MUST** output ONLY the commit message, nothing else.
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
You stopped without calling submit_result. This is reminder {{retryCount}} of {{maxRetries}}.
|
|
3
3
|
|
|
4
4
|
You **MUST** call submit_result as your only action now. Choose one:
|
|
5
|
-
- If task is complete:
|
|
6
|
-
- If task failed
|
|
5
|
+
- If task is complete: call submit_result with your result in the `data` field
|
|
6
|
+
- If task failed: call submit_result with an `error` field describing what happened
|
|
7
7
|
|
|
8
|
-
You **MUST NOT**
|
|
8
|
+
You **MUST NOT** give up if you can still complete the task through exploration (using available tools or repo context). If you submit an error, you **MUST** include what you tried and the exact blocker.
|
|
9
9
|
|
|
10
10
|
You **MUST NOT** output text without a tool call. You **MUST** call submit_result to finish.
|
|
11
11
|
</system-reminder>
|
|
@@ -29,11 +29,11 @@ Your result **MUST** match this TypeScript interface:
|
|
|
29
29
|
{{/if}}
|
|
30
30
|
|
|
31
31
|
{{SECTION_SEPERATOR "Giving Up"}}
|
|
32
|
-
If you cannot complete the assignment, you **MUST** call `submit_result` exactly once with
|
|
32
|
+
If you cannot complete the assignment, you **MUST** call `submit_result` exactly once with an `error` message describing what you tried and the exact blocker.
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
You **MUST NOT**
|
|
36
|
-
You **MUST NOT**
|
|
34
|
+
Giving up is a last resort.
|
|
35
|
+
You **MUST NOT** give up due to uncertainty or missing information obtainable via tools or repo context.
|
|
36
|
+
You **MUST NOT** give up due to requiring a design, you can derive that yourself, more than capable of that.
|
|
37
37
|
|
|
38
38
|
Proceed with the best approach using the most reasonable option.
|
|
39
39
|
|
|
@@ -159,6 +159,19 @@ Semantic questions **MUST** be answered with semantic tools.
|
|
|
159
159
|
- What is this thing? → `lsp hover`
|
|
160
160
|
{{/has}}
|
|
161
161
|
|
|
162
|
+
{{#if eagerTasks}}
|
|
163
|
+
<eager-tasks>
|
|
164
|
+
You **SHOULD** delegate work to subagents by default. Working alone is the exception, not the rule.
|
|
165
|
+
|
|
166
|
+
Use the Task tool unless the change is:
|
|
167
|
+
- A single-file edit under ~30 lines
|
|
168
|
+
- A direct answer or explanation with no code changes
|
|
169
|
+
- A command the user asked you to run yourself
|
|
170
|
+
|
|
171
|
+
For everything else — multi-file changes, refactors, new features, test additions, investigations — break the work into tasks and delegate. Err on the side of delegating. You are an orchestrator first, a coder second.
|
|
172
|
+
</eager-tasks>
|
|
173
|
+
{{/if}}
|
|
174
|
+
|
|
162
175
|
{{#has tools "ssh"}}
|
|
163
176
|
### SSH: match commands to host shell
|
|
164
177
|
|