@aaroncql/pim-agent 0.0.1 → 0.2.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 +94 -66
- package/bin/pim.ts +55 -3
- package/package.json +20 -5
- package/src/extensions/_init/index.ts +3 -2
- package/src/extensions/apply-patch/coordinator.ts +49 -0
- package/src/extensions/apply-patch/executor.ts +566 -0
- package/src/extensions/apply-patch/index.ts +74 -0
- package/src/extensions/apply-patch/matcher.ts +66 -0
- package/src/extensions/apply-patch/model.ts +34 -0
- package/src/extensions/apply-patch/parser.ts +381 -0
- package/src/extensions/apply-patch/render.ts +261 -0
- package/src/extensions/apply-patch/schema.ts +43 -0
- package/src/extensions/apply-patch/types.ts +30 -0
- package/src/extensions/bash/index.ts +3 -3
- package/src/extensions/edit/index.ts +2 -1
- package/src/extensions/glob/index.ts +3 -1
- package/src/extensions/glob/schema.ts +2 -1
- package/src/extensions/grep/index.ts +3 -1
- package/src/extensions/grep/render.ts +18 -4
- package/src/extensions/grep/schema.ts +1 -1
- package/src/extensions/read/index.ts +36 -9
- package/src/extensions/read/render.ts +31 -3
- package/src/extensions/subagent/index.ts +4 -1
- package/src/extensions/todo/index.ts +4 -3
- package/src/extensions/web-search/index.ts +2 -1
- package/src/extensions/write/index.ts +2 -1
- package/src/shared/PatchSummary.ts +82 -0
- package/src/telegram/Renderer.ts +190 -4
- 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
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type Static, Type } from "typebox";
|
|
2
|
+
|
|
3
|
+
export const applyPatchSchema = Type.Object({
|
|
4
|
+
input: Type.String({
|
|
5
|
+
description: "Patch text wrapped in *** Begin Patch / *** End Patch.",
|
|
6
|
+
}),
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
export type ApplyPatchInput = Static<typeof applyPatchSchema>;
|
|
10
|
+
|
|
11
|
+
const ALIAS_KEYS = ["patch", "patchText", "patch_text"] as const;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Forgive the JSON-key choice, trust the grammar. Accepts `{input}` (canonical,
|
|
15
|
+
* handled above), `{patch}`, `{patchText}`/`{patch_text}`, or a bare string,
|
|
16
|
+
* normalizing to `{input}` and stripping the alias key so the unknown-key
|
|
17
|
+
* rejection in `Tools.wrap` passes. Validation of the actual envelope happens
|
|
18
|
+
* in the parser.
|
|
19
|
+
*/
|
|
20
|
+
export function prepareApplyPatchArguments(rawArgs: unknown): ApplyPatchInput {
|
|
21
|
+
if (typeof rawArgs === "string") {
|
|
22
|
+
return { input: rawArgs };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (rawArgs === null || typeof rawArgs !== "object") {
|
|
26
|
+
return rawArgs as ApplyPatchInput;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const record = rawArgs as Record<string, unknown>;
|
|
30
|
+
if (typeof record.input === "string") {
|
|
31
|
+
return rawArgs as ApplyPatchInput;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
for (const key of ALIAS_KEYS) {
|
|
35
|
+
const value = record[key];
|
|
36
|
+
if (typeof value === "string") {
|
|
37
|
+
const { [key]: _dropped, ...rest } = record;
|
|
38
|
+
return { ...rest, input: value } as ApplyPatchInput;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return rawArgs as ApplyPatchInput;
|
|
43
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export type UpdateChunk = {
|
|
2
|
+
readonly changeContext: string | undefined;
|
|
3
|
+
readonly oldLines: readonly string[];
|
|
4
|
+
readonly newLines: readonly string[];
|
|
5
|
+
readonly isEndOfFile: boolean;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export type AddHunk = {
|
|
9
|
+
readonly kind: "add";
|
|
10
|
+
readonly path: string;
|
|
11
|
+
readonly contents: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type DeleteHunk = {
|
|
15
|
+
readonly kind: "delete";
|
|
16
|
+
readonly path: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type UpdateHunk = {
|
|
20
|
+
readonly kind: "update";
|
|
21
|
+
readonly path: string;
|
|
22
|
+
readonly movePath: string | undefined;
|
|
23
|
+
readonly chunks: readonly UpdateChunk[];
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type Hunk = AddHunk | DeleteHunk | UpdateHunk;
|
|
27
|
+
|
|
28
|
+
export type Patch = {
|
|
29
|
+
readonly hunks: readonly Hunk[];
|
|
30
|
+
};
|
|
@@ -37,9 +37,9 @@ export default function (pi: ExtensionAPI): void {
|
|
|
37
37
|
name: "bash",
|
|
38
38
|
label: "bash",
|
|
39
39
|
description:
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
"Execute a bash command in the cwd. " +
|
|
41
|
+
"Returns exit code, signal (if any), and stdout/stderr captured separately. " +
|
|
42
|
+
"Prefer commands that emit only what you need; keep output as small as possible.",
|
|
43
43
|
parameters: bashSchema,
|
|
44
44
|
renderShell: "self",
|
|
45
45
|
executionMode: "sequential",
|
|
@@ -12,7 +12,8 @@ export default function (pi: ExtensionAPI): void {
|
|
|
12
12
|
name: "edit",
|
|
13
13
|
label: "edit",
|
|
14
14
|
description:
|
|
15
|
-
"Replace strings in a UTF-8 text file.
|
|
15
|
+
"Replace strings in a UTF-8 text file. " +
|
|
16
|
+
"Prefer edit over write for changes to existing files.",
|
|
16
17
|
parameters: editSchema,
|
|
17
18
|
renderShell: "self",
|
|
18
19
|
executionMode: "sequential",
|
|
@@ -52,7 +52,9 @@ export default function (pi: ExtensionAPI): void {
|
|
|
52
52
|
name: "glob",
|
|
53
53
|
label: "glob",
|
|
54
54
|
description:
|
|
55
|
-
"Find files by glob pattern under a directory, sorted newest first.
|
|
55
|
+
"Find files by glob pattern under a directory, sorted newest first. " +
|
|
56
|
+
"Skips gitignored paths and dotfiles unless requested. " +
|
|
57
|
+
"Use glob to enumerate files instead of bash with find, fd, ls -R, or similar.",
|
|
56
58
|
parameters: globSchema,
|
|
57
59
|
renderShell: "self",
|
|
58
60
|
executionMode: "parallel",
|
|
@@ -7,7 +7,8 @@ export const GLOB_PATH_FORMATS = ["relative", "absolute"] as const;
|
|
|
7
7
|
|
|
8
8
|
export const globSchema = Type.Object({
|
|
9
9
|
pattern: Type.String({
|
|
10
|
-
description:
|
|
10
|
+
description:
|
|
11
|
+
"Glob pattern relative to path (eg. **/*.ts). Brace expansion spans sibling dirs (eg. {src,docs}/**/*.ts).",
|
|
11
12
|
}),
|
|
12
13
|
path: Type.Optional(
|
|
13
14
|
Type.String({
|
|
@@ -55,7 +55,9 @@ export default function (pi: ExtensionAPI): void {
|
|
|
55
55
|
name: "grep",
|
|
56
56
|
label: "grep",
|
|
57
57
|
description:
|
|
58
|
-
"Search UTF-8 text files with a JavaScript regex.
|
|
58
|
+
"Search UTF-8 text files with a JavaScript regex. " +
|
|
59
|
+
"Directory scans skip binary files, gitignored paths, and dotfiles unless requested; direct file paths are always searched. " +
|
|
60
|
+
"Use grep to search file contents instead of bash with grep, rg, ag, find -exec, or similar.",
|
|
59
61
|
parameters: grepSchema,
|
|
60
62
|
renderShell: "self",
|
|
61
63
|
executionMode: "parallel",
|
|
@@ -84,17 +84,31 @@ export type TitleOptions = {
|
|
|
84
84
|
|
|
85
85
|
export function formatTitle(options: TitleOptions): string {
|
|
86
86
|
const pattern = formatPattern(options.pattern);
|
|
87
|
-
const
|
|
87
|
+
const resolved =
|
|
88
88
|
options.path === undefined
|
|
89
89
|
? undefined
|
|
90
90
|
: Paths.resolve(options.path, options.cwd);
|
|
91
|
-
const
|
|
92
|
-
|
|
91
|
+
const dir =
|
|
92
|
+
resolved === undefined || resolved === options.cwd
|
|
93
|
+
? undefined
|
|
94
|
+
: Paths.displayRelative(resolved, options.cwd);
|
|
95
|
+
const target = joinTarget(dir, options.glob);
|
|
96
|
+
const location = target ? ` in ${target}` : "";
|
|
93
97
|
const suffix =
|
|
94
98
|
options.fileCount === undefined
|
|
95
99
|
? ""
|
|
96
100
|
: ` (${options.fileCount} ${options.fileCount === 1 ? "file" : "files"})`;
|
|
97
|
-
return `${pattern}
|
|
101
|
+
return `${pattern}${location}${suffix}`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function joinTarget(
|
|
105
|
+
dir: string | undefined,
|
|
106
|
+
glob: string | undefined
|
|
107
|
+
): string | undefined {
|
|
108
|
+
if (glob === undefined) {
|
|
109
|
+
return dir;
|
|
110
|
+
}
|
|
111
|
+
return dir === undefined ? glob : `${dir}/${glob}`;
|
|
98
112
|
}
|
|
99
113
|
|
|
100
114
|
function formatPattern(pattern: string | undefined): string {
|
|
@@ -25,7 +25,7 @@ export const grepSchema = Type.Object({
|
|
|
25
25
|
glob: Type.Optional(
|
|
26
26
|
Type.String({
|
|
27
27
|
description:
|
|
28
|
-
"Relative glob filter under path when path is a directory. Gitignored files and dotfiles are skipped during directory scans unless includeIgnored/includeDotfiles is true.",
|
|
28
|
+
"Relative glob filter under path when path is a directory. Brace expansion spans sibling dirs (eg. {src,docs}/**/*.ts). Gitignored files and dotfiles are skipped during directory scans unless includeIgnored/includeDotfiles is true.",
|
|
29
29
|
})
|
|
30
30
|
),
|
|
31
31
|
exclude: Type.Optional(
|
|
@@ -3,18 +3,23 @@ import { Paths } from "../../shared/Paths";
|
|
|
3
3
|
import { Renderer } from "../../shared/Renderer";
|
|
4
4
|
import { Tools } from "../../shared/Tools";
|
|
5
5
|
import { buildReadRange, readFile } from "./read";
|
|
6
|
-
import {
|
|
6
|
+
import { renderTitlePath, type ReadTitleOutcome } from "./render";
|
|
7
7
|
import { type ReadInput, readSchema } from "./schema";
|
|
8
8
|
|
|
9
9
|
const PREVIEW_LINES = 10;
|
|
10
10
|
|
|
11
|
+
type ReadRenderState = {
|
|
12
|
+
outcome?: ReadTitleOutcome;
|
|
13
|
+
};
|
|
14
|
+
|
|
11
15
|
export default function (pi: ExtensionAPI): void {
|
|
12
16
|
Tools.register(pi, {
|
|
13
17
|
name: "read",
|
|
14
18
|
label: "read",
|
|
15
19
|
description:
|
|
16
|
-
"Read a local UTF-8 text file.
|
|
17
|
-
|
|
20
|
+
"Read a local UTF-8 text file. " +
|
|
21
|
+
"Output is `LINE:CONTENT` with no space after the colon. " +
|
|
22
|
+
"Capped at 32KB per call; lines longer than 2000 chars are truncated.",
|
|
18
23
|
parameters: readSchema,
|
|
19
24
|
renderShell: "self",
|
|
20
25
|
executionMode: "parallel",
|
|
@@ -58,12 +63,17 @@ export default function (pi: ExtensionAPI): void {
|
|
|
58
63
|
},
|
|
59
64
|
renderCall(args, theme, context) {
|
|
60
65
|
const input = (args ?? {}) as Partial<ReadInput>;
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
66
|
+
const state = context.state as ReadRenderState;
|
|
67
|
+
const title = renderTitlePath(
|
|
68
|
+
{
|
|
69
|
+
path: input.path,
|
|
70
|
+
cwd: context.cwd,
|
|
71
|
+
start: input.start,
|
|
72
|
+
end: input.end,
|
|
73
|
+
outcome: state.outcome,
|
|
74
|
+
},
|
|
75
|
+
theme
|
|
76
|
+
);
|
|
67
77
|
return Renderer.renderToolCallTitle({
|
|
68
78
|
label: "Read",
|
|
69
79
|
title,
|
|
@@ -72,6 +82,23 @@ export default function (pi: ExtensionAPI): void {
|
|
|
72
82
|
});
|
|
73
83
|
},
|
|
74
84
|
renderResult(result, options, theme, context) {
|
|
85
|
+
const state = context.state as ReadRenderState;
|
|
86
|
+
|
|
87
|
+
if (!options.isPartial && state.outcome === undefined) {
|
|
88
|
+
const details = result.details as ReadTitleOutcome | undefined;
|
|
89
|
+
|
|
90
|
+
if (
|
|
91
|
+
typeof details?.visibleStart === "number" &&
|
|
92
|
+
typeof details.visibleEnd === "number"
|
|
93
|
+
) {
|
|
94
|
+
state.outcome = {
|
|
95
|
+
visibleStart: details.visibleStart,
|
|
96
|
+
visibleEnd: details.visibleEnd,
|
|
97
|
+
};
|
|
98
|
+
context.invalidate();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
75
102
|
return Renderer.renderBorderedResult({
|
|
76
103
|
result,
|
|
77
104
|
options,
|
|
@@ -1,24 +1,52 @@
|
|
|
1
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
1
2
|
import { Paths } from "../../shared/Paths";
|
|
2
3
|
|
|
4
|
+
export type ReadTitleOutcome = {
|
|
5
|
+
readonly visibleStart: number;
|
|
6
|
+
readonly visibleEnd: number;
|
|
7
|
+
};
|
|
8
|
+
|
|
3
9
|
export type TitlePathOptions = {
|
|
4
10
|
readonly path: string | undefined;
|
|
5
11
|
readonly cwd: string;
|
|
6
12
|
readonly start: number | undefined;
|
|
7
13
|
readonly end: number | undefined;
|
|
14
|
+
readonly outcome?: ReadTitleOutcome;
|
|
8
15
|
};
|
|
9
16
|
|
|
10
17
|
export function formatTitlePath(options: TitlePathOptions): string {
|
|
18
|
+
const { path, range } = formatTitlePathParts(options);
|
|
19
|
+
return `${path}${range}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function renderTitlePath(
|
|
23
|
+
options: TitlePathOptions,
|
|
24
|
+
theme: Theme
|
|
25
|
+
): string {
|
|
26
|
+
const { path, range } = formatTitlePathParts(options);
|
|
27
|
+
return `${path}${range === "" ? "" : theme.fg("muted", range)}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function formatTitlePathParts(options: TitlePathOptions): {
|
|
31
|
+
readonly path: string;
|
|
32
|
+
readonly range: string;
|
|
33
|
+
} {
|
|
11
34
|
const path = options.path
|
|
12
35
|
? Paths.displayRelative(options.path, options.cwd)
|
|
13
36
|
: "...";
|
|
14
|
-
const range = formatRange(options.start, options.end);
|
|
15
|
-
return
|
|
37
|
+
const range = formatRange(options.start, options.end, options.outcome);
|
|
38
|
+
return { path, range };
|
|
16
39
|
}
|
|
17
40
|
|
|
18
41
|
function formatRange(
|
|
19
42
|
start: number | undefined,
|
|
20
|
-
end: number | undefined
|
|
43
|
+
end: number | undefined,
|
|
44
|
+
outcome: ReadTitleOutcome | undefined
|
|
21
45
|
): string {
|
|
46
|
+
if (outcome !== undefined) {
|
|
47
|
+
return `:${outcome.visibleStart}-${outcome.visibleEnd}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
22
50
|
if (start === undefined && end === undefined) {
|
|
23
51
|
return "";
|
|
24
52
|
}
|
|
@@ -9,7 +9,10 @@ export default function (pi: ExtensionAPI): void {
|
|
|
9
9
|
name: "subagent",
|
|
10
10
|
label: "subagent",
|
|
11
11
|
description:
|
|
12
|
-
"
|
|
12
|
+
"Run a task in an isolated subagent with a fresh context. " +
|
|
13
|
+
"The subagent inherits the currently active tools, except subagent itself. " +
|
|
14
|
+
"Multiple subagent calls in one turn run in parallel. " +
|
|
15
|
+
"Subagent output returned to the main agent is capped at 32KB.",
|
|
13
16
|
parameters: subagentSchema,
|
|
14
17
|
renderShell: "self",
|
|
15
18
|
executionMode: "parallel",
|
|
@@ -61,9 +61,10 @@ export default function (pi: ExtensionAPI): void {
|
|
|
61
61
|
name: "todo",
|
|
62
62
|
label: "todo",
|
|
63
63
|
description:
|
|
64
|
-
"
|
|
65
|
-
"Each call replaces the entire list; include every item in priority order. " +
|
|
66
|
-
"
|
|
64
|
+
"Update the session task list. " +
|
|
65
|
+
"Each call replaces the entire list; include every item that should remain, in priority order. " +
|
|
66
|
+
"Status values are pending, in_progress, completed, and cancelled. " +
|
|
67
|
+
"At most one item may be in_progress.",
|
|
67
68
|
parameters: todoSchema,
|
|
68
69
|
renderShell: "self",
|
|
69
70
|
executionMode: "sequential",
|
|
@@ -49,7 +49,8 @@ export default function (pi: ExtensionAPI): void {
|
|
|
49
49
|
name: "web_search",
|
|
50
50
|
label: "web_search",
|
|
51
51
|
description:
|
|
52
|
-
"Search the web.
|
|
52
|
+
"Search the web. " +
|
|
53
|
+
"Returns ranked results with title, URL, and a short snippet.",
|
|
53
54
|
parameters: webSearchSchema,
|
|
54
55
|
renderShell: "self",
|
|
55
56
|
executionMode: "parallel",
|
|
@@ -12,7 +12,8 @@ export default function (pi: ExtensionAPI): void {
|
|
|
12
12
|
name: "write",
|
|
13
13
|
label: "write",
|
|
14
14
|
description:
|
|
15
|
-
"Create or overwrite UTF-8 text files.
|
|
15
|
+
"Create or overwrite UTF-8 text files. " +
|
|
16
|
+
"Use write only for new files or full rewrites.",
|
|
16
17
|
parameters: writeSchema,
|
|
17
18
|
renderShell: "self",
|
|
18
19
|
executionMode: "sequential",
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
export type PatchOp = {
|
|
2
|
+
readonly kind: "add" | "delete" | "update";
|
|
3
|
+
readonly path: string;
|
|
4
|
+
readonly movePath?: string;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
const MOVE_TO_MARKER = "*** Move to:";
|
|
8
|
+
const FILE_MARKERS: ReadonlyArray<readonly [PatchOp["kind"], string]> = [
|
|
9
|
+
["add", "*** Add File: "],
|
|
10
|
+
["delete", "*** Delete File: "],
|
|
11
|
+
["update", "*** Update File: "],
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
type MutableOp = {
|
|
15
|
+
readonly kind: PatchOp["kind"];
|
|
16
|
+
readonly path: string;
|
|
17
|
+
movePath?: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Lightweight, fault-tolerant scan of V4A patch text into a per-file summary.
|
|
22
|
+
* For renderers that need to label a patch without depending on the apply-patch
|
|
23
|
+
* grammar parser; unrecognized lines are ignored rather than throwing.
|
|
24
|
+
*/
|
|
25
|
+
export class PatchSummary {
|
|
26
|
+
public static fromText(input: string): readonly PatchOp[] {
|
|
27
|
+
const ops: MutableOp[] = [];
|
|
28
|
+
|
|
29
|
+
for (const raw of input.split("\n")) {
|
|
30
|
+
const line = raw.trim();
|
|
31
|
+
const op = PatchSummary.fileOp(line);
|
|
32
|
+
if (op) {
|
|
33
|
+
ops.push(op);
|
|
34
|
+
} else if (line.startsWith(MOVE_TO_MARKER)) {
|
|
35
|
+
const current = ops.at(-1);
|
|
36
|
+
if (current?.kind === "update") {
|
|
37
|
+
current.movePath = PatchSummary.clean(
|
|
38
|
+
line.slice(MOVE_TO_MARKER.length)
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return ops;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// The first affected path, without building the full per-file summary — for
|
|
48
|
+
// callers that only need a title (e.g. a tool-call header on the render path).
|
|
49
|
+
public static firstPath(input: string): string | undefined {
|
|
50
|
+
for (const raw of input.split("\n")) {
|
|
51
|
+
const op = PatchSummary.fileOp(raw.trim());
|
|
52
|
+
if (op) {
|
|
53
|
+
return op.path;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private static fileOp(line: string): MutableOp | undefined {
|
|
60
|
+
for (const [kind, marker] of FILE_MARKERS) {
|
|
61
|
+
if (line.startsWith(marker)) {
|
|
62
|
+
return { kind, path: PatchSummary.clean(line.slice(marker.length)) };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private static clean(raw: string): string {
|
|
69
|
+
let path = raw.trim();
|
|
70
|
+
if (path.startsWith("@")) {
|
|
71
|
+
path = path.slice(1).trim();
|
|
72
|
+
}
|
|
73
|
+
if (path.length >= 2) {
|
|
74
|
+
const first = path[0]!;
|
|
75
|
+
const last = path.at(-1)!;
|
|
76
|
+
if ((first === '"' || first === "'" || first === "`") && first === last) {
|
|
77
|
+
path = path.slice(1, -1);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return path;
|
|
81
|
+
}
|
|
82
|
+
}
|