@stevecastaneda/paseo-progress 1.13.1 → 1.14.1

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.
@@ -19706,7 +19706,6 @@ var QUESTION_MODE_RULES = {
19706
19706
  // shared/events.ts
19707
19707
  var PROGRESS_FILE_NAME = "progress.jsonl";
19708
19708
  var PANEL_OPENED_NAME = "progress-panel-opened";
19709
- var LEGACY_FOLDER = ".scratch";
19710
19709
  var EVENT_VERSION = 1;
19711
19710
  var TICKET_STATUSES = ["not_started", "working", "blocked", "done", "skipped"];
19712
19711
  var ticketStatusSchema = external_exports.enum(TICKET_STATUSES);
@@ -20270,10 +20269,16 @@ import { dirname, isAbsolute, join as join2, resolve, sep } from "node:path";
20270
20269
  function outside(inside) {
20271
20270
  return inside === ".." || inside.startsWith(`..${sep}`) || isAbsolute(inside);
20272
20271
  }
20272
+ function inGitFolder(inside) {
20273
+ return inside.split(/[\\/]/).some((name) => name.toLowerCase() === ".git");
20274
+ }
20275
+ function hasGitEntry(directory) {
20276
+ return existsSync(join2(directory, ".git"));
20277
+ }
20273
20278
  function findRoot(cwd) {
20274
20279
  let directory = resolve(cwd);
20275
20280
  while (true) {
20276
- if (existsSync(join2(directory, ".git"))) return directory;
20281
+ if (hasGitEntry(directory)) return directory;
20277
20282
  const parent = dirname(directory);
20278
20283
  if (parent === directory) return resolve(cwd);
20279
20284
  directory = parent;
@@ -20385,33 +20390,33 @@ async function installLauncher(path, platform = process.platform) {
20385
20390
  import { execFile } from "node:child_process";
20386
20391
  import { createHash, randomUUID } from "node:crypto";
20387
20392
  import { realpathSync } from "node:fs";
20388
- import { appendFile as appendFile2, mkdir as mkdir4, readdir, readFile as readFile3, rename, rm as rm2, rmdir, stat as stat3, writeFile as writeFile3 } from "node:fs/promises";
20393
+ import { lstat, mkdir as mkdir4, readdir, realpath, rename, rm as rm2, stat as stat3, writeFile as writeFile3 } from "node:fs/promises";
20389
20394
  import { homedir } from "node:os";
20390
- import { basename as basename2, dirname as dirname3, join as join4, normalize, resolve as resolve2 } from "node:path";
20395
+ import { basename as basename2, dirname as dirname3, join as join4, normalize, resolve as resolve2, sep as sep2 } from "node:path";
20391
20396
  import { promisify } from "node:util";
20392
- var LOCK_NAME2 = `${PROGRESS_FILE_NAME}.lock`;
20393
20397
  var SAVE_RUNS_KEY = "paseo-progress.saveRuns";
20394
- var LEGACY_GITIGNORE = `# Added by the progress plugin for Paseo so its files stay out of git.
20395
- ${PROGRESS_FILE_NAME}
20396
- ${LOCK_NAME2}/
20397
- ${PANEL_OPENED_NAME}
20398
- .gitignore
20399
- `;
20400
- async function git(cwd, args) {
20398
+ async function git(root, args) {
20401
20399
  try {
20402
- const { stdout } = await promisify(execFile)("git", args, { cwd });
20400
+ const { stdout } = await promisify(execFile)("git", ["-C", resolve2(root), ...args], { cwd: dirname3(process.execPath) });
20403
20401
  return { code: 0, stdout };
20404
20402
  } catch (error62) {
20405
20403
  const failed = error62;
20406
20404
  return { code: typeof failed.code === "number" ? failed.code : -1, stdout: failed.stdout ?? "" };
20407
20405
  }
20408
20406
  }
20409
- async function exists2(path) {
20410
- return Boolean(await stat3(path).catch(() => null));
20411
- }
20412
20407
  function stateRoot(home = homedir()) {
20413
20408
  return join4(home, ".local", "state", "paseo-progress");
20414
20409
  }
20410
+ async function writeAtomic(path, text2) {
20411
+ await mkdir4(dirname3(path), { recursive: true });
20412
+ const temp = `${path}.${randomUUID()}.tmp`;
20413
+ try {
20414
+ await writeFile3(temp, text2);
20415
+ await rename(temp, path);
20416
+ } finally {
20417
+ await rm2(temp, { force: true });
20418
+ }
20419
+ }
20415
20420
  function outsideDirectory(root, home = homedir()) {
20416
20421
  let real = root;
20417
20422
  try {
@@ -20421,51 +20426,18 @@ function outsideDirectory(root, home = homedir()) {
20421
20426
  const hash2 = createHash("sha1").update(real).digest("hex").slice(0, 8);
20422
20427
  return join4(stateRoot(home), `${basename2(real)}-${hash2}`);
20423
20428
  }
20424
- var trackedLegacy = /* @__PURE__ */ new Map();
20425
- async function legacyTracked(root, mtimeMs) {
20426
- const cached2 = trackedLegacy.get(root);
20427
- if (cached2 && cached2.mtimeMs === mtimeMs) return cached2.tracked;
20428
- const { code } = await git(root, ["ls-files", "--error-unmatch", "--", `${LEGACY_FOLDER}/${PROGRESS_FILE_NAME}`]);
20429
- trackedLegacy.set(root, { mtimeMs, tracked: code === 0 });
20430
- return code === 0;
20431
- }
20432
- async function findStore(root, home = homedir()) {
20429
+ function findStore(root, home = homedir()) {
20433
20430
  const directory = outsideDirectory(root, home);
20434
- const store = { directory, shown: directory.startsWith(home) ? `~${directory.slice(home.length)}` : directory };
20435
- const legacy = join4(root, LEGACY_FOLDER);
20436
- const info = await stat3(join4(legacy, PROGRESS_FILE_NAME)).catch(() => null);
20437
- if (!info) return store;
20438
- if (await legacyTracked(root, info.mtimeMs)) return { directory: legacy, shown: LEGACY_FOLDER };
20439
- await moveLegacy(legacy, directory);
20440
- return store;
20441
- }
20442
- async function moveLegacy(legacy, directory) {
20443
- await mkdir4(directory, { recursive: true });
20444
- await withLock(join4(legacy, LOCK_NAME2), async () => {
20445
- const text2 = await readFile3(join4(legacy, PROGRESS_FILE_NAME), "utf8").catch(() => null);
20446
- if (text2 === null) return;
20447
- await withLock(join4(directory, LOCK_NAME2), async () => {
20448
- const current = await readFile3(join4(directory, PROGRESS_FILE_NAME), "utf8").catch(() => "");
20449
- const lead = current && !current.endsWith("\n") ? "\n" : "";
20450
- await appendFile2(join4(directory, PROGRESS_FILE_NAME), `${lead}${text2}`);
20451
- });
20452
- await rm2(join4(legacy, PROGRESS_FILE_NAME));
20453
- if (await exists2(join4(legacy, PANEL_OPENED_NAME))) {
20454
- await writeFile3(join4(directory, PANEL_OPENED_NAME), "");
20455
- await rm2(join4(legacy, PANEL_OPENED_NAME));
20456
- }
20457
- });
20458
- const ignore = await readFile3(join4(legacy, ".gitignore"), "utf8").catch(() => null);
20459
- if (ignore === LEGACY_GITIGNORE) await rm2(join4(legacy, ".gitignore"));
20460
- await rmdir(legacy).catch(() => {
20461
- });
20431
+ return { directory, shown: directory.startsWith(home) ? `~${directory.slice(home.length)}` : directory };
20462
20432
  }
20463
20433
  var configFiles = /* @__PURE__ */ new Map();
20464
20434
  var savedFolders = /* @__PURE__ */ new Map();
20465
20435
  async function configFile(root) {
20436
+ if (!hasGitEntry(root)) return null;
20466
20437
  if (!configFiles.has(root)) {
20467
20438
  const { code, stdout } = await git(root, ["rev-parse", "--git-common-dir"]);
20468
- configFiles.set(root, code === 0 ? join4(resolve2(root, stdout.trim()), "config") : null);
20439
+ if (code !== 0) return null;
20440
+ configFiles.set(root, join4(resolve2(root, stdout.trim()), "config"));
20469
20441
  }
20470
20442
  return configFiles.get(root);
20471
20443
  }
@@ -20476,10 +20448,19 @@ async function historyFolder(root) {
20476
20448
  const cached2 = savedFolders.get(file2);
20477
20449
  if (cached2 && cached2.mtimeMs === mtimeMs) return cached2.folder;
20478
20450
  const { stdout } = await git(root, ["config", "--get", SAVE_RUNS_KEY]);
20479
- const folder = stdout.trim() || null;
20451
+ const checked = checkFolder(stdout);
20452
+ const folder = "folder" in checked ? checked.folder : null;
20480
20453
  savedFolders.set(file2, { mtimeMs, folder });
20481
20454
  return folder;
20482
20455
  }
20456
+ function checkFolder(raw) {
20457
+ const trimmed = raw.trim().replace(/\/+$/, "");
20458
+ if (!trimmed) return { error: "Choose a folder" };
20459
+ const folder = normalize(trimmed);
20460
+ if (outside(folder) || folder === ".") return { error: "Choose a folder inside the repo" };
20461
+ if (inGitFolder(folder)) return { error: "Choose a folder outside .git" };
20462
+ return { folder };
20463
+ }
20483
20464
  function runFileName(startedAt, title) {
20484
20465
  const stamp = startedAt.replace(/\.\d+Z$/, "").replace(/[-:]/g, "").replace("T", "-");
20485
20466
  const slug = title.toLowerCase().normalize("NFKD").replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 60);
@@ -20488,29 +20469,27 @@ function runFileName(startedAt, title) {
20488
20469
  async function saveRunCopy(root, text2) {
20489
20470
  const folder = await historyFolder(root);
20490
20471
  if (!folder) return;
20491
- const lines = text2.split("\n").filter((line) => line.trim());
20492
- let start = -1;
20493
- let event = null;
20494
- for (let index = lines.length - 1; index >= 0; index--) {
20495
- try {
20496
- const parsed = JSON.parse(lines[index]);
20497
- if (parsed?.type === "run.start") {
20498
- start = index;
20499
- event = parsed;
20500
- break;
20501
- }
20502
- } catch {
20503
- }
20504
- }
20505
- if (start < 0 || !event?.ts || !event.title) return;
20506
- const directory = join4(root, folder);
20507
- await mkdir4(directory, { recursive: true });
20508
- await writeFile3(join4(directory, runFileName(event.ts, event.title)), `${lines.slice(start).join("\n")}
20472
+ const [start] = latestRun(parseProgress(text2).events);
20473
+ if (start?.event.type !== "run.start") return;
20474
+ const lines = text2.split("\n").slice(start.line - 1).filter((line) => line.trim());
20475
+ const directory = await runsFolder(root, folder);
20476
+ if (!directory) return;
20477
+ await writeAtomic(join4(directory, runFileName(start.event.ts, start.event.title)), `${lines.join("\n")}
20509
20478
  `);
20510
20479
  }
20480
+ async function runsFolder(root, folder) {
20481
+ let directory = await realpath(root);
20482
+ for (const name of folder.split(sep2)) {
20483
+ directory = join4(directory, name);
20484
+ const info = await lstat(directory).catch(() => null);
20485
+ if (!info) await mkdir4(directory);
20486
+ else if (!info.isDirectory()) return null;
20487
+ }
20488
+ return directory;
20489
+ }
20511
20490
 
20512
20491
  // server/question-mode.ts
20513
- import { readFile as readFile4, rm as rm3 } from "node:fs/promises";
20492
+ import { readFile as readFile3, rm as rm3 } from "node:fs/promises";
20514
20493
  import { homedir as homedir2 } from "node:os";
20515
20494
  import { join as join5 } from "node:path";
20516
20495
  var mirrorSchema = external_exports.object({ questionMode: external_exports.enum(QUESTION_MODES) });
@@ -20519,7 +20498,7 @@ function questionModePath(home = homedir2()) {
20519
20498
  }
20520
20499
  async function readQuestionMode(home = homedir2()) {
20521
20500
  try {
20522
- return mirrorSchema.parse(JSON.parse(await readFile4(questionModePath(home), "utf8"))).questionMode;
20501
+ return mirrorSchema.parse(JSON.parse(await readFile3(questionModePath(home), "utf8"))).questionMode;
20523
20502
  } catch {
20524
20503
  return DEFAULT_QUESTION_MODE;
20525
20504
  }
@@ -21055,6 +21034,12 @@ function guessKind(target, isUrl, cwd) {
21055
21034
  var SHOW_SUMMARY = "Print the dashboard as text: what the user sees in the Progress panel.";
21056
21035
  var INSTALL_SUMMARY = "Write the paseo-progress launcher (default ~/.local/bin/paseo-progress). It runs this command from whichever copy of the plugin Paseo is running.";
21057
21036
  var DEFAULT_LAUNCHER = "~/.local/bin/paseo-progress";
21037
+ function commandSummary(name) {
21038
+ if (name === "show") return SHOW_SUMMARY;
21039
+ const command = commands[name];
21040
+ if (!command) throw new Error(`No command named ${name}.`);
21041
+ return command.summary;
21042
+ }
21058
21043
  function help() {
21059
21044
  return [
21060
21045
  `Usage: ${USAGE_NAME} <command>`,
@@ -21166,5 +21151,6 @@ if (isMain()) {
21166
21151
  );
21167
21152
  }
21168
21153
  export {
21154
+ commandSummary,
21169
21155
  runCli
21170
21156
  };
Binary file
Binary file
Binary file
package/index.client.tsx CHANGED
@@ -7,6 +7,8 @@ import { ProgressLanguageProvider } from "./client/language";
7
7
  import { addPageScreen } from "./client/page-screen";
8
8
  import { PdfPanel } from "./client/pdf";
9
9
  import { showsPdfs } from "./shared/attachments";
10
+ import { CHAT_ROW_KIND, CHAT_ROW_VERSION, chatRowSchema, transformChatRow } from "./client/chat-row";
11
+ import { ChatRowView } from "./client/chat-row-view";
10
12
 
11
13
  const SETTINGS_ID = "settings";
12
14
 
@@ -45,8 +47,17 @@ export default function contribute(client: PluginClientContext) {
45
47
  icon: "Settings",
46
48
  Component: (props) => <ProgressLanguageProvider><QuestionSettings {...props} /></ProgressLanguageProvider>,
47
49
  });
50
+ const stopTransformer = client.addTimelineTransformer({ id: "tool-calls", query: { itemType: "tool_call" }, transform: transformChatRow });
51
+ const stopRenderer = client.addTimelineRenderer({
52
+ kind: CHAT_ROW_KIND,
53
+ version: CHAT_ROW_VERSION,
54
+ schema: chatRowSchema,
55
+ Component: (props) => <ProgressLanguageProvider><ChatRowView {...props} /></ProgressLanguageProvider>,
56
+ });
48
57
 
49
58
  return () => {
59
+ void stopTransformer();
60
+ void stopRenderer();
50
61
  stopPage();
51
62
  void stopPdf();
52
63
  void stopSettings();
package/index.server.ts CHANGED
@@ -4,8 +4,9 @@ import { handleInstallLauncher, handleLauncherStatus } from "./server/launcher";
4
4
  import { handleCanOpenOnHost, handleOpenDeliverable, handlePreviewDeliverable, handleReadDeliverablePiece } from "./server/open";
5
5
  import { mirrorQuestionMode } from "./server/question-mode";
6
6
  import { handleInstallSkill, handleReadSkill, handleSkillStatus, repairSkill } from "./server/skill";
7
+ import { attachProgressTools, handleToolsStatus } from "./server/tool";
7
8
  import { handleDismissWhatsNew, handleGetWhatsNew } from "./server/whats-new";
8
- import { canOpenOnHost, dismissWhatsNew, getAttention, getDashboard, getLauncherStatus, getSkillStatus, getWhatsNew, installLauncher, installSkill, listFolders, readSkill, markPanelOpened, openDeliverable, previewDeliverable, readDeliverablePiece, setHistoryFolder } from "./shared/rpc";
9
+ import { canOpenOnHost, dismissWhatsNew, getAttention, getDashboard, getLauncherStatus, getSkillStatus, getToolsStatus, getWhatsNew, installLauncher, installSkill, listFolders, readSkill, markPanelOpened, openDeliverable, previewDeliverable, readDeliverablePiece, setHistoryFolder } from "./shared/rpc";
9
10
  import { displaySettings, questionSettings } from "./shared/settings";
10
11
 
11
12
  export default function contribute(server: PluginServerContext) {
@@ -23,12 +24,14 @@ export default function contribute(server: PluginServerContext) {
23
24
  server.handle(readSkill, handleReadSkill);
24
25
  server.handle(listFolders, handleListFolders);
25
26
  server.handle(setHistoryFolder, handleSetHistoryFolder);
27
+ server.handle(getToolsStatus, handleToolsStatus);
26
28
  server.handle(getWhatsNew, handleGetWhatsNew);
27
29
  server.handle(dismissWhatsNew, handleDismissWhatsNew);
28
30
  // Settles a new install before any panel opens. Opening one makes a
29
31
  // worktree folder, which would look like an earlier version's.
30
32
  void handleGetWhatsNew().catch(() => {});
31
33
  void repairSkill();
34
+ attachProgressTools(server);
32
35
  server.registerSettings(displaySettings);
33
36
  const mirror = mirrorQuestionMode(server.registerSettings(questionSettings));
34
37
  return mirror.stop;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stevecastaneda/paseo-progress",
3
- "version": "1.13.1",
4
- "description": "A live progress dashboard per worktree for Paseo, drawn from a file agents write with the paseo-progress command.",
3
+ "version": "1.14.1",
4
+ "description": "A live progress dashboard per worktree for Paseo, drawn from a file agents write with the Progress tools or the paseo-progress command.",
5
5
  "license": "MIT",
6
6
  "author": "Steve Castaneda",
7
7
  "repository": {
@@ -50,8 +50,8 @@
50
50
  "postpack": "rm -rf dist"
51
51
  },
52
52
  "devDependencies": {
53
- "@getpaseo/client": "0.13.0",
54
- "@getpaseo/plugin": "0.13.0",
53
+ "@getpaseo/client": "0.14.0",
54
+ "@getpaseo/plugin": "0.14.0",
55
55
  "@tanstack/react-query": "^5.90.11",
56
56
  "@types/node": "^20.9.0",
57
57
  "@types/react": "~19.2.0",
package/paseo-plugin.json CHANGED
@@ -6,7 +6,9 @@
6
6
  "images/2-ticket.png",
7
7
  "images/3-question.png",
8
8
  "images/4-pill.png",
9
- "images/5-settings.png"
9
+ "images/5-settings.png",
10
+ "images/6-chat-rows.png",
11
+ "images/7-empty-tools-note.png"
10
12
  ],
11
13
  "requirements": {
12
14
  "paseo": ">=0.13.0"
package/server/cli.ts CHANGED
@@ -494,6 +494,14 @@ const SHOW_SUMMARY = "Print the dashboard as text: what the user sees in the Pro
494
494
  const INSTALL_SUMMARY = "Write the paseo-progress launcher (default ~/.local/bin/paseo-progress). It runs this command from whichever copy of the plugin Paseo is running.";
495
495
  const DEFAULT_LAUNCHER = "~/.local/bin/paseo-progress";
496
496
 
497
+ // The agent tools describe themselves with the summaries agents see in --help.
498
+ export function commandSummary(name: string): string {
499
+ if (name === "show") return SHOW_SUMMARY;
500
+ const command = commands[name];
501
+ if (!command) throw new Error(`No command named ${name}.`);
502
+ return command.summary;
503
+ }
504
+
497
505
  function help(): string {
498
506
  return [
499
507
  `Usage: ${USAGE_NAME} <command>`,
package/server/open.ts CHANGED
@@ -8,8 +8,8 @@ import { promisify } from "node:util";
8
8
  import { findAttachment, type FolderEntry, imageMimeType, isPieced, type PiecedKind, previewKind, splitRef, textFormat, videoMimeType } from "../shared/attachments.ts";
9
9
  import { prepareMarkdown, previewText } from "../shared/markdown.ts";
10
10
  import { readProgress } from "./progress-file.ts";
11
- import { findRoot, outside } from "./paths.ts";
12
- import { findStore } from "./store.ts";
11
+ import { findRoot, hasGitEntry, inGitFolder, outside } from "./paths.ts";
12
+ import { findStore, git } from "./store.ts";
13
13
 
14
14
  export type Opener = (path: string) => Promise<void>;
15
15
 
@@ -127,7 +127,7 @@ export async function previewDeliverable(directory: string, id: string): Promise
127
127
  }
128
128
  }
129
129
  if (size > MAX_PREVIEW_BYTES) throw new Error(tooLarge(path, size));
130
- if (kind === "html") return { kind, html: await inlineLocalFiles(await readFile(realTarget, "utf8"), dirname(realTarget), realRoot), bytes: size };
130
+ if (kind === "html") return { kind, html: await inlineLocalFiles(await readFile(realTarget, "utf8"), realTarget, realRoot), bytes: size };
131
131
  const data = await readFile(realTarget);
132
132
  return { kind, dataUri: `data:${imageMimeType(realTarget)};base64,${data.toString("base64")}`, bytes: size };
133
133
  }
@@ -146,8 +146,29 @@ async function listFolder(target: string): Promise<{ entries: FolderEntry[]; tru
146
146
  // The preview gets only the page, so the files beside it that it loads (its
147
147
  // screenshots, a script, a stylesheet) are written into it. Only files inside the
148
148
  // worktree, up to MAX_PREVIEW_BYTES in all; anything else is left as written and
149
- // fails to load. Web addresses are left alone.
150
- async function inlineLocalFiles(html: string, directory: string, root: string): Promise<string> {
149
+ // fails to load. Web addresses are left alone. A cloned repo's page runs here,
150
+ // so it must not read and send .env or .git files: it gets a file git says it
151
+ // keeps, or, when git ignores the page, like an agent's report in .scratch, a
152
+ // file in the page's own folder or below. Never one in a .git folder. When git
153
+ // can't answer, as for a file in a submodule or when git fails, the file stays out.
154
+ async function inlineLocalFiles(html: string, page: string, root: string): Promise<string> {
155
+ const directory = dirname(page);
156
+ const inRepo = hasGitEntry(root);
157
+ // -v -n prints a line for every path git answers for, so a git that fails
158
+ // with no answer never counts as "not ignored". The "./" stops git reading a
159
+ // name like ":(glob)x" as a pattern.
160
+ const ignoreCheck = async (path: string): Promise<"ignored" | "kept" | "unknown"> => {
161
+ if (!inRepo) return "kept";
162
+ const { code, stdout } = await git(root, ["check-ignore", "-v", "-n", "--", `./${relative(root, path).split(nodePath.sep).join("/")}`]);
163
+ if (!stdout.trim()) return "unknown";
164
+ return code === 0 ? "ignored" : code === 1 ? "kept" : "unknown";
165
+ };
166
+ const pageIgnored = (await ignoreCheck(page)) === "ignored";
167
+ const allowed = async (file: string) => {
168
+ if (inGitFolder(relative(root, file))) return false;
169
+ if ((await ignoreCheck(file)) === "kept") return true;
170
+ return pageIgnored && !outside(relative(directory, file));
171
+ };
151
172
  let room = MAX_PREVIEW_BYTES - Buffer.byteLength(html);
152
173
  const inline = async (url: string, encode: (data: Buffer, file: string) => string): Promise<string | null> => {
153
174
  if (!url || /^([a-z][a-z0-9+.-]*:|[/\\#?])/i.test(url)) return null;
@@ -155,6 +176,7 @@ async function inlineLocalFiles(html: string, directory: string, root: string):
155
176
  const file = await realpath(resolve(directory, decodeURI(url.replace(/[?#].*$/, ""))));
156
177
  const stats = await stat(file);
157
178
  if (outside(relative(root, file)) || !stats.isFile() || stats.size > room) return null;
179
+ if (!(await allowed(file))) return null;
158
180
  const encoded = encode(await readFile(file), file);
159
181
  if (encoded.length > room) return null;
160
182
  room -= encoded.length;
package/server/paths.ts CHANGED
@@ -7,14 +7,26 @@ export function outside(inside: string): boolean {
7
7
  return inside === ".." || inside.startsWith(`..${sep}`) || isAbsolute(inside);
8
8
  }
9
9
 
10
- // The worktree root is the nearest directory with a `.git` entry (a folder in
11
- // a checkout, a file in a linked worktree). Outside git, use the directory.
12
- // The command and the panel both start from here, so a Paseo project set at a
13
- // subfolder of the repo still reads the file the agent writes.
10
+ // Whether a path from relative(root, target) is in a .git folder, a nested
11
+ // checkout's included. Windows writes either separator, and Windows and macOS
12
+ // match names in any case.
13
+ export function inGitFolder(inside: string): boolean {
14
+ return inside.split(/[\\/]/).some((name) => name.toLowerCase() === ".git");
15
+ }
16
+
17
+ // A `.git` entry: a folder in a checkout, a file in a linked worktree.
18
+ export function hasGitEntry(directory: string): boolean {
19
+ return existsSync(join(directory, ".git"));
20
+ }
21
+
22
+ // The worktree root is the nearest directory with a `.git` entry. Outside git,
23
+ // use the directory. The command and the panel both start from here, so a
24
+ // Paseo project set at a subfolder of the repo still reads the file the agent
25
+ // writes.
14
26
  export function findRoot(cwd: string): string {
15
27
  let directory = resolve(cwd);
16
28
  while (true) {
17
- if (existsSync(join(directory, ".git"))) return directory;
29
+ if (hasGitEntry(directory)) return directory;
18
30
  const parent = dirname(directory);
19
31
  if (parent === directory) return resolve(cwd);
20
32
  directory = parent;