@oh-my-pi/pi-coding-agent 14.7.7 → 14.8.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 +19 -0
- package/README.md +1 -0
- package/package.json +7 -7
- package/src/commit/agentic/index.ts +2 -1
- package/src/config/model-registry.ts +18 -10
- package/src/edit/file-read-cache.ts +95 -0
- package/src/edit/index.ts +1 -0
- package/src/edit/modes/hashline.ts +137 -2
- package/src/extensibility/extensions/loader.ts +5 -2
- package/src/extensibility/plugins/legacy-pi-compat.ts +166 -0
- package/src/extensibility/plugins/loader.ts +3 -7
- package/src/internal-urls/docs-index.generated.ts +2 -1
- package/src/modes/components/model-selector.ts +22 -2
- package/src/modes/components/status-line/segments.ts +2 -2
- package/src/modes/components/status-line-segment-editor.ts +2 -2
- package/src/modes/controllers/mcp-command-controller.ts +4 -12
- package/src/prompts/system/subagent-system-prompt.md +3 -1
- package/src/prompts/system/subagent-yield-reminder.md +7 -6
- package/src/prompts/tools/eval.md +1 -1
- package/src/prompts/tools/hashline.md +17 -3
- package/src/sdk.ts +20 -2
- package/src/task/executor.ts +2 -1
- package/src/tools/eval.ts +10 -0
- package/src/tools/fs-cache-invalidation.ts +2 -2
- package/src/tools/index.ts +6 -0
- package/src/tools/read.ts +68 -11
- package/src/tools/search.ts +10 -4
- package/src/utils/git.ts +10 -4
- package/src/workspace-tree.ts +101 -11
package/src/tools/read.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type { Component } from "@oh-my-pi/pi-tui";
|
|
|
8
8
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
9
9
|
import { getRemoteDir, prompt, readImageMetadata, untilAborted } from "@oh-my-pi/pi-utils";
|
|
10
10
|
import { type Static, Type } from "@sinclair/typebox";
|
|
11
|
+
import { getFileReadCache } from "../edit/file-read-cache";
|
|
11
12
|
import { formatHashLine, formatHashLines, formatLineHash, HL_BODY_SEP } from "../edit/line-hash";
|
|
12
13
|
import { isNotebookPath, readEditableNotebookText } from "../edit/notebook";
|
|
13
14
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
@@ -163,6 +164,33 @@ function countTextLines(text: string): number {
|
|
|
163
164
|
}
|
|
164
165
|
const READ_CHUNK_SIZE = 8 * 1024;
|
|
165
166
|
|
|
167
|
+
/**
|
|
168
|
+
* Number of unanchored context lines to include before/after a user-requested
|
|
169
|
+
* range. Anchor-stale failures are heavily concentrated on edits whose anchors
|
|
170
|
+
* land just outside the most recent read window — a few lines of pre-anchored
|
|
171
|
+
* context covers off-by-one anchor selection without much cost.
|
|
172
|
+
*/
|
|
173
|
+
const RANGE_CONTEXT_LINES = 3;
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Expand a [start, end) range with ±RANGE_CONTEXT_LINES context lines on the
|
|
177
|
+
* sides where the user actually constrained the range. A start of 0 (no
|
|
178
|
+
* explicit offset) does not get leading context — that's already an open-ended
|
|
179
|
+
* read from the top.
|
|
180
|
+
*/
|
|
181
|
+
function expandRangeWithContext(
|
|
182
|
+
requestedStart: number,
|
|
183
|
+
requestedEnd: number,
|
|
184
|
+
totalLines: number,
|
|
185
|
+
expandStart: boolean,
|
|
186
|
+
expandEnd: boolean,
|
|
187
|
+
): { startLine: number; endLine: number } {
|
|
188
|
+
return {
|
|
189
|
+
startLine: expandStart ? Math.max(0, requestedStart - RANGE_CONTEXT_LINES) : requestedStart,
|
|
190
|
+
endLine: expandEnd ? Math.min(totalLines, requestedEnd + RANGE_CONTEXT_LINES) : requestedEnd,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
166
194
|
async function streamLinesFromFile(
|
|
167
195
|
filePath: string,
|
|
168
196
|
startLine: number,
|
|
@@ -645,9 +673,26 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
645
673
|
const details = options.details ?? {};
|
|
646
674
|
const allLines = text.split("\n");
|
|
647
675
|
const totalLines = allLines.length;
|
|
648
|
-
|
|
649
|
-
|
|
676
|
+
// User-requested 0-indexed range start. Lines BEFORE this are leading
|
|
677
|
+
// context (added below if offset is explicit).
|
|
678
|
+
const requestedStart = offset ? Math.max(0, offset - 1) : 0;
|
|
650
679
|
const ignoreResultLimits = options.ignoreResultLimits ?? false;
|
|
680
|
+
const requestedEnd =
|
|
681
|
+
limit !== undefined && !ignoreResultLimits
|
|
682
|
+
? Math.min(requestedStart + limit, allLines.length)
|
|
683
|
+
: allLines.length;
|
|
684
|
+
// Expand only on sides the user actually constrained: leading context
|
|
685
|
+
// when offset>1, trailing context when a finite limit was set.
|
|
686
|
+
const expanded = expandRangeWithContext(
|
|
687
|
+
requestedStart,
|
|
688
|
+
requestedEnd,
|
|
689
|
+
allLines.length,
|
|
690
|
+
offset !== undefined && offset > 1,
|
|
691
|
+
limit !== undefined && !ignoreResultLimits,
|
|
692
|
+
);
|
|
693
|
+
const startLine = expanded.startLine;
|
|
694
|
+
const endLineExpanded = expanded.endLine;
|
|
695
|
+
const startLineDisplay = startLine + 1;
|
|
651
696
|
|
|
652
697
|
const resultBuilder = toolResult(details);
|
|
653
698
|
if (options.sourcePath) {
|
|
@@ -660,20 +705,19 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
660
705
|
resultBuilder.sourceInternal(options.sourceInternal);
|
|
661
706
|
}
|
|
662
707
|
|
|
663
|
-
if (
|
|
708
|
+
if (requestedStart >= allLines.length) {
|
|
664
709
|
const suggestion =
|
|
665
710
|
allLines.length === 0
|
|
666
711
|
? `The ${options.entityLabel} is empty.`
|
|
667
712
|
: `Use :1 to read from the start, or :${allLines.length} to read the last line.`;
|
|
668
713
|
return resultBuilder
|
|
669
714
|
.text(
|
|
670
|
-
`Line ${
|
|
715
|
+
`Line ${requestedStart + 1} is beyond end of ${options.entityLabel} (${allLines.length} lines total). ${suggestion}`,
|
|
671
716
|
)
|
|
672
717
|
.done();
|
|
673
718
|
}
|
|
674
719
|
|
|
675
|
-
const endLine =
|
|
676
|
-
limit !== undefined && !ignoreResultLimits ? Math.min(startLine + limit, allLines.length) : allLines.length;
|
|
720
|
+
const endLine = endLineExpanded;
|
|
677
721
|
const selectedContent = allLines.slice(startLine, endLine).join("\n");
|
|
678
722
|
const userLimitedLines = limit !== undefined && !ignoreResultLimits ? endLine - startLine : undefined;
|
|
679
723
|
const truncation = ignoreResultLimits ? noTruncResult(selectedContent) : truncateHead(selectedContent);
|
|
@@ -1318,13 +1362,20 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1318
1362
|
if (!content) {
|
|
1319
1363
|
// Raw text or line-range mode
|
|
1320
1364
|
const { offset, limit } = selToOffsetLimit(parsed);
|
|
1321
|
-
|
|
1365
|
+
// User-requested 0-indexed range start. Lines BEFORE this become
|
|
1366
|
+
// leading context (added below if offset is explicit).
|
|
1367
|
+
const requestedStart = offset ? Math.max(0, offset - 1) : 0;
|
|
1368
|
+
const expandStart = offset !== undefined && offset > 1;
|
|
1369
|
+
const expandEnd = limit !== undefined;
|
|
1370
|
+
const leadingContext = expandStart ? Math.min(requestedStart, RANGE_CONTEXT_LINES) : 0;
|
|
1371
|
+
const trailingContext = expandEnd ? RANGE_CONTEXT_LINES : 0;
|
|
1372
|
+
const startLine = requestedStart - leadingContext;
|
|
1322
1373
|
const startLineDisplay = startLine + 1;
|
|
1323
1374
|
|
|
1324
1375
|
const DEFAULT_LIMIT = this.#defaultLimit;
|
|
1325
1376
|
const effectiveLimit = limit ?? DEFAULT_LIMIT;
|
|
1326
|
-
const maxLinesToCollect = Math.min(effectiveLimit, DEFAULT_MAX_LINES);
|
|
1327
|
-
const selectedLineLimit = effectiveLimit;
|
|
1377
|
+
const maxLinesToCollect = Math.min(effectiveLimit + leadingContext + trailingContext, DEFAULT_MAX_LINES);
|
|
1378
|
+
const selectedLineLimit = effectiveLimit + leadingContext + trailingContext;
|
|
1328
1379
|
// Scale byte budget with line limit so the configured line count actually fits.
|
|
1329
1380
|
// Assume ~512 bytes/line average; never go below the shared default.
|
|
1330
1381
|
const maxBytesForRead = Math.max(DEFAULT_MAX_BYTES, maxLinesToCollect * 512);
|
|
@@ -1348,13 +1399,15 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1348
1399
|
} = streamResult;
|
|
1349
1400
|
|
|
1350
1401
|
// Check if offset is out of bounds - return graceful message instead of throwing
|
|
1351
|
-
if (
|
|
1402
|
+
if (requestedStart >= totalFileLines) {
|
|
1352
1403
|
const suggestion =
|
|
1353
1404
|
totalFileLines === 0
|
|
1354
1405
|
? "The file is empty."
|
|
1355
1406
|
: `Use :1 to read from the start, or :${totalFileLines} to read the last line.`;
|
|
1356
1407
|
return toolResult<ReadToolDetails>({ resolvedPath: absolutePath, suffixResolution })
|
|
1357
|
-
.text(
|
|
1408
|
+
.text(
|
|
1409
|
+
`Line ${requestedStart + 1} is beyond end of file (${totalFileLines} lines total). ${suggestion}`,
|
|
1410
|
+
)
|
|
1358
1411
|
.done();
|
|
1359
1412
|
}
|
|
1360
1413
|
|
|
@@ -1378,6 +1431,10 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1378
1431
|
firstLineExceedsLimit,
|
|
1379
1432
|
};
|
|
1380
1433
|
|
|
1434
|
+
if (collectedLines.length > 0 && !firstLineExceedsLimit) {
|
|
1435
|
+
getFileReadCache(this.session).recordContiguous(absolutePath, startLineDisplay, collectedLines);
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1381
1438
|
const isRawMode = parsed.kind === "raw";
|
|
1382
1439
|
const shouldAddHashLines = !isRawMode && displayMode.hashLines;
|
|
1383
1440
|
const shouldAddLineNumbers = isRawMode ? false : shouldAddHashLines ? false : displayMode.lineNumbers;
|
package/src/tools/search.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type { Component } from "@oh-my-pi/pi-tui";
|
|
|
6
6
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
7
7
|
import { prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
8
8
|
import { type Static, Type } from "@sinclair/typebox";
|
|
9
|
+
import { getFileReadCache } from "../edit/file-read-cache";
|
|
9
10
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
10
11
|
import type { Theme } from "../modes/theme/theme";
|
|
11
12
|
import searchDescription from "../prompts/tools/search.md" with { type: "text" };
|
|
@@ -345,27 +346,32 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
345
346
|
}
|
|
346
347
|
return nextWidth;
|
|
347
348
|
}, 0);
|
|
349
|
+
const cacheEntries: Array<readonly [number, string]> = [];
|
|
348
350
|
for (const match of fileMatches) {
|
|
349
|
-
const pushLine = (lineNumber: number, line: string, isMatch: boolean) => {
|
|
351
|
+
const pushLine = (lineNumber: number, line: string, isMatch: boolean, recordable: boolean) => {
|
|
350
352
|
modelOut.push(formatMatchLine(lineNumber, line, isMatch, { useHashLines }));
|
|
351
353
|
displayOut.push(formatCodeFrameLine(isMatch ? "*" : " ", lineNumber, line, lineNumberWidth));
|
|
354
|
+
if (recordable) cacheEntries.push([lineNumber, line] as const);
|
|
352
355
|
};
|
|
353
356
|
if (match.contextBefore) {
|
|
354
357
|
for (const ctx of match.contextBefore) {
|
|
355
|
-
pushLine(ctx.lineNumber, ctx.line, false);
|
|
358
|
+
pushLine(ctx.lineNumber, ctx.line, false, true);
|
|
356
359
|
}
|
|
357
360
|
}
|
|
358
|
-
pushLine(match.lineNumber, match.line, true);
|
|
361
|
+
pushLine(match.lineNumber, match.line, true, !match.truncated);
|
|
359
362
|
if (match.truncated) {
|
|
360
363
|
linesTruncated = true;
|
|
361
364
|
}
|
|
362
365
|
if (match.contextAfter) {
|
|
363
366
|
for (const ctx of match.contextAfter) {
|
|
364
|
-
pushLine(ctx.lineNumber, ctx.line, false);
|
|
367
|
+
pushLine(ctx.lineNumber, ctx.line, false, true);
|
|
365
368
|
}
|
|
366
369
|
}
|
|
367
370
|
fileMatchCounts.set(relativePath, (fileMatchCounts.get(relativePath) ?? 0) + 1);
|
|
368
371
|
}
|
|
372
|
+
if (cacheEntries.length > 0) {
|
|
373
|
+
getFileReadCache(this.session).recordSparse(path.resolve(searchPath, relativePath), cacheEntries);
|
|
374
|
+
}
|
|
369
375
|
return { model: modelOut, display: displayOut };
|
|
370
376
|
};
|
|
371
377
|
if (isDirectory) {
|
package/src/utils/git.ts
CHANGED
|
@@ -40,6 +40,12 @@ export type HunkSelection = {
|
|
|
40
40
|
hunks: { type: "all" } | { type: "indices"; indices: number[] } | { type: "lines"; start: number; end: number };
|
|
41
41
|
};
|
|
42
42
|
|
|
43
|
+
export interface StageHunksOptions {
|
|
44
|
+
readonly diffCached?: boolean;
|
|
45
|
+
readonly rawDiff?: string;
|
|
46
|
+
readonly signal?: AbortSignal;
|
|
47
|
+
}
|
|
48
|
+
|
|
43
49
|
export interface DiffOptions {
|
|
44
50
|
readonly allowFailure?: boolean;
|
|
45
51
|
readonly base?: string;
|
|
@@ -803,10 +809,10 @@ export const stage = {
|
|
|
803
809
|
await runEffect(cwd, args, { signal });
|
|
804
810
|
},
|
|
805
811
|
|
|
806
|
-
/** Selectively stage hunks from the working tree diff. */
|
|
807
|
-
async hunks(cwd: string, selections: HunkSelection[],
|
|
812
|
+
/** Selectively stage hunks from the provided diff or the current working tree diff. */
|
|
813
|
+
async hunks(cwd: string, selections: HunkSelection[], options: StageHunksOptions = {}): Promise<void> {
|
|
808
814
|
if (selections.length === 0) return;
|
|
809
|
-
const rawDiff = await diff(cwd, { cached:
|
|
815
|
+
const rawDiff = options.rawDiff ?? (await diff(cwd, { cached: options.diffCached, signal: options.signal }));
|
|
810
816
|
const fileDiffs = parseFileDiffs(rawDiff);
|
|
811
817
|
const fileDiffMap = new Map(fileDiffs.map(entry => [entry.filename, entry]));
|
|
812
818
|
const patchParts: string[] = [];
|
|
@@ -833,7 +839,7 @@ export const stage = {
|
|
|
833
839
|
|
|
834
840
|
const patchText = patch.join(patchParts);
|
|
835
841
|
if (!patchText.trim()) return;
|
|
836
|
-
await patch.applyText(cwd, patchText, { cached: true, signal });
|
|
842
|
+
await patch.applyText(cwd, patchText, { cached: true, signal: options.signal });
|
|
837
843
|
},
|
|
838
844
|
|
|
839
845
|
/** Unstage files. Empty array unstages all (`git reset`). */
|
package/src/workspace-tree.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { glob } from "@oh-my-pi/pi-natives";
|
|
4
|
-
import { formatAge, formatBytes } from "@oh-my-pi/pi-utils";
|
|
4
|
+
import { $which, formatAge, formatBytes, logger } from "@oh-my-pi/pi-utils";
|
|
5
5
|
|
|
6
6
|
export interface DirectoryTree {
|
|
7
7
|
rootPath: string;
|
|
@@ -35,6 +35,13 @@ export interface DirectoryTreeOptions {
|
|
|
35
35
|
cache?: boolean;
|
|
36
36
|
/** Rendered label for the root line. */
|
|
37
37
|
rootLabel?: string;
|
|
38
|
+
/**
|
|
39
|
+
* Pre-built map of `parentRelativePath` → child name set used in place of
|
|
40
|
+
* native directory listing. When provided, the tree builder consults this
|
|
41
|
+
* map for child enumeration instead of `glob` / `readdir`. Stat calls per
|
|
42
|
+
* displayed node are still performed for mtime/size/dir-ness.
|
|
43
|
+
*/
|
|
44
|
+
childIndex?: ReadonlyMap<string, ReadonlySet<string>>;
|
|
38
45
|
}
|
|
39
46
|
|
|
40
47
|
const WORKSPACE_TREE_MAX_DEPTH = 3;
|
|
@@ -81,6 +88,7 @@ interface ResolvedDirectoryTreeOptions {
|
|
|
81
88
|
gitignore: boolean;
|
|
82
89
|
cache: boolean;
|
|
83
90
|
rootLabel: string;
|
|
91
|
+
childIndex: ReadonlyMap<string, ReadonlySet<string>> | null;
|
|
84
92
|
}
|
|
85
93
|
|
|
86
94
|
interface RenderLine {
|
|
@@ -122,6 +130,7 @@ function resolveDirectoryTreeOptions(options: DirectoryTreeOptions): ResolvedDir
|
|
|
122
130
|
gitignore: options.gitignore ?? false,
|
|
123
131
|
cache: options.cache ?? true,
|
|
124
132
|
rootLabel: options.rootLabel ?? ".",
|
|
133
|
+
childIndex: options.childIndex ?? null,
|
|
125
134
|
};
|
|
126
135
|
}
|
|
127
136
|
|
|
@@ -157,6 +166,10 @@ async function listDirectChildNames(
|
|
|
157
166
|
parent: DirectoryTreeNode,
|
|
158
167
|
options: ResolvedDirectoryTreeOptions,
|
|
159
168
|
): Promise<string[]> {
|
|
169
|
+
if (options.childIndex) {
|
|
170
|
+
const names = options.childIndex.get(parent.relativePath);
|
|
171
|
+
return names ? Array.from(names) : [];
|
|
172
|
+
}
|
|
160
173
|
if (!options.gitignore) {
|
|
161
174
|
const directoryPath = parent.relativePath ? path.join(rootPath, parent.relativePath) : rootPath;
|
|
162
175
|
return await fs.readdir(directoryPath);
|
|
@@ -377,19 +390,96 @@ export async function buildDirectoryTree(rootPath: string, options: DirectoryTre
|
|
|
377
390
|
};
|
|
378
391
|
}
|
|
379
392
|
|
|
393
|
+
/**
|
|
394
|
+
* Build a `parentRelativePath` → child name index from a flat list of POSIX
|
|
395
|
+
* paths. Intermediate directory components are inferred from path segments;
|
|
396
|
+
* the index covers every ancestor directory implied by the input.
|
|
397
|
+
*/
|
|
398
|
+
function buildChildIndexFromPaths(paths: readonly string[]): Map<string, Set<string>> {
|
|
399
|
+
const index = new Map<string, Set<string>>();
|
|
400
|
+
const ensure = (parent: string): Set<string> => {
|
|
401
|
+
let bucket = index.get(parent);
|
|
402
|
+
if (!bucket) {
|
|
403
|
+
bucket = new Set<string>();
|
|
404
|
+
index.set(parent, bucket);
|
|
405
|
+
}
|
|
406
|
+
return bucket;
|
|
407
|
+
};
|
|
408
|
+
for (const raw of paths) {
|
|
409
|
+
if (!raw) continue;
|
|
410
|
+
const normalized = raw.replace(/\\/g, "/");
|
|
411
|
+
const parts = normalized.split("/").filter(segment => segment.length > 0);
|
|
412
|
+
if (parts.length === 0) continue;
|
|
413
|
+
for (let i = 0; i < parts.length; i += 1) {
|
|
414
|
+
const parent = parts.slice(0, i).join("/");
|
|
415
|
+
const segment = parts[i];
|
|
416
|
+
if (segment !== undefined) ensure(parent).add(segment);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return index;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const GIT_LS_FILES_TIMEOUT_MS = 3000;
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* List tracked + untracked-not-ignored files at `rootPath` via `git ls-files`.
|
|
426
|
+
* Returns `null` when git is unavailable, the directory is not inside a
|
|
427
|
+
* worktree, or the call fails / times out — caller falls back to native
|
|
428
|
+
* directory listing.
|
|
429
|
+
*/
|
|
430
|
+
async function tryListGitFiles(rootPath: string): Promise<string[] | null> {
|
|
431
|
+
const gitPath = $which("git");
|
|
432
|
+
if (!gitPath) return null;
|
|
433
|
+
const signal = AbortSignal.timeout(GIT_LS_FILES_TIMEOUT_MS);
|
|
434
|
+
try {
|
|
435
|
+
const child = Bun.spawn([gitPath, "ls-files", "--cached", "--others", "--exclude-standard", "-z"], {
|
|
436
|
+
cwd: rootPath,
|
|
437
|
+
stdout: "pipe",
|
|
438
|
+
stderr: "pipe",
|
|
439
|
+
stdin: "ignore",
|
|
440
|
+
signal,
|
|
441
|
+
});
|
|
442
|
+
const [stdout, exitCode] = await Promise.all([
|
|
443
|
+
new Response(child.stdout as ReadableStream<Uint8Array>).text(),
|
|
444
|
+
child.exited,
|
|
445
|
+
]);
|
|
446
|
+
if (exitCode !== 0) return null;
|
|
447
|
+
if (!stdout) return [];
|
|
448
|
+
// `-z` separates entries with NUL; trailing NUL after final entry.
|
|
449
|
+
return stdout.split("\0").filter(entry => entry.length > 0);
|
|
450
|
+
} catch (error) {
|
|
451
|
+
logger.debug("git ls-files failed; falling back to native directory listing", {
|
|
452
|
+
rootPath,
|
|
453
|
+
error: error instanceof Error ? error.message : String(error),
|
|
454
|
+
});
|
|
455
|
+
return null;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
380
459
|
export async function buildWorkspaceTree(cwd: string): Promise<WorkspaceTree> {
|
|
381
460
|
const rootPath = path.resolve(cwd);
|
|
461
|
+
const baseOptions = {
|
|
462
|
+
maxDepth: WORKSPACE_TREE_MAX_DEPTH,
|
|
463
|
+
directoryEntryLimit: WORKSPACE_TREE_DIR_LIMIT,
|
|
464
|
+
lineCap: WORKSPACE_TREE_LINE_CAP,
|
|
465
|
+
excludedDirectoryNames: WORKSPACE_TREE_EXCLUDED_DIRS,
|
|
466
|
+
hidden: false,
|
|
467
|
+
cache: true,
|
|
468
|
+
rootLabel: ".",
|
|
469
|
+
} satisfies DirectoryTreeOptions;
|
|
470
|
+
|
|
382
471
|
try {
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
}
|
|
472
|
+
const gitFiles = await tryListGitFiles(rootPath);
|
|
473
|
+
if (gitFiles !== null) {
|
|
474
|
+
// Git already applied gitignore + tracking semantics: bypass native
|
|
475
|
+
// recursive scan and feed the index directly to the tree builder.
|
|
476
|
+
return await buildDirectoryTree(rootPath, {
|
|
477
|
+
...baseOptions,
|
|
478
|
+
gitignore: false,
|
|
479
|
+
childIndex: buildChildIndexFromPaths(gitFiles),
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
return await buildDirectoryTree(rootPath, { ...baseOptions, gitignore: true });
|
|
393
483
|
} catch {
|
|
394
484
|
return emptyWorkspaceTree(rootPath);
|
|
395
485
|
}
|