Simplify workspace creation with inline worktree API

Replace the multi-step new-agent route flow with a single
create_paseo_worktree endpoint that registers the workspace immediately
and creates the git worktree in the background. The sidebar now calls
this endpoint directly and shows a creating spinner inline.

Also auto-resolves the base branch from origin/HEAD when not explicitly
provided, removes the unused Tauri WebSocket transport layer, and
normalizes loopback endpoints to localhost.

Closes #125
This commit is contained in:
Mohamed Boudra
2026-03-19 23:33:35 +07:00
parent 3d4ac57bd0
commit 6ab97c579e
21 changed files with 514 additions and 531 deletions
@@ -1,7 +1,6 @@
import { describe, expect, it } from "vitest";
import {
buildHostAgentDetailRoute,
buildHostNewAgentRoute,
buildHostRootRoute,
buildHostWorkspaceAgentRoute,
buildHostWorkspaceFileRoute,
@@ -66,16 +65,6 @@ describe("workspace route parsing", () => {
expect(buildHostRootRoute("local")).toBe("/h/local");
});
it("builds host new-agent routes", () => {
expect(buildHostNewAgentRoute("local")).toBe("/h/local/new-agent");
expect(
buildHostNewAgentRoute("local", {
workingDir: "/tmp/repo",
worktreeMode: "create",
})
).toBe("/h/local/new-agent?workingDir=%2Ftmp%2Frepo&worktreeMode=create");
});
it("builds workspace routes with open intent query", () => {
expect(buildHostWorkspaceAgentRoute("local", "/tmp/repo", "agent-1")).toBe(
"/h/local/workspace/L3RtcC9yZXBv?open=agent%3Aagent-1"
-37
View File
@@ -399,40 +399,6 @@ export function buildHostAgentsRoute(serverId: string): string {
return `${base}/agents`;
}
export function buildHostNewAgentRoute(
serverId: string,
options?: {
workingDir?: string;
provider?: string;
modeId?: string;
model?: string;
thinkingOptionId?: string;
worktreeMode?: "create" | "attach";
}
): string {
const base = buildHostRootRoute(serverId);
if (base === "/") {
return "/";
}
const searchParams = new URLSearchParams();
const entries: Array<[string, string | undefined]> = [
["workingDir", options?.workingDir],
["provider", options?.provider],
["modeId", options?.modeId],
["model", options?.model],
["thinkingOptionId", options?.thinkingOptionId],
["worktreeMode", options?.worktreeMode],
];
for (const [key, value] of entries) {
const normalized = trimNonEmpty(value);
if (normalized) {
searchParams.set(key, normalized);
}
}
const search = searchParams.toString();
return search ? `${base}/new-agent?${search}` : `${base}/new-agent`;
}
export function buildHostOpenProjectRoute(serverId: string): string {
const base = buildHostRootRoute(serverId);
if (base === "/") {
@@ -466,9 +432,6 @@ export function mapPathnameToServer(
if (suffix.startsWith("agents")) {
return `${base}/agents`;
}
if (suffix.startsWith("new-agent")) {
return `${base}/new-agent`;
}
if (suffix.startsWith("open-project")) {
return `${base}/open-project`;
}
@@ -1,143 +0,0 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
const tauriMock = vi.hoisted(() => {
let listenerCleanup: (() => void) | null = null;
let disconnectCount = 0;
const connect = vi.fn(async () => {
let listenerActive = false;
return {
addListener: vi.fn(() => {
listenerActive = true;
listenerCleanup = vi.fn(() => {
if (!listenerActive) {
throw new TypeError(
"undefined is not an object (evaluating 'listeners[eventId].handlerId')"
);
}
listenerActive = false;
});
return listenerCleanup;
}),
send: vi.fn(async () => undefined),
disconnect: vi.fn(async () => {
disconnectCount += 1;
listenerCleanup?.();
}),
};
});
return {
connect,
getListenerCleanup: () => listenerCleanup,
getDisconnectCount: () => disconnectCount,
resetState: () => {
listenerCleanup = null;
disconnectCount = 0;
},
};
});
vi.mock("./tauri", () => ({
getTauri: () => ({
websocket: {
connect: tauriMock.connect,
},
}),
}));
describe("tauri-daemon-transport", () => {
beforeEach(() => {
tauriMock.connect.mockClear();
tauriMock.resetState();
});
it("does not unregister the websocket listener twice during close", async () => {
const mod = await import("./tauri-daemon-transport");
const transportFactory = mod.createTauriWebSocketTransportFactory();
expect(transportFactory).not.toBeNull();
const transport = transportFactory!({ url: "ws://localhost:6767/ws" });
await Promise.resolve();
expect(() => transport.close()).not.toThrow();
await Promise.resolve();
expect(tauriMock.getListenerCleanup()).toHaveBeenCalledTimes(1);
});
it("disconnects the websocket only once after the connection is already open", async () => {
const mod = await import("./tauri-daemon-transport");
const transportFactory = mod.createTauriWebSocketTransportFactory();
expect(transportFactory).not.toBeNull();
const transport = transportFactory!({ url: "ws://localhost:6767/ws" });
await Promise.resolve();
await Promise.resolve();
expect(() => transport.close()).not.toThrow();
await Promise.resolve();
expect(tauriMock.getDisconnectCount()).toBe(1);
expect(tauriMock.getListenerCleanup()).toHaveBeenCalledTimes(1);
});
it("disconnects only once when close happens during async connect cleanup", async () => {
let resolveConnect!: (socket: any) => void;
let resolveSend!: () => void;
tauriMock.connect.mockImplementationOnce(
() =>
new Promise((resolve) => {
resolveConnect = resolve;
})
);
const mod = await import("./tauri-daemon-transport");
const transportFactory = mod.createTauriWebSocketTransportFactory();
expect(transportFactory).not.toBeNull();
const disconnect = vi.fn(async () => undefined);
const addListener = vi.fn(() => vi.fn());
const send = vi.fn(
() =>
new Promise<void>((resolve) => {
resolveSend = resolve;
})
);
const transport = transportFactory!({ url: "ws://localhost:6767/ws" });
transport.send("queued-before-connect");
resolveConnect({ addListener, send, disconnect });
await Promise.resolve();
transport.close();
resolveSend();
await Promise.resolve();
await Promise.resolve();
expect(disconnect).toHaveBeenCalledTimes(1);
});
it("swallows the tauri duplicate-listener cleanup error when disconnect throws synchronously", async () => {
tauriMock.connect.mockImplementationOnce(async () => ({
addListener: vi.fn(() => vi.fn()),
send: vi.fn(async () => undefined),
disconnect: vi.fn(() => {
throw new TypeError(
"undefined is not an object (evaluating 'listeners[eventId].handlerId')"
);
}),
}));
const mod = await import("./tauri-daemon-transport");
const transportFactory = mod.createTauriWebSocketTransportFactory();
expect(transportFactory).not.toBeNull();
const transport = transportFactory!({ url: "ws://localhost:6767/ws" });
await Promise.resolve();
expect(() => transport.close()).not.toThrow();
});
});
@@ -1,256 +0,0 @@
import type { DaemonTransport, DaemonTransportFactory } from "@server/client/daemon-client";
import { getTauri } from "@/utils/tauri";
type TauriWebSocketMessage =
| { type: "Text"; data: string }
| { type: "Binary"; data: number[] }
| { type: "Ping"; data: number[] }
| { type: "Pong"; data: number[] }
| { type: "Close"; data: { code: number; reason: string } | null };
type TauriWebSocketConnection = {
addListener(cb: (msg: TauriWebSocketMessage) => void): () => void;
send(message: string | number[] | TauriWebSocketMessage): Promise<void>;
disconnect(): Promise<void>;
};
type TauriWebSocketModule = {
connect(url: string, config?: unknown): Promise<TauriWebSocketConnection>;
};
function isTauriDuplicateListenerCleanupError(error: unknown): boolean {
return (
error instanceof TypeError &&
error.message.includes("listeners[eventId].handlerId")
);
}
function toTauriOutgoingMessage(
data: string | Uint8Array | ArrayBuffer
): string | number[] {
if (typeof data === "string") {
return data;
}
if (data instanceof ArrayBuffer) {
return Array.from(new Uint8Array(data));
}
return Array.from(data);
}
function getTauriWebSocketModule(): TauriWebSocketModule | null {
const ws = getTauri()?.websocket;
if (ws && typeof ws.connect === "function") {
return ws as TauriWebSocketModule;
}
return null;
}
export function createTauriWebSocketTransportFactory(): DaemonTransportFactory | null {
if (getTauri() === null) {
return null;
}
return ({ url }) => {
let ws: TauriWebSocketConnection | null = null;
let wsListenerCleanup: (() => void) | null = null;
let disposed = false;
let opened = false;
let closeEmitted = false;
let disconnectPromise: Promise<void> | null = null;
const openHandlers = new Set<() => void>();
const closeHandlers = new Set<(event?: unknown) => void>();
const errorHandlers = new Set<(event?: unknown) => void>();
const messageHandlers = new Set<(data: unknown) => void>();
const pendingSends: Array<string | number[]> = [];
let closeRequested: { code?: number; reason?: string } | null = null;
const emitOpen = () => {
if (disposed || opened) return;
opened = true;
for (const handler of openHandlers) {
try {
handler();
} catch {
// no-op
}
}
};
const emitClose = (event?: unknown) => {
if (disposed || closeEmitted) return;
closeEmitted = true;
for (const handler of closeHandlers) {
try {
handler(event);
} catch {
// no-op
}
}
};
const emitError = (event?: unknown) => {
if (disposed) return;
for (const handler of errorHandlers) {
try {
handler(event);
} catch {
// no-op
}
}
};
const emitMessage = (data: unknown) => {
if (disposed) return;
for (const handler of messageHandlers) {
try {
handler(data);
} catch {
// no-op
}
}
};
const disconnectSocket = (socket: TauriWebSocketConnection) => {
if (disconnectPromise) {
return disconnectPromise;
}
disconnectPromise = Promise.resolve()
.then(() => socket.disconnect())
.catch((error) => {
if (!isTauriDuplicateListenerCleanupError(error)) {
emitError(error);
}
})
.then(() => undefined);
return disconnectPromise;
};
const connect = async () => {
try {
let module: TauriWebSocketModule | null = getTauriWebSocketModule();
if (!module) {
const start = Date.now();
while (!module && Date.now() - start < 2000) {
await new Promise((r) => setTimeout(r, 50));
module = getTauriWebSocketModule();
}
}
if (!module) {
throw new Error(
"Tauri WebSocket plugin is not available (expected getTauri().websocket). Did you enable tauri-plugin-websocket and websocket:default capability?"
);
}
const socket = await module.connect(url);
if (disposed) {
await disconnectSocket(socket);
return;
}
ws = socket;
wsListenerCleanup = ws.addListener((msg) => {
if (msg.type === "Text") {
emitMessage({ data: msg.data });
return;
}
if (msg.type === "Binary") {
emitMessage({ data: new Uint8Array(msg.data) });
return;
}
if (msg.type === "Close") {
emitClose(msg.data ?? undefined);
return;
}
});
emitOpen();
while (pendingSends.length > 0) {
const next = pendingSends.shift();
if (!next) break;
try {
await ws.send(next);
} catch (error) {
emitError(error);
}
}
if (closeRequested) {
await disconnectSocket(ws);
emitClose(closeRequested);
}
} catch (error) {
emitError(error);
}
};
void connect();
const transport: DaemonTransport = {
send: (data) => {
if (disposed) return;
const outgoing = toTauriOutgoingMessage(data);
if (!ws) {
pendingSends.push(outgoing);
return;
}
void ws.send(outgoing).catch((error) => emitError(error));
},
close: (code?: number, reason?: string) => {
if (disposed) return;
closeRequested = { code, reason };
if (!ws) return;
void disconnectSocket(ws).finally(() => emitClose(closeRequested));
},
onMessage: (handler) => {
messageHandlers.add(handler);
return () => messageHandlers.delete(handler);
},
onOpen: (handler) => {
openHandlers.add(handler);
if (opened) {
try {
handler();
} catch {
// no-op
}
}
return () => openHandlers.delete(handler);
},
onClose: (handler) => {
closeHandlers.add(handler);
if (closeEmitted) {
try {
handler(closeRequested ?? undefined);
} catch {
// no-op
}
}
return () => closeHandlers.delete(handler);
},
onError: (handler) => {
errorHandlers.add(handler);
return () => errorHandlers.delete(handler);
},
};
return {
...transport,
close: (code?: number, reason?: string) => {
wsListenerCleanup = null;
try {
transport.close(code, reason);
} catch (error) {
if (!isTauriDuplicateListenerCleanupError(error)) {
throw error;
}
}
disposed = true;
ws = null;
},
};
};
}
@@ -7,7 +7,6 @@ import {
buildLocalDaemonTransportUrl,
createTauriLocalDaemonTransportFactory,
} from "./managed-tauri-daemon-transport";
import { createTauriWebSocketTransportFactory } from "./tauri-daemon-transport";
function normalizeNonEmptyString(value: unknown): string | null {
if (typeof value !== "string") return null;
@@ -50,7 +49,6 @@ export async function buildClientConfig(
serverId?: string
): Promise<DaemonClientConfig> {
const clientId = await getOrCreateClientId();
const tauriTransportFactory = createTauriWebSocketTransportFactory();
const localTransportFactory = createTauriLocalDaemonTransportFactory();
const base = {
clientId,
@@ -61,9 +59,7 @@ export async function buildClientConfig(
? localTransportFactory
? { transportFactory: localTransportFactory }
: {}
: tauriTransportFactory
? { transportFactory: tauriTransportFactory }
: {}),
: {}),
};
if (connection.type === "directSocket" || connection.type === "directPipe") {