feat: terminal reattach, explorer tab memory, worktree terminals

This commit is contained in:
Mohamed Boudra
2026-02-15 14:05:29 +07:00
parent 1f72ce63cd
commit 9244f1fbd6
31 changed files with 2009 additions and 385 deletions
@@ -2,6 +2,7 @@ import { describe, it, expect, beforeEach, afterEach } from "vitest";
import {
createWorktree,
deletePaseoWorktree,
getWorktreeTerminalSpecs,
isPaseoOwnedWorktreeCwd,
listPaseoWorktrees,
slugify,
@@ -262,6 +263,43 @@ describe("createWorktree", () => {
// Verify worktree was cleaned up
expect(existsSync(expectedWorktreePath)).toBe(false);
});
it("reads worktree terminal specs from paseo.json with optional name", async () => {
const paseoConfig = {
worktree: {
terminals: [
{ name: "Dev Server", command: "npm run dev" },
{ command: "cd packages/app && npm run dev" },
],
},
};
writeFileSync(join(repoDir, "paseo.json"), JSON.stringify(paseoConfig));
expect(getWorktreeTerminalSpecs(repoDir)).toEqual([
{ name: "Dev Server", command: "npm run dev" },
{ command: "cd packages/app && npm run dev" },
]);
});
it("filters invalid worktree terminal specs", async () => {
const paseoConfig = {
worktree: {
terminals: [
null,
{},
{ name: " ", command: " " },
{ name: " Watch ", command: "npm run watch", cwd: "packages/app" },
{ name: 123, command: "npm run test" },
],
},
};
writeFileSync(join(repoDir, "paseo.json"), JSON.stringify(paseoConfig));
expect(getWorktreeTerminalSpecs(repoDir)).toEqual([
{ name: "Watch", command: "npm run watch" },
{ command: "npm run test" },
]);
});
});
describe("paseo worktree manager", () => {
+42
View File
@@ -11,6 +11,7 @@ interface PaseoConfig {
worktree?: {
setup?: string[];
destroy?: string[];
terminals?: WorktreeTerminalConfig[];
};
}
@@ -33,6 +34,11 @@ export type WorktreeSetupCommandResult = {
exitCode: number | null;
};
export interface WorktreeTerminalConfig {
name?: string;
command: string;
}
export class WorktreeSetupError extends Error {
readonly results: WorktreeSetupCommandResult[];
@@ -107,6 +113,42 @@ export function getWorktreeDestroyCommands(repoRoot: string): string[] {
return destroyCommands.filter((cmd) => typeof cmd === "string" && cmd.trim().length > 0);
}
export function getWorktreeTerminalSpecs(repoRoot: string): WorktreeTerminalConfig[] {
const config = readPaseoConfig(repoRoot);
const terminals = config?.worktree?.terminals;
if (!Array.isArray(terminals) || terminals.length === 0) {
return [];
}
const specs: WorktreeTerminalConfig[] = [];
for (const terminal of terminals) {
if (!terminal || typeof terminal !== "object") {
continue;
}
const rawCommand = terminal.command;
if (typeof rawCommand !== "string") {
continue;
}
const command = rawCommand.trim();
if (!command) {
continue;
}
const rawName = terminal.name;
const name = typeof rawName === "string" && rawName.trim().length > 0
? rawName.trim()
: undefined;
specs.push({
...(name ? { name } : {}),
command,
});
}
return specs;
}
async function execSetupCommand(
command: string,
options: { cwd: string; env: NodeJS.ProcessEnv }