@oh-my-pi/pi-coding-agent 15.10.11 → 15.10.12
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 +44 -0
- package/dist/cli.js +5349 -5328
- package/dist/types/cli/args.d.ts +1 -0
- package/dist/types/cli-commands.d.ts +12 -0
- package/dist/types/commands/launch.d.ts +4 -0
- package/dist/types/config/api-key-resolver.d.ts +3 -0
- package/dist/types/config/model-registry.d.ts +1 -0
- package/dist/types/config/model-resolver.d.ts +18 -0
- package/dist/types/config/settings-schema.d.ts +29 -1
- package/dist/types/config/settings.d.ts +7 -0
- package/dist/types/edit/hashline/noop-loop-guard.d.ts +72 -0
- package/dist/types/eval/py/executor.d.ts +5 -0
- package/dist/types/eval/py/kernel.d.ts +6 -1
- package/dist/types/eval/py/runtime.d.ts +9 -0
- package/dist/types/exec/bash-executor.d.ts +2 -0
- package/dist/types/extensibility/extensions/runner.d.ts +3 -2
- package/dist/types/extensibility/extensions/types.d.ts +3 -0
- package/dist/types/memory-backend/index.d.ts +1 -0
- package/dist/types/memory-backend/runtime.d.ts +4 -0
- package/dist/types/memory-backend/types.d.ts +66 -1
- package/dist/types/modes/index.d.ts +3 -3
- package/dist/types/modes/interactive-mode.d.ts +7 -2
- package/dist/types/modes/oauth-manual-input.d.ts +7 -0
- package/dist/types/modes/rpc/rpc-client.d.ts +39 -2
- package/dist/types/modes/rpc/rpc-mode.d.ts +31 -2
- package/dist/types/modes/rpc/rpc-subagents.d.ts +24 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +75 -1
- package/dist/types/modes/setup-wizard/index.d.ts +5 -1
- package/dist/types/modes/setup-wizard/lazy.d.ts +2 -0
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/secrets/index.d.ts +1 -1
- package/dist/types/secrets/obfuscator.d.ts +8 -2
- package/dist/types/session/agent-session.d.ts +14 -2
- package/dist/types/session/streaming-output.d.ts +23 -0
- package/dist/types/slash-commands/acp-builtins.d.ts +16 -0
- package/dist/types/slash-commands/builtin-registry.d.ts +1 -0
- package/dist/types/slash-commands/types.d.ts +1 -1
- package/dist/types/system-prompt.d.ts +2 -0
- package/dist/types/task/executor.d.ts +1 -0
- package/dist/types/task/index.d.ts +2 -2
- package/dist/types/task/types.d.ts +8 -0
- package/dist/types/thinking.d.ts +4 -0
- package/dist/types/tiny/title-client.d.ts +11 -0
- package/dist/types/tiny/title-protocol.d.ts +1 -0
- package/dist/types/tools/index.d.ts +6 -0
- package/dist/types/utils/git.d.ts +15 -2
- package/dist/types/utils/title-generator.d.ts +3 -2
- package/package.json +10 -10
- package/src/auto-thinking/classifier.ts +1 -0
- package/src/cli/args.ts +3 -0
- package/src/cli-commands.ts +29 -0
- package/src/cli.ts +8 -9
- package/src/commands/launch.ts +4 -0
- package/src/commit/model-selection.ts +3 -2
- package/src/config/api-key-resolver.ts +8 -6
- package/src/config/model-registry.ts +97 -30
- package/src/config/model-resolver.ts +60 -0
- package/src/config/settings-schema.ts +43 -15
- package/src/config/settings.ts +61 -3
- package/src/edit/hashline/execute.ts +39 -2
- package/src/edit/hashline/noop-loop-guard.ts +99 -0
- package/src/eval/completion-bridge.ts +1 -0
- package/src/eval/py/executor.ts +29 -7
- package/src/eval/py/index.ts +6 -1
- package/src/eval/py/kernel.ts +31 -11
- package/src/eval/py/runtime.ts +37 -0
- package/src/exec/bash-executor.ts +82 -3
- package/src/extensibility/extensions/get-commands-handler.ts +2 -1
- package/src/extensibility/extensions/runner.ts +6 -1
- package/src/extensibility/extensions/types.ts +3 -0
- package/src/hindsight/bank.ts +17 -2
- package/src/internal-urls/docs-index.generated.ts +3 -3
- package/src/main.ts +18 -6
- package/src/memories/index.ts +2 -0
- package/src/memory-backend/index.ts +1 -0
- package/src/memory-backend/local-backend.ts +9 -0
- package/src/memory-backend/off-backend.ts +9 -0
- package/src/memory-backend/runtime.ts +66 -0
- package/src/memory-backend/types.ts +81 -1
- package/src/mnemopi/backend.ts +151 -4
- package/src/modes/acp/acp-agent.ts +119 -11
- package/src/modes/components/assistant-message.ts +19 -21
- package/src/modes/components/footer.ts +3 -1
- package/src/modes/components/status-line/component.ts +118 -34
- package/src/modes/controllers/command-controller.ts +1 -1
- package/src/modes/controllers/input-controller.ts +1 -0
- package/src/modes/controllers/mcp-command-controller.ts +38 -3
- package/src/modes/index.ts +3 -21
- package/src/modes/interactive-mode.ts +39 -9
- package/src/modes/oauth-manual-input.ts +30 -3
- package/src/modes/rpc/rpc-client.ts +154 -3
- package/src/modes/rpc/rpc-mode.ts +97 -12
- package/src/modes/rpc/rpc-subagents.ts +265 -0
- package/src/modes/rpc/rpc-types.ts +81 -1
- package/src/modes/setup-wizard/index.ts +12 -2
- package/src/modes/setup-wizard/lazy.ts +16 -0
- package/src/modes/types.ts +2 -0
- package/src/sdk.ts +8 -1
- package/src/secrets/index.ts +8 -1
- package/src/secrets/obfuscator.ts +39 -18
- package/src/session/agent-session.ts +179 -54
- package/src/session/streaming-output.ts +166 -10
- package/src/slash-commands/acp-builtins.ts +24 -0
- package/src/slash-commands/builtin-registry.ts +20 -0
- package/src/slash-commands/types.ts +1 -1
- package/src/system-prompt.ts +14 -0
- package/src/task/executor.ts +13 -12
- package/src/task/index.ts +9 -8
- package/src/task/render.ts +18 -3
- package/src/task/types.ts +9 -0
- package/src/thinking.ts +7 -0
- package/src/tiny/title-client.ts +34 -5
- package/src/tiny/title-protocol.ts +1 -1
- package/src/tiny/worker.ts +6 -4
- package/src/tools/bash.ts +46 -5
- package/src/tools/image-gen.ts +11 -4
- package/src/tools/index.ts +13 -1
- package/src/tools/inspect-image.ts +1 -0
- package/src/utils/commit-message-generator.ts +1 -0
- package/src/utils/git.ts +267 -13
- package/src/utils/title-generator.ts +24 -5
|
@@ -29,7 +29,7 @@ export interface TinyTitleProgressEvent {
|
|
|
29
29
|
|
|
30
30
|
export type TinyTitleWorkerInbound =
|
|
31
31
|
| { type: "ping"; id: string }
|
|
32
|
-
| { type: "generate"; id: string; modelKey: TinyTitleLocalModelKey; message: string }
|
|
32
|
+
| { type: "generate"; id: string; modelKey: TinyTitleLocalModelKey; message: string; systemPrompt?: string }
|
|
33
33
|
| { type: "complete"; id: string; modelKey: TinyLocalModelKey; prompt: string; maxTokens?: number }
|
|
34
34
|
| { type: "download"; id: string; modelKey: TinyLocalModelKey };
|
|
35
35
|
|
package/src/tiny/worker.ts
CHANGED
|
@@ -436,9 +436,10 @@ async function loadPipeline(
|
|
|
436
436
|
return loaded;
|
|
437
437
|
}
|
|
438
438
|
|
|
439
|
-
function buildPrompt(generator: TextGenerationPipeline, message: string): string {
|
|
439
|
+
function buildPrompt(generator: TextGenerationPipeline, message: string, systemPrompt?: string): string {
|
|
440
|
+
const selectedSystemPrompt = systemPrompt?.trim() || TINY_TITLE_SYSTEM_PROMPT;
|
|
440
441
|
const chat = [
|
|
441
|
-
{ role: "system", content:
|
|
442
|
+
{ role: "system", content: selectedSystemPrompt },
|
|
442
443
|
{ role: "user", content: formatTitleUserMessage(message) },
|
|
443
444
|
];
|
|
444
445
|
const chatTemplateOptions = {
|
|
@@ -464,9 +465,10 @@ async function generateTitle(
|
|
|
464
465
|
requestId: string,
|
|
465
466
|
modelKey: TinyTitleLocalModelKey,
|
|
466
467
|
message: string,
|
|
468
|
+
systemPrompt?: string,
|
|
467
469
|
): Promise<string | null> {
|
|
468
470
|
const generator = await loadPipeline(modelKey, transport, requestId);
|
|
469
|
-
const promptText = buildPrompt(generator, message);
|
|
471
|
+
const promptText = buildPrompt(generator, message, systemPrompt);
|
|
470
472
|
const transformers = await loadTransformers(transport, requestId, modelKey);
|
|
471
473
|
const output = (await generator(promptText, {
|
|
472
474
|
max_new_tokens: TITLE_MAX_NEW_TOKENS,
|
|
@@ -548,7 +550,7 @@ async function handleQueuedRequest(
|
|
|
548
550
|
transport.send({ type: "completion", id: request.id, text });
|
|
549
551
|
return;
|
|
550
552
|
}
|
|
551
|
-
const title = await generateTitle(transport, request.id, request.modelKey, request.message);
|
|
553
|
+
const title = await generateTitle(transport, request.id, request.modelKey, request.message, request.systemPrompt);
|
|
552
554
|
transport.send({ type: "title", id: request.id, title });
|
|
553
555
|
} catch (error) {
|
|
554
556
|
transport.send({ type: "error", id: request.id, error: errorText(error) });
|
package/src/tools/bash.ts
CHANGED
|
@@ -1212,6 +1212,22 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
|
|
|
1212
1212
|
const details = result.details;
|
|
1213
1213
|
const outputBlock = new CachedOutputBlock();
|
|
1214
1214
|
|
|
1215
|
+
// Per-instance cache for the expensive inner lines computation. Mirrors
|
|
1216
|
+
// the eval-renderer pattern (`eval-render.ts:709-752`): without this,
|
|
1217
|
+
// every TUI repaint (one per keystroke when a long transcript is on
|
|
1218
|
+
// screen) re-runs `split` / `replaceTabs` / `truncateToVisualLines` over
|
|
1219
|
+
// the whole stored output for every bash row in scrollback. With a
|
|
1220
|
+
// 50KB-tail bash result times hundreds of rows, that re-rendering is
|
|
1221
|
+
// what pinned the main thread in issue #2081 and made keystrokes feel
|
|
1222
|
+
// like the CPU was at 100%. The cache key includes every render input
|
|
1223
|
+
// that materially affects the produced lines.
|
|
1224
|
+
let cachedWidth: number | undefined;
|
|
1225
|
+
let cachedPreviewLines: number | undefined;
|
|
1226
|
+
let cachedExpanded: boolean | undefined;
|
|
1227
|
+
let cachedRawOutput: string | undefined;
|
|
1228
|
+
let cachedIsPartial: boolean | undefined;
|
|
1229
|
+
let cachedLines: readonly string[] | undefined;
|
|
1230
|
+
|
|
1215
1231
|
return markFramedBlockComponent({
|
|
1216
1232
|
render: (width: number): readonly string[] => {
|
|
1217
1233
|
// REACTIVE: read mutable options at render time
|
|
@@ -1223,6 +1239,19 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
|
|
|
1223
1239
|
// Strip the LLM-facing notice appended by wrappedExecute so we don't
|
|
1224
1240
|
// double-print it alongside the styled warning line below.
|
|
1225
1241
|
const rawOutput = renderContext?.output ?? result.content?.find(c => c.type === "text")?.text ?? "";
|
|
1242
|
+
|
|
1243
|
+
const isPartial = options.isPartial === true;
|
|
1244
|
+
|
|
1245
|
+
if (
|
|
1246
|
+
cachedLines !== undefined &&
|
|
1247
|
+
cachedWidth === width &&
|
|
1248
|
+
cachedPreviewLines === previewLines &&
|
|
1249
|
+
cachedExpanded === expanded &&
|
|
1250
|
+
cachedRawOutput === rawOutput &&
|
|
1251
|
+
cachedIsPartial === isPartial
|
|
1252
|
+
) {
|
|
1253
|
+
return cachedLines;
|
|
1254
|
+
}
|
|
1226
1255
|
const strippedOutput = stripOutputNotice(rawOutput, details?.meta);
|
|
1227
1256
|
const withoutExit = stripExitCodeNotice(strippedOutput, details?.exitCode);
|
|
1228
1257
|
const withoutWall = stripWallTimeNotice(withoutExit, details?.wallTimeMs);
|
|
@@ -1299,15 +1328,13 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
|
|
|
1299
1328
|
if (timeoutLine) outputLines.push(timeoutLine);
|
|
1300
1329
|
if (warningLine) outputLines.push(warningLine);
|
|
1301
1330
|
|
|
1302
|
-
|
|
1331
|
+
const framed = outputBlock.render(
|
|
1303
1332
|
{
|
|
1304
1333
|
header,
|
|
1305
|
-
state:
|
|
1334
|
+
state: isPartial ? "pending" : isError ? "error" : "success",
|
|
1306
1335
|
sections: [
|
|
1307
1336
|
{
|
|
1308
|
-
lines:
|
|
1309
|
-
? capPreviewLines(cmdLines ?? [], uiTheme, { expanded })
|
|
1310
|
-
: (cmdLines ?? []),
|
|
1337
|
+
lines: isPartial ? capPreviewLines(cmdLines ?? [], uiTheme, { expanded }) : (cmdLines ?? []),
|
|
1311
1338
|
},
|
|
1312
1339
|
{ label: uiTheme.fg("toolTitle", "Output"), lines: outputLines },
|
|
1313
1340
|
],
|
|
@@ -1315,9 +1342,23 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
|
|
|
1315
1342
|
},
|
|
1316
1343
|
uiTheme,
|
|
1317
1344
|
);
|
|
1345
|
+
|
|
1346
|
+
cachedWidth = width;
|
|
1347
|
+
cachedPreviewLines = previewLines;
|
|
1348
|
+
cachedExpanded = expanded;
|
|
1349
|
+
cachedRawOutput = rawOutput;
|
|
1350
|
+
cachedIsPartial = isPartial;
|
|
1351
|
+
cachedLines = framed;
|
|
1352
|
+
return framed;
|
|
1318
1353
|
},
|
|
1319
1354
|
invalidate: () => {
|
|
1320
1355
|
outputBlock.invalidate();
|
|
1356
|
+
cachedLines = undefined;
|
|
1357
|
+
cachedWidth = undefined;
|
|
1358
|
+
cachedPreviewLines = undefined;
|
|
1359
|
+
cachedExpanded = undefined;
|
|
1360
|
+
cachedRawOutput = undefined;
|
|
1361
|
+
cachedIsPartial = undefined;
|
|
1321
1362
|
},
|
|
1322
1363
|
});
|
|
1323
1364
|
},
|
package/src/tools/image-gen.ts
CHANGED
|
@@ -472,8 +472,13 @@ function parseAntigravityCredentials(raw: string): ParsedAntigravityCredentials
|
|
|
472
472
|
return null;
|
|
473
473
|
}
|
|
474
474
|
|
|
475
|
-
async function findAntigravityCredentials(
|
|
476
|
-
|
|
475
|
+
async function findAntigravityCredentials(
|
|
476
|
+
modelRegistry: ModelRegistry,
|
|
477
|
+
sessionId?: string,
|
|
478
|
+
): Promise<ImageApiKey | null> {
|
|
479
|
+
const apiKey = await modelRegistry.getApiKeyForProvider("google-antigravity", sessionId, {
|
|
480
|
+
modelId: DEFAULT_ANTIGRAVITY_MODEL,
|
|
481
|
+
});
|
|
477
482
|
if (!apiKey) return null;
|
|
478
483
|
|
|
479
484
|
const parsed = parseAntigravityCredentials(apiKey);
|
|
@@ -523,7 +528,7 @@ async function findImageApiKey(
|
|
|
523
528
|
if (openAI) return openAI;
|
|
524
529
|
// Fall through to auto-detect if preferred provider key not found.
|
|
525
530
|
} else if (preferredImageProvider === "antigravity" && modelRegistry) {
|
|
526
|
-
const antigravity = await findAntigravityCredentials(modelRegistry);
|
|
531
|
+
const antigravity = await findAntigravityCredentials(modelRegistry, sessionId);
|
|
527
532
|
if (antigravity) return antigravity;
|
|
528
533
|
// Fall through to auto-detect if preferred provider key not found.
|
|
529
534
|
} else if (preferredImageProvider === "gemini") {
|
|
@@ -547,7 +552,7 @@ async function findImageApiKey(
|
|
|
547
552
|
if (openAI) return openAI;
|
|
548
553
|
|
|
549
554
|
if (modelRegistry) {
|
|
550
|
-
const antigravity = await findAntigravityCredentials(modelRegistry);
|
|
555
|
+
const antigravity = await findAntigravityCredentials(modelRegistry, sessionId);
|
|
551
556
|
if (antigravity) return antigravity;
|
|
552
557
|
}
|
|
553
558
|
|
|
@@ -1052,6 +1057,7 @@ export const imageGenTool: CustomTool<typeof imageGenSchema, ImageGenToolDetails
|
|
|
1052
1057
|
const hostedKey: ApiKey = ctx.modelRegistry.resolver(hostedModel.provider, {
|
|
1053
1058
|
sessionId,
|
|
1054
1059
|
baseUrl: hostedModel.baseUrl,
|
|
1060
|
+
modelId: hostedModel.id,
|
|
1055
1061
|
});
|
|
1056
1062
|
|
|
1057
1063
|
const parsed = await withAuth(
|
|
@@ -1113,6 +1119,7 @@ export const imageGenTool: CustomTool<typeof imageGenSchema, ImageGenToolDetails
|
|
|
1113
1119
|
const prompt = assemblePrompt(params);
|
|
1114
1120
|
const antigravityKey: ApiKey = ctx.modelRegistry.resolver("google-antigravity", {
|
|
1115
1121
|
sessionId,
|
|
1122
|
+
modelId: DEFAULT_ANTIGRAVITY_MODEL,
|
|
1116
1123
|
});
|
|
1117
1124
|
|
|
1118
1125
|
const response = await withAuth(
|
package/src/tools/index.ts
CHANGED
|
@@ -320,6 +320,13 @@ export interface ToolSession {
|
|
|
320
320
|
* model for each file. Lazily initialized by `getDiagnosticsLedger`. */
|
|
321
321
|
diagnosticsLedger?: import("../lsp/diagnostics-ledger").DiagnosticsLedger;
|
|
322
322
|
|
|
323
|
+
/** Per-session ledger of consecutive byte-identical no-op edits, keyed by
|
|
324
|
+
* canonical file path. The hashline executor escalates a soft no-op hint
|
|
325
|
+
* to a thrown error once the same payload no-ops `NOOP_HARD_LIMIT` times,
|
|
326
|
+
* breaking subagent loops that ignore the textual hint (issue #2081).
|
|
327
|
+
* Lazily initialized by `getNoopLoopGuard`. */
|
|
328
|
+
noopLoopGuard?: import("../edit/hashline/noop-loop-guard").NoopLoopGuard;
|
|
329
|
+
|
|
323
330
|
/** Queue a hidden message to be injected at the next agent turn. */
|
|
324
331
|
queueDeferredMessage?(message: CustomMessage): void;
|
|
325
332
|
/** Queue late LSP diagnostics (arrived after an edit/write returned) to be shown
|
|
@@ -463,7 +470,12 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
463
470
|
!allowJs &&
|
|
464
471
|
(requestedTools === undefined || requestedTools.includes("eval"))
|
|
465
472
|
) {
|
|
466
|
-
const availability = await logger.time(
|
|
473
|
+
const availability = await logger.time(
|
|
474
|
+
"createTools:pythonCheck",
|
|
475
|
+
checkPythonKernelAvailability,
|
|
476
|
+
session.cwd,
|
|
477
|
+
session.settings.get("python.interpreter")?.trim() || undefined,
|
|
478
|
+
);
|
|
467
479
|
pythonAvailable = availability.ok;
|
|
468
480
|
if (!availability.ok) {
|
|
469
481
|
logger.warn("Python kernel unavailable and JS backend disabled; eval will be unavailable", {
|
|
@@ -141,6 +141,7 @@ export class InspectImageTool implements AgentTool<typeof inspectImageSchema, In
|
|
|
141
141
|
apiKey: modelRegistry.resolver(model.provider, {
|
|
142
142
|
sessionId: this.session.getSessionId?.() ?? undefined,
|
|
143
143
|
baseUrl: model.baseUrl,
|
|
144
|
+
modelId: model.id,
|
|
144
145
|
}),
|
|
145
146
|
signal,
|
|
146
147
|
},
|
|
@@ -115,6 +115,7 @@ export async function generateCommitMessage(
|
|
|
115
115
|
apiKey: registry.resolver(candidate.model.provider, {
|
|
116
116
|
sessionId,
|
|
117
117
|
baseUrl: candidate.model.baseUrl,
|
|
118
|
+
modelId: candidate.model.id,
|
|
118
119
|
}),
|
|
119
120
|
maxTokens,
|
|
120
121
|
reasoning: toReasoningEffort(candidate.thinkingLevel),
|
package/src/utils/git.ts
CHANGED
|
@@ -27,6 +27,7 @@ export interface GitRepository {
|
|
|
27
27
|
gitEntryPath: string;
|
|
28
28
|
headPath: string;
|
|
29
29
|
repoRoot: string;
|
|
30
|
+
isReftable?: boolean;
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
export interface GitStatusSummary {
|
|
@@ -476,6 +477,31 @@ async function resolveCommonDir(gitDir: string): Promise<string> {
|
|
|
476
477
|
if (!relative) return gitDir;
|
|
477
478
|
return path.resolve(gitDir, relative);
|
|
478
479
|
}
|
|
480
|
+
function isLinkedWorktree(repository: GitRepository): boolean {
|
|
481
|
+
return (
|
|
482
|
+
repository.gitDir !== repository.commonDir &&
|
|
483
|
+
getEntryTypeSync(path.join(repository.gitDir, "commondir")) === "file"
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
async function isLinkedWorktreeAsync(repository: GitRepository): Promise<boolean> {
|
|
488
|
+
return (
|
|
489
|
+
repository.gitDir !== repository.commonDir &&
|
|
490
|
+
(await getEntryType(path.join(repository.gitDir, "commondir"))) === "file"
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function primaryRootFromRepositorySync(repository: GitRepository): string {
|
|
495
|
+
if (path.basename(repository.commonDir) === ".git") return path.dirname(repository.commonDir);
|
|
496
|
+
if (isLinkedWorktree(repository)) return repository.commonDir;
|
|
497
|
+
return repository.repoRoot;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
async function primaryRootFromRepository(repository: GitRepository): Promise<string> {
|
|
501
|
+
if (path.basename(repository.commonDir) === ".git") return path.dirname(repository.commonDir);
|
|
502
|
+
if (await isLinkedWorktreeAsync(repository)) return repository.commonDir;
|
|
503
|
+
return repository.repoRoot;
|
|
504
|
+
}
|
|
479
505
|
|
|
480
506
|
function resolveRepoFromEntrySync(repoRoot: string, gitEntryPath: string, entryType: EntryType): GitRepository | null {
|
|
481
507
|
const gitDir = resolveGitDirSync(gitEntryPath, entryType);
|
|
@@ -560,7 +586,174 @@ function parsePackedRefs(content: string | null, targetRef: string): string | nu
|
|
|
560
586
|
return null;
|
|
561
587
|
}
|
|
562
588
|
|
|
589
|
+
function stripGitConfigComments(line: string): string {
|
|
590
|
+
let clean = "";
|
|
591
|
+
let inQuotes = false;
|
|
592
|
+
for (let i = 0; i < line.length; i++) {
|
|
593
|
+
const char = line[i];
|
|
594
|
+
if (char === '"') {
|
|
595
|
+
inQuotes = !inQuotes;
|
|
596
|
+
clean += char;
|
|
597
|
+
} else if (!inQuotes && (char === ";" || char === "#")) {
|
|
598
|
+
break;
|
|
599
|
+
} else {
|
|
600
|
+
clean += char;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
return clean.trim();
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function parseGitConfigHasReftable(content: string): boolean {
|
|
607
|
+
let inExtensions = false;
|
|
608
|
+
for (const line of content.split("\n")) {
|
|
609
|
+
const trimmed = stripGitConfigComments(line);
|
|
610
|
+
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
|
611
|
+
const section = trimmed.slice(1, -1).trim().toLowerCase();
|
|
612
|
+
inExtensions = section === "extensions";
|
|
613
|
+
} else if (inExtensions) {
|
|
614
|
+
const eqIndex = trimmed.indexOf("=");
|
|
615
|
+
if (eqIndex !== -1) {
|
|
616
|
+
const key = trimmed.slice(0, eqIndex).trim().toLowerCase();
|
|
617
|
+
let value = trimmed.slice(eqIndex + 1).trim();
|
|
618
|
+
if (key === "refstorage") {
|
|
619
|
+
if (value.startsWith('"') && value.endsWith('"')) {
|
|
620
|
+
value = value.slice(1, -1).trim();
|
|
621
|
+
}
|
|
622
|
+
const lowerValue = value.toLowerCase();
|
|
623
|
+
if (lowerValue === "reftable" || lowerValue.startsWith("reftable:")) {
|
|
624
|
+
return true;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
return false;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function isReftableRepoSync(repository: GitRepository): boolean {
|
|
634
|
+
if (repository.isReftable !== undefined) return repository.isReftable;
|
|
635
|
+
const configPath = path.join(repository.commonDir, "config");
|
|
636
|
+
const content = readOptionalTextSync(configPath);
|
|
637
|
+
repository.isReftable = content ? parseGitConfigHasReftable(content) : false;
|
|
638
|
+
return repository.isReftable;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
async function isReftableRepo(repository: GitRepository): Promise<boolean> {
|
|
642
|
+
if (repository.isReftable !== undefined) return repository.isReftable;
|
|
643
|
+
const configPath = path.join(repository.commonDir, "config");
|
|
644
|
+
const content = await readOptionalText(configPath);
|
|
645
|
+
repository.isReftable = content ? parseGitConfigHasReftable(content) : false;
|
|
646
|
+
return repository.isReftable;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
async function resolveHeadStateReftable(repository: GitRepository, signal?: AbortSignal): Promise<GitHeadState | null> {
|
|
650
|
+
throwIfAborted(signal);
|
|
651
|
+
const symResult = await git(repository.repoRoot, ["symbolic-ref", "HEAD"], { readOnly: true, signal }).catch(err => {
|
|
652
|
+
if (signal?.aborted || (err instanceof Error && (err.name === "AbortError" || err.name === "ToolAbortError"))) {
|
|
653
|
+
throw err;
|
|
654
|
+
}
|
|
655
|
+
return null;
|
|
656
|
+
});
|
|
657
|
+
throwIfAborted(signal);
|
|
658
|
+
const revResult = await git(repository.repoRoot, ["rev-parse", "--verify", "HEAD"], {
|
|
659
|
+
readOnly: true,
|
|
660
|
+
signal,
|
|
661
|
+
}).catch(err => {
|
|
662
|
+
if (signal?.aborted || (err instanceof Error && (err.name === "AbortError" || err.name === "ToolAbortError"))) {
|
|
663
|
+
throw err;
|
|
664
|
+
}
|
|
665
|
+
return null;
|
|
666
|
+
});
|
|
667
|
+
const commit = revResult && revResult.exitCode === 0 ? revResult.stdout.trim() || null : null;
|
|
668
|
+
|
|
669
|
+
if (symResult && symResult.exitCode === 0) {
|
|
670
|
+
const ref = symResult.stdout.trim();
|
|
671
|
+
const branchName = ref.startsWith(LOCAL_BRANCH_PREFIX) ? ref.slice(LOCAL_BRANCH_PREFIX.length) : null;
|
|
672
|
+
return {
|
|
673
|
+
...repository,
|
|
674
|
+
kind: "ref",
|
|
675
|
+
ref,
|
|
676
|
+
branchName,
|
|
677
|
+
commit,
|
|
678
|
+
headContent: `${HEAD_REF_PREFIX} ${ref}`,
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
return {
|
|
683
|
+
...repository,
|
|
684
|
+
kind: "detached",
|
|
685
|
+
commit,
|
|
686
|
+
headContent: commit || "",
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function resolveHeadStateReftableSync(repository: GitRepository): GitHeadState | null {
|
|
691
|
+
ensureAvailable();
|
|
692
|
+
const symArgs = withShortLivedGitConfig(withNoOptionalLocks(["symbolic-ref", "HEAD"]));
|
|
693
|
+
const symResult = Bun.spawnSync(["git", ...symArgs], {
|
|
694
|
+
cwd: repository.repoRoot,
|
|
695
|
+
stdout: "pipe",
|
|
696
|
+
stderr: "pipe",
|
|
697
|
+
windowsHide: true,
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
const revArgs = withShortLivedGitConfig(withNoOptionalLocks(["rev-parse", "--verify", "HEAD"]));
|
|
701
|
+
const revResult = Bun.spawnSync(["git", ...revArgs], {
|
|
702
|
+
cwd: repository.repoRoot,
|
|
703
|
+
stdout: "pipe",
|
|
704
|
+
stderr: "pipe",
|
|
705
|
+
windowsHide: true,
|
|
706
|
+
});
|
|
707
|
+
const commit = revResult.exitCode === 0 ? new TextDecoder().decode(revResult.stdout).trim() || null : null;
|
|
708
|
+
|
|
709
|
+
if (symResult.exitCode === 0) {
|
|
710
|
+
const ref = new TextDecoder().decode(symResult.stdout).trim();
|
|
711
|
+
const branchName = ref.startsWith(LOCAL_BRANCH_PREFIX) ? ref.slice(LOCAL_BRANCH_PREFIX.length) : null;
|
|
712
|
+
return {
|
|
713
|
+
...repository,
|
|
714
|
+
kind: "ref",
|
|
715
|
+
ref,
|
|
716
|
+
branchName,
|
|
717
|
+
commit,
|
|
718
|
+
headContent: `${HEAD_REF_PREFIX} ${ref}`,
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
return {
|
|
723
|
+
...repository,
|
|
724
|
+
kind: "detached",
|
|
725
|
+
commit,
|
|
726
|
+
headContent: commit || "",
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
|
|
563
730
|
function readRefSync(repository: GitRepository, targetRef: string): string | null {
|
|
731
|
+
if (isReftableRepoSync(repository)) {
|
|
732
|
+
ensureAvailable();
|
|
733
|
+
const symArgs = withShortLivedGitConfig(withNoOptionalLocks(["symbolic-ref", targetRef]));
|
|
734
|
+
const symResult = Bun.spawnSync(["git", ...symArgs], {
|
|
735
|
+
cwd: repository.repoRoot,
|
|
736
|
+
stdout: "pipe",
|
|
737
|
+
stderr: "pipe",
|
|
738
|
+
windowsHide: true,
|
|
739
|
+
});
|
|
740
|
+
if (symResult.exitCode === 0) {
|
|
741
|
+
const stdoutText = new TextDecoder().decode(symResult.stdout).trim();
|
|
742
|
+
return `${HEAD_REF_PREFIX} ${stdoutText}`;
|
|
743
|
+
}
|
|
744
|
+
const revArgs = withShortLivedGitConfig(withNoOptionalLocks(["rev-parse", "--verify", targetRef]));
|
|
745
|
+
const revResult = Bun.spawnSync(["git", ...revArgs], {
|
|
746
|
+
cwd: repository.repoRoot,
|
|
747
|
+
stdout: "pipe",
|
|
748
|
+
stderr: "pipe",
|
|
749
|
+
windowsHide: true,
|
|
750
|
+
});
|
|
751
|
+
if (revResult.exitCode === 0) {
|
|
752
|
+
return new TextDecoder().decode(revResult.stdout).trim() || null;
|
|
753
|
+
}
|
|
754
|
+
return null;
|
|
755
|
+
}
|
|
756
|
+
|
|
564
757
|
for (const dir of getRefLookupDirs(repository)) {
|
|
565
758
|
const value = normalizeRefValue(readOptionalTextSync(path.join(dir, targetRef)));
|
|
566
759
|
if (value) return value;
|
|
@@ -572,7 +765,42 @@ function readRefSync(repository: GitRepository, targetRef: string): string | nul
|
|
|
572
765
|
return null;
|
|
573
766
|
}
|
|
574
767
|
|
|
575
|
-
async function readRef(repository: GitRepository, targetRef: string): Promise<string | null> {
|
|
768
|
+
async function readRef(repository: GitRepository, targetRef: string, signal?: AbortSignal): Promise<string | null> {
|
|
769
|
+
if (await isReftableRepo(repository)) {
|
|
770
|
+
throwIfAborted(signal);
|
|
771
|
+
const symResult = await git(repository.repoRoot, ["symbolic-ref", targetRef], { readOnly: true, signal }).catch(
|
|
772
|
+
err => {
|
|
773
|
+
if (
|
|
774
|
+
signal?.aborted ||
|
|
775
|
+
(err instanceof Error && (err.name === "AbortError" || err.name === "ToolAbortError"))
|
|
776
|
+
) {
|
|
777
|
+
throw err;
|
|
778
|
+
}
|
|
779
|
+
return null;
|
|
780
|
+
},
|
|
781
|
+
);
|
|
782
|
+
if (symResult && symResult.exitCode === 0) {
|
|
783
|
+
return `${HEAD_REF_PREFIX} ${symResult.stdout.trim()}`;
|
|
784
|
+
}
|
|
785
|
+
throwIfAborted(signal);
|
|
786
|
+
const revResult = await git(repository.repoRoot, ["rev-parse", "--verify", targetRef], {
|
|
787
|
+
readOnly: true,
|
|
788
|
+
signal,
|
|
789
|
+
}).catch(err => {
|
|
790
|
+
if (
|
|
791
|
+
signal?.aborted ||
|
|
792
|
+
(err instanceof Error && (err.name === "AbortError" || err.name === "ToolAbortError"))
|
|
793
|
+
) {
|
|
794
|
+
throw err;
|
|
795
|
+
}
|
|
796
|
+
return null;
|
|
797
|
+
});
|
|
798
|
+
if (revResult && revResult.exitCode === 0) {
|
|
799
|
+
return revResult.stdout.trim() || null;
|
|
800
|
+
}
|
|
801
|
+
return null;
|
|
802
|
+
}
|
|
803
|
+
|
|
576
804
|
for (const dir of getRefLookupDirs(repository)) {
|
|
577
805
|
const value = normalizeRefValue(await readOptionalText(path.join(dir, targetRef)));
|
|
578
806
|
if (value) return value;
|
|
@@ -997,7 +1225,7 @@ export const branch = {
|
|
|
997
1225
|
const repository = await resolveRepository(cwd);
|
|
998
1226
|
if (repository) {
|
|
999
1227
|
for (const refPath of DEFAULT_BRANCH_REFS) {
|
|
1000
|
-
const target = await readRef(repository, refPath);
|
|
1228
|
+
const target = await readRef(repository, refPath, signal);
|
|
1001
1229
|
const branchName = parseDefaultBranchRef(refPath, target);
|
|
1002
1230
|
if (branchName) return branchName;
|
|
1003
1231
|
}
|
|
@@ -1095,7 +1323,7 @@ export const ref = {
|
|
|
1095
1323
|
async exists(cwd: string, refName: string, signal?: AbortSignal): Promise<boolean> {
|
|
1096
1324
|
if (refName === "HEAD") return (await head.sha(cwd, signal)) !== null;
|
|
1097
1325
|
const repository = await resolveRepository(cwd);
|
|
1098
|
-
if (repository && refName.startsWith("refs/")) return (await readRef(repository, refName)) !== null;
|
|
1326
|
+
if (repository && refName.startsWith("refs/")) return (await readRef(repository, refName, signal)) !== null;
|
|
1099
1327
|
const result = await git(cwd, ["show-ref", "--verify", "--quiet", refName], { readOnly: true, signal });
|
|
1100
1328
|
return result.exitCode === 0;
|
|
1101
1329
|
},
|
|
@@ -1104,7 +1332,7 @@ export const ref = {
|
|
|
1104
1332
|
async resolve(cwd: string, refName: string, signal?: AbortSignal): Promise<string | null> {
|
|
1105
1333
|
if (refName === "HEAD") return head.sha(cwd, signal);
|
|
1106
1334
|
const repository = await resolveRepository(cwd);
|
|
1107
|
-
if (repository && refName.startsWith("refs/")) return readRef(repository, refName);
|
|
1335
|
+
if (repository && refName.startsWith("refs/")) return readRef(repository, refName, signal);
|
|
1108
1336
|
const result = await git(cwd, ["rev-parse", refName], { readOnly: true, signal });
|
|
1109
1337
|
if (result.exitCode !== 0) return null;
|
|
1110
1338
|
return result.stdout.trim() || null;
|
|
@@ -1397,9 +1625,12 @@ export const ls = {
|
|
|
1397
1625
|
|
|
1398
1626
|
export const head = {
|
|
1399
1627
|
/** Full HEAD state (branch, commit, repo info). */
|
|
1400
|
-
async resolve(cwd: string): Promise<GitHeadState | null> {
|
|
1628
|
+
async resolve(cwd: string, signal?: AbortSignal): Promise<GitHeadState | null> {
|
|
1401
1629
|
const repository = await resolveRepository(cwd);
|
|
1402
1630
|
if (!repository) return null;
|
|
1631
|
+
if (await isReftableRepo(repository)) {
|
|
1632
|
+
return resolveHeadStateReftable(repository, signal);
|
|
1633
|
+
}
|
|
1403
1634
|
const content = await readOptionalText(repository.headPath);
|
|
1404
1635
|
if (content === null) return null;
|
|
1405
1636
|
return parseHeadState(repository, content);
|
|
@@ -1409,6 +1640,9 @@ export const head = {
|
|
|
1409
1640
|
resolveSync(cwd: string): GitHeadState | null {
|
|
1410
1641
|
const repository = resolveRepositorySync(cwd);
|
|
1411
1642
|
if (!repository) return null;
|
|
1643
|
+
if (isReftableRepoSync(repository)) {
|
|
1644
|
+
return resolveHeadStateReftableSync(repository);
|
|
1645
|
+
}
|
|
1412
1646
|
const content = readOptionalTextSync(repository.headPath);
|
|
1413
1647
|
if (content === null) return null;
|
|
1414
1648
|
return parseHeadStateSync(repository, content);
|
|
@@ -1416,7 +1650,7 @@ export const head = {
|
|
|
1416
1650
|
|
|
1417
1651
|
/** Current HEAD commit SHA. */
|
|
1418
1652
|
async sha(cwd: string, signal?: AbortSignal): Promise<string | null> {
|
|
1419
|
-
const headState = await head.resolve(cwd);
|
|
1653
|
+
const headState = await head.resolve(cwd, signal);
|
|
1420
1654
|
if (headState?.commit) return headState.commit;
|
|
1421
1655
|
const result = await git(cwd, ["rev-parse", "HEAD"], { readOnly: true, signal });
|
|
1422
1656
|
if (result.exitCode !== 0) return null;
|
|
@@ -1445,13 +1679,10 @@ export const repo = {
|
|
|
1445
1679
|
return result.stdout.trim() || null;
|
|
1446
1680
|
},
|
|
1447
1681
|
|
|
1448
|
-
/** Resolve the primary
|
|
1682
|
+
/** Resolve the primary checkout root, or the shared common dir for bare-repo worktrees. */
|
|
1449
1683
|
async primaryRoot(cwd: string, signal?: AbortSignal): Promise<string | null> {
|
|
1450
1684
|
const repository = await resolveRepository(cwd);
|
|
1451
|
-
if (repository)
|
|
1452
|
-
if (path.basename(repository.commonDir) === ".git") return path.dirname(repository.commonDir);
|
|
1453
|
-
return repository.repoRoot;
|
|
1454
|
-
}
|
|
1685
|
+
if (repository) return primaryRootFromRepository(repository);
|
|
1455
1686
|
const repoRoot = await repo.root(cwd, signal);
|
|
1456
1687
|
if (!repoRoot) return null;
|
|
1457
1688
|
const commonDir = await runText(repoRoot, ["rev-parse", "--path-format=absolute", "--git-common-dir"], {
|
|
@@ -1462,6 +1693,19 @@ export const repo = {
|
|
|
1462
1693
|
return repoRoot;
|
|
1463
1694
|
},
|
|
1464
1695
|
|
|
1696
|
+
/**
|
|
1697
|
+
* Sync sibling of {@link primaryRoot}. Resolves only via on-disk `.git`/
|
|
1698
|
+
* `commondir` walking — no subprocess fallback — so it stays usable from
|
|
1699
|
+
* paths where async I/O is impractical (e.g. `computeBankScope`). Returns
|
|
1700
|
+
* `null` when `cwd` is outside a repository. Bare-repo worktrees resolve to
|
|
1701
|
+
* the shared common dir (`foo.git`) because they have no primary checkout.
|
|
1702
|
+
*/
|
|
1703
|
+
primaryRootSync(cwd: string): string | null {
|
|
1704
|
+
const repository = resolveRepositorySync(cwd);
|
|
1705
|
+
if (!repository) return null;
|
|
1706
|
+
return primaryRootFromRepositorySync(repository);
|
|
1707
|
+
},
|
|
1708
|
+
|
|
1465
1709
|
/** Full GitRepository metadata (sync). */
|
|
1466
1710
|
resolveSync(cwd: string): GitRepository | null {
|
|
1467
1711
|
return resolveRepositorySync(cwd);
|
|
@@ -1471,11 +1715,21 @@ export const repo = {
|
|
|
1471
1715
|
resolve(cwd: string): Promise<GitRepository | null> {
|
|
1472
1716
|
return resolveRepository(cwd);
|
|
1473
1717
|
},
|
|
1718
|
+
|
|
1719
|
+
/** Check if the repository uses the reftable reference storage format (sync). */
|
|
1720
|
+
isReftableSync(repository: GitRepository): boolean {
|
|
1721
|
+
return isReftableRepoSync(repository);
|
|
1722
|
+
},
|
|
1723
|
+
|
|
1724
|
+
/** Check if the repository uses the reftable reference storage format. */
|
|
1725
|
+
isReftable(repository: GitRepository): Promise<boolean> {
|
|
1726
|
+
return isReftableRepo(repository);
|
|
1727
|
+
},
|
|
1474
1728
|
};
|
|
1475
1729
|
|
|
1476
1730
|
// Helper used during head resolution — defined here to reference `head` namespace.
|
|
1477
|
-
async function resolveHead(cwd: string): Promise<GitHeadState | null> {
|
|
1478
|
-
return head.resolve(cwd);
|
|
1731
|
+
async function resolveHead(cwd: string, signal?: AbortSignal): Promise<GitHeadState | null> {
|
|
1732
|
+
return head.resolve(cwd, signal);
|
|
1479
1733
|
}
|
|
1480
1734
|
|
|
1481
1735
|
// ════════════════════════════════════════════════════════════════════════════
|