@aaroncql/pim-agent 0.0.1 → 0.1.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/README.md +19 -8
- package/bin/pim.ts +55 -3
- package/package.json +20 -5
- package/src/extensions/_init/index.ts +3 -2
- package/src/extensions/bash/capture.test.ts +0 -126
- package/src/extensions/bash/format.test.ts +0 -240
- package/src/extensions/bash/run.test.ts +0 -262
- package/src/extensions/command-picker/ranker.test.ts +0 -46
- package/src/extensions/edit/edit.test.ts +0 -285
- package/src/extensions/file-picker/catalog.test.ts +0 -263
- package/src/extensions/file-picker/index.test.ts +0 -168
- package/src/extensions/file-picker/ranker.test.ts +0 -94
- package/src/extensions/footer/git.test.ts +0 -76
- package/src/extensions/footer/index.test.ts +0 -161
- package/src/extensions/footer/segments.test.ts +0 -164
- package/src/extensions/glob/glob.test.ts +0 -171
- package/src/extensions/glob/index.test.ts +0 -68
- package/src/extensions/glob/render.test.ts +0 -126
- package/src/extensions/grep/grep.test.ts +0 -387
- package/src/extensions/grep/index.test.ts +0 -68
- package/src/extensions/grep/render.test.ts +0 -269
- package/src/extensions/read/read.test.ts +0 -177
- package/src/extensions/read/render.test.ts +0 -61
- package/src/extensions/subagent/index.test.ts +0 -44
- package/src/extensions/subagent/render.test.ts +0 -292
- package/src/extensions/subagent/subagent.test.ts +0 -315
- package/src/extensions/system-prompt/prompt.test.ts +0 -64
- package/src/extensions/todo/index.test.ts +0 -244
- package/src/extensions/todo/render.test.ts +0 -180
- package/src/extensions/todo/todo.test.ts +0 -222
- package/src/extensions/tps/index.test.ts +0 -254
- package/src/extensions/web-fetch/WebViewMarkdownSnapshot.test.ts +0 -119
- package/src/extensions/web-fetch/fetch.test.ts +0 -244
- package/src/extensions/web-fetch/render.test.ts +0 -56
- package/src/extensions/web-search/ExaMcpClient.test.ts +0 -143
- package/src/extensions/web-search/render.test.ts +0 -21
- package/src/extensions/web-search/search.test.ts +0 -53
- package/src/extensions/working-indicator/index.test.ts +0 -21
- package/src/extensions/write/render.test.ts +0 -64
- package/src/extensions/write/write.test.ts +0 -108
- package/src/shared/DiffLines.test.ts +0 -193
- package/src/shared/DiffRenderer.test.ts +0 -206
- package/src/shared/EditMatcher.test.ts +0 -123
- package/src/shared/FileScanner.test.ts +0 -158
- package/src/shared/FuzzyMatcher.test.ts +0 -114
- package/src/shared/GitignoreFilter.test.ts +0 -64
- package/src/shared/Lines.test.ts +0 -25
- package/src/shared/McpClient.test.ts +0 -235
- package/src/shared/OutputBudget.test.ts +0 -99
- package/src/shared/Paths.test.ts +0 -51
- package/src/shared/PimSettings.test.ts +0 -90
- package/src/shared/Renderer.test.ts +0 -190
- package/src/shared/SpillCache.test.ts +0 -94
- package/src/shared/Tools.test.ts +0 -392
- package/src/telegram/Config.test.ts +0 -275
- package/src/telegram/Markdown.test.ts +0 -143
- package/src/telegram/Renderer.test.ts +0 -216
- package/src/telegram/SessionRegistry.test.ts +0 -89
- package/src/telegram/TaskScheduler.test.ts +0 -278
- package/src/telegram/TaskTool.test.ts +0 -179
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
-
import { createFooterWidget, getTotalCost } from "./index";
|
|
4
|
-
import type { GitState } from "./git";
|
|
5
|
-
|
|
6
|
-
function deferred<T>(): {
|
|
7
|
-
readonly promise: Promise<T>;
|
|
8
|
-
readonly resolve: (value: T) => void;
|
|
9
|
-
} {
|
|
10
|
-
let resolve!: (value: T) => void;
|
|
11
|
-
const promise = new Promise<T>((r) => {
|
|
12
|
-
resolve = r;
|
|
13
|
-
});
|
|
14
|
-
return { promise, resolve };
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
async function flushPromises(): Promise<void> {
|
|
18
|
-
await Promise.resolve();
|
|
19
|
-
await Promise.resolve();
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function assistant(cost: number): unknown {
|
|
23
|
-
return {
|
|
24
|
-
type: "message",
|
|
25
|
-
message: {
|
|
26
|
-
role: "assistant",
|
|
27
|
-
usage: {
|
|
28
|
-
cost: {
|
|
29
|
-
total: cost,
|
|
30
|
-
},
|
|
31
|
-
},
|
|
32
|
-
},
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
describe("getTotalCost", () => {
|
|
37
|
-
test("sums assistant costs across all session entries", () => {
|
|
38
|
-
const ctx = {
|
|
39
|
-
sessionManager: {
|
|
40
|
-
getEntries: () => [
|
|
41
|
-
assistant(1.25),
|
|
42
|
-
{
|
|
43
|
-
type: "message",
|
|
44
|
-
message: {
|
|
45
|
-
role: "user",
|
|
46
|
-
},
|
|
47
|
-
},
|
|
48
|
-
assistant(2.5),
|
|
49
|
-
],
|
|
50
|
-
},
|
|
51
|
-
} as unknown as ExtensionContext;
|
|
52
|
-
|
|
53
|
-
expect(getTotalCost(ctx)).toBe(3.75);
|
|
54
|
-
});
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
describe("createFooterWidget", () => {
|
|
58
|
-
test("coalesces git refreshes while one is in flight", async () => {
|
|
59
|
-
const first = deferred<GitState>();
|
|
60
|
-
const second = deferred<GitState>();
|
|
61
|
-
const fetches: Promise<GitState>[] = [];
|
|
62
|
-
let branchHandler: () => void = () => {};
|
|
63
|
-
let gitWatchHandler: () => void = () => {};
|
|
64
|
-
let branchUnsubscribed = false;
|
|
65
|
-
let gitWatchDisposed = false;
|
|
66
|
-
let renderRequests = 0;
|
|
67
|
-
|
|
68
|
-
const ctx = {
|
|
69
|
-
cwd: "/repo",
|
|
70
|
-
sessionManager: {
|
|
71
|
-
getEntries: () => [],
|
|
72
|
-
},
|
|
73
|
-
} as unknown as ExtensionContext;
|
|
74
|
-
|
|
75
|
-
const widget = createFooterWidget(
|
|
76
|
-
ctx,
|
|
77
|
-
{
|
|
78
|
-
requestRender: () => {
|
|
79
|
-
renderRequests++;
|
|
80
|
-
},
|
|
81
|
-
},
|
|
82
|
-
{
|
|
83
|
-
onBranchChange: (handler) => {
|
|
84
|
-
branchHandler = handler;
|
|
85
|
-
return () => {
|
|
86
|
-
branchUnsubscribed = true;
|
|
87
|
-
};
|
|
88
|
-
},
|
|
89
|
-
},
|
|
90
|
-
{
|
|
91
|
-
fetchGitStatus: () => {
|
|
92
|
-
const promise = fetches.length === 0 ? first.promise : second.promise;
|
|
93
|
-
fetches.push(promise);
|
|
94
|
-
return promise;
|
|
95
|
-
},
|
|
96
|
-
watchGitDir: (_cwd, handler) => {
|
|
97
|
-
gitWatchHandler = handler;
|
|
98
|
-
return () => {
|
|
99
|
-
gitWatchDisposed = true;
|
|
100
|
-
};
|
|
101
|
-
},
|
|
102
|
-
renderFooterLine: (_width, _ctx, gitState) => gitState.branch ?? "none",
|
|
103
|
-
getTotalCost: () => 0,
|
|
104
|
-
}
|
|
105
|
-
);
|
|
106
|
-
|
|
107
|
-
expect(fetches).toHaveLength(1);
|
|
108
|
-
|
|
109
|
-
branchHandler();
|
|
110
|
-
gitWatchHandler();
|
|
111
|
-
expect(fetches).toHaveLength(1);
|
|
112
|
-
|
|
113
|
-
first.resolve({ branch: "main", dirty: false, ahead: 0, behind: 0 });
|
|
114
|
-
await flushPromises();
|
|
115
|
-
expect(fetches).toHaveLength(2);
|
|
116
|
-
|
|
117
|
-
second.resolve({ branch: "next", dirty: true, ahead: 1, behind: 0 });
|
|
118
|
-
await flushPromises();
|
|
119
|
-
expect(fetches).toHaveLength(2);
|
|
120
|
-
expect(renderRequests).toBe(2);
|
|
121
|
-
expect(widget.render(80)).toEqual(["next"]);
|
|
122
|
-
|
|
123
|
-
widget.dispose();
|
|
124
|
-
expect(branchUnsubscribed).toBe(true);
|
|
125
|
-
expect(gitWatchDisposed).toBe(true);
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
test("requests render only when git state changes", async () => {
|
|
129
|
-
const refresh = deferred<GitState>();
|
|
130
|
-
let renderRequests = 0;
|
|
131
|
-
|
|
132
|
-
const widget = createFooterWidget(
|
|
133
|
-
{
|
|
134
|
-
cwd: "/repo",
|
|
135
|
-
sessionManager: {
|
|
136
|
-
getEntries: () => [],
|
|
137
|
-
},
|
|
138
|
-
} as unknown as ExtensionContext,
|
|
139
|
-
{
|
|
140
|
-
requestRender: () => {
|
|
141
|
-
renderRequests++;
|
|
142
|
-
},
|
|
143
|
-
},
|
|
144
|
-
{
|
|
145
|
-
onBranchChange: () => () => {},
|
|
146
|
-
},
|
|
147
|
-
{
|
|
148
|
-
fetchGitStatus: () => refresh.promise,
|
|
149
|
-
watchGitDir: () => () => {},
|
|
150
|
-
renderFooterLine: () => "",
|
|
151
|
-
getTotalCost: () => 0,
|
|
152
|
-
}
|
|
153
|
-
);
|
|
154
|
-
|
|
155
|
-
refresh.resolve({ branch: null, dirty: false, ahead: 0, behind: 0 });
|
|
156
|
-
await flushPromises();
|
|
157
|
-
|
|
158
|
-
expect(renderRequests).toBe(0);
|
|
159
|
-
widget.dispose();
|
|
160
|
-
});
|
|
161
|
-
});
|
|
@@ -1,164 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
-
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
4
|
-
import { renderFooterLine } from "./segments";
|
|
5
|
-
|
|
6
|
-
function stripAnsi(s: string): string {
|
|
7
|
-
let out = "";
|
|
8
|
-
for (let i = 0; i < s.length; i++) {
|
|
9
|
-
if (s.charCodeAt(i) === 27 && s[i + 1] === "[") {
|
|
10
|
-
i += 2;
|
|
11
|
-
while (i < s.length && s[i] !== "m") {
|
|
12
|
-
i++;
|
|
13
|
-
}
|
|
14
|
-
} else {
|
|
15
|
-
out += s[i]!;
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
return out;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function createCtx(
|
|
22
|
-
branch: readonly unknown[] = [],
|
|
23
|
-
options: {
|
|
24
|
-
readonly cwd?: string;
|
|
25
|
-
readonly model?: { readonly id: string; readonly reasoning?: boolean };
|
|
26
|
-
} = {}
|
|
27
|
-
): ExtensionContext {
|
|
28
|
-
return {
|
|
29
|
-
sessionManager: {
|
|
30
|
-
getCwd: () => options.cwd ?? "/home/aaroncql/dev/pim-agent",
|
|
31
|
-
getBranch: () => branch,
|
|
32
|
-
},
|
|
33
|
-
getContextUsage: () => ({
|
|
34
|
-
tokens: 200_000,
|
|
35
|
-
contextWindow: 200_000,
|
|
36
|
-
percent: 50,
|
|
37
|
-
}),
|
|
38
|
-
model: options.model ?? {
|
|
39
|
-
id: "gpt-5.5",
|
|
40
|
-
reasoning: true,
|
|
41
|
-
},
|
|
42
|
-
} as unknown as ExtensionContext;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
describe("renderFooterLine", () => {
|
|
46
|
-
test("does not exceed narrow terminal widths", () => {
|
|
47
|
-
const ctx = createCtx();
|
|
48
|
-
const widths = [0, 1, 2, 3, 4, 8, 10, 12, 16, 20, 40];
|
|
49
|
-
|
|
50
|
-
for (const width of widths) {
|
|
51
|
-
const line = renderFooterLine(
|
|
52
|
-
width,
|
|
53
|
-
ctx,
|
|
54
|
-
{
|
|
55
|
-
branch: "feat/some-very-long-branch",
|
|
56
|
-
dirty: true,
|
|
57
|
-
ahead: 12,
|
|
58
|
-
behind: 3,
|
|
59
|
-
},
|
|
60
|
-
12.34
|
|
61
|
-
);
|
|
62
|
-
|
|
63
|
-
expect(visibleWidth(line)).toBeLessThanOrEqual(width);
|
|
64
|
-
}
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
test("drops lower-priority segments as width tightens", () => {
|
|
68
|
-
const ctx = createCtx(
|
|
69
|
-
[{ type: "thinking_level_change", thinkingLevel: "medium" }],
|
|
70
|
-
{ cwd: "/x/proj" }
|
|
71
|
-
);
|
|
72
|
-
const git = {
|
|
73
|
-
branch: "main",
|
|
74
|
-
dirty: true,
|
|
75
|
-
ahead: 2,
|
|
76
|
-
behind: 0,
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
expect(stripAnsi(renderFooterLine(200, ctx, git, 1.23))).toContain(
|
|
80
|
-
"gpt-5.5"
|
|
81
|
-
);
|
|
82
|
-
expect(stripAnsi(renderFooterLine(200, ctx, git, 1.23))).toContain("$1.23");
|
|
83
|
-
expect(stripAnsi(renderFooterLine(200, ctx, git, 1.23))).toContain("main");
|
|
84
|
-
expect(stripAnsi(renderFooterLine(200, ctx, git, 1.23))).toContain(
|
|
85
|
-
"50.0%/200K"
|
|
86
|
-
);
|
|
87
|
-
|
|
88
|
-
const withoutModel = stripAnsi(renderFooterLine(50, ctx, git, 1.23));
|
|
89
|
-
expect(withoutModel).not.toContain("gpt-5.5");
|
|
90
|
-
expect(withoutModel).toContain("$1.23");
|
|
91
|
-
expect(withoutModel).toContain("main");
|
|
92
|
-
expect(withoutModel).toContain("50.0%/200K");
|
|
93
|
-
|
|
94
|
-
const withoutCost = stripAnsi(renderFooterLine(40, ctx, git, 1.23));
|
|
95
|
-
expect(withoutCost).not.toContain("$1.23");
|
|
96
|
-
expect(withoutCost).toContain("main");
|
|
97
|
-
expect(withoutCost).toContain("50.0%/200K");
|
|
98
|
-
|
|
99
|
-
const withoutGit = stripAnsi(renderFooterLine(35, ctx, git, 1.23));
|
|
100
|
-
expect(withoutGit).not.toContain("main");
|
|
101
|
-
expect(withoutGit).toContain("/x/proj");
|
|
102
|
-
expect(withoutGit).toContain("50.0%/200K");
|
|
103
|
-
|
|
104
|
-
const cwdOnly = stripAnsi(renderFooterLine(20, ctx, git, 1.23));
|
|
105
|
-
expect(cwdOnly).toContain("/x/proj");
|
|
106
|
-
expect(cwdOnly).not.toContain("50.0%/200K");
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
test("renders latest reasoning level for reasoning models", () => {
|
|
110
|
-
const medium = stripAnsi(
|
|
111
|
-
renderFooterLine(
|
|
112
|
-
120,
|
|
113
|
-
createCtx([{ type: "thinking_level_change", thinkingLevel: "medium" }]),
|
|
114
|
-
{ branch: null, dirty: false, ahead: 0, behind: 0 },
|
|
115
|
-
0
|
|
116
|
-
)
|
|
117
|
-
);
|
|
118
|
-
expect(medium).toContain("gpt-5.5");
|
|
119
|
-
expect(medium).toContain("med");
|
|
120
|
-
|
|
121
|
-
const latestWins = stripAnsi(
|
|
122
|
-
renderFooterLine(
|
|
123
|
-
120,
|
|
124
|
-
createCtx([
|
|
125
|
-
{ type: "thinking_level_change", thinkingLevel: "minimal" },
|
|
126
|
-
{ type: "thinking_level_change", thinkingLevel: "xhigh" },
|
|
127
|
-
]),
|
|
128
|
-
{ branch: null, dirty: false, ahead: 0, behind: 0 },
|
|
129
|
-
0
|
|
130
|
-
)
|
|
131
|
-
);
|
|
132
|
-
expect(latestWins).toContain("xhigh");
|
|
133
|
-
expect(latestWins).not.toContain("min");
|
|
134
|
-
|
|
135
|
-
const noLevel = stripAnsi(
|
|
136
|
-
renderFooterLine(
|
|
137
|
-
120,
|
|
138
|
-
createCtx(),
|
|
139
|
-
{ branch: null, dirty: false, ahead: 0, behind: 0 },
|
|
140
|
-
0
|
|
141
|
-
)
|
|
142
|
-
);
|
|
143
|
-
expect(noLevel).toContain("off");
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
test("omits reasoning level for non-reasoning models", () => {
|
|
147
|
-
const line = stripAnsi(
|
|
148
|
-
renderFooterLine(
|
|
149
|
-
120,
|
|
150
|
-
createCtx(
|
|
151
|
-
[{ type: "thinking_level_change", thinkingLevel: "medium" }],
|
|
152
|
-
{
|
|
153
|
-
model: { id: "gpt-5.5" },
|
|
154
|
-
}
|
|
155
|
-
),
|
|
156
|
-
{ branch: null, dirty: false, ahead: 0, behind: 0 },
|
|
157
|
-
0
|
|
158
|
-
)
|
|
159
|
-
);
|
|
160
|
-
|
|
161
|
-
expect(line).toContain("gpt-5.5");
|
|
162
|
-
expect(line).not.toContain("med");
|
|
163
|
-
});
|
|
164
|
-
});
|
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
import { mkdir, mkdtemp, rm, utimes, writeFile } from "node:fs/promises";
|
|
2
|
-
import { tmpdir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { afterAll, describe, expect, test } from "bun:test";
|
|
5
|
-
import { findFiles } from "./glob";
|
|
6
|
-
|
|
7
|
-
const tempRoots: string[] = [];
|
|
8
|
-
|
|
9
|
-
const tempRoot = async (): Promise<string> => {
|
|
10
|
-
const root = await mkdtemp(join(tmpdir(), "pim-glob-tool-"));
|
|
11
|
-
tempRoots.push(root);
|
|
12
|
-
return root;
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
afterAll(async () => {
|
|
16
|
-
await Promise.all(
|
|
17
|
-
tempRoots.map((root) => rm(root, { force: true, recursive: true }))
|
|
18
|
-
);
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
const defaultScanOptions = {
|
|
22
|
-
includeDotfiles: false,
|
|
23
|
-
includeIgnored: false,
|
|
24
|
-
} as const;
|
|
25
|
-
|
|
26
|
-
describe("findFiles", () => {
|
|
27
|
-
test("sorts by recency desc with path-asc tiebreak when mtimes are equal", async () => {
|
|
28
|
-
const root = await tempRoot();
|
|
29
|
-
const older = join(root, "older.ts");
|
|
30
|
-
const tieB = join(root, "b.ts");
|
|
31
|
-
const tieA = join(root, "a.ts");
|
|
32
|
-
|
|
33
|
-
await writeFile(older, "", "utf8");
|
|
34
|
-
await writeFile(tieA, "", "utf8");
|
|
35
|
-
await writeFile(tieB, "", "utf8");
|
|
36
|
-
|
|
37
|
-
await utimes(
|
|
38
|
-
older,
|
|
39
|
-
new Date("2024-01-01T00:00:00Z"),
|
|
40
|
-
new Date("2024-01-01T00:00:00Z")
|
|
41
|
-
);
|
|
42
|
-
await utimes(
|
|
43
|
-
tieA,
|
|
44
|
-
new Date("2024-01-02T00:00:00Z"),
|
|
45
|
-
new Date("2024-01-02T00:00:00Z")
|
|
46
|
-
);
|
|
47
|
-
await utimes(
|
|
48
|
-
tieB,
|
|
49
|
-
new Date("2024-01-02T00:00:00Z"),
|
|
50
|
-
new Date("2024-01-02T00:00:00Z")
|
|
51
|
-
);
|
|
52
|
-
|
|
53
|
-
const matches = await findFiles(root, "**/*.ts", defaultScanOptions);
|
|
54
|
-
|
|
55
|
-
expect(matches.map((match) => match.path)).toEqual([tieA, tieB, older]);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
test("respects gitignore, dotfiles, and the always-ignored defaults", async () => {
|
|
59
|
-
const root = await tempRoot();
|
|
60
|
-
const src = join(root, "src");
|
|
61
|
-
const ignored = join(src, "ignored.ts");
|
|
62
|
-
const kept = join(src, "kept.ts");
|
|
63
|
-
const nodeModules = join(root, "node_modules", "pkg", "x.ts");
|
|
64
|
-
const dot = join(root, ".secret", "x.ts");
|
|
65
|
-
|
|
66
|
-
await mkdir(src, { recursive: true });
|
|
67
|
-
await mkdir(join(root, "node_modules", "pkg"), { recursive: true });
|
|
68
|
-
await mkdir(join(root, ".secret"), { recursive: true });
|
|
69
|
-
await writeFile(join(root, ".gitignore"), "ignored.ts\n", "utf8");
|
|
70
|
-
await writeFile(ignored, "", "utf8");
|
|
71
|
-
await writeFile(kept, "", "utf8");
|
|
72
|
-
await writeFile(nodeModules, "", "utf8");
|
|
73
|
-
await writeFile(dot, "", "utf8");
|
|
74
|
-
|
|
75
|
-
const matches = await findFiles(root, "**/*.ts", defaultScanOptions);
|
|
76
|
-
|
|
77
|
-
expect(matches.map((match) => match.path)).toEqual([kept]);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
test("can include dotfiles and ignored paths", async () => {
|
|
81
|
-
const root = await tempRoot();
|
|
82
|
-
const kept = join(root, "kept.ts");
|
|
83
|
-
const ignored = join(root, "ignored.ts");
|
|
84
|
-
const dot = join(root, ".secret", "x.ts");
|
|
85
|
-
|
|
86
|
-
await mkdir(join(root, ".secret"), { recursive: true });
|
|
87
|
-
await writeFile(join(root, ".gitignore"), "ignored.ts\n", "utf8");
|
|
88
|
-
await writeFile(kept, "", "utf8");
|
|
89
|
-
await writeFile(ignored, "", "utf8");
|
|
90
|
-
await writeFile(dot, "", "utf8");
|
|
91
|
-
|
|
92
|
-
const matches = await findFiles(root, "**/*.ts", {
|
|
93
|
-
includeDotfiles: true,
|
|
94
|
-
includeIgnored: true,
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
expect(matches.map((match) => match.path).sort()).toEqual(
|
|
98
|
-
[dot, ignored, kept].sort()
|
|
99
|
-
);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
test("filters by glob pattern extension", async () => {
|
|
103
|
-
const root = await tempRoot();
|
|
104
|
-
const ts = join(root, "a.ts");
|
|
105
|
-
const md = join(root, "a.md");
|
|
106
|
-
|
|
107
|
-
await writeFile(ts, "", "utf8");
|
|
108
|
-
await writeFile(md, "", "utf8");
|
|
109
|
-
|
|
110
|
-
const matches = await findFiles(root, "**/*.ts", defaultScanOptions);
|
|
111
|
-
|
|
112
|
-
expect(matches.map((match) => match.path)).toEqual([ts]);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
test("excludes a single glob pattern", async () => {
|
|
116
|
-
const root = await tempRoot();
|
|
117
|
-
const source = join(root, "src", "app.ts");
|
|
118
|
-
const test = join(root, "src", "app.test.ts");
|
|
119
|
-
|
|
120
|
-
await mkdir(join(root, "src"), { recursive: true });
|
|
121
|
-
await writeFile(source, "", "utf8");
|
|
122
|
-
await writeFile(test, "", "utf8");
|
|
123
|
-
|
|
124
|
-
const matches = await findFiles(root, "**/*.ts", {
|
|
125
|
-
...defaultScanOptions,
|
|
126
|
-
exclude: ["**/*.test.ts"],
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
expect(matches.map((match) => match.path)).toEqual([source]);
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
test("excludes multiple glob patterns", async () => {
|
|
133
|
-
const root = await tempRoot();
|
|
134
|
-
const source = join(root, "src", "app.ts");
|
|
135
|
-
const test = join(root, "src", "app.test.ts");
|
|
136
|
-
const generated = join(root, "src", "generated", "types.ts");
|
|
137
|
-
|
|
138
|
-
await mkdir(join(root, "src", "generated"), { recursive: true });
|
|
139
|
-
await writeFile(source, "", "utf8");
|
|
140
|
-
await writeFile(test, "", "utf8");
|
|
141
|
-
await writeFile(generated, "", "utf8");
|
|
142
|
-
|
|
143
|
-
const matches = await findFiles(root, "**/*.ts", {
|
|
144
|
-
...defaultScanOptions,
|
|
145
|
-
exclude: ["**/*.test.ts", "src/generated/**"],
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
expect(matches.map((match) => match.path)).toEqual([source]);
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
test("throws an actionable error when the path does not exist", async () => {
|
|
152
|
-
const root = await tempRoot();
|
|
153
|
-
const missing = join(root, "nope");
|
|
154
|
-
|
|
155
|
-
await expect(
|
|
156
|
-
findFiles(missing, "**/*", defaultScanOptions)
|
|
157
|
-
).rejects.toThrow(
|
|
158
|
-
`Path not found: ${missing}. Use glob to locate the file or directory, or verify the path.`
|
|
159
|
-
);
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
test("throws an actionable error when path is a file, not a directory", async () => {
|
|
163
|
-
const root = await tempRoot();
|
|
164
|
-
const file = join(root, "notes.txt");
|
|
165
|
-
await writeFile(file, "hello", "utf8");
|
|
166
|
-
|
|
167
|
-
await expect(findFiles(file, "**/*", defaultScanOptions)).rejects.toThrow(
|
|
168
|
-
`Glob path must be a directory: ${file}. Drop "path" and put the filename in "pattern", or use the read tool to inspect a single file.`
|
|
169
|
-
);
|
|
170
|
-
});
|
|
171
|
-
});
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import type {
|
|
3
|
-
AgentToolResult,
|
|
4
|
-
ExtensionAPI,
|
|
5
|
-
Theme,
|
|
6
|
-
ToolDefinition,
|
|
7
|
-
} from "@earendil-works/pi-coding-agent";
|
|
8
|
-
import registerGlob from "./index";
|
|
9
|
-
|
|
10
|
-
const stubTheme = {
|
|
11
|
-
bold: (text: string) => text,
|
|
12
|
-
fg: (_color: string, text: string) => text,
|
|
13
|
-
} as unknown as Theme;
|
|
14
|
-
|
|
15
|
-
function registeredTool(): ToolDefinition {
|
|
16
|
-
let tool: ToolDefinition | undefined;
|
|
17
|
-
registerGlob({
|
|
18
|
-
registerTool(def: ToolDefinition): void {
|
|
19
|
-
tool = def;
|
|
20
|
-
},
|
|
21
|
-
} as unknown as ExtensionAPI);
|
|
22
|
-
|
|
23
|
-
if (tool === undefined) {
|
|
24
|
-
throw new Error("glob tool was not registered");
|
|
25
|
-
}
|
|
26
|
-
return tool;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
describe("glob tool renderer", () => {
|
|
30
|
-
test("updates the visible call title with the file count when the result renders", () => {
|
|
31
|
-
const tool = registeredTool();
|
|
32
|
-
const args = { pattern: "**/*.ts" };
|
|
33
|
-
const state = {};
|
|
34
|
-
const callContext = {
|
|
35
|
-
args,
|
|
36
|
-
toolCallId: "glob-1",
|
|
37
|
-
invalidate: () => {},
|
|
38
|
-
lastComponent: undefined,
|
|
39
|
-
state,
|
|
40
|
-
cwd: "/repo",
|
|
41
|
-
executionStarted: true,
|
|
42
|
-
argsComplete: true,
|
|
43
|
-
isPartial: false,
|
|
44
|
-
expanded: false,
|
|
45
|
-
showImages: true,
|
|
46
|
-
isError: false,
|
|
47
|
-
};
|
|
48
|
-
const callComponent = tool.renderCall!(args, stubTheme, callContext);
|
|
49
|
-
|
|
50
|
-
expect(callComponent.render(120).join("\n")).not.toContain("(2 files)");
|
|
51
|
-
|
|
52
|
-
const result: AgentToolResult<unknown> = {
|
|
53
|
-
content: [{ type: "text", text: "src/a.ts\nsrc/b.ts" }],
|
|
54
|
-
details: { fileCount: 2 },
|
|
55
|
-
};
|
|
56
|
-
tool.renderResult!(
|
|
57
|
-
result,
|
|
58
|
-
{ expanded: false, isPartial: false },
|
|
59
|
-
stubTheme,
|
|
60
|
-
{
|
|
61
|
-
...callContext,
|
|
62
|
-
lastComponent: undefined,
|
|
63
|
-
}
|
|
64
|
-
);
|
|
65
|
-
|
|
66
|
-
expect(callComponent.render(120).join("\n")).toContain("(2 files)");
|
|
67
|
-
});
|
|
68
|
-
});
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import type { GlobMatch } from "./glob";
|
|
3
|
-
import { formatTitle, renderFiles } from "./render";
|
|
4
|
-
|
|
5
|
-
const fixture: readonly GlobMatch[] = [
|
|
6
|
-
{ path: "/repo/newer.ts", mtime: 2_000 },
|
|
7
|
-
{ path: "/repo/older.ts", mtime: 1_000 },
|
|
8
|
-
];
|
|
9
|
-
|
|
10
|
-
const relativeOptions = {
|
|
11
|
-
cwd: "/repo",
|
|
12
|
-
pathFormat: "relative",
|
|
13
|
-
} as const;
|
|
14
|
-
|
|
15
|
-
const absoluteOptions = {
|
|
16
|
-
cwd: "/repo",
|
|
17
|
-
pathFormat: "absolute",
|
|
18
|
-
} as const;
|
|
19
|
-
|
|
20
|
-
describe("renderFiles", () => {
|
|
21
|
-
test("joins paths with newlines, newest first, relative by default", () => {
|
|
22
|
-
const outcome = renderFiles(fixture, 1000, relativeOptions);
|
|
23
|
-
expect(outcome.body).toBe("newer.ts\nolder.ts");
|
|
24
|
-
expect(outcome.totalItems).toBe(2);
|
|
25
|
-
expect(outcome.visibleItems).toBe(2);
|
|
26
|
-
expect(outcome.truncated).toBe(false);
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
test("can render absolute paths", () => {
|
|
30
|
-
const outcome = renderFiles(fixture, 1000, absoluteOptions);
|
|
31
|
-
expect(outcome.body).toBe("/repo/newer.ts\n/repo/older.ts");
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
test("flips truncated when results exceed headLimit", () => {
|
|
35
|
-
const outcome = renderFiles(fixture, 1, relativeOptions);
|
|
36
|
-
expect(outcome.body).toBe("newer.ts");
|
|
37
|
-
expect(outcome.truncated).toBe(true);
|
|
38
|
-
expect(outcome.visibleItems).toBe(1);
|
|
39
|
-
expect(outcome.totalItems).toBe(2);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
test("returns a no-match outcome when there are no results", () => {
|
|
43
|
-
const outcome = renderFiles([], 1000, relativeOptions);
|
|
44
|
-
expect(outcome.body).toBe("No matches.");
|
|
45
|
-
expect(outcome.truncated).toBe(false);
|
|
46
|
-
expect(outcome.totalItems).toBe(0);
|
|
47
|
-
expect(outcome.visibleItems).toBe(0);
|
|
48
|
-
});
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
describe("formatTitle", () => {
|
|
52
|
-
test("uses relative path under cwd", () => {
|
|
53
|
-
const title = formatTitle({
|
|
54
|
-
pattern: "**/*.ts",
|
|
55
|
-
path: "/repo/src",
|
|
56
|
-
cwd: "/repo",
|
|
57
|
-
});
|
|
58
|
-
expect(title).toBe("**/*.ts in src");
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
test("omits location when path is undefined", () => {
|
|
62
|
-
const title = formatTitle({
|
|
63
|
-
pattern: "**/*.ts",
|
|
64
|
-
path: undefined,
|
|
65
|
-
cwd: "/repo",
|
|
66
|
-
});
|
|
67
|
-
expect(title).toBe("**/*.ts");
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
test("omits location when path resolves to cwd", () => {
|
|
71
|
-
const title = formatTitle({
|
|
72
|
-
pattern: "**/*.ts",
|
|
73
|
-
path: ".",
|
|
74
|
-
cwd: "/repo",
|
|
75
|
-
});
|
|
76
|
-
expect(title).toBe("**/*.ts");
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
test("omits location when path is the absolute cwd", () => {
|
|
80
|
-
const title = formatTitle({
|
|
81
|
-
pattern: "**/*.ts",
|
|
82
|
-
path: "/repo",
|
|
83
|
-
cwd: "/repo",
|
|
84
|
-
});
|
|
85
|
-
expect(title).toBe("**/*.ts");
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
test("uses '...' placeholder when pattern is undefined", () => {
|
|
89
|
-
const title = formatTitle({
|
|
90
|
-
pattern: undefined,
|
|
91
|
-
path: undefined,
|
|
92
|
-
cwd: "/repo",
|
|
93
|
-
});
|
|
94
|
-
expect(title).toBe("...");
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
test("appends pluralized file count when provided", () => {
|
|
98
|
-
const title = formatTitle({
|
|
99
|
-
pattern: "**/*.ts",
|
|
100
|
-
path: "/repo/src",
|
|
101
|
-
cwd: "/repo",
|
|
102
|
-
fileCount: 3,
|
|
103
|
-
});
|
|
104
|
-
expect(title).toBe("**/*.ts in src (3 files)");
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
test("uses singular noun for a single file", () => {
|
|
108
|
-
const title = formatTitle({
|
|
109
|
-
pattern: "**/*.ts",
|
|
110
|
-
path: undefined,
|
|
111
|
-
cwd: "/repo",
|
|
112
|
-
fileCount: 1,
|
|
113
|
-
});
|
|
114
|
-
expect(title).toBe("**/*.ts (1 file)");
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
test("shows zero count without omitting the suffix", () => {
|
|
118
|
-
const title = formatTitle({
|
|
119
|
-
pattern: "**/*.ts",
|
|
120
|
-
path: undefined,
|
|
121
|
-
cwd: "/repo",
|
|
122
|
-
fileCount: 0,
|
|
123
|
-
});
|
|
124
|
-
expect(title).toBe("**/*.ts (0 files)");
|
|
125
|
-
});
|
|
126
|
-
});
|