@oh-my-pi/pi-coding-agent 17.0.3 → 17.0.5
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 +88 -35
- package/dist/cli.js +3540 -3521
- package/dist/types/advisor/advise-tool.d.ts +3 -0
- package/dist/types/advisor/runtime.d.ts +7 -1
- package/dist/types/async/job-manager.d.ts +2 -0
- package/dist/types/config/model-registry.d.ts +7 -0
- package/dist/types/config/model-resolver.d.ts +8 -0
- package/dist/types/config/models-config-schema.d.ts +10 -0
- package/dist/types/config/models-config.d.ts +6 -0
- package/dist/types/dap/client.d.ts +10 -0
- package/dist/types/dap/types.d.ts +6 -5
- package/dist/types/extensibility/extensions/runner.d.ts +4 -2
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +16 -0
- package/dist/types/launch/spawn-options.d.ts +10 -0
- package/dist/types/launch/spawn-options.test.d.ts +1 -0
- package/dist/types/mnemopi/state.d.ts +32 -9
- package/dist/types/modes/components/status-line/component.d.ts +2 -3
- package/dist/types/modes/components/status-line/types.d.ts +3 -1
- package/dist/types/modes/components/tool-execution.d.ts +2 -0
- package/dist/types/modes/components/transcript-container.d.ts +4 -3
- package/dist/types/modes/components/tree-selector.d.ts +6 -2
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/print-mode.d.ts +4 -0
- package/dist/types/modes/rpc/rpc-input.d.ts +5 -0
- package/dist/types/modes/rpc/rpc-mode.d.ts +1 -1
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/sdk.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +19 -1
- package/dist/types/session/messages.d.ts +6 -0
- package/dist/types/task/types.d.ts +6 -6
- package/dist/types/tools/browser/aria/aria-snapshot.d.ts +17 -7
- package/dist/types/tools/browser/cmux/socket-client.d.ts +2 -0
- package/dist/types/tools/browser/tab-protocol.d.ts +2 -0
- package/dist/types/tools/browser/tab-worker.d.ts +2 -0
- package/dist/types/tools/gh.d.ts +5 -3
- package/dist/types/tools/hub/index.d.ts +1 -1
- package/dist/types/tools/hub/jobs.d.ts +1 -0
- package/dist/types/tools/hub/types.d.ts +2 -0
- package/dist/types/tools/tool-timeouts.d.ts +1 -1
- package/dist/types/utils/git.d.ts +33 -0
- package/dist/types/web/search/providers/codex.d.ts +1 -1
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +150 -0
- package/src/advisor/advise-tool.ts +4 -0
- package/src/advisor/runtime.ts +38 -14
- package/src/async/job-manager.ts +3 -0
- package/src/cli/bench-cli.ts +8 -2
- package/src/cli/dry-balance-cli.ts +1 -0
- package/src/config/model-registry.ts +89 -8
- package/src/config/model-resolver.ts +78 -14
- package/src/config/models-config-schema.ts +1 -0
- package/src/config/settings.ts +3 -1
- package/src/dap/client.ts +168 -1
- package/src/dap/config.ts +51 -1
- package/src/dap/session.ts +575 -234
- package/src/dap/types.ts +6 -5
- package/src/discovery/agents.ts +2 -2
- package/src/extensibility/extensions/runner.ts +6 -4
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +26 -0
- package/src/launch/broker.ts +34 -31
- package/src/launch/client.ts +7 -1
- package/src/launch/spawn-options.test.ts +31 -0
- package/src/launch/spawn-options.ts +17 -0
- package/src/lsp/types.ts +5 -1
- package/src/main.ts +17 -4
- package/src/mcp/transports/stdio.test.ts +9 -1
- package/src/mnemopi/backend.ts +1 -1
- package/src/mnemopi/embed-worker.ts +8 -7
- package/src/mnemopi/state.ts +45 -18
- package/src/modes/components/ask-dialog.ts +137 -73
- package/src/modes/components/status-line/component.ts +2 -2
- package/src/modes/components/status-line/segments.ts +20 -3
- package/src/modes/components/status-line/types.ts +3 -1
- package/src/modes/components/tool-execution.ts +5 -0
- package/src/modes/components/transcript-container.ts +18 -111
- package/src/modes/components/tree-selector.ts +10 -4
- package/src/modes/controllers/event-controller.ts +1 -2
- package/src/modes/controllers/input-controller.ts +1 -1
- package/src/modes/controllers/selector-controller.ts +29 -8
- package/src/modes/interactive-mode.ts +40 -8
- package/src/modes/noninteractive-dispose.test.ts +12 -1
- package/src/modes/print-mode.ts +21 -9
- package/src/modes/rpc/rpc-input.ts +38 -0
- package/src/modes/rpc/rpc-mode.ts +7 -2
- package/src/modes/setup-wizard/scenes/sign-in.ts +3 -1
- package/src/modes/types.ts +2 -0
- package/src/modes/utils/ui-helpers.ts +3 -2
- package/src/prompts/tools/browser.md +3 -2
- package/src/prompts/tools/debug.md +2 -7
- package/src/prompts/tools/github.md +6 -1
- package/src/prompts/tools/hub.md +4 -2
- package/src/prompts/tools/task-async-contract.md +1 -0
- package/src/prompts/tools/task.md +9 -2
- package/src/sdk.ts +38 -6
- package/src/session/agent-session.ts +395 -162
- package/src/session/messages.test.ts +91 -0
- package/src/session/messages.ts +248 -110
- package/src/session/session-loader.ts +31 -3
- package/src/slash-commands/builtin-registry.ts +1 -0
- package/src/stt/recorder.ts +68 -55
- package/src/task/executor.ts +59 -33
- package/src/task/index.ts +46 -9
- package/src/task/types.ts +11 -8
- package/src/task/worktree.ts +10 -0
- package/src/tools/browser/aria/aria-snapshot.ts +36 -8
- package/src/tools/browser/cmux/cmux-tab.ts +2 -1
- package/src/tools/browser/cmux/socket-client.ts +139 -3
- package/src/tools/browser/run-cancellation.ts +4 -0
- package/src/tools/browser/tab-protocol.ts +2 -0
- package/src/tools/browser/tab-supervisor.ts +21 -11
- package/src/tools/browser/tab-worker.ts +199 -33
- package/src/tools/debug.ts +3 -0
- package/src/tools/gh.ts +40 -2
- package/src/tools/hub/index.ts +4 -1
- package/src/tools/hub/jobs.ts +42 -1
- package/src/tools/hub/types.ts +2 -0
- package/src/tools/tool-timeouts.ts +1 -1
- package/src/utils/git.ts +237 -0
- package/src/web/search/index.ts +9 -5
- package/src/web/search/providers/codex.ts +195 -99
package/src/tools/gh.ts
CHANGED
|
@@ -249,6 +249,7 @@ const RUN_FAILURE_CONCLUSIONS = new Set(["failure", "timed_out", "cancelled", "a
|
|
|
249
249
|
const JOB_FAILURE_CONCLUSIONS = new Set(["failure", "timed_out", "cancelled", "action_required"]);
|
|
250
250
|
const GITHUB_READONLY_OPS: ReadonlySet<string> = new Set([
|
|
251
251
|
"repo_view",
|
|
252
|
+
"file_read",
|
|
252
253
|
"search_issues",
|
|
253
254
|
"search_prs",
|
|
254
255
|
"search_code",
|
|
@@ -259,10 +260,11 @@ const GITHUB_READONLY_OPS: ReadonlySet<string> = new Set([
|
|
|
259
260
|
|
|
260
261
|
const githubSchema = type({
|
|
261
262
|
op: type(
|
|
262
|
-
"'repo_view' | 'pr_create' | 'pr_checkout' | 'pr_push' | 'search_issues' | 'search_prs' | 'search_code' | 'search_commits' | 'search_repos' | 'run_watch'",
|
|
263
|
+
"'repo_view' | 'file_read' | 'pr_create' | 'pr_checkout' | 'pr_push' | 'search_issues' | 'search_prs' | 'search_code' | 'search_commits' | 'search_repos' | 'run_watch'",
|
|
263
264
|
).describe("github operation"),
|
|
264
265
|
"repo?": type("string").describe("owner/repo"),
|
|
265
266
|
"branch?": type("string").describe("branch"),
|
|
267
|
+
"path?": type("string").describe("repository-relative file path"),
|
|
266
268
|
"pr?": type("string | string[]").describe("pr number, url, or branch"),
|
|
267
269
|
"force?": type("boolean").describe("reset existing local branch"),
|
|
268
270
|
"forceWithLease?": type("boolean").describe("force-with-lease push"),
|
|
@@ -2455,7 +2457,7 @@ export class GithubTool implements AgentTool<typeof githubSchema, GhToolDetails>
|
|
|
2455
2457
|
const op = typeof rawOp === "string" ? rawOp : "";
|
|
2456
2458
|
return GITHUB_READONLY_OPS.has(op) ? "read" : "exec";
|
|
2457
2459
|
};
|
|
2458
|
-
readonly summary = "Interact with GitHub
|
|
2460
|
+
readonly summary = "Interact with GitHub repositories, files, pull requests, and Actions";
|
|
2459
2461
|
readonly loadMode = "discoverable";
|
|
2460
2462
|
readonly label = "GitHub";
|
|
2461
2463
|
readonly description = prompt.render(githubDescription);
|
|
@@ -2480,6 +2482,8 @@ export class GithubTool implements AgentTool<typeof githubSchema, GhToolDetails>
|
|
|
2480
2482
|
switch (params.op) {
|
|
2481
2483
|
case "repo_view":
|
|
2482
2484
|
return executeRepoView(this.session, params, signal);
|
|
2485
|
+
case "file_read":
|
|
2486
|
+
return executeFileRead(this.session, params, signal);
|
|
2483
2487
|
case "pr_create":
|
|
2484
2488
|
return executePrCreate(this.session, params, signal);
|
|
2485
2489
|
case "pr_checkout":
|
|
@@ -2525,6 +2529,40 @@ async function executeRepoView(
|
|
|
2525
2529
|
return buildTextResult(formatRepoView(data, { repo, branch }), data.url);
|
|
2526
2530
|
}
|
|
2527
2531
|
|
|
2532
|
+
async function executeFileRead(
|
|
2533
|
+
session: ToolSession,
|
|
2534
|
+
params: GithubInput,
|
|
2535
|
+
signal: AbortSignal | undefined,
|
|
2536
|
+
): Promise<AgentToolResult<GhToolDetails>> {
|
|
2537
|
+
const repo = await resolveGitHubRepo(session.cwd, normalizeOptionalString(params.repo), undefined, signal);
|
|
2538
|
+
const filePath = requireNonEmpty(normalizeOptionalString(params.path), "path");
|
|
2539
|
+
if (filePath.startsWith("/")) {
|
|
2540
|
+
throw new ToolError("path must be repository-relative");
|
|
2541
|
+
}
|
|
2542
|
+
const branch = normalizeOptionalString(params.branch);
|
|
2543
|
+
const endpointPath = filePath
|
|
2544
|
+
.split("/")
|
|
2545
|
+
.map(segment => encodeURIComponent(segment))
|
|
2546
|
+
.join("/");
|
|
2547
|
+
const args = [
|
|
2548
|
+
"api",
|
|
2549
|
+
`/repos/${repo}/contents/${endpointPath}`,
|
|
2550
|
+
"--method",
|
|
2551
|
+
"GET",
|
|
2552
|
+
"-H",
|
|
2553
|
+
"Accept: application/vnd.github.raw+json",
|
|
2554
|
+
];
|
|
2555
|
+
if (branch) {
|
|
2556
|
+
args.push("-f", `ref=${branch}`);
|
|
2557
|
+
}
|
|
2558
|
+
const text = await git.github.text(session.cwd, args, signal, {
|
|
2559
|
+
repoProvided: true,
|
|
2560
|
+
trimOutput: false,
|
|
2561
|
+
});
|
|
2562
|
+
const sourceUrl = `https://github.com/${repo}/blob/${encodeURIComponent(branch ?? "HEAD")}/${endpointPath}`;
|
|
2563
|
+
return buildTextResult(text, sourceUrl, { repo, branch });
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2528
2566
|
// ────────────────────────────────────────────────────────────────────────────
|
|
2529
2567
|
// Cached issue/PR view fetchers
|
|
2530
2568
|
//
|
package/src/tools/hub/index.ts
CHANGED
|
@@ -159,7 +159,10 @@ export class HubTool implements AgentTool<typeof hubSchema, HubDetails> {
|
|
|
159
159
|
readonly description: string;
|
|
160
160
|
readonly parameters = hubSchema;
|
|
161
161
|
readonly strict = true;
|
|
162
|
-
readonly interruptible =
|
|
162
|
+
readonly interruptible = (params: Partial<HubParams>): boolean => {
|
|
163
|
+
if (params.op === "wait") return true;
|
|
164
|
+
return params.op === "logs" && params.follow === true;
|
|
165
|
+
};
|
|
163
166
|
readonly loadMode = "essential";
|
|
164
167
|
|
|
165
168
|
readonly examples: readonly ToolExample<typeof hubSchema.infer>[] = [
|
package/src/tools/hub/jobs.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type { AgentToolResult } from "@oh-my-pi/pi-agent-core";
|
|
|
8
8
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
9
9
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
10
10
|
import type { AsyncJob, AsyncJobManager } from "../../async";
|
|
11
|
+
import { settings } from "../../config/settings";
|
|
11
12
|
import type { RenderResultOptions } from "../../extensibility/custom-tools/types";
|
|
12
13
|
import { shimmerEnabled, shimmerText } from "../../modes/theme/shimmer";
|
|
13
14
|
import type { Theme } from "../../modes/theme/theme";
|
|
@@ -134,6 +135,7 @@ interface TrackedJobLike {
|
|
|
134
135
|
status: string;
|
|
135
136
|
label: string;
|
|
136
137
|
startTime: number;
|
|
138
|
+
latestDetails?: Record<string, unknown>;
|
|
137
139
|
resultText?: string;
|
|
138
140
|
errorText?: string;
|
|
139
141
|
}
|
|
@@ -143,12 +145,34 @@ export function snapshotJobs(session: ToolSession, jobs: TrackedJobLike[]): JobS
|
|
|
143
145
|
return jobs.map(j => {
|
|
144
146
|
const current = session.asyncJobManager?.getJob(j.id);
|
|
145
147
|
const latest = current ?? j;
|
|
148
|
+
let resolvedModel: string | undefined;
|
|
149
|
+
if (latest.type === "task") {
|
|
150
|
+
const progressValue = latest.latestDetails?.progress;
|
|
151
|
+
if (Array.isArray(progressValue)) {
|
|
152
|
+
let progressRecord: Record<string, unknown> | undefined;
|
|
153
|
+
for (const item of progressValue) {
|
|
154
|
+
if (!item || typeof item !== "object") continue;
|
|
155
|
+
const candidate = item as Record<string, unknown>;
|
|
156
|
+
if (!progressRecord) progressRecord = candidate;
|
|
157
|
+
if (candidate.id === latest.id) {
|
|
158
|
+
progressRecord = candidate;
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const modelValue = progressRecord?.resolvedModel;
|
|
163
|
+
if (typeof modelValue === "string") {
|
|
164
|
+
const trimmed = modelValue.trim();
|
|
165
|
+
if (trimmed) resolvedModel = trimmed;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
146
169
|
return {
|
|
147
170
|
id: latest.id,
|
|
148
171
|
type: latest.type,
|
|
149
172
|
status: latest.status as JobSnapshot["status"],
|
|
150
173
|
label: latest.label,
|
|
151
174
|
durationMs: Math.max(0, now - latest.startTime),
|
|
175
|
+
...(resolvedModel ? { resolvedModel } : {}),
|
|
152
176
|
...(latest.resultText ? { resultText: latest.resultText } : {}),
|
|
153
177
|
...(latest.errorText ? { errorText: latest.errorText } : {}),
|
|
154
178
|
};
|
|
@@ -354,6 +378,7 @@ const PREVIEW_LINES_EXPANDED = 4;
|
|
|
354
378
|
const LABEL_LINES_COLLAPSED = 1;
|
|
355
379
|
const LABEL_LINES_EXPANDED = 3;
|
|
356
380
|
const PREVIEW_LINE_WIDTH = 80;
|
|
381
|
+
const MODEL_BADGE_MAX_WIDTH = 48;
|
|
357
382
|
|
|
358
383
|
function statusToIcon(status: JobSnapshot["status"]): ToolUIStatus {
|
|
359
384
|
switch (status) {
|
|
@@ -545,6 +570,20 @@ export function jobsRenderResult(
|
|
|
545
570
|
visibleLabelLines[visibleLabelLines.length - 1] = `${last} …`;
|
|
546
571
|
}
|
|
547
572
|
const durationText = uiTheme.fg("dim", formatDuration(job.durationMs));
|
|
573
|
+
const modelText =
|
|
574
|
+
job.type === "task" &&
|
|
575
|
+
typeof job.resolvedModel === "string" &&
|
|
576
|
+
job.resolvedModel.trim() &&
|
|
577
|
+
settings.get("task.showResolvedModelBadge")
|
|
578
|
+
? `${uiTheme.sep.dot}${uiTheme.fg(
|
|
579
|
+
"dim",
|
|
580
|
+
truncateToWidth(
|
|
581
|
+
replaceTabs(job.resolvedModel.trim()),
|
|
582
|
+
MODEL_BADGE_MAX_WIDTH,
|
|
583
|
+
Ellipsis.Unicode,
|
|
584
|
+
),
|
|
585
|
+
)}`
|
|
586
|
+
: "";
|
|
548
587
|
// Running rows in a live block shimmer their label; once the block
|
|
549
588
|
// stops animating (sealed, or a settled snapshot — spinnerFrame
|
|
550
589
|
// cleared) they render static so scrollback never keeps a mid-sweep
|
|
@@ -556,7 +595,9 @@ export function jobsRenderResult(
|
|
|
556
595
|
? shimmerText(headRaw, uiTheme)
|
|
557
596
|
: uiTheme.fg("accent", headRaw)
|
|
558
597
|
: uiTheme.fg("toolOutput", headRaw);
|
|
559
|
-
lines.push(
|
|
598
|
+
lines.push(
|
|
599
|
+
`${icon}${idPart} ${typeBadge} ${headLabel}${modelText}${modelText ? uiTheme.sep.dot : " "}${durationText}`,
|
|
600
|
+
);
|
|
560
601
|
for (let i = 1; i < visibleLabelLines.length; i++) {
|
|
561
602
|
lines.push(` ${uiTheme.fg("toolOutput", visibleLabelLines[i]!)}`);
|
|
562
603
|
}
|
package/src/tools/hub/types.ts
CHANGED
|
@@ -46,6 +46,8 @@ export interface JobSnapshot {
|
|
|
46
46
|
status: "running" | "completed" | "failed" | "cancelled";
|
|
47
47
|
label: string;
|
|
48
48
|
durationMs: number;
|
|
49
|
+
/** Effective task model selector, including an explicit reasoning suffix when configured. */
|
|
50
|
+
resolvedModel?: string;
|
|
49
51
|
resultText?: string;
|
|
50
52
|
errorText?: string;
|
|
51
53
|
}
|
|
@@ -13,7 +13,7 @@ export const TOOL_TIMEOUTS = {
|
|
|
13
13
|
browser: { default: 30, min: 1, max: 300 },
|
|
14
14
|
ssh: { default: 60, min: 1, max: 3600 },
|
|
15
15
|
fetch: { default: 20, min: 1, max: 45 },
|
|
16
|
-
lsp: { default: 20, min: 5, max:
|
|
16
|
+
lsp: { default: 20, min: 5, max: 300 },
|
|
17
17
|
debug: { default: 30, min: 5, max: 300 },
|
|
18
18
|
} as const satisfies Record<string, ToolTimeoutConfig>;
|
|
19
19
|
|
package/src/utils/git.ts
CHANGED
|
@@ -637,6 +637,10 @@ async function readOptionalText(filePath: string): Promise<string | null> {
|
|
|
637
637
|
return retryOnEintr(async () => await Bun.file(filePath).text());
|
|
638
638
|
}
|
|
639
639
|
|
|
640
|
+
async function readOptionalBytes(filePath: string): Promise<Uint8Array | null> {
|
|
641
|
+
return retryOnEintr(async () => await Bun.file(filePath).bytes());
|
|
642
|
+
}
|
|
643
|
+
|
|
640
644
|
function parseGitDirPointer(content: string): string | null {
|
|
641
645
|
const match = /^gitdir:\s*(.+)\s*$/iu.exec(content.trim());
|
|
642
646
|
return match?.[1] ?? null;
|
|
@@ -1379,6 +1383,239 @@ export async function writeTree(cwd: string, options: Pick<CommandOptions, "env"
|
|
|
1379
1383
|
return (await runText(cwd, ["write-tree"], options)).trim();
|
|
1380
1384
|
}
|
|
1381
1385
|
|
|
1386
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
1387
|
+
// API: worktree isolation
|
|
1388
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
1389
|
+
|
|
1390
|
+
/** Outcome of {@link detachGitDir}. */
|
|
1391
|
+
export type DetachGitDirResult =
|
|
1392
|
+
/** `worktreeRoot` had no `.git`; nothing to detach. */
|
|
1393
|
+
| "no-git"
|
|
1394
|
+
/** `.git` already resolves to an independent object DB — left untouched. */
|
|
1395
|
+
| "independent"
|
|
1396
|
+
/** Detached into a standalone repo borrowing `sourceCommonDir`'s objects. */
|
|
1397
|
+
| "detached";
|
|
1398
|
+
|
|
1399
|
+
/**
|
|
1400
|
+
* Sever a copied/mounted working tree from the git metadata it shares with a
|
|
1401
|
+
* source checkout, turning it into a standalone repository that borrows the
|
|
1402
|
+
* source object database through `objects/info/alternates`.
|
|
1403
|
+
*
|
|
1404
|
+
* Isolation backends (reflink/apfs/btrfs/rcopy…) materialise `merged` by
|
|
1405
|
+
* copying `worktreeRoot` byte-for-byte. When `worktreeRoot` is a **linked git
|
|
1406
|
+
* worktree** its `.git` is a pointer file (`gitdir: …/worktrees/<name>`), so
|
|
1407
|
+
* the copy still resolves HEAD/index/refs through the source repo — a task's
|
|
1408
|
+
* `git checkout`/`commit` inside the isolation then mutates the *parent*
|
|
1409
|
+
* checkout. The rcopy `git worktree add` path leaks the other way: task
|
|
1410
|
+
* branches land in the shared ref namespace and stack on each other.
|
|
1411
|
+
*
|
|
1412
|
+
* After detaching, the working tree keeps its files verbatim while:
|
|
1413
|
+
* - HEAD, refs, and the index are frozen to the snapshot at call time;
|
|
1414
|
+
* - all commits/branches the task creates stay private to the isolation;
|
|
1415
|
+
* - objects resolve against `sourceCommonDir` via alternates, so history reads
|
|
1416
|
+
* and later `git fetch <merged>` object transfer keep working;
|
|
1417
|
+
* - the source checkout's HEAD, branch, index, and working tree are untouched.
|
|
1418
|
+
*
|
|
1419
|
+
* A full-copy `.git` (non-worktree source) already owns its object DB and is
|
|
1420
|
+
* returned as `"independent"` without modification. `worktreeRoot` without a
|
|
1421
|
+
* `.git` yields `"no-git"`.
|
|
1422
|
+
*/
|
|
1423
|
+
export async function detachGitDir(worktreeRoot: string, sourceCommonDir: string): Promise<DetachGitDirResult> {
|
|
1424
|
+
ensureAvailable();
|
|
1425
|
+
const gitEntry = path.join(worktreeRoot, ".git");
|
|
1426
|
+
let entryStat: fs.Stats;
|
|
1427
|
+
try {
|
|
1428
|
+
entryStat = await fs.promises.lstat(gitEntry);
|
|
1429
|
+
} catch (err) {
|
|
1430
|
+
if (isEnoent(err)) return "no-git";
|
|
1431
|
+
throw err;
|
|
1432
|
+
}
|
|
1433
|
+
// Canonicalize both sides before comparing: `rev-parse` resolves symlinks
|
|
1434
|
+
// (macOS `/tmp` → `/private/tmp`) while callers derive `sourceCommonDir`
|
|
1435
|
+
// lexically from the session cwd. A lexical mismatch here would silently
|
|
1436
|
+
// classify a shared linked-worktree copy as "independent" and skip the
|
|
1437
|
+
// detach entirely — leaving the parent-mutation leak in place.
|
|
1438
|
+
const parentCommon = await fs.promises.realpath(sourceCommonDir).catch(() => path.resolve(sourceCommonDir));
|
|
1439
|
+
const isoCommonRaw = (
|
|
1440
|
+
await runText(worktreeRoot, ["rev-parse", "--path-format=absolute", "--git-common-dir"], {
|
|
1441
|
+
readOnly: true,
|
|
1442
|
+
})
|
|
1443
|
+
).trim();
|
|
1444
|
+
const isoCommon = await fs.promises.realpath(isoCommonRaw).catch(() => path.resolve(isoCommonRaw));
|
|
1445
|
+
// A full-copy `.git` already resolves to its own object DB — leave it alone.
|
|
1446
|
+
if (isoCommon !== parentCommon) return "independent";
|
|
1447
|
+
|
|
1448
|
+
// Snapshot the state the standalone repo must preserve. HEAD may be a branch
|
|
1449
|
+
// ref (normal checkout), detached, or unborn (a fresh/orphan branch with no
|
|
1450
|
+
// commits — a linked worktree still shares the parent ref namespace, so it
|
|
1451
|
+
// must be severed too). Refs are frozen so `baseSha..branch` ranges and
|
|
1452
|
+
// history reads keep resolving after the source moves on.
|
|
1453
|
+
const headSha = (await tryText(worktreeRoot, ["rev-parse", "HEAD"], { readOnly: true }))?.trim() ?? "";
|
|
1454
|
+
const headRef = (await tryText(worktreeRoot, ["symbolic-ref", "-q", "HEAD"], { readOnly: true }))?.trim() ?? "";
|
|
1455
|
+
const refDump = headSha
|
|
1456
|
+
? (
|
|
1457
|
+
await runText(worktreeRoot, ["for-each-ref", "--format=%(objectname) %(refname)"], {
|
|
1458
|
+
readOnly: true,
|
|
1459
|
+
})
|
|
1460
|
+
).trim()
|
|
1461
|
+
: "";
|
|
1462
|
+
const objectFormat =
|
|
1463
|
+
(await tryText(worktreeRoot, ["rev-parse", "--show-object-format"], { readOnly: true }))?.trim() || "sha1";
|
|
1464
|
+
const userName = await config.get(worktreeRoot, "user.name");
|
|
1465
|
+
const userEmail = await config.get(worktreeRoot, "user.email");
|
|
1466
|
+
|
|
1467
|
+
// Preserve the index verbatim rather than round-tripping through
|
|
1468
|
+
// write-tree/read-tree: the raw index carries skip-worktree bits (sparse
|
|
1469
|
+
// checkout), assume-unchanged flags, and exact stage entries. A rebuilt
|
|
1470
|
+
// index drops skip-worktree, so files intentionally absent from a sparse
|
|
1471
|
+
// working tree would read as deletions and delta capture would apply those
|
|
1472
|
+
// deletions back to the parent. Sparse config + patterns are carried too so
|
|
1473
|
+
// later git operations in the isolation keep honouring the sparse view.
|
|
1474
|
+
const indexPath = (
|
|
1475
|
+
await runText(worktreeRoot, ["rev-parse", "--path-format=absolute", "--git-path", "index"], {
|
|
1476
|
+
readOnly: true,
|
|
1477
|
+
})
|
|
1478
|
+
).trim();
|
|
1479
|
+
const indexBytes = await readOptionalBytes(indexPath);
|
|
1480
|
+
const sparseCheckout = await config.get(worktreeRoot, "core.sparseCheckout");
|
|
1481
|
+
const sparseCone = await config.get(worktreeRoot, "core.sparseCheckoutCone");
|
|
1482
|
+
const sparsePatternPath = (
|
|
1483
|
+
await runText(worktreeRoot, ["rev-parse", "--path-format=absolute", "--git-path", "info/sparse-checkout"], {
|
|
1484
|
+
readOnly: true,
|
|
1485
|
+
})
|
|
1486
|
+
).trim();
|
|
1487
|
+
const sparsePatterns = await readOptionalText(sparsePatternPath);
|
|
1488
|
+
// Status parity with the source: an explicit core.filemode (e.g. false on
|
|
1489
|
+
// mounts ignoring the executable bit) must carry over, or the re-inited
|
|
1490
|
+
// repo's platform default makes clean files read as mode-changed and delta
|
|
1491
|
+
// capture would apply bogus chmod diffs back to the parent.
|
|
1492
|
+
const fileMode = await config.get(worktreeRoot, "core.fileMode");
|
|
1493
|
+
// A split index references sharedindex.* files beside the source index;
|
|
1494
|
+
// restoring the raw index without them makes every git read fail. Carry the
|
|
1495
|
+
// shared files (and the config) alongside the verbatim index bytes.
|
|
1496
|
+
const splitIndex = await config.get(worktreeRoot, "core.splitIndex");
|
|
1497
|
+
const sharedIndexFiles: Array<{ name: string; bytes: Uint8Array }> = [];
|
|
1498
|
+
if (indexBytes) {
|
|
1499
|
+
const indexDir = path.dirname(indexPath);
|
|
1500
|
+
let entries: string[] = [];
|
|
1501
|
+
try {
|
|
1502
|
+
entries = await fs.promises.readdir(indexDir);
|
|
1503
|
+
} catch {}
|
|
1504
|
+
for (const name of entries) {
|
|
1505
|
+
if (!name.startsWith("sharedindex.")) continue;
|
|
1506
|
+
const bytes = await readOptionalBytes(path.join(indexDir, name));
|
|
1507
|
+
if (bytes) sharedIndexFiles.push({ name, bytes });
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
// A shallow source deliberately lacks parents beyond its `shallow` boundary
|
|
1511
|
+
// file; without it, history traversal over the borrowed objects treats the
|
|
1512
|
+
// boundary commit's missing parent as corruption.
|
|
1513
|
+
const shallowBoundary = await readOptionalText(path.join(parentCommon, "shallow"));
|
|
1514
|
+
|
|
1515
|
+
// A pointer `.git` file whose worktree-admin dir back-references this exact
|
|
1516
|
+
// tree is the rcopy `git worktree add` registration. Remove that admin entry
|
|
1517
|
+
// so the source repo's worktree list stops tracking the isolation. A pointer
|
|
1518
|
+
// referencing the *source's* admin (a copied linked-worktree `.git`) is not
|
|
1519
|
+
// ours to delete — only the local pointer file is discarded. Compare via
|
|
1520
|
+
// realpath: git canonicalizes the back-reference (e.g. macOS `/var` →
|
|
1521
|
+
// `/private/var`), so a lexical path comparison would miss the match and
|
|
1522
|
+
// leave a stale registration in the source repo's worktree list.
|
|
1523
|
+
let ownWorktreeAdmin: string | undefined;
|
|
1524
|
+
if (entryStat.isFile()) {
|
|
1525
|
+
const pointer = parseGitDirPointer((await readOptionalText(gitEntry)) ?? "");
|
|
1526
|
+
if (pointer) {
|
|
1527
|
+
const adminDir = path.resolve(path.dirname(gitEntry), pointer);
|
|
1528
|
+
const backRef = (await readOptionalText(path.join(adminDir, "gitdir")))?.trim();
|
|
1529
|
+
if (backRef) {
|
|
1530
|
+
const [realBackRef, realGitEntry] = await Promise.all([
|
|
1531
|
+
fs.promises.realpath(backRef).catch(() => path.resolve(backRef)),
|
|
1532
|
+
fs.promises.realpath(gitEntry).catch(() => path.resolve(gitEntry)),
|
|
1533
|
+
]);
|
|
1534
|
+
if (realBackRef === realGitEntry) ownWorktreeAdmin = adminDir;
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
await fs.promises.rm(gitEntry, { recursive: true, force: true });
|
|
1540
|
+
if (ownWorktreeAdmin) await fs.promises.rm(ownWorktreeAdmin, { recursive: true, force: true });
|
|
1541
|
+
|
|
1542
|
+
// Preserve the checked-out branch name so an unborn HEAD (fresh/orphan
|
|
1543
|
+
// branch with no commits) keeps its symbolic ref after `init` rather than
|
|
1544
|
+
// snapping to the init default; born HEADs get the ref rewritten below anyway.
|
|
1545
|
+
const initArgs = ["init", "--object-format", objectFormat, "-q"];
|
|
1546
|
+
const initialBranch = headRef.startsWith(LOCAL_BRANCH_PREFIX) ? headRef.slice(LOCAL_BRANCH_PREFIX.length) : "";
|
|
1547
|
+
if (initialBranch) initArgs.push("-b", initialBranch);
|
|
1548
|
+
await runEffect(worktreeRoot, initArgs);
|
|
1549
|
+
const objectsInfo = path.join(gitEntry, "objects", "info");
|
|
1550
|
+
await fs.promises.mkdir(objectsInfo, { recursive: true });
|
|
1551
|
+
const alternates = [path.join(parentCommon, "objects")];
|
|
1552
|
+
const chained = await readOptionalText(path.join(parentCommon, "objects", "info", "alternates"));
|
|
1553
|
+
if (chained) {
|
|
1554
|
+
for (const line of chained.split("\n")) {
|
|
1555
|
+
const entry = line.trim();
|
|
1556
|
+
if (!entry) continue;
|
|
1557
|
+
alternates.push(path.isAbsolute(entry) ? entry : path.resolve(parentCommon, "objects", entry));
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
await Bun.write(path.join(objectsInfo, "alternates"), `${alternates.join("\n")}\n`);
|
|
1561
|
+
|
|
1562
|
+
// Freeze refs when HEAD is born. Point HEAD at the raw SHA first so
|
|
1563
|
+
// `update-ref` writes land even for the branch HEAD currently names, then
|
|
1564
|
+
// restore the symbolic HEAD. An unborn HEAD has no refs to freeze; `init -b`
|
|
1565
|
+
// above already set the symbolic HEAD to the unborn branch.
|
|
1566
|
+
if (headSha) {
|
|
1567
|
+
await Bun.write(path.join(gitEntry, "HEAD"), `${headSha}\n`);
|
|
1568
|
+
if (refDump) {
|
|
1569
|
+
const commands = refDump
|
|
1570
|
+
.split("\n")
|
|
1571
|
+
.filter(Boolean)
|
|
1572
|
+
.map(line => {
|
|
1573
|
+
const sep = line.indexOf(" ");
|
|
1574
|
+
return `create ${line.slice(sep + 1)} ${line.slice(0, sep)}`;
|
|
1575
|
+
})
|
|
1576
|
+
.join("\n");
|
|
1577
|
+
await runEffect(worktreeRoot, ["update-ref", "--stdin"], { stdin: `${commands}\n` });
|
|
1578
|
+
}
|
|
1579
|
+
if (headRef) await Bun.write(path.join(gitEntry, "HEAD"), `ref: ${headRef}\n`);
|
|
1580
|
+
} else if (headRef && !initialBranch) {
|
|
1581
|
+
// Unborn detached HEAD (no branch, no commit) — restore the raw ref target.
|
|
1582
|
+
await Bun.write(path.join(gitEntry, "HEAD"), `ref: ${headRef}\n`);
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
// Carry the source identity so isolated commits have an author.
|
|
1586
|
+
if (userName) await config.set(worktreeRoot, "user.name", userName);
|
|
1587
|
+
if (userEmail) await config.set(worktreeRoot, "user.email", userEmail);
|
|
1588
|
+
if (fileMode !== undefined) await config.set(worktreeRoot, "core.fileMode", fileMode);
|
|
1589
|
+
if (splitIndex !== undefined) await config.set(worktreeRoot, "core.splitIndex", splitIndex);
|
|
1590
|
+
// Preserve the shallow boundary so history traversal over the borrowed
|
|
1591
|
+
// object DB stops at the boundary instead of failing on missing parents.
|
|
1592
|
+
if (shallowBoundary !== null) await Bun.write(path.join(gitEntry, "shallow"), shallowBoundary);
|
|
1593
|
+
|
|
1594
|
+
// Restore sparse-checkout state before the index so skip-worktree entries
|
|
1595
|
+
// keep resolving against the carried patterns.
|
|
1596
|
+
if (sparseCheckout) await config.set(worktreeRoot, "core.sparseCheckout", sparseCheckout);
|
|
1597
|
+
if (sparseCone) await config.set(worktreeRoot, "core.sparseCheckoutCone", sparseCone);
|
|
1598
|
+
if (sparsePatterns !== null) {
|
|
1599
|
+
const infoDir = path.join(gitEntry, "info");
|
|
1600
|
+
await fs.promises.mkdir(infoDir, { recursive: true });
|
|
1601
|
+
await Bun.write(path.join(infoDir, "sparse-checkout"), sparsePatterns);
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
// Restore the index verbatim (skip-worktree, assume-unchanged, exact stage
|
|
1605
|
+
// entries) so the working tree's dirty set — including sparse-excluded files
|
|
1606
|
+
// — matches the source. Fall back to rebuilding from HEAD only when the
|
|
1607
|
+
// source had no index (a bare-ish/never-staged checkout).
|
|
1608
|
+
if (indexBytes) {
|
|
1609
|
+
for (const shared of sharedIndexFiles) {
|
|
1610
|
+
await Bun.write(path.join(gitEntry, shared.name), shared.bytes);
|
|
1611
|
+
}
|
|
1612
|
+
await Bun.write(path.join(gitEntry, "index"), indexBytes);
|
|
1613
|
+
} else if (headSha) {
|
|
1614
|
+
await readTree(worktreeRoot, headSha);
|
|
1615
|
+
}
|
|
1616
|
+
return "detached";
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1382
1619
|
// ════════════════════════════════════════════════════════════════════════════
|
|
1383
1620
|
// API: show
|
|
1384
1621
|
// ════════════════════════════════════════════════════════════════════════════
|
package/src/web/search/index.ts
CHANGED
|
@@ -134,10 +134,7 @@ async function executeSearch(
|
|
|
134
134
|
const explicitProvider = params.provider;
|
|
135
135
|
let candidates: SearchProviderCandidate[];
|
|
136
136
|
if (explicitProvider && explicitProvider !== "auto") {
|
|
137
|
-
|
|
138
|
-
candidates = (await provider.isExplicitlyAvailable(authStorage))
|
|
139
|
-
? [{ id: explicitProvider, explicit: true }]
|
|
140
|
-
: resolveProviderCandidates("auto");
|
|
137
|
+
candidates = [{ id: explicitProvider, explicit: true }];
|
|
141
138
|
} else if (explicitProvider === "auto") {
|
|
142
139
|
// Explicit `--provider auto` bypasses the configured preferred provider
|
|
143
140
|
// for this invocation; exclusions still apply.
|
|
@@ -175,7 +172,13 @@ async function executeSearch(
|
|
|
175
172
|
const available = candidate.explicit
|
|
176
173
|
? await provider.isExplicitlyAvailable(authStorage)
|
|
177
174
|
: await provider.isAvailable(authStorage);
|
|
178
|
-
if (!available) continue;
|
|
175
|
+
if (!available && !candidate.explicit) continue;
|
|
176
|
+
if (!available && candidate.explicit) {
|
|
177
|
+
throw new SearchProviderError(
|
|
178
|
+
provider.id,
|
|
179
|
+
`${provider.label} web search is unavailable. Configure its credentials or select the automatic provider chain.`,
|
|
180
|
+
);
|
|
181
|
+
}
|
|
179
182
|
availableProviderCount++;
|
|
180
183
|
lastProvider = provider;
|
|
181
184
|
|
|
@@ -213,6 +216,7 @@ async function executeSearch(
|
|
|
213
216
|
// summary error), masking the cancellation.
|
|
214
217
|
throwIfAborted(signal);
|
|
215
218
|
failures.push({ provider: provider ?? providerMeta, error });
|
|
219
|
+
if (candidate.explicit) break;
|
|
216
220
|
}
|
|
217
221
|
}
|
|
218
222
|
|