@qilin-agent/workspace-changes 0.0.0-stage → 3.1.4
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/LICENSE +21 -0
- package/README.i18n.yaml +37 -0
- package/README.md +105 -2
- package/README.zh.md +106 -0
- package/lib/index.js +1155 -0
- package/lib/types/capture.d.ts +47 -0
- package/lib/types/capture.js +107 -0
- package/lib/types/compare.d.ts +19 -0
- package/lib/types/compare.js +56 -0
- package/lib/types/git.d.ts +140 -0
- package/lib/types/git.js +224 -0
- package/lib/types/index.d.ts +34 -0
- package/lib/types/index.js +146 -0
- package/lib/types/numstat.d.ts +19 -0
- package/lib/types/numstat.js +45 -0
- package/lib/types/paths.d.ts +65 -0
- package/lib/types/paths.js +106 -0
- package/lib/types/recorder.d.ts +126 -0
- package/lib/types/recorder.js +370 -0
- package/lib/types/types.d.ts +163 -0
- package/lib/types/types.js +2 -0
- package/package.json +56 -3
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Summarizes the files each top-level turn changed from git working-tree
|
|
3
|
+
* snapshots taken at turn start and turn end, plus whole-file captures taken
|
|
4
|
+
* around each file-tool edit for paths git does not cover, and serves each
|
|
5
|
+
* listed file's before-and-after comparison on demand. Each summary is
|
|
6
|
+
* announced by a `workspace/changes` Session event that carries only the turn
|
|
7
|
+
* number; summaries and comparisons are served through the `workspaceChanges`
|
|
8
|
+
* service until the Session is disposed. Outside a git repository, or without
|
|
9
|
+
* git, the summary lists file-tool edits only.
|
|
10
|
+
*/
|
|
11
|
+
import { homedir, tmpdir } from 'node:os';
|
|
12
|
+
import z from '@qilin-agent/schemastery';
|
|
13
|
+
import { GitRunner } from "./git.js";
|
|
14
|
+
import { TurnRecorder } from "./recorder.js";
|
|
15
|
+
/** Stable Loader identity. */
|
|
16
|
+
export const name = 'workspace-changes';
|
|
17
|
+
/** Services used to run git and observe turns. */
|
|
18
|
+
export const inject = ['subprocess'];
|
|
19
|
+
/** Schemastery validation for {@link Config}. */
|
|
20
|
+
export const Config = z.object({
|
|
21
|
+
timeoutMs: z.number().default(30_000),
|
|
22
|
+
outputMaxBytes: z.number().default(8 * 1024 * 1024),
|
|
23
|
+
maxFiles: z.number().default(500),
|
|
24
|
+
maxFileBytes: z.number().default(2 * 1024 * 1024),
|
|
25
|
+
diffTimeoutMs: z.number().default(100),
|
|
26
|
+
});
|
|
27
|
+
function eligible(session) {
|
|
28
|
+
const { cwd, origin, delegationDepth } = session.header;
|
|
29
|
+
return origin === 'subagent' || (delegationDepth ?? 0) > 0 ? undefined : cwd;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Resolve the git executable once. On macOS the Xcode stub at `/usr/bin/git`
|
|
33
|
+
* opens an installer dialog instead of running, so it counts as absent until
|
|
34
|
+
* developer tools are selected.
|
|
35
|
+
* @param ctx - subprocess capability.
|
|
36
|
+
* @param signal - plugin lifetime.
|
|
37
|
+
* @returns the executable path, or null when git is unavailable.
|
|
38
|
+
*/
|
|
39
|
+
async function resolveGit(ctx, signal) {
|
|
40
|
+
let executable;
|
|
41
|
+
try {
|
|
42
|
+
executable = await ctx.subprocess.resolveExecutable('git', undefined, signal);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
if (process.platform !== 'darwin' || executable !== '/usr/bin/git')
|
|
48
|
+
return executable;
|
|
49
|
+
const probe = ctx.subprocess.spawn({
|
|
50
|
+
argv: ['/usr/bin/xcode-select', '-p'], cwd: homedir(),
|
|
51
|
+
stdio: { stdin: 'ignore', stdout: { maxBytes: 4096 }, stderr: { maxBytes: 4096 } }, graceMs: 1_000, signal,
|
|
52
|
+
});
|
|
53
|
+
const outcome = await probe.done.catch(() => ({ exitCode: null }));
|
|
54
|
+
return outcome.exitCode === 0 ? executable : null;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Observe top-level turns of every Session with a working directory, capture
|
|
58
|
+
* file-tool edits, announce change summaries, and serve them with their
|
|
59
|
+
* comparisons as `workspaceChanges`.
|
|
60
|
+
* @param ctx - host context with `subprocess`.
|
|
61
|
+
* @param config - validated bounds.
|
|
62
|
+
*/
|
|
63
|
+
export function apply(ctx, config) {
|
|
64
|
+
for (const [field, value] of [
|
|
65
|
+
['timeoutMs', config.timeoutMs], ['outputMaxBytes', config.outputMaxBytes], ['maxFiles', config.maxFiles],
|
|
66
|
+
['maxFileBytes', config.maxFileBytes], ['diffTimeoutMs', config.diffTimeoutMs],
|
|
67
|
+
]) {
|
|
68
|
+
if (!Number.isSafeInteger(value) || value < 1)
|
|
69
|
+
throw new Error(`workspace-changes requires a positive integer ${field}`);
|
|
70
|
+
}
|
|
71
|
+
const lifetime = new AbortController();
|
|
72
|
+
const recorders = new Map();
|
|
73
|
+
const byId = new Map();
|
|
74
|
+
const forget = (session) => {
|
|
75
|
+
const recorder = recorders.get(session);
|
|
76
|
+
recorders.delete(session);
|
|
77
|
+
byId.delete(session.id);
|
|
78
|
+
return recorder?.dispose() ?? Promise.resolve();
|
|
79
|
+
};
|
|
80
|
+
ctx.effect(() => async () => {
|
|
81
|
+
lifetime.abort();
|
|
82
|
+
await Promise.all([...recorders.keys()].map(forget));
|
|
83
|
+
});
|
|
84
|
+
const service = {
|
|
85
|
+
summary: (sessionId, seq) => byId.get(sessionId)?.summary(seq),
|
|
86
|
+
session: (sessionId) => {
|
|
87
|
+
const recorder = byId.get(sessionId);
|
|
88
|
+
if (recorder === undefined)
|
|
89
|
+
return undefined;
|
|
90
|
+
const folded = recorder.sessionChanges();
|
|
91
|
+
return folded.files.length === 0 ? undefined : folded;
|
|
92
|
+
},
|
|
93
|
+
diff: (sessionId, seq, index, signal) => byId.get(sessionId)?.diff(seq, index, signal) ?? Promise.resolve(undefined),
|
|
94
|
+
};
|
|
95
|
+
ctx.provide('workspaceChanges', service);
|
|
96
|
+
let runner;
|
|
97
|
+
const gitRunner = () => {
|
|
98
|
+
runner ??= resolveGit(ctx, lifetime.signal).then((executable) => {
|
|
99
|
+
if (executable === null) {
|
|
100
|
+
ctx.logger.info('workspace-changes: git is unavailable; only file-tool edits are summarized');
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
return new GitRunner(ctx.subprocess, executable, { timeoutMs: config.timeoutMs, outputMaxBytes: config.outputMaxBytes });
|
|
104
|
+
});
|
|
105
|
+
return runner;
|
|
106
|
+
};
|
|
107
|
+
const recorderFor = (session, cwd) => {
|
|
108
|
+
let recorder = recorders.get(session);
|
|
109
|
+
if (recorder === undefined) {
|
|
110
|
+
recorder = new TurnRecorder(session, cwd, {
|
|
111
|
+
git: gitRunner(), tempRoot: tmpdir(), maxFiles: config.maxFiles,
|
|
112
|
+
maxFileBytes: config.maxFileBytes, diffTimeoutMs: config.diffTimeoutMs,
|
|
113
|
+
warn: (message) => { ctx.logger.warn(message); },
|
|
114
|
+
});
|
|
115
|
+
recorders.set(session, recorder);
|
|
116
|
+
byId.set(session.id, recorder);
|
|
117
|
+
}
|
|
118
|
+
return recorder;
|
|
119
|
+
};
|
|
120
|
+
ctx.on('session/event', (session, event) => {
|
|
121
|
+
if (event.type === 'turn/start') {
|
|
122
|
+
const cwd = eligible(session);
|
|
123
|
+
if (cwd !== undefined)
|
|
124
|
+
recorderFor(session, cwd).start(event.data.turn);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (event.type === 'tool/result')
|
|
128
|
+
recorders.get(session)?.observe(event);
|
|
129
|
+
else if (event.type === 'turn/end')
|
|
130
|
+
recorders.get(session)?.end(event.data.turn);
|
|
131
|
+
});
|
|
132
|
+
ctx.on('session/disposed', (session) => { void forget(session); });
|
|
133
|
+
ctx.on('agent/turn-stopping', async ({ agent, turn }) => {
|
|
134
|
+
await recorders.get(agent.session)?.stopping(turn);
|
|
135
|
+
});
|
|
136
|
+
ctx.on('tools/pre-execute', async (exec, next) => {
|
|
137
|
+
const session = exec.agent?.session;
|
|
138
|
+
const recorder = session === undefined ? undefined : recorders.get(session);
|
|
139
|
+
if (recorder !== undefined) {
|
|
140
|
+
recorder.capture(exec.name, exec.arguments);
|
|
141
|
+
await recorder.settled();
|
|
142
|
+
}
|
|
143
|
+
return next();
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Parsing of `git diff-tree --numstat -z` output. */
|
|
2
|
+
/** One `--numstat` record; paths are slash-separated and relative to the repository root. */
|
|
3
|
+
export interface NumstatEntry {
|
|
4
|
+
path: string;
|
|
5
|
+
/** The path before a detected rename; absent for a file that kept its path. */
|
|
6
|
+
oldPath?: string;
|
|
7
|
+
added: number;
|
|
8
|
+
deleted: number;
|
|
9
|
+
binary: boolean;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Parse NUL-terminated numstat records. A rename record carries an empty path
|
|
13
|
+
* followed by the old and new paths.
|
|
14
|
+
* @param output - complete stdout of `git diff-tree -r -M -z --numstat`.
|
|
15
|
+
* @returns records in git's output order.
|
|
16
|
+
* @throws when a record is malformed, which indicates truncated output.
|
|
17
|
+
*/
|
|
18
|
+
export declare function parseNumstat(output: string): NumstatEntry[];
|
|
19
|
+
//# sourceMappingURL=numstat.d.ts.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** Parsing of `git diff-tree --numstat -z` output. */
|
|
2
|
+
/**
|
|
3
|
+
* Parse NUL-terminated numstat records. A rename record carries an empty path
|
|
4
|
+
* followed by the old and new paths.
|
|
5
|
+
* @param output - complete stdout of `git diff-tree -r -M -z --numstat`.
|
|
6
|
+
* @returns records in git's output order.
|
|
7
|
+
* @throws when a record is malformed, which indicates truncated output.
|
|
8
|
+
*/
|
|
9
|
+
export function parseNumstat(output) {
|
|
10
|
+
const queue = output.split('\0');
|
|
11
|
+
if (queue.at(-1) !== '')
|
|
12
|
+
throw new Error('numstat output is not NUL-terminated');
|
|
13
|
+
queue.pop();
|
|
14
|
+
const entries = [];
|
|
15
|
+
while (queue.length > 0) {
|
|
16
|
+
const record = queue.shift();
|
|
17
|
+
// Only the first two tabs separate fields; a file name keeps its own tabs.
|
|
18
|
+
const first = record.indexOf('\t');
|
|
19
|
+
const second = first < 0 ? -1 : record.indexOf('\t', first + 1);
|
|
20
|
+
if (second < 0)
|
|
21
|
+
throw new Error(`malformed numstat record: ${record}`);
|
|
22
|
+
const added = record.slice(0, first);
|
|
23
|
+
const deleted = record.slice(first + 1, second);
|
|
24
|
+
const path = record.slice(second + 1);
|
|
25
|
+
let target = path;
|
|
26
|
+
let oldPath;
|
|
27
|
+
if (target === '') {
|
|
28
|
+
oldPath = queue.shift();
|
|
29
|
+
const renamed = queue.shift();
|
|
30
|
+
if (oldPath === undefined || renamed === undefined)
|
|
31
|
+
throw new Error('malformed numstat rename record');
|
|
32
|
+
target = renamed;
|
|
33
|
+
}
|
|
34
|
+
const binary = added === '-';
|
|
35
|
+
entries.push({
|
|
36
|
+
path: target,
|
|
37
|
+
...oldPath === undefined ? {} : { oldPath },
|
|
38
|
+
added: binary ? 0 : Number(added),
|
|
39
|
+
deleted: binary ? 0 : Number(deleted),
|
|
40
|
+
binary,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return entries;
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=numstat.js.map
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slash-separated form of a native relative path.
|
|
3
|
+
* @param path - native path.
|
|
4
|
+
* @returns the same path with `/` separators.
|
|
5
|
+
*/
|
|
6
|
+
export declare function toPosix(path: string): string;
|
|
7
|
+
/**
|
|
8
|
+
* Whether `path` is `root` or lies under it.
|
|
9
|
+
* @param root - absolute directory.
|
|
10
|
+
* @param path - absolute path to test.
|
|
11
|
+
* @returns true for the root itself and every descendant.
|
|
12
|
+
*/
|
|
13
|
+
export declare function isInside(root: string, path: string): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Canonical spellings of the temporary directories a workspace-write sandbox
|
|
16
|
+
* grants: the host `/tmp` and the platform temp area, each also in its
|
|
17
|
+
* symlink-resolved form so `/tmp` and `/private/tmp` match alike.
|
|
18
|
+
* @param candidates - directories to canonicalize.
|
|
19
|
+
* @returns absolute directory paths.
|
|
20
|
+
*/
|
|
21
|
+
export declare function temporaryRoots(candidates?: readonly string[]): Promise<string[]>;
|
|
22
|
+
/**
|
|
23
|
+
* Symlink-resolved path. A path that does not exist yet is resolved through
|
|
24
|
+
* its nearest existing ancestor, so a file created through a directory
|
|
25
|
+
* symlink has the same canonical spelling before and after it exists.
|
|
26
|
+
* @param path - absolute path.
|
|
27
|
+
* @returns the canonical spelling git reports for the path.
|
|
28
|
+
*/
|
|
29
|
+
export declare function canonicalPath(path: string): Promise<string>;
|
|
30
|
+
/**
|
|
31
|
+
* Whether a file lives under a temporary root, where the model keeps scratch work.
|
|
32
|
+
* @param path - absolute file path.
|
|
33
|
+
* @param roots - {@link temporaryRoots}.
|
|
34
|
+
* @returns true for scratch paths that never enter the change summary.
|
|
35
|
+
*/
|
|
36
|
+
export declare function isTemporaryPath(path: string, roots: readonly string[]): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Sort key and label of a changed file; see `WorkspaceChangedFile.display`.
|
|
39
|
+
* @param absolute - canonical absolute file path.
|
|
40
|
+
* @param cwd - canonical Session working directory.
|
|
41
|
+
* @param root - repository top-level directory.
|
|
42
|
+
* @param home - canonical home directory, or empty to skip the `~` form.
|
|
43
|
+
* @returns the slash-separated display path.
|
|
44
|
+
*/
|
|
45
|
+
export declare function displayPathOf(absolute: string, cwd: string, root: string, home: string): string;
|
|
46
|
+
/**
|
|
47
|
+
* The durable `path` field: relative inside the working directory, absolute elsewhere.
|
|
48
|
+
* @param absolute - canonical absolute file path.
|
|
49
|
+
* @param cwd - canonical Session working directory.
|
|
50
|
+
* @returns the path the Web client opens the file through.
|
|
51
|
+
*/
|
|
52
|
+
export declare function durablePathOf(absolute: string, cwd: string): string;
|
|
53
|
+
/**
|
|
54
|
+
* Code-unit order of display paths, which places `../` and absolute paths
|
|
55
|
+
* before letters and matches git's own listing order for relative paths.
|
|
56
|
+
* @param a - first file.
|
|
57
|
+
* @param b - second file.
|
|
58
|
+
* @returns negative, zero, or positive as `Array.prototype.sort` expects.
|
|
59
|
+
*/
|
|
60
|
+
export declare function compareDisplay(a: {
|
|
61
|
+
display: string;
|
|
62
|
+
}, b: {
|
|
63
|
+
display: string;
|
|
64
|
+
}): number;
|
|
65
|
+
//# sourceMappingURL=paths.d.ts.map
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/** Path classification and display forms for changed files. */
|
|
2
|
+
import { realpath } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { basename, dirname, isAbsolute, join, relative, sep } from 'node:path';
|
|
5
|
+
/**
|
|
6
|
+
* Slash-separated form of a native relative path.
|
|
7
|
+
* @param path - native path.
|
|
8
|
+
* @returns the same path with `/` separators.
|
|
9
|
+
*/
|
|
10
|
+
export function toPosix(path) {
|
|
11
|
+
return path.split(sep).join('/');
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Whether `path` is `root` or lies under it.
|
|
15
|
+
* @param root - absolute directory.
|
|
16
|
+
* @param path - absolute path to test.
|
|
17
|
+
* @returns true for the root itself and every descendant.
|
|
18
|
+
*/
|
|
19
|
+
export function isInside(root, path) {
|
|
20
|
+
const rel = relative(root, path);
|
|
21
|
+
return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel));
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Canonical spellings of the temporary directories a workspace-write sandbox
|
|
25
|
+
* grants: the host `/tmp` and the platform temp area, each also in its
|
|
26
|
+
* symlink-resolved form so `/tmp` and `/private/tmp` match alike.
|
|
27
|
+
* @param candidates - directories to canonicalize.
|
|
28
|
+
* @returns absolute directory paths.
|
|
29
|
+
*/
|
|
30
|
+
export async function temporaryRoots(candidates = ['/tmp', tmpdir()]) {
|
|
31
|
+
const roots = new Set();
|
|
32
|
+
for (const root of candidates) {
|
|
33
|
+
roots.add(root);
|
|
34
|
+
roots.add(await canonicalPath(root));
|
|
35
|
+
}
|
|
36
|
+
return [...roots];
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Symlink-resolved path. A path that does not exist yet is resolved through
|
|
40
|
+
* its nearest existing ancestor, so a file created through a directory
|
|
41
|
+
* symlink has the same canonical spelling before and after it exists.
|
|
42
|
+
* @param path - absolute path.
|
|
43
|
+
* @returns the canonical spelling git reports for the path.
|
|
44
|
+
*/
|
|
45
|
+
export async function canonicalPath(path) {
|
|
46
|
+
const missing = [];
|
|
47
|
+
let head = path;
|
|
48
|
+
while (true) {
|
|
49
|
+
try {
|
|
50
|
+
return join(await realpath(head), ...missing);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// A missing or unreadable component is kept lexically under its nearest resolvable ancestor;
|
|
54
|
+
// a path with no existing ancestor but the root keeps its spelling entirely.
|
|
55
|
+
const parent = dirname(head);
|
|
56
|
+
if (parent === head || dirname(parent) === parent)
|
|
57
|
+
return path;
|
|
58
|
+
missing.unshift(basename(head));
|
|
59
|
+
head = parent;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Whether a file lives under a temporary root, where the model keeps scratch work.
|
|
65
|
+
* @param path - absolute file path.
|
|
66
|
+
* @param roots - {@link temporaryRoots}.
|
|
67
|
+
* @returns true for scratch paths that never enter the change summary.
|
|
68
|
+
*/
|
|
69
|
+
export function isTemporaryPath(path, roots) {
|
|
70
|
+
return roots.some(root => isInside(root, path));
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Sort key and label of a changed file; see `WorkspaceChangedFile.display`.
|
|
74
|
+
* @param absolute - canonical absolute file path.
|
|
75
|
+
* @param cwd - canonical Session working directory.
|
|
76
|
+
* @param root - repository top-level directory.
|
|
77
|
+
* @param home - canonical home directory, or empty to skip the `~` form.
|
|
78
|
+
* @returns the slash-separated display path.
|
|
79
|
+
*/
|
|
80
|
+
export function displayPathOf(absolute, cwd, root, home) {
|
|
81
|
+
if (isInside(cwd, absolute) || isInside(root, absolute))
|
|
82
|
+
return toPosix(relative(cwd, absolute));
|
|
83
|
+
if (home !== '' && isInside(home, absolute))
|
|
84
|
+
return `~/${toPosix(relative(home, absolute))}`;
|
|
85
|
+
return toPosix(absolute);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The durable `path` field: relative inside the working directory, absolute elsewhere.
|
|
89
|
+
* @param absolute - canonical absolute file path.
|
|
90
|
+
* @param cwd - canonical Session working directory.
|
|
91
|
+
* @returns the path the Web client opens the file through.
|
|
92
|
+
*/
|
|
93
|
+
export function durablePathOf(absolute, cwd) {
|
|
94
|
+
return isInside(cwd, absolute) ? toPosix(relative(cwd, absolute)) : absolute;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Code-unit order of display paths, which places `../` and absolute paths
|
|
98
|
+
* before letters and matches git's own listing order for relative paths.
|
|
99
|
+
* @param a - first file.
|
|
100
|
+
* @param b - second file.
|
|
101
|
+
* @returns negative, zero, or positive as `Array.prototype.sort` expects.
|
|
102
|
+
*/
|
|
103
|
+
export function compareDisplay(a, b) {
|
|
104
|
+
return a.display < b.display ? -1 : a.display > b.display ? 1 : 0;
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=paths.js.map
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { Session, SessionEvent } from '@qilin-agent/session';
|
|
2
|
+
import { type GitRunner } from './git.ts';
|
|
3
|
+
import type { WorkspaceChangesSummary, WorkspaceFileDiff, WorkspaceSessionChanges } from './types.ts';
|
|
4
|
+
/** Facts shared by every recorder of one plugin instance. */
|
|
5
|
+
export interface RecorderEnvironment {
|
|
6
|
+
/** Resolves to the runner, or null when git is unavailable and no snapshot is taken. */
|
|
7
|
+
git: Promise<GitRunner | null>;
|
|
8
|
+
/** Directory that receives each Session's temporary directory. */
|
|
9
|
+
tempRoot: string;
|
|
10
|
+
/** Maximum files carried by one summary. */
|
|
11
|
+
maxFiles: number;
|
|
12
|
+
/** Inclusive byte cap on a captured copy and on a snapshot blob read for a comparison. */
|
|
13
|
+
maxFileBytes: number;
|
|
14
|
+
/** Milliseconds a line comparison may run before it degrades to whole-file replacement. */
|
|
15
|
+
diffTimeoutMs: number;
|
|
16
|
+
/** Failure reporter; a failed turn records nothing and the next turn retries. */
|
|
17
|
+
warn: (message: string) => void;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Serializes one Session's recording work: the turn-start snapshot, the
|
|
21
|
+
* whole-file capture before each file-tool mutation, the turn-end snapshot
|
|
22
|
+
* with its diff, and the appended `workspace/changes` event whose summary and
|
|
23
|
+
* comparisons this recorder keeps. Snapshot objects and captured copies live in
|
|
24
|
+
* a temporary directory owned by the recorder; disposal removes it together
|
|
25
|
+
* with the summaries. Tool execution waits for pending work so a snapshot or
|
|
26
|
+
* capture never races a mutation. A working directory outside any repository,
|
|
27
|
+
* or a Host without git, gets no snapshot; its summary lists the files the file
|
|
28
|
+
* tools changed.
|
|
29
|
+
*/
|
|
30
|
+
export declare class TurnRecorder {
|
|
31
|
+
private readonly session;
|
|
32
|
+
private readonly cwd;
|
|
33
|
+
private readonly env;
|
|
34
|
+
private chain;
|
|
35
|
+
/** The open turn; before the first `turn/start` it is an empty placeholder no event can match. */
|
|
36
|
+
private state;
|
|
37
|
+
/** Canonical paths, resolved by the first turn. */
|
|
38
|
+
private paths;
|
|
39
|
+
/** The located repository, reused across turns once found; null keeps retrying each turn. */
|
|
40
|
+
private repository;
|
|
41
|
+
/** Temporary directory holding this Session's snapshot objects, scratch indexes, and captured copies. */
|
|
42
|
+
private scratch;
|
|
43
|
+
/** Records by the sequence of the event that announced them. */
|
|
44
|
+
private readonly records;
|
|
45
|
+
private readonly lifetime;
|
|
46
|
+
constructor(session: Session, cwd: string, env: RecorderEnvironment);
|
|
47
|
+
/**
|
|
48
|
+
* Open a turn with fresh per-turn state and queue its baseline snapshot.
|
|
49
|
+
* @param turn - the turn number from `turn/start`.
|
|
50
|
+
*/
|
|
51
|
+
start(turn: number): void;
|
|
52
|
+
/**
|
|
53
|
+
* Queue the capture of the path a file tool is about to mutate, before the
|
|
54
|
+
* tool runs; only the turn's first mutation of a path captures it. Await
|
|
55
|
+
* {@link settled} afterwards so the tool cannot overtake the capture.
|
|
56
|
+
* @param name - wire tool name.
|
|
57
|
+
* @param args - parsed call arguments.
|
|
58
|
+
*/
|
|
59
|
+
capture(name: string, args: unknown): void;
|
|
60
|
+
/**
|
|
61
|
+
* Remember a settled tool result, so a record after `turn/end` covers it.
|
|
62
|
+
* @param event - the appended `tool/result` event.
|
|
63
|
+
*/
|
|
64
|
+
observe(event: SessionEvent<'tool/result'>): void;
|
|
65
|
+
/**
|
|
66
|
+
* Record the turn's changes inside the turn, before `turn/end` commits.
|
|
67
|
+
* @param turn - the stopping turn.
|
|
68
|
+
* @returns after the event is appended or the attempt failed.
|
|
69
|
+
*/
|
|
70
|
+
stopping(turn: number): Promise<void>;
|
|
71
|
+
/**
|
|
72
|
+
* Record after `turn/end` unless a record was already attempted after the turn's last tool result.
|
|
73
|
+
* @param turn - the turn number from `turn/end`.
|
|
74
|
+
*/
|
|
75
|
+
end(turn: number): void;
|
|
76
|
+
/** Resolves once every queued snapshot, capture, and record has settled. */
|
|
77
|
+
settled(): Promise<void>;
|
|
78
|
+
/**
|
|
79
|
+
* The summary announced by one `workspace/changes` event of this Session.
|
|
80
|
+
* @param seq - the event's sequence number.
|
|
81
|
+
* @returns the summary, or undefined for a sequence this recorder did not announce.
|
|
82
|
+
*/
|
|
83
|
+
summary(seq: number): WorkspaceChangesSummary | undefined;
|
|
84
|
+
/**
|
|
85
|
+
* Every file this Session's recorded turns changed, folded by path.
|
|
86
|
+
*
|
|
87
|
+
* The per-file counts and flags come from the LATEST turn that listed the
|
|
88
|
+
* file, so a rewrite reads as the delta it left rather than as the churn of
|
|
89
|
+
* every rewrite; `turns` carries that churn. The Session totals come from
|
|
90
|
+
* each turn's own complete counts, which include files that turn's list cap
|
|
91
|
+
* omitted.
|
|
92
|
+
* @returns the folded list in `display` order, empty before the first recorded turn.
|
|
93
|
+
*/
|
|
94
|
+
sessionChanges(): WorkspaceSessionChanges;
|
|
95
|
+
/**
|
|
96
|
+
* Compare one listed file's contents at turn start and turn end.
|
|
97
|
+
* @param seq - the announcing event's sequence number.
|
|
98
|
+
* @param index - the file's index in the summary's `files`.
|
|
99
|
+
* @param signal - cancels the reads.
|
|
100
|
+
* @returns the comparison, or undefined for an unknown sequence or index, or once disposed.
|
|
101
|
+
* @throws when a read fails while the recorder lives.
|
|
102
|
+
*/
|
|
103
|
+
diff(seq: number, index: number, signal: AbortSignal): Promise<WorkspaceFileDiff | undefined>;
|
|
104
|
+
/**
|
|
105
|
+
* Abort queued work, forget every record, and remove the temporary directory.
|
|
106
|
+
* @returns once the temporary directory is gone.
|
|
107
|
+
*/
|
|
108
|
+
dispose(): Promise<void>;
|
|
109
|
+
private enqueue;
|
|
110
|
+
/** A failure after disposal is expected cancellation and stays silent. */
|
|
111
|
+
private warnUnlessDisposed;
|
|
112
|
+
/** This Session's temporary directory, created on first use. */
|
|
113
|
+
private scratchDir;
|
|
114
|
+
/** The repository enclosing the working directory, located once; null keeps retrying each turn. */
|
|
115
|
+
private locate;
|
|
116
|
+
/** One side's text, null for an absent file, or {@link OVERSIZED} for a snapshot side beyond the byte cap. */
|
|
117
|
+
private readSide;
|
|
118
|
+
private record;
|
|
119
|
+
/**
|
|
120
|
+
* The listing of a captured pair: an oversized side lists the file without
|
|
121
|
+
* counts and refuses its comparison, a binary side likewise, and two text
|
|
122
|
+
* sides carry the counts of their line comparison.
|
|
123
|
+
*/
|
|
124
|
+
private compared;
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=recorder.d.ts.map
|