@oh-my-pi/pi-coding-agent 15.10.11 → 15.11.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 +103 -2
- package/dist/cli.js +5790 -5731
- package/dist/types/async/index.d.ts +0 -1
- package/dist/types/cli/args.d.ts +1 -0
- package/dist/types/cli/gallery-fixtures/types.d.ts +5 -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/keybindings.d.ts +6 -1
- 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 +85 -34
- 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/export/html/template.generated.d.ts +1 -1
- package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
- package/dist/types/extensibility/extensions/runner.d.ts +3 -2
- package/dist/types/extensibility/extensions/types.d.ts +3 -0
- package/dist/types/extensibility/shared-events.d.ts +2 -2
- package/dist/types/internal-urls/history-protocol.d.ts +14 -0
- package/dist/types/internal-urls/index.d.ts +1 -0
- package/dist/types/internal-urls/types.d.ts +1 -1
- package/dist/types/irc/bus.d.ts +66 -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/components/agent-hub.d.ts +30 -0
- package/dist/types/modes/components/compaction-summary-message.d.ts +10 -4
- package/dist/types/modes/components/custom-editor.d.ts +2 -0
- package/dist/types/modes/components/tool-execution.d.ts +8 -0
- package/dist/types/modes/components/ttsr-notification.d.ts +5 -1
- package/dist/types/modes/components/welcome.d.ts +3 -9
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
- package/dist/types/modes/index.d.ts +3 -3
- package/dist/types/modes/interactive-mode.d.ts +10 -4
- 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/theme/theme.d.ts +2 -1
- package/dist/types/modes/types.d.ts +5 -2
- package/dist/types/modes/utils/ui-helpers.d.ts +1 -1
- package/dist/types/registry/agent-lifecycle.d.ts +51 -0
- package/dist/types/registry/agent-registry.d.ts +16 -5
- 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 +49 -32
- package/dist/types/session/messages.d.ts +2 -4
- package/dist/types/session/session-history-format.d.ts +12 -0
- package/dist/types/session/session-manager.d.ts +21 -3
- package/dist/types/session/streaming-output.d.ts +46 -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 +12 -2
- package/dist/types/task/index.d.ts +13 -6
- package/dist/types/task/output-manager.d.ts +0 -7
- package/dist/types/task/repair-args.d.ts +8 -7
- package/dist/types/task/types.d.ts +63 -51
- 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/browser/tab-worker.d.ts +3 -1
- package/dist/types/tools/find.d.ts +0 -11
- package/dist/types/tools/grouped-file-output.d.ts +0 -49
- package/dist/types/tools/index.d.ts +7 -3
- package/dist/types/tools/irc.d.ts +76 -38
- package/dist/types/tools/job.d.ts +7 -1
- package/dist/types/utils/git.d.ts +15 -2
- package/dist/types/utils/title-generator.d.ts +3 -2
- package/examples/extensions/with-deps/package.json +1 -0
- package/package.json +11 -10
- package/scripts/bundle-dist.ts +28 -19
- package/src/async/index.ts +0 -1
- package/src/auto-thinking/classifier.ts +1 -0
- package/src/cli/args.ts +3 -0
- package/src/cli/gallery-cli.ts +1 -1
- package/src/cli/gallery-fixtures/agentic.ts +230 -115
- package/src/cli/gallery-fixtures/types.ts +5 -0
- package/src/cli-commands.ts +29 -0
- package/src/cli.ts +28 -15
- package/src/commands/launch.ts +4 -0
- package/src/commit/agentic/tools/analyze-file.ts +38 -19
- package/src/commit/model-selection.ts +3 -2
- package/src/config/api-key-resolver.ts +8 -6
- package/src/config/keybindings.ts +6 -1
- package/src/config/model-registry.ts +97 -30
- package/src/config/model-resolver.ts +60 -0
- package/src/config/settings-schema.ts +99 -55
- package/src/config/settings.ts +68 -3
- package/src/edit/hashline/execute.ts +39 -2
- package/src/edit/hashline/noop-loop-guard.ts +99 -0
- package/src/eval/__tests__/agent-bridge.test.ts +5 -3
- package/src/eval/agent-bridge.ts +3 -16
- package/src/eval/completion-bridge.ts +1 -0
- package/src/eval/js/shared/prelude.txt +1 -1
- 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/prelude.py +5 -6
- package/src/eval/py/runtime.ts +37 -0
- package/src/exec/bash-executor.ts +82 -3
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +38 -13
- package/src/extensibility/custom-tools/types.ts +2 -2
- 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/extensibility/shared-events.ts +2 -2
- package/src/hindsight/bank.ts +17 -2
- package/src/internal-urls/docs-index.generated.ts +11 -11
- package/src/internal-urls/history-protocol.ts +113 -0
- package/src/internal-urls/index.ts +1 -0
- package/src/internal-urls/router.ts +3 -1
- package/src/internal-urls/types.ts +1 -1
- package/src/irc/bus.ts +292 -0
- package/src/main.ts +26 -66
- 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/{session-observer-overlay.ts → agent-hub.ts} +586 -367
- package/src/modes/components/assistant-message.ts +19 -21
- package/src/modes/components/compaction-summary-message.ts +68 -32
- package/src/modes/components/custom-editor.ts +10 -0
- package/src/modes/components/footer.ts +3 -1
- package/src/modes/components/status-line/component.ts +118 -34
- package/src/modes/components/tool-execution.ts +31 -1
- package/src/modes/components/ttsr-notification.ts +72 -30
- package/src/modes/components/welcome.ts +9 -33
- package/src/modes/controllers/command-controller.ts +1 -1
- package/src/modes/controllers/event-controller.ts +65 -0
- package/src/modes/controllers/extension-ui-controller.ts +8 -8
- package/src/modes/controllers/input-controller.ts +19 -2
- package/src/modes/controllers/mcp-command-controller.ts +38 -3
- package/src/modes/controllers/selector-controller.ts +21 -17
- package/src/modes/index.ts +3 -21
- package/src/modes/interactive-mode.ts +47 -22
- 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/theme/theme.ts +18 -5
- package/src/modes/types.ts +5 -5
- package/src/modes/utils/hotkeys-markdown.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +51 -49
- package/src/prompts/system/irc-incoming.md +3 -4
- package/src/prompts/system/orchestrate-notice.md +2 -2
- package/src/prompts/system/subagent-system-prompt.md +0 -5
- package/src/prompts/system/system-prompt.md +1 -0
- package/src/prompts/system/workflow-notice.md +2 -2
- package/src/prompts/tools/eval.md +3 -3
- package/src/prompts/tools/irc.md +29 -19
- package/src/prompts/tools/read.md +2 -2
- package/src/prompts/tools/task-summary.md +5 -16
- package/src/prompts/tools/task.md +38 -29
- package/src/registry/agent-lifecycle.ts +218 -0
- package/src/registry/agent-registry.ts +16 -5
- package/src/sdk.ts +37 -10
- package/src/secrets/index.ts +8 -1
- package/src/secrets/obfuscator.ts +39 -18
- package/src/session/agent-session.ts +422 -291
- package/src/session/messages.ts +11 -78
- package/src/session/session-history-format.ts +246 -0
- package/src/session/session-manager.ts +59 -5
- package/src/session/streaming-output.ts +226 -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 +851 -461
- package/src/task/index.ts +721 -796
- package/src/task/output-manager.ts +0 -11
- package/src/task/render.ts +148 -63
- package/src/task/repair-args.ts +21 -9
- package/src/task/types.ts +82 -66
- 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/ask.ts +4 -2
- package/src/tools/bash.ts +61 -10
- package/src/tools/browser/tab-worker.ts +26 -7
- package/src/tools/browser.ts +28 -1
- package/src/tools/find.ts +2 -27
- package/src/tools/grouped-file-output.ts +1 -118
- package/src/tools/image-gen.ts +11 -4
- package/src/tools/index.ts +17 -13
- package/src/tools/inspect-image.ts +1 -0
- package/src/tools/irc.ts +596 -171
- package/src/tools/job.ts +41 -7
- package/src/tools/read.ts +57 -1
- package/src/tools/renderers.ts +2 -0
- package/src/tools/resolve.ts +4 -1
- package/src/utils/commit-message-generator.ts +1 -0
- package/src/utils/git.ts +267 -13
- package/src/utils/title-generator.ts +24 -5
- package/dist/types/async/support.d.ts +0 -2
- package/dist/types/modes/components/session-observer-overlay.d.ts +0 -11
- package/dist/types/task/simple-mode.d.ts +0 -8
- package/src/async/support.ts +0 -5
- package/src/task/simple-mode.ts +0 -27
package/src/tools/job.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { Component } from "@oh-my-pi/pi-tui";
|
|
|
3
3
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
4
4
|
import { prompt } from "@oh-my-pi/pi-utils";
|
|
5
5
|
import * as z from "zod/v4";
|
|
6
|
-
import {
|
|
6
|
+
import type { AsyncJob, AsyncJobManager } from "../async";
|
|
7
7
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
8
8
|
import type { Theme } from "../modes/theme/theme";
|
|
9
9
|
import jobDescription from "../prompts/tools/job.md" with { type: "text" };
|
|
@@ -65,6 +65,19 @@ export interface JobToolDetails {
|
|
|
65
65
|
cancelled?: { id: string; status: CancelStatus }[];
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
/**
|
|
69
|
+
* A poll snapshot where every watched job is still running and nothing was
|
|
70
|
+
* cancelled — pure "still waiting" noise once a newer poll exists. The TUI
|
|
71
|
+
* keeps such a block un-finalized (displaceable) so a follow-up `job` call
|
|
72
|
+
* replaces it instead of stacking another waiting frame in the transcript.
|
|
73
|
+
*/
|
|
74
|
+
export function isWaitingPollDetails(details: unknown): boolean {
|
|
75
|
+
const d = details as JobToolDetails | undefined;
|
|
76
|
+
if (!d || !Array.isArray(d.jobs) || d.jobs.length === 0) return false;
|
|
77
|
+
if (d.cancelled?.length) return false;
|
|
78
|
+
return d.jobs.every(job => job?.status === "running");
|
|
79
|
+
}
|
|
80
|
+
|
|
68
81
|
export class JobTool implements AgentTool<typeof jobSchema, JobToolDetails> {
|
|
69
82
|
readonly name = "job";
|
|
70
83
|
readonly approval = "read" as const;
|
|
@@ -78,11 +91,6 @@ export class JobTool implements AgentTool<typeof jobSchema, JobToolDetails> {
|
|
|
78
91
|
this.description = prompt.render(jobDescription);
|
|
79
92
|
}
|
|
80
93
|
|
|
81
|
-
static createIf(session: ToolSession): JobTool | null {
|
|
82
|
-
if (!isBackgroundJobSupportEnabled(session.settings)) return null;
|
|
83
|
-
return new JobTool(session);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
94
|
async execute(
|
|
87
95
|
_toolCallId: string,
|
|
88
96
|
params: JobParams,
|
|
@@ -378,6 +386,30 @@ function statusToColor(status: JobSnapshot["status"]): ToolUIColor {
|
|
|
378
386
|
}
|
|
379
387
|
}
|
|
380
388
|
|
|
389
|
+
/**
|
|
390
|
+
* Task job results are delivered in the model-facing `<task-result>` envelope
|
|
391
|
+
* (prompts/tools/task-summary.md) so the parent agent can parse status and the
|
|
392
|
+
* `agent://` pointer. The wrapper markup is noise to a human — preview the
|
|
393
|
+
* inner <output>/<preview> body instead.
|
|
394
|
+
*/
|
|
395
|
+
function stripTaskResultEnvelope(text: string): string {
|
|
396
|
+
if (!text.startsWith("<task-result")) return text;
|
|
397
|
+
const body = /<(output|preview)(?:\s[^>]*)?>\n?([\s\S]*?)\n?<\/\1>/.exec(text)?.[2];
|
|
398
|
+
return body?.trim() || text;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Pretty-printed JSON output wastes the collapsed one-line preview on a lone
|
|
403
|
+
* "{" — flatten structured-looking bodies onto a single line. Slice first:
|
|
404
|
+
* downstream truncation keeps at most a few hundred columns, so collapsing
|
|
405
|
+
* whitespace across a multi-KB body would be pure waste.
|
|
406
|
+
*/
|
|
407
|
+
function flattenStructuredPreview(text: string): string {
|
|
408
|
+
const first = text[0];
|
|
409
|
+
if (first !== "{" && first !== "[") return text;
|
|
410
|
+
return text.slice(0, PREVIEW_LINES_EXPANDED * PREVIEW_LINE_WIDTH * 2).replace(/\s+/g, " ");
|
|
411
|
+
}
|
|
412
|
+
|
|
381
413
|
function describeTarget(args: JobRenderArgs | undefined): string {
|
|
382
414
|
const poll = args?.poll ?? [];
|
|
383
415
|
const cancel = args?.cancel ?? [];
|
|
@@ -494,7 +526,9 @@ export const jobToolRenderer = {
|
|
|
494
526
|
lines.push(` ${uiTheme.fg("toolOutput", visibleLabelLines[i]!)}`);
|
|
495
527
|
}
|
|
496
528
|
|
|
497
|
-
const preview =
|
|
529
|
+
const preview = flattenStructuredPreview(
|
|
530
|
+
stripTaskResultEnvelope(job.errorText?.trim() || job.resultText?.trim() || ""),
|
|
531
|
+
);
|
|
498
532
|
if (preview) {
|
|
499
533
|
const maxLines = expanded ? PREVIEW_LINES_EXPANDED : PREVIEW_LINES_COLLAPSED;
|
|
500
534
|
const previewLines = getPreviewLines(preview, maxLines, PREVIEW_LINE_WIDTH, Ellipsis.Unicode);
|
package/src/tools/read.ts
CHANGED
|
@@ -736,6 +736,17 @@ interface ResolvedSqliteReadPath {
|
|
|
736
736
|
/** Per-execute memo of suffix-glob lookups; `null` records a confirmed miss. */
|
|
737
737
|
type SuffixMatchCache = Map<string, { absolutePath: string; displayPath: string } | null>;
|
|
738
738
|
|
|
739
|
+
/**
|
|
740
|
+
* Repeated whole-file reads of the same path pin stale copies in context.
|
|
741
|
+
* From this per-session read count onward, file reads carry a trailing nudge
|
|
742
|
+
* to prefer narrower re-reads.
|
|
743
|
+
*/
|
|
744
|
+
const REPEAT_READ_NOTICE_THRESHOLD = 3;
|
|
745
|
+
|
|
746
|
+
function formatRepeatReadNotice(count: number): string {
|
|
747
|
+
return `[note: read #${count} of this file this session — after edits, prefer the context echoed in the edit result or a narrow range re-read]`;
|
|
748
|
+
}
|
|
749
|
+
|
|
739
750
|
/**
|
|
740
751
|
* Read tool implementation.
|
|
741
752
|
*
|
|
@@ -754,6 +765,8 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
754
765
|
readonly #autoResizeImages: boolean;
|
|
755
766
|
readonly #defaultLimit: number;
|
|
756
767
|
readonly #inspectImageEnabled: boolean;
|
|
768
|
+
/** Successful file reads per resolved base path (selector stripped) this session. */
|
|
769
|
+
readonly #readCounts = new Map<string, number>();
|
|
757
770
|
|
|
758
771
|
constructor(private readonly session: ToolSession) {
|
|
759
772
|
const displayMode = resolveFileDisplayMode(session);
|
|
@@ -772,6 +785,19 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
772
785
|
});
|
|
773
786
|
}
|
|
774
787
|
|
|
788
|
+
/**
|
|
789
|
+
* Count a file read of `absolutePath` and return the repeat-read nudge once
|
|
790
|
+
* the per-session count reaches {@link REPEAT_READ_NOTICE_THRESHOLD}.
|
|
791
|
+
* Non-file sources (URLs, internal resources, directories, archives,
|
|
792
|
+
* SQLite, images) are never counted.
|
|
793
|
+
*/
|
|
794
|
+
#repeatReadNotice(absolutePath: string): string | undefined {
|
|
795
|
+
const count = (this.#readCounts.get(absolutePath) ?? 0) + 1;
|
|
796
|
+
this.#readCounts.set(absolutePath, count);
|
|
797
|
+
if (count < REPEAT_READ_NOTICE_THRESHOLD) return undefined;
|
|
798
|
+
return formatRepeatReadNotice(count);
|
|
799
|
+
}
|
|
800
|
+
|
|
775
801
|
async #tryReadDelimitedPaths(
|
|
776
802
|
readPath: string,
|
|
777
803
|
signal?: AbortSignal,
|
|
@@ -948,6 +974,8 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
948
974
|
ignoreResultLimits?: boolean;
|
|
949
975
|
raw?: boolean;
|
|
950
976
|
immutable?: boolean;
|
|
977
|
+
/** Trailing repeat-read nudge; appended at the very end of the text. */
|
|
978
|
+
repeatNotice?: string;
|
|
951
979
|
},
|
|
952
980
|
): AgentToolResult<ReadToolDetails> {
|
|
953
981
|
const displayMode = resolveFileDisplayMode(this.session, { raw: options.raw, immutable: options.immutable });
|
|
@@ -1092,6 +1120,9 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1092
1120
|
: formatLineEntries(buildLineEntries(endLine), startLineDisplay);
|
|
1093
1121
|
}
|
|
1094
1122
|
|
|
1123
|
+
if (options.repeatNotice) {
|
|
1124
|
+
outputText += `\n${options.repeatNotice}`;
|
|
1125
|
+
}
|
|
1095
1126
|
resultBuilder.text(outputText);
|
|
1096
1127
|
if (truncationInfo) {
|
|
1097
1128
|
resultBuilder.truncation(truncationInfo.result, truncationInfo.options);
|
|
@@ -1117,6 +1148,8 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1117
1148
|
entityLabel: string;
|
|
1118
1149
|
raw?: boolean;
|
|
1119
1150
|
immutable?: boolean;
|
|
1151
|
+
/** Trailing repeat-read nudge; appended at the very end of the text. */
|
|
1152
|
+
repeatNotice?: string;
|
|
1120
1153
|
},
|
|
1121
1154
|
): AgentToolResult<ReadToolDetails> {
|
|
1122
1155
|
const displayMode = resolveFileDisplayMode(this.session, { raw: options.raw, immutable: options.immutable });
|
|
@@ -1177,8 +1210,11 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1177
1210
|
const bound = range.endLine !== undefined ? `${range.startLine}-${range.endLine}` : `${range.startLine}`;
|
|
1178
1211
|
notices.push(`[Range ${bound} is beyond end of ${options.entityLabel} (${totalLines} lines total); skipped]`);
|
|
1179
1212
|
}
|
|
1180
|
-
|
|
1213
|
+
let finalText =
|
|
1181
1214
|
notices.length > 0 ? (outputText ? `${outputText}\n${notices.join("\n")}` : notices.join("\n")) : outputText;
|
|
1215
|
+
if (options.repeatNotice) {
|
|
1216
|
+
finalText = finalText ? `${finalText}\n${options.repeatNotice}` : options.repeatNotice;
|
|
1217
|
+
}
|
|
1182
1218
|
resultBuilder.text(finalText);
|
|
1183
1219
|
return resultBuilder.done();
|
|
1184
1220
|
}
|
|
@@ -1196,6 +1232,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1196
1232
|
parsed: ParsedSelector,
|
|
1197
1233
|
displayMode: { hashLines: boolean; lineNumbers: boolean },
|
|
1198
1234
|
suffixResolution: { from: string; to: string } | undefined,
|
|
1235
|
+
repeatNotice: string | undefined,
|
|
1199
1236
|
signal: AbortSignal | undefined,
|
|
1200
1237
|
): Promise<{
|
|
1201
1238
|
outputText: string;
|
|
@@ -1215,6 +1252,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1215
1252
|
sourcePath: absolutePath,
|
|
1216
1253
|
entityLabel: "file",
|
|
1217
1254
|
raw: rawSelector,
|
|
1255
|
+
repeatNotice,
|
|
1218
1256
|
});
|
|
1219
1257
|
if (suffixResolution) {
|
|
1220
1258
|
const notice = `[Path '${suffixResolution.from}' not found; resolved to '${suffixResolution.to}' via suffix match]`;
|
|
@@ -1896,6 +1934,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1896
1934
|
let details: ReadToolDetails = {};
|
|
1897
1935
|
let sourcePath: string | undefined;
|
|
1898
1936
|
let columnTruncated = 0;
|
|
1937
|
+
let repeatNotice: string | undefined;
|
|
1899
1938
|
let truncationInfo:
|
|
1900
1939
|
| { result: TruncationResult; options: { direction: "head"; startLine?: number; totalFileLines?: number } }
|
|
1901
1940
|
| undefined;
|
|
@@ -1960,11 +1999,13 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1960
1999
|
}
|
|
1961
2000
|
} else if (isNotebookPath(absolutePath) && !isRawSelector(parsed)) {
|
|
1962
2001
|
const notebookText = await readEditableNotebookText(absolutePath, localReadPath);
|
|
2002
|
+
repeatNotice = this.#repeatReadNotice(absolutePath);
|
|
1963
2003
|
if (isMultiRange(parsed) && parsed.kind === "lines") {
|
|
1964
2004
|
return this.#buildInMemoryMultiRangeResult(notebookText, parsed.ranges, {
|
|
1965
2005
|
details: { resolvedPath: absolutePath },
|
|
1966
2006
|
sourcePath: absolutePath,
|
|
1967
2007
|
entityLabel: "notebook",
|
|
2008
|
+
repeatNotice,
|
|
1968
2009
|
});
|
|
1969
2010
|
}
|
|
1970
2011
|
const { offset, limit } = selToOffsetLimit(parsed);
|
|
@@ -1972,11 +2013,13 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1972
2013
|
details: { resolvedPath: absolutePath },
|
|
1973
2014
|
sourcePath: absolutePath,
|
|
1974
2015
|
entityLabel: "notebook",
|
|
2016
|
+
repeatNotice,
|
|
1975
2017
|
});
|
|
1976
2018
|
} else if (shouldConvertWithMarkit) {
|
|
1977
2019
|
// Convert document via markit.
|
|
1978
2020
|
const result = await convertFileWithMarkit(absolutePath, signal);
|
|
1979
2021
|
if (result.ok) {
|
|
2022
|
+
repeatNotice = this.#repeatReadNotice(absolutePath);
|
|
1980
2023
|
// Route the converted markdown through the in-memory text builder
|
|
1981
2024
|
// so line-range selectors (`file.pdf:50-100`, `:5-16,40-80`) and
|
|
1982
2025
|
// raw mode apply against the converted output. Without this,
|
|
@@ -1987,6 +2030,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1987
2030
|
details: { resolvedPath: absolutePath },
|
|
1988
2031
|
sourcePath: absolutePath,
|
|
1989
2032
|
entityLabel: "document",
|
|
2033
|
+
repeatNotice,
|
|
1990
2034
|
});
|
|
1991
2035
|
}
|
|
1992
2036
|
const { offset, limit } = selToOffsetLimit(parsed);
|
|
@@ -1995,6 +2039,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1995
2039
|
sourcePath: absolutePath,
|
|
1996
2040
|
entityLabel: "document",
|
|
1997
2041
|
raw: isRawSelector(parsed),
|
|
2042
|
+
repeatNotice,
|
|
1998
2043
|
});
|
|
1999
2044
|
} else if (result.error) {
|
|
2000
2045
|
content = [{ type: "text", text: `[Cannot read ${ext} file: ${result.error || "conversion failed"}]` }];
|
|
@@ -2002,6 +2047,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2002
2047
|
content = [{ type: "text", text: `[Cannot read ${ext} file: conversion failed]` }];
|
|
2003
2048
|
}
|
|
2004
2049
|
} else {
|
|
2050
|
+
repeatNotice = this.#repeatReadNotice(absolutePath);
|
|
2005
2051
|
if (
|
|
2006
2052
|
parsed.kind === "none" &&
|
|
2007
2053
|
this.session.settings.get("read.summarize.enabled") &&
|
|
@@ -2043,6 +2089,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2043
2089
|
parsed,
|
|
2044
2090
|
displayMode,
|
|
2045
2091
|
suffixResolution,
|
|
2092
|
+
repeatNotice,
|
|
2046
2093
|
undefined, // plain-file read: deterministic and fast, never abort mid-read
|
|
2047
2094
|
);
|
|
2048
2095
|
if (multiResult.bridgeResult) return multiResult.bridgeResult;
|
|
@@ -2066,6 +2113,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2066
2113
|
sourcePath: absolutePath,
|
|
2067
2114
|
entityLabel: "file",
|
|
2068
2115
|
raw: isRawSelector(parsed),
|
|
2116
|
+
repeatNotice,
|
|
2069
2117
|
});
|
|
2070
2118
|
if (suffixResolution) {
|
|
2071
2119
|
const notice = `[Path '${suffixResolution.from}' not found; resolved to '${suffixResolution.to}' via suffix match]`;
|
|
@@ -2367,6 +2415,14 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2367
2415
|
content = [{ type: "text", text: notice }, ...content];
|
|
2368
2416
|
}
|
|
2369
2417
|
}
|
|
2418
|
+
if (repeatNotice) {
|
|
2419
|
+
// Trailing nudge goes at the very end of the textual result so it never
|
|
2420
|
+
// disturbs hashline tag headers or inline notices.
|
|
2421
|
+
const lastText = content.findLast((c): c is TextContent => c.type === "text");
|
|
2422
|
+
if (lastText) {
|
|
2423
|
+
lastText.text = `${lastText.text}\n${repeatNotice}`;
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2370
2426
|
const resultBuilder = toolResult(details).content(content);
|
|
2371
2427
|
if (sourcePath) {
|
|
2372
2428
|
resultBuilder.sourcePath(sourcePath);
|
package/src/tools/renderers.ts
CHANGED
|
@@ -21,6 +21,7 @@ import { evalToolRenderer } from "./eval-render";
|
|
|
21
21
|
import { findToolRenderer } from "./find";
|
|
22
22
|
import { githubToolRenderer } from "./gh-renderer";
|
|
23
23
|
import { inspectImageToolRenderer } from "./inspect-image-renderer";
|
|
24
|
+
import { ircToolRenderer } from "./irc";
|
|
24
25
|
import { jobToolRenderer } from "./job";
|
|
25
26
|
import { recallToolRenderer, reflectToolRenderer, retainToolRenderer } from "./memory-render";
|
|
26
27
|
import { readToolRenderer } from "./read";
|
|
@@ -58,6 +59,7 @@ export const toolRenderers: Record<string, ToolRenderer> = {
|
|
|
58
59
|
search: searchToolRenderer as ToolRenderer,
|
|
59
60
|
lsp: lspToolRenderer as ToolRenderer,
|
|
60
61
|
inspect_image: inspectImageToolRenderer as ToolRenderer,
|
|
62
|
+
irc: ircToolRenderer as ToolRenderer,
|
|
61
63
|
read: readToolRenderer as ToolRenderer,
|
|
62
64
|
job: jobToolRenderer as ToolRenderer,
|
|
63
65
|
resolve: resolveToolRenderer as ToolRenderer,
|
package/src/tools/resolve.ts
CHANGED
|
@@ -241,7 +241,10 @@ export const resolveToolRenderer = {
|
|
|
241
241
|
const isApply = action === "apply" && !result.isError;
|
|
242
242
|
const isFailedApply = action === "apply" && result.isError;
|
|
243
243
|
const bgColor = result.isError ? "error" : isApply ? "success" : "warning";
|
|
244
|
-
|
|
244
|
+
// Bare symbol: the line is wrapped in inverse(fg(...)), so any embedded fg
|
|
245
|
+
// reset (styledSymbol/status glyphs carry their own \x1b[39m) would drop the
|
|
246
|
+
// inverse block back to the default background mid-line.
|
|
247
|
+
const icon = uiTheme.symbol(isApply ? "tool.resolve" : "status.error");
|
|
245
248
|
const verb = isApply ? "Accept" : isFailedApply ? "Failed" : "Discard";
|
|
246
249
|
const separator = ": ";
|
|
247
250
|
const separatorIndex = label.indexOf(separator);
|
|
@@ -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
|
// ════════════════════════════════════════════════════════════════════════════
|