@stevecastaneda/paseo-progress 0.1.7 → 0.1.8

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.
@@ -16,10 +16,11 @@ export function isPreviewable(attachment: Attachment): boolean {
16
16
  return canPreview(attachment, { browser: Platform.OS === "web" });
17
17
  }
18
18
 
19
- // Full path on the daemon host for a stored path.
20
- export function absolutePath(path: string, workspaceDirectory: string): string {
19
+ // Full path on the daemon host for a stored path. `root` is the worktree
20
+ // root the dashboard was read from, not the workspace directory.
21
+ export function absolutePath(path: string, root: string): string {
21
22
  if (path.startsWith("/") || /^[A-Za-z]:[\\/]/.test(path)) return path;
22
- const base = workspaceDirectory.replace(/[\\/]+$/, "");
23
+ const base = root.replace(/[\\/]+$/, "");
23
24
  return path === "./" ? `${base}/` : `${base}/${path}`;
24
25
  }
25
26
 
package/client/panel.tsx CHANGED
@@ -54,7 +54,7 @@ function WorkspaceProgress({ theme, workspaceId, host, layout, navigation }: Plu
54
54
  <Presence show={Boolean(result && !result.configured)}>{result && !result.configured ? <EmptyState colors={colors} /> : null}</Presence>
55
55
  {/* Keyed by run: a new run starts with fresh tabs, pages and dialogs. */}
56
56
  {result?.configured ? <DashboardView key={result.dashboard.run?.startedAt ?? "none"} colors={colors} dashboard={result.dashboard} agentRunning={agentRunning} compact={layout.compact}
57
- workspaceId={workspaceId} workspaceDirectory={directory ?? ""} navigation={navigation} /> : null}
57
+ workspaceId={workspaceId} workspaceDirectory={result.root} navigation={navigation} /> : null}
58
58
  </ScrollView>
59
59
  );
60
60
  }
@@ -28,12 +28,14 @@ export function AttentionPopover({ theme, host, layout, workspaceId, close, open
28
28
  const directory = useWorkspace(workspaceId, (workspace) => workspace.directory);
29
29
  const query = useDashboard(host.id, workspaceId, directory);
30
30
  const dashboard = query.data?.configured ? query.data.dashboard : null;
31
+ // Stored paths are relative to the worktree root, which can sit above the workspace directory.
32
+ const root = query.data?.root ?? directory ?? "";
31
33
  const copy = useCopy();
32
34
  // Previews open in a dialog over the popover, which stays open to return to.
33
35
  // Links and other files leave Paseo, so the popover closes for those.
34
36
  const [previewing, setPreviewing] = useState<{ attachment: Attachment; group: Attachment[] } | null>(null);
35
37
  const group = useRef<Attachment[]>([]);
36
- const opener = useAttachmentOpener({ workspaceId, workspaceDirectory: directory ?? "", onPreview: (attachment) => setPreviewing({ attachment, group: previewable(group.current) }) });
38
+ const opener = useAttachmentOpener({ workspaceId, workspaceDirectory: root, onPreview: (attachment) => setPreviewing({ attachment, group: previewable(group.current) }) });
37
39
  return (
38
40
  <View style={{ width: 360, maxWidth: "100%", gap: 8 }}>
39
41
  {dashboard ? (
@@ -52,7 +54,7 @@ export function AttentionPopover({ theme, host, layout, workspaceId, close, open
52
54
  {query.error ? `Could not read progress: ${query.error.message}` : "Loading…"}
53
55
  </Text>
54
56
  )}
55
- <PreviewDialog colors={colors} attachment={previewing?.attachment ?? null} workspaceId={workspaceId} workspaceDirectory={directory ?? ""}
57
+ <PreviewDialog colors={colors} attachment={previewing?.attachment ?? null} workspaceId={workspaceId} workspaceDirectory={root}
56
58
  onClose={() => setPreviewing(null)} onOpenOnHost={(attachment) => void opener.openOnHost(attachment)}
57
59
  gallery={previewing ? { items: previewing.group, onSelect: (attachment) => setPreviewing({ ...previewing, attachment }) } : undefined} />
58
60
  <View style={{ paddingTop: touch ? 12 : 8, borderTopWidth: 1, borderTopColor: colors.border }}>
@@ -1,13 +1,13 @@
1
1
  // The command agents run to record progress. Each command appends one event
2
2
  // to the worktree's progress file and prints what it recorded.
3
- import { existsSync, realpathSync, statSync } from "node:fs";
4
- import { dirname, join, relative, resolve } from "node:path";
3
+ import { realpathSync, statSync } from "node:fs";
4
+ import { relative, resolve } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { parseArgs } from "node:util";
7
7
  import { isSettled } from "../shared/dashboard.js";
8
8
  import { DELIVERABLE_KINDS, PROGRESS_FILE, TICKET_STATUSES } from "../shared/events.js";
9
9
  import { appendProgress, InvalidEvent, readProgress } from "./progress-file.js";
10
- import { outside } from "./paths.js";
10
+ import { findRoot, outside } from "./paths.js";
11
11
  import { installLauncher } from "./launcher.js";
12
12
  import { dashboardText } from "../shared/show.js";
13
13
  import { imageMimeType } from "../shared/attachments.js";
@@ -436,19 +436,6 @@ function help() {
436
436
  ` ${INSTALL_SUMMARY}`,
437
437
  ].join("\n");
438
438
  }
439
- // The worktree root is the nearest directory with a `.git` entry (a folder in
440
- // a checkout, a file in a linked worktree). Outside git, use the directory.
441
- export function findRoot(cwd) {
442
- let directory = resolve(cwd);
443
- while (true) {
444
- if (existsSync(join(directory, ".git")))
445
- return directory;
446
- const parent = dirname(directory);
447
- if (parent === directory)
448
- return resolve(cwd);
449
- directory = parent;
450
- }
451
- }
452
439
  export async function runCli(argv, options) {
453
440
  const out = options.out ?? ((line) => console.log(line));
454
441
  const now = options.now ?? (() => new Date());
@@ -1,6 +1,22 @@
1
- import { isAbsolute, sep } from "node:path";
1
+ import { existsSync } from "node:fs";
2
+ import { dirname, isAbsolute, join, resolve, sep } from "node:path";
2
3
  // Whether a path from relative(root, target) leaves the root. "..notes" is a
3
4
  // name inside it, not a step out.
4
5
  export function outside(inside) {
5
6
  return inside === ".." || inside.startsWith(`..${sep}`) || isAbsolute(inside);
6
7
  }
8
+ // The worktree root is the nearest directory with a `.git` entry (a folder in
9
+ // a checkout, a file in a linked worktree). Outside git, use the directory.
10
+ // The command and the panel both start from here, so a Paseo project set at a
11
+ // subfolder of the repo still reads the file the agent writes.
12
+ export function findRoot(cwd) {
13
+ let directory = resolve(cwd);
14
+ while (true) {
15
+ if (existsSync(join(directory, ".git")))
16
+ return directory;
17
+ const parent = dirname(directory);
18
+ if (parent === directory)
19
+ return resolve(cwd);
20
+ directory = parent;
21
+ }
22
+ }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@stevecastaneda/paseo-progress",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@stevecastaneda/paseo-progress",
9
- "version": "0.1.7",
9
+ "version": "0.1.8",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@getpaseo/client": "^0.9.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stevecastaneda/paseo-progress",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "A live progress dashboard per worktree for Paseo, drawn from a file agents write with the paseo-progress command.",
5
5
  "license": "MIT",
6
6
  "author": "Steve Castaneda",
package/server/cli.ts CHANGED
@@ -1,13 +1,13 @@
1
1
  // The command agents run to record progress. Each command appends one event
2
2
  // to the worktree's progress file and prints what it recorded.
3
- import { existsSync, realpathSync, statSync } from "node:fs";
4
- import { dirname, join, relative, resolve } from "node:path";
3
+ import { realpathSync, statSync } from "node:fs";
4
+ import { relative, resolve } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { parseArgs } from "node:util";
7
7
  import { isSettled, type Dashboard, type NextIds } from "../shared/dashboard.ts";
8
8
  import { DELIVERABLE_KINDS, PROGRESS_FILE, TICKET_STATUSES } from "../shared/events.ts";
9
9
  import { appendProgress, InvalidEvent, readProgress, type EventDraft } from "./progress-file.ts";
10
- import { outside } from "./paths.ts";
10
+ import { findRoot, outside } from "./paths.ts";
11
11
  import { installLauncher } from "./launcher.ts";
12
12
  import { dashboardText } from "../shared/show.ts";
13
13
  import { imageMimeType } from "../shared/attachments.ts";
@@ -449,18 +449,6 @@ function help(): string {
449
449
  ].join("\n");
450
450
  }
451
451
 
452
- // The worktree root is the nearest directory with a `.git` entry (a folder in
453
- // a checkout, a file in a linked worktree). Outside git, use the directory.
454
- export function findRoot(cwd: string): string {
455
- let directory = resolve(cwd);
456
- while (true) {
457
- if (existsSync(join(directory, ".git"))) return directory;
458
- const parent = dirname(directory);
459
- if (parent === directory) return resolve(cwd);
460
- directory = parent;
461
- }
462
- }
463
-
464
452
  export async function runCli(argv: string[], options: CliOptions): Promise<number> {
465
453
  const out = options.out ?? ((line: string) => console.log(line));
466
454
  const now = options.now ?? (() => new Date());
@@ -3,6 +3,7 @@ import { createHash } from "node:crypto";
3
3
  import type { Dashboard } from "../shared/dashboard.ts";
4
4
  import type { AttentionResult, DashboardResult } from "../shared/rpc.ts";
5
5
  import { markPanelOpened, readProgress } from "./progress-file.ts";
6
+ import { findRoot } from "./paths.ts";
6
7
 
7
8
  const versions = new WeakMap<Dashboard, { panelOpened: boolean; version: string }>();
8
9
 
@@ -10,7 +11,7 @@ export async function handleMarkPanelOpened(
10
11
  input: { workspaceId: string; workspaceDirectory: string },
11
12
  _context: PluginHandlerContext,
12
13
  ) {
13
- await markPanelOpened(input.workspaceDirectory);
14
+ await markPanelOpened(findRoot(input.workspaceDirectory));
14
15
  return {};
15
16
  }
16
17
 
@@ -20,14 +21,15 @@ export async function handleGetDashboard(
20
21
  input: { workspaceId: string; workspaceDirectory: string; since?: string },
21
22
  _context: PluginHandlerContext,
22
23
  ): Promise<DashboardResult | { unchanged: true; version: string }> {
23
- const result = await readProgress(input.workspaceDirectory);
24
+ const root = findRoot(input.workspaceDirectory);
25
+ const result = await readProgress(root);
24
26
  let cached = versions.get(result.dashboard);
25
27
  if (!cached || cached.panelOpened !== result.panelOpened) {
26
28
  cached = { panelOpened: result.panelOpened, version: createHash("sha1").update(JSON.stringify(result)).digest("base64url") };
27
29
  versions.set(result.dashboard, cached);
28
30
  }
29
31
  const { version } = cached;
30
- return input.since === version ? { unchanged: true, version } : { ...result, version };
32
+ return input.since === version ? { unchanged: true, version } : { ...result, root, version };
31
33
  }
32
34
 
33
35
  // Just what a message-box pill needs, so polling every workspace stays small.
@@ -35,7 +37,7 @@ export async function handleGetAttention(
35
37
  input: { workspaceId: string; workspaceDirectory: string },
36
38
  _context: PluginHandlerContext,
37
39
  ): Promise<AttentionResult> {
38
- const { configured, dashboard, panelOpened } = await readProgress(input.workspaceDirectory);
40
+ const { configured, dashboard, panelOpened } = await readProgress(findRoot(input.workspaceDirectory));
39
41
  return {
40
42
  configured,
41
43
  questions: dashboard.questions.open.length,
package/server/open.ts CHANGED
@@ -6,7 +6,7 @@ import { join, relative, resolve } from "node:path";
6
6
  import { promisify } from "node:util";
7
7
  import { findAttachment, imageMimeType, previewKind } from "../shared/attachments.ts";
8
8
  import { readProgress } from "./progress-file.ts";
9
- import { outside } from "./paths.ts";
9
+ import { findRoot, outside } from "./paths.ts";
10
10
 
11
11
  export type Opener = (path: string) => Promise<void>;
12
12
 
@@ -43,10 +43,11 @@ export const systemOpener: Opener = async (path) => {
43
43
  // so the panel can never reach an arbitrary path. Only files inside the
44
44
  // worktree resolve.
45
45
  async function resolveAttachment(directory: string, ref: string, verb: string): Promise<{ path: string; target: string }> {
46
- const { dashboard } = await readProgress(directory);
46
+ const worktree = findRoot(directory);
47
+ const { dashboard } = await readProgress(worktree);
47
48
  const path = findAttachment(dashboard, ref)?.path;
48
49
  if (!path) throw new Error(`No local ${ref.startsWith("Q") ? "attachment" : "deliverable"} ${ref} in this worktree.`);
49
- const root = await realpath(directory);
50
+ const root = await realpath(worktree);
50
51
  const target = await realpath(resolve(root, path)).catch(() => null);
51
52
  if (!target) throw new Error(`${path} no longer exists.`);
52
53
  const inside = relative(root, target);
package/server/paths.ts CHANGED
@@ -1,7 +1,22 @@
1
- import { isAbsolute, sep } from "node:path";
1
+ import { existsSync } from "node:fs";
2
+ import { dirname, isAbsolute, join, resolve, sep } from "node:path";
2
3
 
3
4
  // Whether a path from relative(root, target) leaves the root. "..notes" is a
4
5
  // name inside it, not a step out.
5
6
  export function outside(inside: string): boolean {
6
7
  return inside === ".." || inside.startsWith(`..${sep}`) || isAbsolute(inside);
7
8
  }
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.
14
+ export function findRoot(cwd: string): string {
15
+ let directory = resolve(cwd);
16
+ while (true) {
17
+ if (existsSync(join(directory, ".git"))) return directory;
18
+ const parent = dirname(directory);
19
+ if (parent === directory) return resolve(cwd);
20
+ directory = parent;
21
+ }
22
+ }
package/shared/rpc.ts CHANGED
@@ -3,8 +3,10 @@ import { z } from "zod";
3
3
  import type { Dashboard } from "./dashboard.ts";
4
4
 
5
5
  // `panelOpened`: the Progress panel has been opened for this worktree before.
6
+ // `root`: the worktree root the file was read from, which stored paths are
7
+ // relative to. It can sit above the workspace directory.
6
8
  // `version` changes whenever anything else in the result does.
7
- export type DashboardResult = { configured: boolean; dashboard: Dashboard; panelOpened: boolean; version: string };
9
+ export type DashboardResult = { configured: boolean; dashboard: Dashboard; panelOpened: boolean; root: string; version: string };
8
10
 
9
11
  export const getDashboard = defineRpc({
10
12
  name: "progress.get",