feat: improve agent workspace flows and directory suggestions

This commit is contained in:
Mohamed Boudra
2026-02-16 23:19:01 +07:00
parent 22da0951d5
commit 170ee27813
49 changed files with 3053 additions and 293 deletions
+81 -14
View File
@@ -4,7 +4,13 @@ import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync } from "fs";
import { join, basename, dirname, resolve, sep } from "path";
import net from "node:net";
import { createNameId } from "mnemonic-id";
import { normalizeBaseRefName, writePaseoWorktreeMetadata } from "./worktree-metadata.js";
import {
normalizeBaseRefName,
readPaseoWorktreeMetadata,
readPaseoWorktreeRuntimePort,
writePaseoWorktreeMetadata,
writePaseoWorktreeRuntimeMetadata,
} from "./worktree-metadata.js";
import { resolvePaseoHome } from "../server/paseo-home.js";
interface PaseoConfig {
@@ -26,6 +32,14 @@ export interface WorktreeConfig {
worktreePath: string;
}
export type WorktreeRuntimeEnv = {
PASEO_SOURCE_CHECKOUT_PATH: string;
PASEO_ROOT_PATH: string;
PASEO_WORKTREE_PATH: string;
PASEO_BRANCH_NAME: string;
PASEO_WORKTREE_PORT: string;
};
export type WorktreeSetupCommandResult = {
command: string;
cwd: string;
@@ -327,6 +341,29 @@ async function getAvailablePort(): Promise<number> {
});
}
async function assertPortAvailable(port: number): Promise<void> {
await new Promise<void>((resolve, reject) => {
const server = net.createServer();
server.once("error", (error: NodeJS.ErrnoException) => {
const message = error?.code === "EADDRINUSE"
? `Persisted worktree port ${port} is already in use`
: error instanceof Error
? error.message
: String(error);
reject(new Error(message));
});
server.listen(port, () => {
server.close((error) => {
if (error) {
reject(error);
return;
}
resolve();
});
});
});
}
async function inferRepoRootPathFromWorktreePath(worktreePath: string): Promise<string> {
try {
const commonDir = await getGitCommonDir(worktreePath);
@@ -360,6 +397,7 @@ export async function runWorktreeSetupCommands(options: {
branchName: string;
cleanupOnFailure: boolean;
repoRootPath?: string;
runtimeEnv?: WorktreeRuntimeEnv;
onEvent?: (event: WorktreeSetupCommandProgressEvent) => void;
}): Promise<WorktreeSetupCommandResult[]> {
// Read paseo.json from the worktree (it will have the same content as the source repo)
@@ -368,21 +406,16 @@ export async function runWorktreeSetupCommands(options: {
return [];
}
const repoRootPath =
options.repoRootPath ?? (await inferRepoRootPathFromWorktreePath(options.worktreePath));
const worktreePort = await getAvailablePort();
const runtimeEnv =
options.runtimeEnv ??
(await resolveWorktreeRuntimeEnv({
worktreePath: options.worktreePath,
branchName: options.branchName,
...(options.repoRootPath ? { repoRootPath: options.repoRootPath } : {}),
}));
const setupEnv = {
...process.env,
// Source checkout path is the original git repo root (shared across worktrees), not the
// worktree itself. This allows setup scripts to copy local files (e.g. .env) from the
// source checkout.
PASEO_SOURCE_CHECKOUT_PATH: repoRootPath,
// Backward-compatible alias.
PASEO_ROOT_PATH: repoRootPath,
PASEO_WORKTREE_PATH: options.worktreePath,
PASEO_BRANCH_NAME: options.branchName,
PASEO_WORKTREE_PORT: String(worktreePort),
...runtimeEnv,
};
const results: WorktreeSetupCommandResult[] = [];
@@ -439,6 +472,40 @@ async function resolveBranchNameForWorktreePath(worktreePath: string): Promise<s
return basename(worktreePath);
}
export async function resolveWorktreeRuntimeEnv(options: {
worktreePath: string;
branchName?: string;
repoRootPath?: string;
}): Promise<WorktreeRuntimeEnv> {
const repoRootPath =
options.repoRootPath ?? (await inferRepoRootPathFromWorktreePath(options.worktreePath));
const branchName =
options.branchName ?? (await resolveBranchNameForWorktreePath(options.worktreePath));
let worktreePort = readPaseoWorktreeRuntimePort(options.worktreePath);
if (worktreePort === null) {
worktreePort = await getAvailablePort();
const metadata = readPaseoWorktreeMetadata(options.worktreePath);
if (metadata) {
writePaseoWorktreeRuntimeMetadata(options.worktreePath, { worktreePort });
}
} else {
await assertPortAvailable(worktreePort);
}
return {
// Source checkout path is the original git repo root (shared across worktrees), not the
// worktree itself. This allows setup scripts to copy local files (e.g. .env) from the
// source checkout.
PASEO_SOURCE_CHECKOUT_PATH: repoRootPath,
// Backward-compatible alias.
PASEO_ROOT_PATH: repoRootPath,
PASEO_WORKTREE_PATH: options.worktreePath,
PASEO_BRANCH_NAME: branchName,
PASEO_WORKTREE_PORT: String(worktreePort),
};
}
export async function runWorktreeDestroyCommands(options: {
worktreePath: string;
branchName?: string;