mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
refactor: simplify worktree script spawning
This commit is contained in:
29
CHANGELOG.md
29
CHANGELOG.md
@@ -3,8 +3,33 @@
|
|||||||
## 0.1.60-beta.1 - 2026-04-20
|
## 0.1.60-beta.1 - 2026-04-20
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- Beta release channel for desktop updates, with a Settings toggle for receiving beta builds before they are promoted to stable.
|
- Scripts and services per worktree — define named commands in `paseo.json`, and long-running services get supervised with their own ports and nice proxy URLs like `http://web.my-app.localhost:6767`. See the [worktrees guide](https://paseo.sh/docs/worktrees).
|
||||||
- Release candidates are now called beta releases, starting with `0.1.60-beta.1`.
|
- Launch scripts and services for a worktree directly from the workspace header.
|
||||||
|
- New Setup tab in every workspace showing setup, teardown, and script progress live.
|
||||||
|
- GitHub checks and PR reviews in the explorer sidebar, with a hover card for the full breakdown.
|
||||||
|
- New worktree creation flow lets you pick a base branch or check out an existing GitHub pull request.
|
||||||
|
- Attach GitHub issues and pull requests to an agent as part of its prompt context.
|
||||||
|
- Pull request pane in the workspace sidebar.
|
||||||
|
- Redesigned Settings screen with modular section navigation.
|
||||||
|
- Per-host provider configuration — set providers, models, and credentials independently on each remote host.
|
||||||
|
- Direct Pi integration replaces the ACP bridge, with faster streaming and fewer hiccups.
|
||||||
|
- Beta release channel — opt in from Settings to receive beta desktop builds before they are promoted to stable.
|
||||||
|
- New-workspace picker ranks branches by recency with fast search.
|
||||||
|
|
||||||
|
### Improved
|
||||||
|
- Workspace and tab switching are dramatically faster on desktop and mobile — you can keep many workspaces open in parallel without lag.
|
||||||
|
- Provider refresh is reliable and no longer stalls on transient failures.
|
||||||
|
- Git and GitHub state stay in sync with local changes like commits, branch switches, and pushes.
|
||||||
|
- Composer attachments redesigned with a cleaner pill layout and an image lightbox.
|
||||||
|
- In-app notifications route to whichever surface you're actually looking at.
|
||||||
|
- Keyboard shortcuts keep working while Settings is open.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Composer textarea shrinks back down after sending on web.
|
||||||
|
- Branch switcher title no longer overflows on narrow rows.
|
||||||
|
- iOS image picker no longer leaves the screen unresponsive after cancelling.
|
||||||
|
- Archiving a worktree recovers cleanly if a previous attempt was interrupted.
|
||||||
|
- Images in agent messages with `~`-prefixed paths load instead of spinning forever.
|
||||||
|
|
||||||
## 0.1.59 - 2026-04-16
|
## 0.1.59 - 2026-04-16
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { scheduler } from "node:timers/promises";
|
|||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
import { findFreePort, ScriptRouteStore } from "./script-proxy.js";
|
import { findFreePort, ScriptRouteStore } from "./script-proxy.js";
|
||||||
import { ScriptHealthMonitor, type ScriptHealthEntry } from "./script-health-monitor.js";
|
import { ScriptHealthMonitor, type ScriptHealthEntry } from "./script-health-monitor.js";
|
||||||
import { spawnWorktreeScripts } from "./worktree-bootstrap.js";
|
import { spawnWorkspaceScript } from "./worktree-bootstrap.js";
|
||||||
import { WorkspaceScriptRuntimeStore } from "./workspace-script-runtime-store.js";
|
import { WorkspaceScriptRuntimeStore } from "./workspace-script-runtime-store.js";
|
||||||
|
|
||||||
type TcpServerHandle = {
|
type TcpServerHandle = {
|
||||||
@@ -433,15 +433,19 @@ describe("ScriptHealthMonitor", () => {
|
|||||||
[];
|
[];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await spawnWorktreeScripts({
|
for (const scriptName of ["typecheck", "api"]) {
|
||||||
repoRoot: workspace.repoDir,
|
await spawnWorkspaceScript({
|
||||||
workspaceId: workspace.repoDir,
|
repoRoot: workspace.repoDir,
|
||||||
branchName: null,
|
workspaceId: workspace.repoDir,
|
||||||
daemonPort: null,
|
projectSlug: "repo",
|
||||||
routeStore,
|
branchName: null,
|
||||||
runtimeStore,
|
scriptName,
|
||||||
terminalManager: createStubTerminalManager(createTerminalCalls) as any,
|
daemonPort: null,
|
||||||
});
|
routeStore,
|
||||||
|
runtimeStore,
|
||||||
|
terminalManager: createStubTerminalManager(createTerminalCalls) as any,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
expect(createTerminalCalls).toHaveLength(2);
|
expect(createTerminalCalls).toHaveLength(2);
|
||||||
expect(routeStore.listRoutes()).toEqual([
|
expect(routeStore.listRoutes()).toEqual([
|
||||||
|
|||||||
@@ -5,11 +5,7 @@ import { join } from "path";
|
|||||||
import { tmpdir } from "os";
|
import { tmpdir } from "os";
|
||||||
|
|
||||||
import type { AgentTimelineItem } from "./agent/agent-sdk-types.js";
|
import type { AgentTimelineItem } from "./agent/agent-sdk-types.js";
|
||||||
import {
|
import { runAsyncWorktreeBootstrap, spawnWorkspaceScript } from "./worktree-bootstrap.js";
|
||||||
runAsyncWorktreeBootstrap,
|
|
||||||
spawnWorkspaceScript,
|
|
||||||
spawnWorktreeScripts,
|
|
||||||
} from "./worktree-bootstrap.js";
|
|
||||||
import { ensureWorkspaceServicePortPlan } from "./workspace-service-port-registry.js";
|
import { ensureWorkspaceServicePortPlan } from "./workspace-service-port-registry.js";
|
||||||
import { ScriptRouteStore } from "./script-proxy.js";
|
import { ScriptRouteStore } from "./script-proxy.js";
|
||||||
import { createBranchChangeRouteHandler } from "./script-route-branch-handler.js";
|
import { createBranchChangeRouteHandler } from "./script-route-branch-handler.js";
|
||||||
@@ -717,17 +713,19 @@ describe("runAsyncWorktreeBootstrap", () => {
|
|||||||
const createTerminalCalls: CreateTerminalCall[] = [];
|
const createTerminalCalls: CreateTerminalCall[] = [];
|
||||||
const terminalRecords: StubTerminalRecord[] = [];
|
const terminalRecords: StubTerminalRecord[] = [];
|
||||||
|
|
||||||
const results = await spawnWorktreeScripts({
|
const result = await spawnWorkspaceScript({
|
||||||
repoRoot: repoDir,
|
repoRoot: repoDir,
|
||||||
workspaceId: repoDir,
|
workspaceId: repoDir,
|
||||||
|
projectSlug: "repo",
|
||||||
branchName: "feature-socket-service",
|
branchName: "feature-socket-service",
|
||||||
|
scriptName: "web",
|
||||||
daemonPort: null,
|
daemonPort: null,
|
||||||
routeStore,
|
routeStore,
|
||||||
runtimeStore,
|
runtimeStore,
|
||||||
terminalManager: createStubTerminalManager(createTerminalCalls, terminalRecords),
|
terminalManager: createStubTerminalManager(createTerminalCalls, terminalRecords),
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(results).toHaveLength(1);
|
expect(result).toBeDefined();
|
||||||
expect(routeStore.listRoutes()).toEqual([]);
|
expect(routeStore.listRoutes()).toEqual([]);
|
||||||
expect(createTerminalCalls).toHaveLength(1);
|
expect(createTerminalCalls).toHaveLength(1);
|
||||||
expect(createTerminalCalls[0]?.cwd).toBe(repoDir);
|
expect(createTerminalCalls[0]?.cwd).toBe(repoDir);
|
||||||
@@ -1344,15 +1342,19 @@ describe("runAsyncWorktreeBootstrap", () => {
|
|||||||
const terminalManager = createTerminalManager();
|
const terminalManager = createTerminalManager();
|
||||||
realTerminalManagers.push(terminalManager);
|
realTerminalManagers.push(terminalManager);
|
||||||
|
|
||||||
await spawnWorktreeScripts({
|
for (const scriptName of ["api", "web"]) {
|
||||||
repoRoot: repoDir,
|
await spawnWorkspaceScript({
|
||||||
workspaceId: repoDir,
|
repoRoot: repoDir,
|
||||||
branchName: "feature-peer-env",
|
workspaceId: repoDir,
|
||||||
daemonPort: 6767,
|
projectSlug: "repo",
|
||||||
routeStore,
|
branchName: "feature-peer-env",
|
||||||
runtimeStore,
|
scriptName,
|
||||||
terminalManager,
|
daemonPort: 6767,
|
||||||
});
|
routeStore,
|
||||||
|
runtimeStore,
|
||||||
|
terminalManager,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const apiEnvPath = join(repoDir, "api-env.json");
|
const apiEnvPath = join(repoDir, "api-env.json");
|
||||||
const webEnvPath = join(repoDir, "web-env.json");
|
const webEnvPath = join(repoDir, "web-env.json");
|
||||||
@@ -1422,10 +1424,12 @@ describe("runAsyncWorktreeBootstrap", () => {
|
|||||||
const runtimeStore = new WorkspaceScriptRuntimeStore();
|
const runtimeStore = new WorkspaceScriptRuntimeStore();
|
||||||
const createTerminalCalls: CreateTerminalCall[] = [];
|
const createTerminalCalls: CreateTerminalCall[] = [];
|
||||||
|
|
||||||
await spawnWorktreeScripts({
|
await spawnWorkspaceScript({
|
||||||
repoRoot: repoDir,
|
repoRoot: repoDir,
|
||||||
workspaceId: repoDir,
|
workspaceId: repoDir,
|
||||||
|
projectSlug: "repo",
|
||||||
branchName: "feature-remote-service",
|
branchName: "feature-remote-service",
|
||||||
|
scriptName: "web",
|
||||||
daemonPort: 6767,
|
daemonPort: 6767,
|
||||||
daemonListenHost: "100.64.0.20",
|
daemonListenHost: "100.64.0.20",
|
||||||
routeStore,
|
routeStore,
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import type { Logger } from "pino";
|
|||||||
import type { TerminalManager } from "../terminal/terminal-manager.js";
|
import type { TerminalManager } from "../terminal/terminal-manager.js";
|
||||||
import type { TerminalSession } from "../terminal/terminal.js";
|
import type { TerminalSession } from "../terminal/terminal.js";
|
||||||
import { buildScriptHostname } from "../utils/script-hostname.js";
|
import { buildScriptHostname } from "../utils/script-hostname.js";
|
||||||
import { deriveProjectSlug } from "./workspace-git-metadata.js";
|
|
||||||
import {
|
import {
|
||||||
getScriptConfigs,
|
getScriptConfigs,
|
||||||
getWorktreeTerminalSpecs,
|
getWorktreeTerminalSpecs,
|
||||||
@@ -922,39 +921,6 @@ export async function spawnWorkspaceScript(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function spawnWorktreeScripts(options: {
|
|
||||||
repoRoot: string;
|
|
||||||
workspaceId: string;
|
|
||||||
branchName: string | null;
|
|
||||||
daemonPort?: number | null;
|
|
||||||
daemonListenHost?: string | null;
|
|
||||||
routeStore: ScriptRouteStore;
|
|
||||||
runtimeStore: WorkspaceScriptRuntimeStore;
|
|
||||||
terminalManager: TerminalManager;
|
|
||||||
logger?: Logger;
|
|
||||||
onLifecycleChanged?: () => void;
|
|
||||||
}): Promise<WorktreeScriptResult[]> {
|
|
||||||
const { repoRoot } = options;
|
|
||||||
const projectSlug = deriveProjectSlug(repoRoot);
|
|
||||||
const scriptConfigs = getScriptConfigs(repoRoot);
|
|
||||||
if (scriptConfigs.size === 0) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const results: WorktreeScriptResult[] = [];
|
|
||||||
for (const scriptName of scriptConfigs.keys()) {
|
|
||||||
results.push(
|
|
||||||
await spawnWorkspaceScript({
|
|
||||||
...options,
|
|
||||||
projectSlug,
|
|
||||||
scriptName,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function teardownWorktreeScripts(options: {
|
export function teardownWorktreeScripts(options: {
|
||||||
hostnames: string[];
|
hostnames: string[];
|
||||||
routeStore: ScriptRouteStore;
|
routeStore: ScriptRouteStore;
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import {
|
|||||||
buildWorktreeSetupDetail,
|
buildWorktreeSetupDetail,
|
||||||
createWorktreeSetupProgressAccumulator,
|
createWorktreeSetupProgressAccumulator,
|
||||||
getWorktreeSetupProgressResults,
|
getWorktreeSetupProgressResults,
|
||||||
spawnWorktreeScripts,
|
|
||||||
} from "./worktree-bootstrap.js";
|
} from "./worktree-bootstrap.js";
|
||||||
import type { TerminalManager } from "../terminal/terminal-manager.js";
|
import type { TerminalManager } from "../terminal/terminal-manager.js";
|
||||||
import type { ScriptRouteStore } from "./script-proxy.js";
|
import type { ScriptRouteStore } from "./script-proxy.js";
|
||||||
@@ -738,28 +737,6 @@ export async function runWorktreeSetupInBackground(
|
|||||||
emitSetupProgress("completed", null);
|
emitSetupProgress("completed", null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
|
||||||
options.shouldBootstrap &&
|
|
||||||
dependencies.terminalManager &&
|
|
||||||
dependencies.scriptRouteStore &&
|
|
||||||
dependencies.scriptRuntimeStore
|
|
||||||
) {
|
|
||||||
await spawnWorktreeScripts({
|
|
||||||
repoRoot: worktree.worktreePath,
|
|
||||||
workspaceId: options.workspaceId,
|
|
||||||
branchName: worktree.branchName,
|
|
||||||
daemonPort: dependencies.getDaemonTcpPort?.() ?? null,
|
|
||||||
daemonListenHost: dependencies.getDaemonTcpHost?.() ?? null,
|
|
||||||
routeStore: dependencies.scriptRouteStore,
|
|
||||||
runtimeStore: dependencies.scriptRuntimeStore,
|
|
||||||
terminalManager: dependencies.terminalManager,
|
|
||||||
logger: dependencies.sessionLogger,
|
|
||||||
onLifecycleChanged: () => {
|
|
||||||
dependencies.onScriptsChanged?.(options.workspaceId, worktree.worktreePath);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof WorktreeSetupError) {
|
if (error instanceof WorktreeSetupError) {
|
||||||
setupResults = error.results;
|
setupResults = error.results;
|
||||||
|
|||||||
Reference in New Issue
Block a user