This repository has been archived on 2026-08-23. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
pi-web/src/server/sessions/spawnTargetResolver.ts
T
Federico Jaramillo Martinez 69b125b001 feat(sessions): surface cross-worktree parent and child sessions
A session spawned into another worktree recorded a parent that no
listing contained, so the row showed only "parent unavailable" and its
parent's row looked childless. Both facts were accurate and useless:
neither said where the related session actually was.

Report both directions from the session store instead. A missing
parent's cwd and id come from its own file header, so one 4 KB read per
distinct missing parent resolves it without listing other workspaces;
children are counted by listing sibling workspaces and matching the
parent path they already recorded, needing no header reads. Reads are
memoized per path because Pi writes headers once, and the cache is
released on dispose. Both directions are best-effort: an unreadable
header or an unlistable worktree leaves a session unannotated rather
than failing the listing.

In the browser, an orphan child keeps the same child marker as a nested
one, dimmed, so it no longer renders as a root; whereabouts are stated
once on the meta line ("parent in feature/foo", "2 children
elsewhere"), where a clamped title cannot hide them. A "Go to parent
session" action switches to the owning workspace and selects the
parent. Live session.created events keep child counts current instead
of leaving them stale until the next listing.

Session and workspace paths reach the browser from two producers: store
enumeration for a listing, and the live runtime for a broadcast. They
are now compared through one normalizing helper, so tree nesting and
child counts cannot silently miss a link when only a trailing separator
differs.

Extract the shared "workspaces of the project containing this cwd"
lookup out of ProjectScopedSpawnTargetResolver so spawn targeting and
child counting share one implementation, and register it regardless of
whether spawning is enabled: children can predate a config change, and
the tree should stay honest about them either way.
2026-07-28 11:56:21 +02:00

59 lines
2.8 KiB
TypeScript

import { cwdPathsEqual } from "../workingDirectory.js";
import { RegisteredProjectWorkspaceCwds, type ProjectWorkspaceCwds, type ProjectWorkspaceCwdsDeps } from "../workspaces/projectWorkspaceCwds.js";
/**
* Decision describing whether a LLM-spawned session may target a given cwd.
*
* - `allowed: true` carries the canonical workspace path to start the session in
* (always one of the project's known workspace paths, so it is guaranteed
* visible in the web UI).
* - `not-registered` means the spawning session's cwd belongs to no registered
* project, so spawning must be refused to preserve visibility.
* - `out-of-project` means the requested cwd is not a workspace of the spawning
* session's project; `allowedCwds` lists the valid targets for the caller to
* surface.
*/
export type SpawnTargetDecision =
| { allowed: true; cwd: string }
| { allowed: false; reason: "not-registered" }
| { allowed: false; reason: "out-of-project"; allowedCwds: string[] };
/**
* Owns the rule that keeps LLM-spawned sessions visible: a spawned session may
* only target a workspace (worktree, or root) of the registered project that
* owns the spawning session. The rule is evaluated live so a worktree the agent
* just created with `git worktree add` is included.
*/
export interface SpawnTargetResolver {
/**
* Decide whether a session spawned from `spawningCwd` may target
* `requestedCwd` (defaulting to `spawningCwd` when omitted), returning the
* canonical target cwd when allowed.
*/
resolveSpawnTarget(spawningCwd: string, requestedCwd: string | undefined): Promise<SpawnTargetDecision>;
}
export type ProjectScopedSpawnTargetResolverDeps = ProjectWorkspaceCwdsDeps;
/**
* Default resolver composing the project registry and live worktree discovery.
* It finds the registered project whose current workspace set contains the
* spawning session's cwd, then validates the requested target against that set.
*/
export class ProjectScopedSpawnTargetResolver implements SpawnTargetResolver {
private readonly projectWorkspaces: ProjectWorkspaceCwds;
constructor(deps: ProjectScopedSpawnTargetResolverDeps) {
this.projectWorkspaces = new RegisteredProjectWorkspaceCwds(deps);
}
async resolveSpawnTarget(spawningCwd: string, requestedCwd: string | undefined): Promise<SpawnTargetDecision> {
const allowedCwds = await this.projectWorkspaces.forCwd(spawningCwd);
if (allowedCwds === undefined) return { allowed: false, reason: "not-registered" };
const target = requestedCwd === undefined || requestedCwd === "" ? spawningCwd : requestedCwd;
const match = allowedCwds.find((path) => cwdPathsEqual(path, target));
if (match === undefined) return { allowed: false, reason: "out-of-project", allowedCwds };
return { allowed: true, cwd: match };
}
}