my-durable 0.0.25 → 0.0.26
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/apps/dpi-backend/src/git.ts +176 -17
- package/apps/dpi-backend/src/routes.ts +11 -1
- package/apps/dpi-backend/src/store.ts +39 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/dist/assets/index-C9OQe54o.js +79 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/dist/assets/index-DwYK0DEy.css +1 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/dist/index.html +2 -2
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/GitPanel.vue +559 -57
- package/apps/dpi-sdk/src/client.ts +43 -8
- package/apps/dpi-sdk/src/protocol/rest-routes.ts +1 -0
- package/package.json +1 -1
- package/apps/dpi-playground/apps/dpi-playground-fe/dist/assets/index-CsBzwiFM.js +0 -78
- package/apps/dpi-playground/apps/dpi-playground-fe/dist/assets/index-DM_ddsmz.css +0 -1
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* worktree-add, worktree-remove
|
|
10
10
|
*/
|
|
11
11
|
import { execFile } from "node:child_process";
|
|
12
|
-
import {
|
|
12
|
+
import { appendFile } from "node:fs/promises";
|
|
13
|
+
import { dirname, isAbsolute, join } from "node:path";
|
|
13
14
|
|
|
14
15
|
const GIT_TIMEOUT = 15_000;
|
|
15
16
|
/** `git status` on a big Windows checkout (AV scanning, node_modules) can
|
|
@@ -58,8 +59,16 @@ export interface GitStatus {
|
|
|
58
59
|
worktreeOf?: string;
|
|
59
60
|
worktrees?: { path: string; branch: string | null; head: string }[];
|
|
60
61
|
/** Changed files (porcelain XY + path), capped — the IntelliJ-style commit
|
|
61
|
-
* dialog checks exactly these. */
|
|
62
|
-
files?: { path: string; x: string; y: string }[];
|
|
62
|
+
* dialog checks exactly these. ±n line counts come from numstat (-1 = binary). */
|
|
63
|
+
files?: { path: string; x: string; y: string; a?: number; d?: number }[];
|
|
64
|
+
/** A merge (with conflicts) is in progress — the banner + abort action. */
|
|
65
|
+
merge?: boolean;
|
|
66
|
+
/** Unmerged paths (UU/AA/DD/AU/UA/DU/UD). */
|
|
67
|
+
conflicts?: number;
|
|
68
|
+
/** Commit identity from config — warn before committing without one. */
|
|
69
|
+
identity?: { name?: string; email?: string };
|
|
70
|
+
/** `origin` URL, parsed by the UI into an open-in-remote link. */
|
|
71
|
+
remoteUrl?: string;
|
|
63
72
|
/** Local branches for pickers — nobody should TYPE a name that exists. */
|
|
64
73
|
branches?: { name: string; current?: boolean; upstream?: string }[];
|
|
65
74
|
}
|
|
@@ -72,6 +81,11 @@ export async function gitStatusBatch(paths: string[]): Promise<Record<string, Gi
|
|
|
72
81
|
return Object.fromEntries(entries);
|
|
73
82
|
}
|
|
74
83
|
|
|
84
|
+
/** Test hook: the TTL cache would serve a pre-mutation answer. */
|
|
85
|
+
export function clearGitStatusCache() {
|
|
86
|
+
statusCache.clear();
|
|
87
|
+
}
|
|
88
|
+
|
|
75
89
|
/** Structured status for a folder: everything the UIs show, one call. */
|
|
76
90
|
export async function gitStatus(path: string): Promise<GitStatus> {
|
|
77
91
|
const cached = statusCache.get(path);
|
|
@@ -86,13 +100,20 @@ export async function gitStatus(path: string): Promise<GitStatus> {
|
|
|
86
100
|
out.root = root;
|
|
87
101
|
// Everything after toplevel runs in PARALLEL — status is the long pole
|
|
88
102
|
// (20s+ on big Windows checkouts) and must not wait for four fast calls.
|
|
89
|
-
const [branchP, headP, statusP, gitDirP, wlP, branchesP] = await Promise.allSettled([
|
|
103
|
+
const [branchP, headP, statusP, gitDirP, wlP, branchesP, numWorkP, numIdxP, mergeP, nameP, emailP, remoteP] = await Promise.allSettled([
|
|
90
104
|
git(path, ["branch", "--show-current"]),
|
|
91
105
|
git(path, ["log", "-1", "--pretty=%h%x09%s"]),
|
|
92
106
|
git(path, ["--no-optional-locks", "status", "--porcelain=v1", "-b"], GIT_SLOW_TIMEOUT),
|
|
93
107
|
git(path, ["rev-parse", "--git-dir"]),
|
|
94
108
|
git(path, ["worktree", "list", "--porcelain"], GIT_SLOW_TIMEOUT),
|
|
95
109
|
git(path, ["for-each-ref", "refs/heads", "--format=%(refname:short)%09%(upstream:short)%09%(if)%(HEAD)%(then)1%(else)0%(end)"]),
|
|
110
|
+
git(path, ["--no-optional-locks", "diff", "--numstat", "--no-renames"], GIT_SLOW_TIMEOUT),
|
|
111
|
+
git(path, ["--no-optional-locks", "diff", "--numstat", "--cached", "--no-renames"]),
|
|
112
|
+
// MERGE_HEAD exists ⇔ a merge is waiting to be resolved.
|
|
113
|
+
git(path, ["rev-parse", "-q", "--verify", "MERGE_HEAD"]),
|
|
114
|
+
git(path, ["config", "user.name"]),
|
|
115
|
+
git(path, ["config", "user.email"]),
|
|
116
|
+
git(path, ["remote", "get-url", "origin"]),
|
|
96
117
|
]);
|
|
97
118
|
const branch = branchP.status === "fulfilled" ? branchP.value : "";
|
|
98
119
|
out.branch = branch.trim() || undefined;
|
|
@@ -121,11 +142,31 @@ export async function gitStatus(path: string): Promise<GitStatus> {
|
|
|
121
142
|
}
|
|
122
143
|
// Per line — a ^-anchored regex on the joined blob only ever sees line 1.
|
|
123
144
|
const body = lines.slice(1).filter(Boolean);
|
|
124
|
-
out.
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
145
|
+
out.merge = mergeP.status === "fulfilled";
|
|
146
|
+
out.conflicts = count(body.join("\n"), /^(UU|AA|DD|AU|UA|DU|UD)/gm);
|
|
147
|
+
out.identity = {
|
|
148
|
+
name: nameP.status === "fulfilled" ? nameP.value.trim() || undefined : undefined,
|
|
149
|
+
email: emailP.status === "fulfilled" ? emailP.value.trim() || undefined : undefined,
|
|
150
|
+
};
|
|
151
|
+
if (remoteP.status === "fulfilled") out.remoteUrl = remoteP.value.trim() || undefined;
|
|
152
|
+
// ±n per file: worktree-vs-index numstat for unstaged paths, cached for staged.
|
|
153
|
+
const numstat = (raw: string) => {
|
|
154
|
+
const m = new Map<string, { a: number; d: number }>();
|
|
155
|
+
for (const l of raw.split("\n")) {
|
|
156
|
+
const mt = l.match(/^(\d+|-)\t(\d+|-)\t(.+)$/);
|
|
157
|
+
if (!mt) continue;
|
|
158
|
+
m.set(mt[3]!, { a: mt[1] === "-" ? -1 : Number(mt[1]), d: mt[2] === "-" ? -1 : Number(mt[2]) });
|
|
159
|
+
}
|
|
160
|
+
return m;
|
|
161
|
+
};
|
|
162
|
+
const workNum = numstat(numWorkP.status === "fulfilled" ? numWorkP.value : "");
|
|
163
|
+
const idxNum = numstat(numIdxP.status === "fulfilled" ? numIdxP.value : "");
|
|
164
|
+
out.files = body.slice(0, 500).map((l) => {
|
|
165
|
+
const p = l.slice(3).replace(/^"|"$/g, "");
|
|
166
|
+
const staged = l[0] !== " " && l[0] !== "?";
|
|
167
|
+
const n = staged ? idxNum.get(p) : workNum.get(p);
|
|
168
|
+
return { path: p, x: l[0] ?? " ", y: l[1] ?? " ", ...(n ? { a: n.a, d: n.d } : {}) };
|
|
169
|
+
});
|
|
129
170
|
if (branchesP.status === "fulfilled")
|
|
130
171
|
out.branches = branchesP.value
|
|
131
172
|
.split("\n")
|
|
@@ -181,7 +222,15 @@ export type GitOp =
|
|
|
181
222
|
| "discard"
|
|
182
223
|
| "diff-file"
|
|
183
224
|
| "stash-push"
|
|
184
|
-
| "stash-pop"
|
|
225
|
+
| "stash-pop"
|
|
226
|
+
| "unstage"
|
|
227
|
+
| "compare"
|
|
228
|
+
| "merge-abort"
|
|
229
|
+
| "show-files"
|
|
230
|
+
| "ignore"
|
|
231
|
+
| "checkpoint"
|
|
232
|
+
| "checkpoints"
|
|
233
|
+
| "rewind";
|
|
185
234
|
|
|
186
235
|
/** Plain string an arg may never contain — we build argv ourselves anyway. */
|
|
187
236
|
const safeRef = (s: string) => /^[\w./@+-]+$/.test(s) && !s.includes("..");
|
|
@@ -198,9 +247,24 @@ export async function gitOp(
|
|
|
198
247
|
files?: string[];
|
|
199
248
|
create?: boolean;
|
|
200
249
|
staged?: boolean;
|
|
250
|
+
/** pull: opt in to --rebase when --ff-only would fail. */
|
|
251
|
+
rebase?: boolean;
|
|
252
|
+
/** push: set upstream (-u origin <ref>). */
|
|
253
|
+
setUpstream?: boolean;
|
|
254
|
+
/** checkpoint name. */
|
|
255
|
+
name?: string;
|
|
256
|
+
/** ignore: pattern lines to append to .gitignore. */
|
|
257
|
+
lines?: string[];
|
|
201
258
|
} = {},
|
|
202
|
-
): Promise<{
|
|
203
|
-
|
|
259
|
+
): Promise<{
|
|
260
|
+
text: string;
|
|
261
|
+
commits?: { hash: string; refs: string; author: string; date: string; subject: string; parents: string[] }[];
|
|
262
|
+
ahead?: number;
|
|
263
|
+
behind?: number;
|
|
264
|
+
changed?: { status: string; path: string }[];
|
|
265
|
+
checkpoints?: { name: string; hash: string; date: string; subject: string }[];
|
|
266
|
+
}> {
|
|
267
|
+
const { ref, message, target, branch, files, create, staged, rebase, setUpstream, name, lines } = opts;
|
|
204
268
|
switch (op) {
|
|
205
269
|
case "diff":
|
|
206
270
|
return { text: await git(path, ["--no-pager", "diff", ...(ref && safeRef(ref) ? [ref] : [])]) };
|
|
@@ -208,9 +272,10 @@ export async function gitOp(
|
|
|
208
272
|
return { text: (await git(path, ["--no-pager", "log", "--oneline", "-n", "30", ...(ref && safeRef(ref) ? [ref] : [])])).trimEnd() };
|
|
209
273
|
case "log-list": {
|
|
210
274
|
// IntelliJ's log window: hash, refs, author, date, subject — structured.
|
|
275
|
+
// %p = parents — the swimlane graph is computed from these on the UI.
|
|
211
276
|
const raw = await git(
|
|
212
277
|
path,
|
|
213
|
-
["--no-pager", "log", "-n", "50", "--date=short", "--pretty=format:%h%x1f%D%x1f%an%x1f%ad%x1f%s"],
|
|
278
|
+
["--no-pager", "log", "-n", "50", "--date=short", "--pretty=format:%h%x1f%D%x1f%an%x1f%ad%x1f%s%x1f%p"],
|
|
214
279
|
);
|
|
215
280
|
return {
|
|
216
281
|
text: "",
|
|
@@ -218,15 +283,24 @@ export async function gitOp(
|
|
|
218
283
|
.split("\n")
|
|
219
284
|
.filter(Boolean)
|
|
220
285
|
.map((l) => {
|
|
221
|
-
const [hash, refs, author, date, subject] = l.split("\x1f");
|
|
222
|
-
return {
|
|
286
|
+
const [hash, refs, author, date, subject, parents] = l.split("\x1f");
|
|
287
|
+
return {
|
|
288
|
+
hash: hash ?? "",
|
|
289
|
+
refs: refs ?? "",
|
|
290
|
+
author: author ?? "",
|
|
291
|
+
date: date ?? "",
|
|
292
|
+
subject: subject ?? "",
|
|
293
|
+
parents: (parents ?? "").split(" ").filter(Boolean),
|
|
294
|
+
};
|
|
223
295
|
}),
|
|
224
296
|
};
|
|
225
297
|
}
|
|
226
298
|
case "fetch":
|
|
227
299
|
return { text: (await git(path, ["fetch", "--all", "--prune"])).trim() || "fetched" };
|
|
228
300
|
case "pull":
|
|
229
|
-
|
|
301
|
+
// --ff-only stays the default; --rebase is an explicit opt-in the UI
|
|
302
|
+
// offers only after a ff-only failure.
|
|
303
|
+
return { text: (await git(path, ["pull", ...(rebase ? ["--rebase"] : ["--ff-only"])])).trim() || "up to date" };
|
|
230
304
|
case "branch":
|
|
231
305
|
return { text: (await git(path, ["--no-pager", "branch", "-vv"])).trimEnd() };
|
|
232
306
|
case "switch": {
|
|
@@ -234,8 +308,13 @@ export async function gitOp(
|
|
|
234
308
|
// create:true = git switch -c — IntelliJ's "new branch" action.
|
|
235
309
|
return { text: (await git(path, ["switch", ...(create ? ["-c"] : []), ref])).trim() };
|
|
236
310
|
}
|
|
237
|
-
case "push":
|
|
311
|
+
case "push": {
|
|
312
|
+
if (setUpstream) {
|
|
313
|
+
if (!ref || !safeRef(ref)) throw Error("push -u needs a branch name");
|
|
314
|
+
return { text: (await git(path, ["push", "-u", "origin", ref], GIT_SLOW_TIMEOUT)).trim() || `pushed, upstream set to origin/${ref}` };
|
|
315
|
+
}
|
|
238
316
|
return { text: (await git(path, ["push"], GIT_SLOW_TIMEOUT)).trim() || "pushed (or nothing to push)" };
|
|
317
|
+
}
|
|
239
318
|
case "stage": {
|
|
240
319
|
if (!files?.length) throw Error("stage needs files");
|
|
241
320
|
return { text: (await git(path, ["add", "--", ...files])).trim() || `staged ${files.length} file(s)` };
|
|
@@ -253,6 +332,86 @@ export async function gitOp(
|
|
|
253
332
|
return { text: (await git(path, ["stash", "push", ...(message ? ["-m", message] : [])])).trim() || "stashed" };
|
|
254
333
|
case "stash-pop":
|
|
255
334
|
return { text: (await git(path, ["stash", "pop"])).trim() || "stash popped" };
|
|
335
|
+
case "unstage": {
|
|
336
|
+
// IntelliJ's Unstage: index only, worktree keeps its changes.
|
|
337
|
+
if (!files?.length) throw Error("unstage needs files");
|
|
338
|
+
try {
|
|
339
|
+
return { text: (await git(path, ["reset", "-q", "HEAD", "--", ...files])).trim() || `unstaged ${files.length} file(s)` };
|
|
340
|
+
} catch {
|
|
341
|
+
// unborn branch: HEAD does not resolve yet — reset against the empty tree.
|
|
342
|
+
return { text: (await git(path, ["reset", "-q", "--", ...files])).trim() || `unstaged ${files.length} file(s)` };
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
case "compare": {
|
|
346
|
+
// ahead/behind vs ANY ref, not just the upstream (orca git-bridge).
|
|
347
|
+
if (!ref || !safeRef(ref)) throw Error("compare needs a branch name");
|
|
348
|
+
const raw = await git(path, ["rev-list", "--left-right", "--count", `${ref}...HEAD`]);
|
|
349
|
+
const [behind = "0", ahead = "0"] = raw.trim().split(/\s+/);
|
|
350
|
+
return { text: `${Number(ahead)} ahead, ${Number(behind)} behind of ${ref}`, ahead: Number(ahead), behind: Number(behind) };
|
|
351
|
+
}
|
|
352
|
+
case "merge-abort":
|
|
353
|
+
try {
|
|
354
|
+
return { text: (await git(path, ["merge", "--abort"])).trim() || "merge aborted" };
|
|
355
|
+
} catch {
|
|
356
|
+
return { text: (await git(path, ["rebase", "--abort"])).trim() || "rebase aborted" };
|
|
357
|
+
}
|
|
358
|
+
case "show-files": {
|
|
359
|
+
// What one commit touched — the log row expander.
|
|
360
|
+
if (!target || !/^[0-9a-f]{4,40}$/.test(target)) throw Error("show-files needs a commit hash");
|
|
361
|
+
const raw = await git(path, ["--no-pager", "show", "--name-status", "--pretty=format:", "--no-renames", "-m", "--first-parent", target]);
|
|
362
|
+
return {
|
|
363
|
+
text: "",
|
|
364
|
+
changed: raw
|
|
365
|
+
.split("\n")
|
|
366
|
+
.map((l) => l.trim())
|
|
367
|
+
.filter(Boolean)
|
|
368
|
+
.slice(0, 200)
|
|
369
|
+
.map((l) => {
|
|
370
|
+
const [status, p] = l.split(/\t/);
|
|
371
|
+
return { status: status ?? "", path: p ?? l };
|
|
372
|
+
}),
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
case "ignore": {
|
|
376
|
+
// Appends to the repo's OWN .gitignore — root comes from git, not the caller.
|
|
377
|
+
if (!lines?.length) throw Error("ignore needs pattern lines");
|
|
378
|
+
if (lines.length > 50) throw Error("ignore takes at most 50 lines");
|
|
379
|
+
for (const l of lines) if (!l.trim() || l.length > 200 || /[\n\r\0]/.test(l)) throw Error("bad ignore pattern");
|
|
380
|
+
const root = (await git(path, ["rev-parse", "--show-toplevel"])).trim();
|
|
381
|
+
await appendFile(join(root, ".gitignore"), `\n${lines.join("\n")}\n`, "utf8");
|
|
382
|
+
return { text: `added to .gitignore: ${lines.join(", ")}` };
|
|
383
|
+
}
|
|
384
|
+
case "checkpoint": {
|
|
385
|
+
// A named ref of HEAD — the rewind target. Never touches the worktree.
|
|
386
|
+
if (!name || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(name)) throw Error("checkpoint needs a name (letters, digits, ._-)");
|
|
387
|
+
const refname = `refs/dpi/checkpoints/${name}`;
|
|
388
|
+
await git(path, ["update-ref", refname, "HEAD"]);
|
|
389
|
+
return { text: `checkpoint ${name} saved` };
|
|
390
|
+
}
|
|
391
|
+
case "checkpoints": {
|
|
392
|
+
const raw = await git(path, [
|
|
393
|
+
"--no-pager", "for-each-ref", "refs/dpi/checkpoints",
|
|
394
|
+
"--format=%(refname:strip=3)%09%(objectname:short)%09%(committerdate:short)%09%(contents:subject)",
|
|
395
|
+
]);
|
|
396
|
+
return {
|
|
397
|
+
text: "",
|
|
398
|
+
checkpoints: raw
|
|
399
|
+
.split("\n")
|
|
400
|
+
.filter(Boolean)
|
|
401
|
+
.slice(0, 50)
|
|
402
|
+
.map((l) => {
|
|
403
|
+
const [full, hash, date, subject] = l.split("\t");
|
|
404
|
+
return { name: full ?? "", hash: hash ?? "", date: date ?? "", subject: (subject ?? "").slice(0, 80) };
|
|
405
|
+
}),
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
case "rewind": {
|
|
409
|
+
// reset --hard to one of OUR checkpoint refs only — never an arbitrary ref.
|
|
410
|
+
if (!name || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(name)) throw Error("rewind needs a checkpoint name");
|
|
411
|
+
const refname = `refs/dpi/checkpoints/${name}`;
|
|
412
|
+
const sha = (await git(path, ["rev-parse", "--verify", refname])).trim();
|
|
413
|
+
return { text: (await git(path, ["reset", "--hard", sha])).trim().split("\n")[0] ?? `rewound to ${name}` };
|
|
414
|
+
}
|
|
256
415
|
case "commit": {
|
|
257
416
|
const msg = (message ?? "").trim();
|
|
258
417
|
if (!msg) throw Error("commit needs a message");
|
|
@@ -11,7 +11,7 @@ import type { DevicePairing } from "./pairing.js";
|
|
|
11
11
|
type Async = (req: Request, res: Response) => Promise<unknown>;
|
|
12
12
|
const route = (fn: Async) => (req: Request, res: Response, next: NextFunction) => fn(req, res).catch(next);
|
|
13
13
|
import { gitOp, gitStatus, gitStatusBatch, type GitOp } from "./git.ts";
|
|
14
|
-
const GIT_OPS: GitOp[] = ["diff", "log", "log-list", "fetch", "pull", "push", "switch", "branch", "commit", "stage", "discard", "diff-file", "stash-push", "stash-pop", "worktree-list", "worktree-add", "worktree-remove"];
|
|
14
|
+
const GIT_OPS: GitOp[] = ["diff", "log", "log-list", "fetch", "pull", "push", "switch", "branch", "commit", "stage", "unstage", "discard", "diff-file", "stash-push", "stash-pop", "compare", "merge-abort", "show-files", "ignore", "checkpoint", "checkpoints", "rewind", "worktree-list", "worktree-add", "worktree-remove"];
|
|
15
15
|
|
|
16
16
|
interface Locals {
|
|
17
17
|
row: SessionRow;
|
|
@@ -138,6 +138,13 @@ export function v1(
|
|
|
138
138
|
message: body.message,
|
|
139
139
|
target: body.target,
|
|
140
140
|
files: Array.isArray(body.files) ? body.files.map(String).slice(0, 500) : undefined,
|
|
141
|
+
// booleans stay booleans: gitOp validates names/refs itself.
|
|
142
|
+
create: body.create === true,
|
|
143
|
+
staged: body.staged === true,
|
|
144
|
+
rebase: body.rebase === true,
|
|
145
|
+
setUpstream: body.setUpstream === true,
|
|
146
|
+
name: typeof body.name === "string" ? body.name : undefined,
|
|
147
|
+
lines: Array.isArray(body.lines) ? body.lines.map(String).slice(0, 50) : undefined,
|
|
141
148
|
});
|
|
142
149
|
res.json(out);
|
|
143
150
|
}));
|
|
@@ -159,6 +166,9 @@ export function v1(
|
|
|
159
166
|
r.patch("/sessions/:s/compaction", route(async (req, res) => res.json(store.setCompaction(L(res).row, req.body ?? {}))));
|
|
160
167
|
r.delete("/sessions/:s", route(async (_req, res) => res.json(await store.archive(L(res).row))));
|
|
161
168
|
r.get("/sessions/:s/events", route(async (_req, res) => sessionStream(store, L(res).row, res)));
|
|
169
|
+
// The zcode flagship, adapted: which files has THIS session's agent touched?
|
|
170
|
+
// Powers "commit session changes" in the git panel.
|
|
171
|
+
r.get("/sessions/:s/git-files", route(async (_req, res) => res.json({ files: await store.sessionTouchedFiles(L(res).row) })));
|
|
162
172
|
r.get(
|
|
163
173
|
"/sessions/:s/tasks",
|
|
164
174
|
route(async (req, res) => {
|
|
@@ -717,6 +717,45 @@ export class Store {
|
|
|
717
717
|
}, ctx);
|
|
718
718
|
}
|
|
719
719
|
|
|
720
|
+
/**
|
|
721
|
+
* zcode's flagship, adapted: the paths this session's agents have written or
|
|
722
|
+
* edited — the "commit session changes" changelist. Read-only scan of every
|
|
723
|
+
* conversation's stored entries for write/edit tool calls; caps keep big
|
|
724
|
+
* sessions cheap. No worktree is touched.
|
|
725
|
+
*/
|
|
726
|
+
async sessionTouchedFiles(row: SessionRow): Promise<string[]> {
|
|
727
|
+
const h = await this.openAgent(row);
|
|
728
|
+
const paths = new Set<string>();
|
|
729
|
+
await h.commit(async (tx) => {
|
|
730
|
+
let convs = await tx.scanConversations({}, 1000);
|
|
731
|
+
const records = [...convs.items];
|
|
732
|
+
while (convs.next && records.length < 100) {
|
|
733
|
+
convs = await tx.scanConversations({}, 1000, convs.next);
|
|
734
|
+
records.push(...convs.items);
|
|
735
|
+
}
|
|
736
|
+
for (const c of records) {
|
|
737
|
+
let page = await tx.scanEntries({ conversationId: c.id }, 500);
|
|
738
|
+
let seen = 0;
|
|
739
|
+
for (;;) {
|
|
740
|
+
for (const e of page.items as unknown as { model?: readonly { content?: unknown }[] }[]) {
|
|
741
|
+
for (const m of e.model ?? []) {
|
|
742
|
+
const blocks = Array.isArray(m.content) ? (m.content as unknown[]) : [];
|
|
743
|
+
for (const b of blocks as { type?: string; name?: string; arguments?: Record<string, unknown> }[]) {
|
|
744
|
+
if (b?.type !== "toolCall" || (b.name !== "write" && b.name !== "edit")) continue;
|
|
745
|
+
const f = b.arguments?.file_path;
|
|
746
|
+
if (typeof f === "string" && f.length > 0 && f.length < 4096) paths.add(f);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
seen += page.items.length;
|
|
751
|
+
if (!page.next || seen >= 4000 || paths.size >= 800) break;
|
|
752
|
+
page = await tx.scanEntries({ conversationId: c.id }, 500, page.next);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}, ctx);
|
|
756
|
+
return [...paths];
|
|
757
|
+
}
|
|
758
|
+
|
|
720
759
|
// ------------------------------------------------------------ agents
|
|
721
760
|
|
|
722
761
|
private names(sessionId: string) {
|