WIP: workspace execution refactor + notification routing + setup store updates
This commit is contained in:
@@ -2,6 +2,7 @@ import path from "node:path";
|
|||||||
import { randomUUID } from "node:crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
import { pathToFileURL } from "node:url";
|
import { pathToFileURL } from "node:url";
|
||||||
import { expect, type Page } from "@playwright/test";
|
import { expect, type Page } from "@playwright/test";
|
||||||
|
import { parseHostWorkspaceRouteFromPathname } from "../../src/utils/host-routes";
|
||||||
import { gotoAppShell } from "./app";
|
import { gotoAppShell } from "./app";
|
||||||
|
|
||||||
type WorkspaceSetupProgressPayload = {
|
type WorkspaceSetupProgressPayload = {
|
||||||
@@ -20,10 +21,52 @@ type WorkspaceSetupDaemonClient = {
|
|||||||
close(): Promise<void>;
|
close(): Promise<void>;
|
||||||
openProject(
|
openProject(
|
||||||
cwd: string,
|
cwd: string,
|
||||||
): Promise<{ workspace: { id: string; name: string } | null; error: string | null }>;
|
): Promise<{
|
||||||
|
workspace: {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
workspaceDirectory: string;
|
||||||
|
projectRootPath: string;
|
||||||
|
} | null;
|
||||||
|
error: string | null;
|
||||||
|
}>;
|
||||||
createPaseoWorktree(
|
createPaseoWorktree(
|
||||||
input: { cwd: string; worktreeSlug?: string },
|
input: { cwd: string; worktreeSlug?: string },
|
||||||
): Promise<{ workspace: { id: string; name: string } | null; error: string | null }>;
|
): Promise<{
|
||||||
|
workspace: {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
workspaceDirectory: string;
|
||||||
|
projectRootPath: string;
|
||||||
|
} | null;
|
||||||
|
error: string | null;
|
||||||
|
}>;
|
||||||
|
fetchWorkspaces(): Promise<{
|
||||||
|
entries: Array<{
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
workspaceDirectory: string;
|
||||||
|
projectRootPath: string;
|
||||||
|
}>;
|
||||||
|
}>;
|
||||||
|
fetchAgents(): Promise<{
|
||||||
|
entries: Array<{
|
||||||
|
agent: { id: string; cwd: string; workspaceId?: string | null };
|
||||||
|
}>;
|
||||||
|
}>;
|
||||||
|
fetchAgent(
|
||||||
|
agentId: string,
|
||||||
|
): Promise<{
|
||||||
|
agent: { id: string; cwd: string } | null;
|
||||||
|
project: unknown | null;
|
||||||
|
} | null>;
|
||||||
|
listTerminals(
|
||||||
|
cwd: string,
|
||||||
|
): Promise<{
|
||||||
|
cwd?: string;
|
||||||
|
terminals: Array<{ id: string; cwd: string; name: string }>;
|
||||||
|
error?: string | null;
|
||||||
|
}>;
|
||||||
subscribeRawMessages(handler: (message: WorkspaceSetupRawMessage) => void): () => void;
|
subscribeRawMessages(handler: (message: WorkspaceSetupRawMessage) => void): () => void;
|
||||||
};
|
};
|
||||||
export type { WorkspaceSetupDaemonClient, WorkspaceSetupProgressPayload };
|
export type { WorkspaceSetupDaemonClient, WorkspaceSetupProgressPayload };
|
||||||
@@ -80,7 +123,12 @@ export function projectNameFromPath(repoPath: string): string {
|
|||||||
|
|
||||||
export async function openHomeWithProject(page: Page, repoPath: string): Promise<void> {
|
export async function openHomeWithProject(page: Page, repoPath: string): Promise<void> {
|
||||||
await gotoAppShell(page);
|
await gotoAppShell(page);
|
||||||
await expect(createWorkspaceButton(page, repoPath)).toBeVisible({ timeout: 30_000 });
|
await expect(
|
||||||
|
page
|
||||||
|
.locator('[data-testid^="sidebar-project-row-"]')
|
||||||
|
.filter({ hasText: projectNameFromPath(repoPath) })
|
||||||
|
.first(),
|
||||||
|
).toBeVisible({ timeout: 30_000 });
|
||||||
}
|
}
|
||||||
|
|
||||||
function createWorkspaceButton(page: Page, repoPath: string) {
|
function createWorkspaceButton(page: Page, repoPath: string) {
|
||||||
@@ -90,14 +138,98 @@ function createWorkspaceButton(page: Page, repoPath: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function revealWorkspaceButton(page: Page, repoPath: string): Promise<void> {
|
async function revealWorkspaceButton(page: Page, repoPath: string): Promise<void> {
|
||||||
await page.getByTestId(`sidebar-project-row-${repoPath}`).hover();
|
await page
|
||||||
|
.locator('[data-testid^="sidebar-project-row-"]')
|
||||||
|
.filter({ hasText: projectNameFromPath(repoPath) })
|
||||||
|
.first()
|
||||||
|
.hover();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createWorkspaceFromSidebar(page: Page, repoPath: string): Promise<void> {
|
export async function createWorkspaceFromSidebar(page: Page, repoPath: string): Promise<void> {
|
||||||
|
const button = createWorkspaceButton(page, repoPath);
|
||||||
await revealWorkspaceButton(page, repoPath);
|
await revealWorkspaceButton(page, repoPath);
|
||||||
await expect(createWorkspaceButton(page, repoPath)).toBeEnabled({ timeout: 30_000 });
|
await expect(button).toBeVisible({ timeout: 30_000 });
|
||||||
await createWorkspaceButton(page, repoPath).click();
|
await expect(button).toBeEnabled({ timeout: 30_000 });
|
||||||
|
await button.click();
|
||||||
await expect(page).toHaveURL(/\/workspace\//, { timeout: 30_000 });
|
await expect(page).toHaveURL(/\/workspace\//, { timeout: 30_000 });
|
||||||
|
await expect(page.getByTestId("workspace-setup-dialog")).toBeVisible({ timeout: 30_000 });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getCurrentWorkspaceIdFromRoute(page: Page): Promise<string> {
|
||||||
|
await expect
|
||||||
|
.poll(
|
||||||
|
() => parseHostWorkspaceRouteFromPathname(new URL(page.url()).pathname)?.workspaceId ?? null,
|
||||||
|
{ timeout: 30_000 },
|
||||||
|
)
|
||||||
|
.not.toBeNull();
|
||||||
|
|
||||||
|
const workspaceId =
|
||||||
|
parseHostWorkspaceRouteFromPathname(new URL(page.url()).pathname)?.workspaceId ?? null;
|
||||||
|
if (!workspaceId) {
|
||||||
|
throw new Error(`Expected a workspace route but found ${page.url()}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return workspaceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
function workspaceSetupDialog(page: Page) {
|
||||||
|
return page.getByTestId("workspace-setup-dialog");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createChatAgentFromWorkspaceSetup(
|
||||||
|
page: Page,
|
||||||
|
input: { message: string },
|
||||||
|
): Promise<void> {
|
||||||
|
const dialog = workspaceSetupDialog(page);
|
||||||
|
await dialog.getByRole("button", { name: /Chat Agent/i }).click();
|
||||||
|
|
||||||
|
const messageInput = dialog.getByRole("textbox", { name: "Message agent..." }).first();
|
||||||
|
await expect(messageInput).toBeVisible({ timeout: 15_000 });
|
||||||
|
await messageInput.fill(input.message);
|
||||||
|
|
||||||
|
await dialog.getByRole("button", { name: "Send message" }).click();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createTerminalAgentFromWorkspaceSetup(
|
||||||
|
page: Page,
|
||||||
|
input: { providerLabel: string; prompt?: string },
|
||||||
|
): Promise<void> {
|
||||||
|
const dialog = workspaceSetupDialog(page);
|
||||||
|
await dialog.getByRole("button", { name: /Terminal Agent/i }).click();
|
||||||
|
|
||||||
|
const providerButton = dialog.getByRole("button", { name: new RegExp(`^${input.providerLabel}$`, "i") }).first();
|
||||||
|
await expect(providerButton).toBeVisible({ timeout: 15_000 });
|
||||||
|
await providerButton.click();
|
||||||
|
|
||||||
|
if (input.prompt) {
|
||||||
|
const promptInput = dialog.getByPlaceholder("Optional").first();
|
||||||
|
await expect(promptInput).toBeVisible({ timeout: 15_000 });
|
||||||
|
await promptInput.fill(input.prompt);
|
||||||
|
}
|
||||||
|
|
||||||
|
await dialog.getByRole("button", { name: "Launch" }).click();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createStandaloneTerminalFromWorkspaceSetup(page: Page): Promise<void> {
|
||||||
|
await workspaceSetupDialog(page)
|
||||||
|
.getByRole("button", { name: /^Terminal Create the workspace/i })
|
||||||
|
.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function waitForWorkspaceSetupDialogToClose(page: Page, timeoutMs = 45_000): Promise<void> {
|
||||||
|
const dialog = workspaceSetupDialog(page);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await expect(dialog).toHaveCount(0, { timeout: timeoutMs });
|
||||||
|
} catch (error) {
|
||||||
|
const dialogText = (await dialog.textContent().catch(() => null))?.replace(/\s+/g, " ").trim();
|
||||||
|
throw new Error(
|
||||||
|
dialogText
|
||||||
|
? `Workspace setup dialog stayed open. Visible text: ${dialogText}`
|
||||||
|
: `Workspace setup dialog did not close within ${timeoutMs}ms`,
|
||||||
|
{ cause: error },
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function expectSetupPanel(page: Page): Promise<void> {
|
export async function expectSetupPanel(page: Page): Promise<void> {
|
||||||
@@ -133,7 +265,58 @@ export async function createWorkspaceThroughDaemon(
|
|||||||
if (!result.workspace || result.error) {
|
if (!result.workspace || result.error) {
|
||||||
throw new Error(result.error ?? `Failed to create workspace for ${input.cwd}`);
|
throw new Error(result.error ?? `Failed to create workspace for ${input.cwd}`);
|
||||||
}
|
}
|
||||||
return result.workspace;
|
return {
|
||||||
|
id: String(result.workspace.id),
|
||||||
|
name: result.workspace.name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function findWorktreeWorkspaceForProject(
|
||||||
|
client: WorkspaceSetupDaemonClient,
|
||||||
|
repoPath: string,
|
||||||
|
): Promise<{
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
projectRootPath: string;
|
||||||
|
workspaceDirectory: string;
|
||||||
|
}> {
|
||||||
|
const payload = await client.fetchWorkspaces();
|
||||||
|
const workspace =
|
||||||
|
payload.entries.find(
|
||||||
|
(entry) =>
|
||||||
|
entry.projectRootPath === repoPath && entry.workspaceDirectory !== repoPath,
|
||||||
|
) ?? null;
|
||||||
|
if (!workspace) {
|
||||||
|
throw new Error(`Failed to find created worktree workspace for ${repoPath}`);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: String(workspace.id),
|
||||||
|
name: workspace.name,
|
||||||
|
projectRootPath: workspace.projectRootPath,
|
||||||
|
workspaceDirectory: workspace.workspaceDirectory,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchWorkspaceById(
|
||||||
|
client: WorkspaceSetupDaemonClient,
|
||||||
|
workspaceId: string,
|
||||||
|
): Promise<{
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
workspaceDirectory: string;
|
||||||
|
projectRootPath: string;
|
||||||
|
}> {
|
||||||
|
const parsedWorkspaceId = Number(workspaceId);
|
||||||
|
if (!Number.isInteger(parsedWorkspaceId)) {
|
||||||
|
throw new Error(`Workspace id is not numeric: ${workspaceId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = await client.fetchWorkspaces();
|
||||||
|
const workspace = payload.entries.find((entry) => entry.id === parsedWorkspaceId) ?? null;
|
||||||
|
if (!workspace) {
|
||||||
|
throw new Error(`Workspace not found: ${workspaceId}`);
|
||||||
|
}
|
||||||
|
return workspace;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function waitForWorkspaceSetupProgress(
|
export async function waitForWorkspaceSetupProgress(
|
||||||
|
|||||||
@@ -50,7 +50,10 @@ async function openProjectViaDaemon(
|
|||||||
if (!result.workspace || result.error) {
|
if (!result.workspace || result.error) {
|
||||||
throw new Error(result.error ?? `Failed to open project ${cwd}`);
|
throw new Error(result.error ?? `Failed to open project ${cwd}`);
|
||||||
}
|
}
|
||||||
return result.workspace;
|
return {
|
||||||
|
id: String(result.workspace.id),
|
||||||
|
name: result.workspace.name,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openWorkspaceFromSidebar(page: import("@playwright/test").Page, workspaceId: string) {
|
async function openWorkspaceFromSidebar(page: import("@playwright/test").Page, workspaceId: string) {
|
||||||
|
|||||||
@@ -0,0 +1,171 @@
|
|||||||
|
import { existsSync } from "node:fs";
|
||||||
|
import { expect, test } from "./fixtures";
|
||||||
|
import { createTempGitRepo } from "./helpers/workspace";
|
||||||
|
import {
|
||||||
|
connectWorkspaceSetupClient,
|
||||||
|
createChatAgentFromWorkspaceSetup,
|
||||||
|
createStandaloneTerminalFromWorkspaceSetup,
|
||||||
|
createTerminalAgentFromWorkspaceSetup,
|
||||||
|
createWorkspaceFromSidebar,
|
||||||
|
findWorktreeWorkspaceForProject,
|
||||||
|
openHomeWithProject,
|
||||||
|
type WorkspaceSetupDaemonClient,
|
||||||
|
} from "./helpers/workspace-setup";
|
||||||
|
|
||||||
|
async function openWorkspaceSetupDialogFromSidebar(
|
||||||
|
page: import("@playwright/test").Page,
|
||||||
|
repoPath: string,
|
||||||
|
): Promise<void> {
|
||||||
|
await openHomeWithProject(page, repoPath);
|
||||||
|
await createWorkspaceFromSidebar(page, repoPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function expectCreatedWorkspaceRoute(
|
||||||
|
client: WorkspaceSetupDaemonClient,
|
||||||
|
originalProjectPath: string,
|
||||||
|
) {
|
||||||
|
await expect
|
||||||
|
.poll(
|
||||||
|
async () => {
|
||||||
|
try {
|
||||||
|
return await findWorktreeWorkspaceForProject(client, originalProjectPath);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ timeout: 30_000 },
|
||||||
|
)
|
||||||
|
.not.toBeNull();
|
||||||
|
|
||||||
|
const workspace = await findWorktreeWorkspaceForProject(client, originalProjectPath);
|
||||||
|
|
||||||
|
expect(workspace.workspaceDirectory).not.toBe(originalProjectPath);
|
||||||
|
expect(existsSync(workspace.workspaceDirectory)).toBe(true);
|
||||||
|
return workspace;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForNewWorkspaceAgent(
|
||||||
|
client: WorkspaceSetupDaemonClient,
|
||||||
|
expectedWorkspaceDirectory: string,
|
||||||
|
agentIdsBefore: Set<string>,
|
||||||
|
) {
|
||||||
|
await expect
|
||||||
|
.poll(
|
||||||
|
async () => {
|
||||||
|
const payload = await client.fetchAgents();
|
||||||
|
return (
|
||||||
|
payload.entries.find(
|
||||||
|
(entry) =>
|
||||||
|
!agentIdsBefore.has(entry.agent.id) &&
|
||||||
|
entry.agent.cwd === expectedWorkspaceDirectory,
|
||||||
|
)?.agent ?? null
|
||||||
|
);
|
||||||
|
},
|
||||||
|
{ timeout: 30_000 },
|
||||||
|
)
|
||||||
|
.not.toBeNull();
|
||||||
|
|
||||||
|
const payload = await client.fetchAgents();
|
||||||
|
const agent =
|
||||||
|
payload.entries.find(
|
||||||
|
(entry) =>
|
||||||
|
!agentIdsBefore.has(entry.agent.id) && entry.agent.cwd === expectedWorkspaceDirectory,
|
||||||
|
)?.agent ?? null;
|
||||||
|
if (!agent) {
|
||||||
|
throw new Error(`Expected a new agent for workspace ${expectedWorkspaceDirectory}`);
|
||||||
|
}
|
||||||
|
return agent;
|
||||||
|
}
|
||||||
|
|
||||||
|
test.describe("Workspace setup runtime authority", () => {
|
||||||
|
test.describe.configure({ retries: 1 });
|
||||||
|
|
||||||
|
test("first chat agent attaches to the created workspace", async ({ page }) => {
|
||||||
|
test.setTimeout(90_000);
|
||||||
|
|
||||||
|
const client = await connectWorkspaceSetupClient();
|
||||||
|
const repo = await createTempGitRepo("workspace-setup-chat-");
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.openProject(repo.path);
|
||||||
|
await openWorkspaceSetupDialogFromSidebar(page, repo.path);
|
||||||
|
const agentIdsBefore = new Set((await client.fetchAgents()).entries.map((entry) => entry.agent.id));
|
||||||
|
|
||||||
|
await createChatAgentFromWorkspaceSetup(page, {
|
||||||
|
message: `workspace-setup-chat-${Date.now()}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const workspace = await expectCreatedWorkspaceRoute(client, repo.path);
|
||||||
|
const agent = await waitForNewWorkspaceAgent(
|
||||||
|
client,
|
||||||
|
workspace.workspaceDirectory,
|
||||||
|
agentIdsBefore,
|
||||||
|
);
|
||||||
|
expect(agent.cwd).toBe(workspace.workspaceDirectory);
|
||||||
|
expect(agent.cwd).not.toBe(repo.path);
|
||||||
|
} finally {
|
||||||
|
await client.close();
|
||||||
|
await repo.cleanup();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("first terminal agent attaches to the created workspace", async ({ page }) => {
|
||||||
|
test.setTimeout(90_000);
|
||||||
|
|
||||||
|
const client = await connectWorkspaceSetupClient();
|
||||||
|
const repo = await createTempGitRepo("workspace-setup-terminal-agent-");
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.openProject(repo.path);
|
||||||
|
await openWorkspaceSetupDialogFromSidebar(page, repo.path);
|
||||||
|
const agentIdsBefore = new Set((await client.fetchAgents()).entries.map((entry) => entry.agent.id));
|
||||||
|
|
||||||
|
await createTerminalAgentFromWorkspaceSetup(page, {
|
||||||
|
providerLabel: "Claude",
|
||||||
|
prompt: `workspace-setup-terminal-agent-${Date.now()}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const workspace = await expectCreatedWorkspaceRoute(client, repo.path);
|
||||||
|
const agent = await waitForNewWorkspaceAgent(
|
||||||
|
client,
|
||||||
|
workspace.workspaceDirectory,
|
||||||
|
agentIdsBefore,
|
||||||
|
);
|
||||||
|
expect(agent.cwd).toBe(workspace.workspaceDirectory);
|
||||||
|
expect(agent.cwd).not.toBe(repo.path);
|
||||||
|
} finally {
|
||||||
|
await client.close();
|
||||||
|
await repo.cleanup();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("first terminal attaches to the created workspace", async ({ page }) => {
|
||||||
|
test.setTimeout(90_000);
|
||||||
|
|
||||||
|
const client = await connectWorkspaceSetupClient();
|
||||||
|
const repo = await createTempGitRepo("workspace-setup-terminal-");
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.openProject(repo.path);
|
||||||
|
await openWorkspaceSetupDialogFromSidebar(page, repo.path);
|
||||||
|
|
||||||
|
await createStandaloneTerminalFromWorkspaceSetup(page);
|
||||||
|
|
||||||
|
const workspace = await expectCreatedWorkspaceRoute(client, repo.path);
|
||||||
|
|
||||||
|
await expect
|
||||||
|
.poll(
|
||||||
|
async () =>
|
||||||
|
(await client.listTerminals(workspace.workspaceDirectory)).terminals.length > 0,
|
||||||
|
{ timeout: 30_000 },
|
||||||
|
)
|
||||||
|
.toBe(true);
|
||||||
|
expect(
|
||||||
|
(await client.listTerminals(repo.path)).terminals.length,
|
||||||
|
).toBe(0);
|
||||||
|
} finally {
|
||||||
|
await client.close();
|
||||||
|
await repo.cleanup();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -3,7 +3,7 @@ import { useLocalSearchParams, useRouter } from "expo-router";
|
|||||||
import { useSessionStore } from "@/stores/session-store";
|
import { useSessionStore } from "@/stores/session-store";
|
||||||
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
|
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
|
||||||
import { buildHostRootRoute } from "@/utils/host-routes";
|
import { buildHostRootRoute } from "@/utils/host-routes";
|
||||||
import { resolveHydratedWorkspaceId } from "@/utils/resolve-hydrated-workspace-id";
|
import { resolveWorkspaceIdByExecutionDirectory } from "@/utils/workspace-execution";
|
||||||
import { prepareWorkspaceTab } from "@/utils/workspace-navigation";
|
import { prepareWorkspaceTab } from "@/utils/workspace-navigation";
|
||||||
|
|
||||||
export default function HostAgentReadyRoute() {
|
export default function HostAgentReadyRoute() {
|
||||||
@@ -33,9 +33,9 @@ export default function HostAgentReadyRoute() {
|
|||||||
if (!serverId || !agentId) {
|
if (!serverId || !agentId) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return resolveHydratedWorkspaceId({
|
return resolveWorkspaceIdByExecutionDirectory({
|
||||||
workspaces: state.sessions[serverId]?.workspaces?.values(),
|
workspaces: state.sessions[serverId]?.workspaces?.values(),
|
||||||
path: state.sessions[serverId]?.agents?.get(agentId)?.cwd,
|
workspaceDirectory: state.sessions[serverId]?.agents?.get(agentId)?.cwd,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -93,9 +93,9 @@ export default function HostAgentReadyRoute() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const cwd = result?.agent?.cwd?.trim();
|
const cwd = result?.agent?.cwd?.trim();
|
||||||
const workspaceId = resolveHydratedWorkspaceId({
|
const workspaceId = resolveWorkspaceIdByExecutionDirectory({
|
||||||
workspaces: sessionWorkspaces?.values(),
|
workspaces: sessionWorkspaces?.values(),
|
||||||
path: cwd,
|
workspaceDirectory: cwd,
|
||||||
});
|
});
|
||||||
if (!workspaceId && !hasHydratedWorkspaces) {
|
if (!workspaceId && !hasHydratedWorkspaces) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
buildHostRootRoute,
|
buildHostRootRoute,
|
||||||
buildHostWorkspaceOpenRoute,
|
buildHostWorkspaceOpenRoute,
|
||||||
} from "@/utils/host-routes";
|
} from "@/utils/host-routes";
|
||||||
import { resolveHydratedWorkspaceId } from "@/utils/resolve-hydrated-workspace-id";
|
import { resolveWorkspaceIdByExecutionDirectory } from "@/utils/workspace-execution";
|
||||||
import { prepareWorkspaceTab } from "@/utils/workspace-navigation";
|
import { prepareWorkspaceTab } from "@/utils/workspace-navigation";
|
||||||
|
|
||||||
const HOST_ROOT_REDIRECT_DELAY_MS = 300;
|
const HOST_ROOT_REDIRECT_DELAY_MS = 300;
|
||||||
@@ -66,9 +66,9 @@ export default function HostIndexRoute() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const primaryAgent = visibleAgents[0];
|
const primaryAgent = visibleAgents[0];
|
||||||
const primaryAgentWorkspaceId = resolveHydratedWorkspaceId({
|
const primaryAgentWorkspaceId = resolveWorkspaceIdByExecutionDirectory({
|
||||||
workspaces: sessionWorkspaces?.values(),
|
workspaces: sessionWorkspaces?.values(),
|
||||||
path: primaryAgent?.cwd,
|
workspaceDirectory: primaryAgent?.cwd,
|
||||||
});
|
});
|
||||||
if (primaryAgent && primaryAgentWorkspaceId) {
|
if (primaryAgent && primaryAgentWorkspaceId) {
|
||||||
router.replace(
|
router.replace(
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { useSessionStore } from "@/stores/session-store";
|
|||||||
import { Archive, SquareTerminal } from "lucide-react-native";
|
import { Archive, SquareTerminal } from "lucide-react-native";
|
||||||
import { getProviderIcon } from "@/components/provider-icons";
|
import { getProviderIcon } from "@/components/provider-icons";
|
||||||
import { buildHostAgentDetailRoute } from "@/utils/host-routes";
|
import { buildHostAgentDetailRoute } from "@/utils/host-routes";
|
||||||
import { resolveHydratedWorkspaceId } from "@/utils/resolve-hydrated-workspace-id";
|
import { resolveWorkspaceIdByExecutionDirectory } from "@/utils/workspace-execution";
|
||||||
import { prepareWorkspaceTab } from "@/utils/workspace-navigation";
|
import { prepareWorkspaceTab } from "@/utils/workspace-navigation";
|
||||||
|
|
||||||
interface AgentListProps {
|
interface AgentListProps {
|
||||||
@@ -244,9 +244,9 @@ export function AgentList({
|
|||||||
|
|
||||||
const serverId = agent.serverId;
|
const serverId = agent.serverId;
|
||||||
const agentId = agent.id;
|
const agentId = agent.id;
|
||||||
const workspaceId = resolveHydratedWorkspaceId({
|
const workspaceId = resolveWorkspaceIdByExecutionDirectory({
|
||||||
workspaces: useSessionStore.getState().sessions[serverId]?.workspaces?.values(),
|
workspaces: useSessionStore.getState().sessions[serverId]?.workspaces?.values(),
|
||||||
path: agent.cwd,
|
workspaceDirectory: agent.cwd,
|
||||||
});
|
});
|
||||||
|
|
||||||
onAgentSelect?.();
|
onAgentSelect?.();
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ import { createMarkdownStyles } from "@/styles/markdown-styles";
|
|||||||
import { MAX_CONTENT_WIDTH } from "@/constants/layout";
|
import { MAX_CONTENT_WIDTH } from "@/constants/layout";
|
||||||
import { getMarkdownListMarker } from "@/utils/markdown-list";
|
import { getMarkdownListMarker } from "@/utils/markdown-list";
|
||||||
import { normalizeInlinePathTarget } from "@/utils/inline-path";
|
import { normalizeInlinePathTarget } from "@/utils/inline-path";
|
||||||
import { resolveHydratedWorkspaceId } from "@/utils/resolve-hydrated-workspace-id";
|
import { resolveWorkspaceIdByExecutionDirectory } from "@/utils/workspace-execution";
|
||||||
import { prepareWorkspaceTab } from "@/utils/workspace-navigation";
|
import { prepareWorkspaceTab } from "@/utils/workspace-navigation";
|
||||||
import { useStableEvent } from "@/hooks/use-stable-event";
|
import { useStableEvent } from "@/hooks/use-stable-event";
|
||||||
import {
|
import {
|
||||||
@@ -133,10 +133,9 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
|
|||||||
);
|
);
|
||||||
|
|
||||||
const workspaceRoot = agent.cwd?.trim() || "";
|
const workspaceRoot = agent.cwd?.trim() || "";
|
||||||
const workspacePath = agent.projectPlacement?.checkout?.cwd?.trim() || workspaceRoot;
|
const workspaceId = resolveWorkspaceIdByExecutionDirectory({
|
||||||
const workspaceId = resolveHydratedWorkspaceId({
|
|
||||||
workspaces: useSessionStore.getState().sessions[resolvedServerId]?.workspaces?.values(),
|
workspaces: useSessionStore.getState().sessions[resolvedServerId]?.workspaces?.values(),
|
||||||
path: workspacePath,
|
workspaceDirectory: workspaceRoot,
|
||||||
});
|
});
|
||||||
const { requestDirectoryListing } = useFileExplorerActions({
|
const { requestDirectoryListing } = useFileExplorerActions({
|
||||||
serverId: resolvedServerId,
|
serverId: resolvedServerId,
|
||||||
|
|||||||
@@ -86,6 +86,10 @@ import { useSessionStore } from "@/stores/session-store";
|
|||||||
import { useWorkspaceSetupStore } from "@/stores/workspace-setup-store";
|
import { useWorkspaceSetupStore } from "@/stores/workspace-setup-store";
|
||||||
import { buildWorkspaceArchiveRedirectRoute } from "@/utils/workspace-archive-navigation";
|
import { buildWorkspaceArchiveRedirectRoute } from "@/utils/workspace-archive-navigation";
|
||||||
import { openExternalUrl } from "@/utils/open-external-url";
|
import { openExternalUrl } from "@/utils/open-external-url";
|
||||||
|
import {
|
||||||
|
requireWorkspaceExecutionDirectory,
|
||||||
|
resolveWorkspaceExecutionDirectory,
|
||||||
|
} from "@/utils/workspace-execution";
|
||||||
|
|
||||||
function toProjectIconDataUri(icon: { mimeType: string; data: string } | null): string | null {
|
function toProjectIconDataUri(icon: { mimeType: string; data: string } | null): string | null {
|
||||||
if (!icon) {
|
if (!icon) {
|
||||||
@@ -674,8 +678,8 @@ function ProjectHeaderRow({
|
|||||||
onWorkspacePress?.();
|
onWorkspacePress?.();
|
||||||
beginWorkspaceSetup({
|
beginWorkspaceSetup({
|
||||||
serverId,
|
serverId,
|
||||||
projectPath: project.iconWorkingDir,
|
sourceDirectory: project.iconWorkingDir,
|
||||||
projectName: displayName,
|
displayName,
|
||||||
creationMethod: "create_worktree",
|
creationMethod: "create_worktree",
|
||||||
navigationMethod: "navigate",
|
navigationMethod: "navigate",
|
||||||
});
|
});
|
||||||
@@ -815,10 +819,13 @@ function WorkspaceRowInner({
|
|||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
const isMobile = Platform.OS !== "web";
|
const isMobile = Platform.OS !== "web";
|
||||||
|
const workspaceDirectory = resolveWorkspaceExecutionDirectory({
|
||||||
|
workspaceDirectory: workspace.workspaceDirectory,
|
||||||
|
});
|
||||||
const prHint = useWorkspacePrHint({
|
const prHint = useWorkspacePrHint({
|
||||||
serverId: workspace.serverId,
|
serverId: workspace.serverId,
|
||||||
cwd: workspace.projectRootPath ?? "",
|
cwd: workspaceDirectory ?? "",
|
||||||
enabled: workspace.projectKind === "git" && Boolean(workspace.projectRootPath),
|
enabled: workspace.projectKind === "git" && Boolean(workspaceDirectory),
|
||||||
});
|
});
|
||||||
const interaction = useLongPressDragInteraction({
|
const interaction = useLongPressDragInteraction({
|
||||||
drag,
|
drag,
|
||||||
@@ -982,12 +989,17 @@ function WorkspaceRowWithMenu({
|
|||||||
(state) => state.sessions[workspace.serverId]?.workspaces ?? EMPTY_WORKSPACES,
|
(state) => state.sessions[workspace.serverId]?.workspaces ?? EMPTY_WORKSPACES,
|
||||||
);
|
);
|
||||||
const [isArchivingWorkspace, setIsArchivingWorkspace] = useState(false);
|
const [isArchivingWorkspace, setIsArchivingWorkspace] = useState(false);
|
||||||
|
const workspaceDirectory = resolveWorkspaceExecutionDirectory({
|
||||||
|
workspaceDirectory: workspace.workspaceDirectory,
|
||||||
|
});
|
||||||
const archiveStatus = useCheckoutGitActionsStore((state) =>
|
const archiveStatus = useCheckoutGitActionsStore((state) =>
|
||||||
state.getStatus({
|
workspaceDirectory
|
||||||
serverId: workspace.serverId,
|
? state.getStatus({
|
||||||
cwd: workspace.workspaceDirectory ?? workspace.projectRootPath ?? "",
|
serverId: workspace.serverId,
|
||||||
actionId: "archive-worktree",
|
cwd: workspaceDirectory,
|
||||||
}),
|
actionId: "archive-worktree",
|
||||||
|
})
|
||||||
|
: "idle",
|
||||||
);
|
);
|
||||||
const isWorktree = workspace.workspaceKind === "worktree";
|
const isWorktree = workspace.workspaceKind === "worktree";
|
||||||
const isArchiving = isWorktree ? archiveStatus === "pending" : isArchivingWorkspace;
|
const isArchiving = isWorktree ? archiveStatus === "pending" : isArchivingWorkspace;
|
||||||
@@ -1025,7 +1037,17 @@ function WorkspaceRowWithMenu({
|
|||||||
if (!confirmed) {
|
if (!confirmed) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const workspaceDirectory = workspace.workspaceDirectory ?? workspace.projectRootPath;
|
let workspaceDirectory: string;
|
||||||
|
try {
|
||||||
|
workspaceDirectory = requireWorkspaceExecutionDirectory({
|
||||||
|
workspaceId: workspace.workspaceId,
|
||||||
|
workspaceDirectory: workspace.workspaceDirectory,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(error instanceof Error ? error.message : "Workspace path not available");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!workspaceDirectory) {
|
if (!workspaceDirectory) {
|
||||||
toast.error("Workspace path not available");
|
toast.error("Workspace path not available");
|
||||||
return;
|
return;
|
||||||
@@ -1050,7 +1072,6 @@ function WorkspaceRowWithMenu({
|
|||||||
redirectAfterArchive,
|
redirectAfterArchive,
|
||||||
toast,
|
toast,
|
||||||
workspace.name,
|
workspace.name,
|
||||||
workspace.projectRootPath,
|
|
||||||
workspace.workspaceDirectory,
|
workspace.workspaceDirectory,
|
||||||
workspace.serverId,
|
workspace.serverId,
|
||||||
workspace.workspaceId,
|
workspace.workspaceId,
|
||||||
@@ -1102,14 +1123,19 @@ function WorkspaceRowWithMenu({
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const handleCopyPath = useCallback(() => {
|
const handleCopyPath = useCallback(() => {
|
||||||
const workspaceDirectory = workspace.workspaceDirectory ?? workspace.projectRootPath;
|
let workspaceDirectory: string;
|
||||||
if (!workspaceDirectory) {
|
try {
|
||||||
toast.error("Workspace path not available");
|
workspaceDirectory = requireWorkspaceExecutionDirectory({
|
||||||
|
workspaceId: workspace.workspaceId,
|
||||||
|
workspaceDirectory: workspace.workspaceDirectory,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(error instanceof Error ? error.message : "Workspace path not available");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
void Clipboard.setStringAsync(workspaceDirectory);
|
void Clipboard.setStringAsync(workspaceDirectory);
|
||||||
toast.copied("Path copied");
|
toast.copied("Path copied");
|
||||||
}, [toast, workspace.projectRootPath, workspace.workspaceDirectory]);
|
}, [toast, workspace.workspaceDirectory, workspace.workspaceId]);
|
||||||
|
|
||||||
const handleCopyBranchName = useCallback(() => {
|
const handleCopyBranchName = useCallback(() => {
|
||||||
void Clipboard.setStringAsync(workspace.name);
|
void Clipboard.setStringAsync(workspace.name);
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ import { useWorkspaceSetupStore } from "@/stores/workspace-setup-store";
|
|||||||
import { normalizeAgentSnapshot } from "@/utils/agent-snapshots";
|
import { normalizeAgentSnapshot } from "@/utils/agent-snapshots";
|
||||||
import { encodeImages } from "@/utils/encode-images";
|
import { encodeImages } from "@/utils/encode-images";
|
||||||
import { toErrorMessage } from "@/utils/error-messages";
|
import { toErrorMessage } from "@/utils/error-messages";
|
||||||
|
import {
|
||||||
|
requireWorkspaceExecutionAuthority,
|
||||||
|
requireWorkspaceRecordId,
|
||||||
|
} from "@/utils/workspace-execution";
|
||||||
import { navigateToPreparedWorkspaceTab } from "@/utils/workspace-navigation";
|
import { navigateToPreparedWorkspaceTab } from "@/utils/workspace-navigation";
|
||||||
import type { MessagePayload } from "./message-input";
|
import type { MessagePayload } from "./message-input";
|
||||||
|
|
||||||
@@ -41,19 +45,21 @@ export function WorkspaceSetupDialog() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const serverId = pendingWorkspaceSetup?.serverId ?? "";
|
const serverId = pendingWorkspaceSetup?.serverId ?? "";
|
||||||
const projectPath = pendingWorkspaceSetup?.projectPath ?? "";
|
const sourceDirectory = pendingWorkspaceSetup?.sourceDirectory ?? "";
|
||||||
const projectName = pendingWorkspaceSetup?.projectName?.trim() ?? "";
|
const displayName = pendingWorkspaceSetup?.displayName?.trim() ?? "";
|
||||||
const workspace = createdWorkspace;
|
const workspace = createdWorkspace;
|
||||||
const client = useHostRuntimeClient(serverId);
|
const client = useHostRuntimeClient(serverId);
|
||||||
const isConnected = useHostRuntimeIsConnected(serverId);
|
const isConnected = useHostRuntimeIsConnected(serverId);
|
||||||
const chatDraft = useAgentInputDraft({
|
const chatDraft = useAgentInputDraft({
|
||||||
draftKey: `workspace-setup:${serverId}:${projectPath}`,
|
draftKey: `workspace-setup:${serverId}:${sourceDirectory}`,
|
||||||
composer: {
|
composer: {
|
||||||
initialServerId: serverId || null,
|
initialServerId: serverId || null,
|
||||||
initialValues: projectPath ? { workingDir: projectPath } : undefined,
|
initialValues: workspace?.workspaceDirectory
|
||||||
|
? { workingDir: workspace.workspaceDirectory }
|
||||||
|
: undefined,
|
||||||
isVisible: pendingWorkspaceSetup !== null,
|
isVisible: pendingWorkspaceSetup !== null,
|
||||||
onlineServerIds: isConnected && serverId ? [serverId] : [],
|
onlineServerIds: isConnected && serverId ? [serverId] : [],
|
||||||
lockedWorkingDir: workspace?.projectRootPath ?? projectPath,
|
lockedWorkingDir: workspace?.workspaceDirectory || undefined,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const composerState = chatDraft.composerState;
|
const composerState = chatDraft.composerState;
|
||||||
@@ -70,7 +76,7 @@ export function WorkspaceSetupDialog() {
|
|||||||
setErrorMessage(null);
|
setErrorMessage(null);
|
||||||
setCreatedWorkspace(null);
|
setCreatedWorkspace(null);
|
||||||
setPendingAction(null);
|
setPendingAction(null);
|
||||||
}, [pendingWorkspaceSetup?.creationMethod, projectPath, serverId]);
|
}, [pendingWorkspaceSetup?.creationMethod, serverId, sourceDirectory]);
|
||||||
|
|
||||||
const handleClose = useCallback(() => {
|
const handleClose = useCallback(() => {
|
||||||
clearWorkspaceSetup();
|
clearWorkspaceSetup();
|
||||||
@@ -116,10 +122,10 @@ export function WorkspaceSetupDialog() {
|
|||||||
const payload =
|
const payload =
|
||||||
pendingWorkspaceSetup.creationMethod === "create_worktree"
|
pendingWorkspaceSetup.creationMethod === "create_worktree"
|
||||||
? await connectedClient.createPaseoWorktree({
|
? await connectedClient.createPaseoWorktree({
|
||||||
cwd: pendingWorkspaceSetup.projectPath,
|
cwd: pendingWorkspaceSetup.sourceDirectory,
|
||||||
worktreeSlug: createNameId(),
|
worktreeSlug: createNameId(),
|
||||||
})
|
})
|
||||||
: await connectedClient.openProject(pendingWorkspaceSetup.projectPath);
|
: await connectedClient.openProject(pendingWorkspaceSetup.sourceDirectory);
|
||||||
|
|
||||||
if (payload.error || !payload.workspace) {
|
if (payload.error || !payload.workspace) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -149,13 +155,13 @@ export function WorkspaceSetupDialog() {
|
|||||||
const current = useWorkspaceSetupStore.getState().pendingWorkspaceSetup;
|
const current = useWorkspaceSetupStore.getState().pendingWorkspaceSetup;
|
||||||
return (
|
return (
|
||||||
current?.serverId === pendingWorkspaceSetup?.serverId &&
|
current?.serverId === pendingWorkspaceSetup?.serverId &&
|
||||||
current?.projectPath === pendingWorkspaceSetup?.projectPath &&
|
current?.sourceDirectory === pendingWorkspaceSetup?.sourceDirectory &&
|
||||||
current?.creationMethod === pendingWorkspaceSetup?.creationMethod
|
current?.creationMethod === pendingWorkspaceSetup?.creationMethod
|
||||||
);
|
);
|
||||||
}, [
|
}, [
|
||||||
pendingWorkspaceSetup?.creationMethod,
|
pendingWorkspaceSetup?.creationMethod,
|
||||||
pendingWorkspaceSetup?.projectPath,
|
|
||||||
pendingWorkspaceSetup?.serverId,
|
pendingWorkspaceSetup?.serverId,
|
||||||
|
pendingWorkspaceSetup?.sourceDirectory,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const handleCreateChatAgent = useCallback(
|
const handleCreateChatAgent = useCallback(
|
||||||
@@ -170,10 +176,11 @@ export function WorkspaceSetupDialog() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const encodedImages = await encodeImages(images);
|
const encodedImages = await encodeImages(images);
|
||||||
const workspaceDirectory = workspace.projectRootPath ?? projectPath;
|
const workspaceDirectory = requireWorkspaceExecutionAuthority({ workspace }).workspaceDirectory;
|
||||||
const agent = await connectedClient.createAgent({
|
const agent = await connectedClient.createAgent({
|
||||||
provider: composerState.selectedProvider,
|
provider: composerState.selectedProvider,
|
||||||
cwd: workspaceDirectory,
|
cwd: workspaceDirectory,
|
||||||
|
workspaceId: requireWorkspaceRecordId(workspace.id),
|
||||||
...(composerState.modeOptions.length > 0 && composerState.selectedMode !== ""
|
...(composerState.modeOptions.length > 0 && composerState.selectedMode !== ""
|
||||||
? { modeId: composerState.selectedMode }
|
? { modeId: composerState.selectedMode }
|
||||||
: {}),
|
: {}),
|
||||||
@@ -227,10 +234,11 @@ export function WorkspaceSetupDialog() {
|
|||||||
throw new Error("Workspace setup composer state is required");
|
throw new Error("Workspace setup composer state is required");
|
||||||
}
|
}
|
||||||
|
|
||||||
const workspaceDirectory = workspace.projectRootPath ?? projectPath;
|
const workspaceDirectory = requireWorkspaceExecutionAuthority({ workspace }).workspaceDirectory;
|
||||||
const agent = await connectedClient.createAgent({
|
const agent = await connectedClient.createAgent({
|
||||||
provider: composerState.selectedProvider,
|
provider: composerState.selectedProvider,
|
||||||
cwd: workspaceDirectory,
|
cwd: workspaceDirectory,
|
||||||
|
workspaceId: requireWorkspaceRecordId(workspace.id),
|
||||||
terminal: true,
|
terminal: true,
|
||||||
...(terminalPrompt.trim() ? { initialPrompt: terminalPrompt.trim() } : {}),
|
...(terminalPrompt.trim() ? { initialPrompt: terminalPrompt.trim() } : {}),
|
||||||
});
|
});
|
||||||
@@ -274,11 +282,7 @@ export function WorkspaceSetupDialog() {
|
|||||||
setErrorMessage(null);
|
setErrorMessage(null);
|
||||||
const workspace = await ensureWorkspace();
|
const workspace = await ensureWorkspace();
|
||||||
const connectedClient = withConnectedClient();
|
const connectedClient = withConnectedClient();
|
||||||
const workspaceDirectory = workspace.projectRootPath ?? projectPath;
|
const workspaceDirectory = requireWorkspaceExecutionAuthority({ workspace }).workspaceDirectory;
|
||||||
|
|
||||||
if (!workspaceDirectory) {
|
|
||||||
throw new Error("Workspace directory not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload = await connectedClient.createTerminal(workspaceDirectory);
|
const payload = await connectedClient.createTerminal(workspaceDirectory);
|
||||||
if (payload.error || !payload.terminal) {
|
if (payload.error || !payload.terminal) {
|
||||||
@@ -304,12 +308,12 @@ export function WorkspaceSetupDialog() {
|
|||||||
const workspaceTitle =
|
const workspaceTitle =
|
||||||
workspace?.name ||
|
workspace?.name ||
|
||||||
workspace?.projectDisplayName ||
|
workspace?.projectDisplayName ||
|
||||||
projectName ||
|
displayName ||
|
||||||
projectPath.split(/[\\/]/).filter(Boolean).pop() ||
|
sourceDirectory.split(/[\\/]/).filter(Boolean).pop() ||
|
||||||
projectPath;
|
sourceDirectory;
|
||||||
const workspacePath = workspace?.projectRootPath || projectPath;
|
const workspacePath = workspace?.workspaceDirectory || "Workspace will be created before launch.";
|
||||||
|
|
||||||
if (!pendingWorkspaceSetup || !projectPath) {
|
if (!pendingWorkspaceSetup || !sourceDirectory) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -378,7 +382,7 @@ export function WorkspaceSetupDialog() {
|
|||||||
</Text>
|
</Text>
|
||||||
<View style={styles.composerCard}>
|
<View style={styles.composerCard}>
|
||||||
<Composer
|
<Composer
|
||||||
agentId={`workspace-setup:${serverId}:${projectPath}`}
|
agentId={`workspace-setup:${serverId}:${sourceDirectory}`}
|
||||||
serverId={serverId}
|
serverId={serverId}
|
||||||
isInputActive={true}
|
isInputActive={true}
|
||||||
onSubmitMessage={handleCreateChatAgent}
|
onSubmitMessage={handleCreateChatAgent}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import { getBindingIdForAction, getDefaultKeysForAction } from "@/keyboard/keybo
|
|||||||
import { useKeyboardShortcutOverrides } from "@/hooks/use-keyboard-shortcut-overrides";
|
import { useKeyboardShortcutOverrides } from "@/hooks/use-keyboard-shortcut-overrides";
|
||||||
import { getShortcutOs } from "@/utils/shortcut-platform";
|
import { getShortcutOs } from "@/utils/shortcut-platform";
|
||||||
import { getIsDesktop } from "@/constants/layout";
|
import { getIsDesktop } from "@/constants/layout";
|
||||||
import { resolveHydratedWorkspaceId } from "@/utils/resolve-hydrated-workspace-id";
|
import { resolveWorkspaceIdByExecutionDirectory } from "@/utils/workspace-execution";
|
||||||
import { prepareWorkspaceTab } from "@/utils/workspace-navigation";
|
import { prepareWorkspaceTab } from "@/utils/workspace-navigation";
|
||||||
import { focusWithRetries } from "@/utils/web-focus";
|
import { focusWithRetries } from "@/utils/web-focus";
|
||||||
|
|
||||||
@@ -221,9 +221,9 @@ export function useCommandCenter() {
|
|||||||
// Don't restore focus back to the prior element after we navigate.
|
// Don't restore focus back to the prior element after we navigate.
|
||||||
clearCommandCenterFocusRestoreElement();
|
clearCommandCenterFocusRestoreElement();
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
const workspaceId = resolveHydratedWorkspaceId({
|
const workspaceId = resolveWorkspaceIdByExecutionDirectory({
|
||||||
workspaces: useSessionStore.getState().sessions[agent.serverId]?.workspaces?.values(),
|
workspaces: useSessionStore.getState().sessions[agent.serverId]?.workspaces?.values(),
|
||||||
path: agent.cwd,
|
workspaceDirectory: agent.cwd,
|
||||||
});
|
});
|
||||||
if (!workspaceId) {
|
if (!workspaceId) {
|
||||||
router.navigate(buildHostAgentDetailRoute(agent.serverId, agent.id) as any);
|
router.navigate(buildHostAgentDetailRoute(agent.serverId, agent.id) as any);
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
|
import { Text, View } from "react-native";
|
||||||
import { FileText } from "lucide-react-native";
|
import { FileText } from "lucide-react-native";
|
||||||
import invariant from "tiny-invariant";
|
import invariant from "tiny-invariant";
|
||||||
import { FilePane } from "@/components/file-pane";
|
import { FilePane } from "@/components/file-pane";
|
||||||
import { usePaneContext } from "@/panels/pane-context";
|
import { usePaneContext } from "@/panels/pane-context";
|
||||||
import type { PanelRegistration } from "@/panels/panel-registry";
|
import type { PanelRegistration } from "@/panels/panel-registry";
|
||||||
|
import { useSessionStore } from "@/stores/session-store";
|
||||||
|
import { resolveWorkspaceExecutionAuthority } from "@/utils/workspace-execution";
|
||||||
|
|
||||||
function useFilePanelDescriptor(target: { kind: "file"; path: string }) {
|
function useFilePanelDescriptor(target: { kind: "file"; path: string }) {
|
||||||
const fileName = target.path.split("/").filter(Boolean).pop() ?? target.path;
|
const fileName = target.path.split("/").filter(Boolean).pop() ?? target.path;
|
||||||
@@ -17,8 +20,27 @@ function useFilePanelDescriptor(target: { kind: "file"; path: string }) {
|
|||||||
|
|
||||||
function FilePanel() {
|
function FilePanel() {
|
||||||
const { serverId, workspaceId, target } = usePaneContext();
|
const { serverId, workspaceId, target } = usePaneContext();
|
||||||
|
const authority = useSessionStore((state) =>
|
||||||
|
resolveWorkspaceExecutionAuthority({
|
||||||
|
workspaces: state.sessions[serverId]?.workspaces,
|
||||||
|
workspaceId,
|
||||||
|
}),
|
||||||
|
);
|
||||||
invariant(target.kind === "file", "FilePanel requires file target");
|
invariant(target.kind === "file", "FilePanel requires file target");
|
||||||
return <FilePane serverId={serverId} workspaceRoot={workspaceId} filePath={target.path} />;
|
if (!authority) {
|
||||||
|
return (
|
||||||
|
<View style={{ flex: 1, alignItems: "center", justifyContent: "center", padding: 16 }}>
|
||||||
|
<Text>Workspace execution directory not found.</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<FilePane
|
||||||
|
serverId={serverId}
|
||||||
|
workspaceRoot={authority.workspaceDirectory}
|
||||||
|
filePath={target.path}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const filePanelRegistration: PanelRegistration<"file"> = {
|
export const filePanelRegistration: PanelRegistration<"file"> = {
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ import { useProviderRecency } from "@/stores/provider-recency-store";
|
|||||||
import { useSessionStore } from "@/stores/session-store";
|
import { useSessionStore } from "@/stores/session-store";
|
||||||
import { normalizeAgentSnapshot } from "@/utils/agent-snapshots";
|
import { normalizeAgentSnapshot } from "@/utils/agent-snapshots";
|
||||||
import { toErrorMessage } from "@/utils/error-messages";
|
import { toErrorMessage } from "@/utils/error-messages";
|
||||||
|
import {
|
||||||
|
getWorkspaceExecutionAuthority,
|
||||||
|
requireWorkspaceRecordId,
|
||||||
|
} from "@/utils/workspace-execution";
|
||||||
|
|
||||||
const MAX_VISIBLE_PROVIDER_TILES = 4;
|
const MAX_VISIBLE_PROVIDER_TILES = 4;
|
||||||
|
|
||||||
@@ -36,11 +40,11 @@ function LauncherPanel() {
|
|||||||
const { serverId, workspaceId, target, retargetCurrentTab, isPaneFocused } = usePaneContext();
|
const { serverId, workspaceId, target, retargetCurrentTab, isPaneFocused } = usePaneContext();
|
||||||
const client = useHostRuntimeClient(serverId);
|
const client = useHostRuntimeClient(serverId);
|
||||||
const isConnected = useHostRuntimeIsConnected(serverId);
|
const isConnected = useHostRuntimeIsConnected(serverId);
|
||||||
const workspaceDirectory = useSessionStore(
|
const workspaces = useSessionStore((state) => state.sessions[serverId]?.workspaces);
|
||||||
(state) =>
|
const workspaceAuthority = getWorkspaceExecutionAuthority({ workspaces, workspaceId });
|
||||||
state.sessions[serverId]?.workspaces.get(workspaceId)?.workspaceDirectory ??
|
const workspaceDirectory = workspaceAuthority.ok
|
||||||
state.sessions[serverId]?.workspaces.get(workspaceId)?.projectRootPath,
|
? workspaceAuthority.authority.workspaceDirectory
|
||||||
);
|
: null;
|
||||||
const { providers, recordUsage } = useProviderRecency();
|
const { providers, recordUsage } = useProviderRecency();
|
||||||
const setAgents = useSessionStore((state) => state.setAgents);
|
const setAgents = useSessionStore((state) => state.setAgents);
|
||||||
const [pendingAction, setPendingAction] = useState<string | null>(null);
|
const [pendingAction, setPendingAction] = useState<string | null>(null);
|
||||||
@@ -62,6 +66,11 @@ function LauncherPanel() {
|
|||||||
setErrorMessage(!workspaceDirectory ? "Workspace directory not found" : "Host is not connected");
|
setErrorMessage(!workspaceDirectory ? "Workspace directory not found" : "Host is not connected");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!workspaceAuthority.ok) {
|
||||||
|
setErrorMessage(workspaceAuthority.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const persistedWorkspaceId = requireWorkspaceRecordId(workspaceAuthority.authority.workspaceId);
|
||||||
|
|
||||||
setPendingAction(providerId);
|
setPendingAction(providerId);
|
||||||
setErrorMessage(null);
|
setErrorMessage(null);
|
||||||
@@ -70,6 +79,7 @@ function LauncherPanel() {
|
|||||||
const agent = await client.createAgent({
|
const agent = await client.createAgent({
|
||||||
provider: providerId,
|
provider: providerId,
|
||||||
cwd: workspaceDirectory,
|
cwd: workspaceDirectory,
|
||||||
|
workspaceId: persistedWorkspaceId,
|
||||||
terminal: true,
|
terminal: true,
|
||||||
});
|
});
|
||||||
recordUsage(providerId);
|
recordUsage(providerId);
|
||||||
@@ -87,7 +97,16 @@ function LauncherPanel() {
|
|||||||
setPendingAction((current) => (current === providerId ? null : current));
|
setPendingAction((current) => (current === providerId ? null : current));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[client, isConnected, recordUsage, retargetCurrentTab, serverId, setAgents, workspaceDirectory],
|
[
|
||||||
|
client,
|
||||||
|
isConnected,
|
||||||
|
recordUsage,
|
||||||
|
retargetCurrentTab,
|
||||||
|
serverId,
|
||||||
|
setAgents,
|
||||||
|
workspaceAuthority,
|
||||||
|
workspaceDirectory,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const openDraftTab = useCallback(() => {
|
const openDraftTab = useCallback(() => {
|
||||||
@@ -131,7 +150,9 @@ function LauncherPanel() {
|
|||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<View style={[styles.content, styles.loadingContent]}>
|
<View style={[styles.content, styles.loadingContent]}>
|
||||||
<ActivityIndicator />
|
<Text style={styles.errorText}>
|
||||||
|
{workspaceAuthority.ok ? "Workspace execution directory not found." : workspaceAuthority.message}
|
||||||
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { Terminal } from "lucide-react-native";
|
import { Terminal } from "lucide-react-native";
|
||||||
import { View } from "react-native";
|
import { Text, View } from "react-native";
|
||||||
import { useIsFocused } from "@react-navigation/native";
|
import { useIsFocused } from "@react-navigation/native";
|
||||||
import invariant from "tiny-invariant";
|
import invariant from "tiny-invariant";
|
||||||
import type { ListTerminalsResponse } from "@server/shared/messages";
|
import type { ListTerminalsResponse } from "@server/shared/messages";
|
||||||
@@ -8,6 +8,7 @@ import { TerminalPane } from "@/components/terminal-pane";
|
|||||||
import { usePaneContext } from "@/panels/pane-context";
|
import { usePaneContext } from "@/panels/pane-context";
|
||||||
import type { PanelDescriptor, PanelRegistration } from "@/panels/panel-registry";
|
import type { PanelDescriptor, PanelRegistration } from "@/panels/panel-registry";
|
||||||
import { useSessionStore } from "@/stores/session-store";
|
import { useSessionStore } from "@/stores/session-store";
|
||||||
|
import { getWorkspaceExecutionAuthority } from "@/utils/workspace-execution";
|
||||||
|
|
||||||
type ListTerminalsPayload = ListTerminalsResponse["payload"];
|
type ListTerminalsPayload = ListTerminalsResponse["payload"];
|
||||||
|
|
||||||
@@ -24,18 +25,22 @@ function useTerminalPanelDescriptor(
|
|||||||
context: { serverId: string; workspaceId: string },
|
context: { serverId: string; workspaceId: string },
|
||||||
): PanelDescriptor {
|
): PanelDescriptor {
|
||||||
const client = useSessionStore((state) => state.sessions[context.serverId]?.client ?? null);
|
const client = useSessionStore((state) => state.sessions[context.serverId]?.client ?? null);
|
||||||
const workspaceDirectory = useSessionStore(
|
const workspaces = useSessionStore((state) => state.sessions[context.serverId]?.workspaces);
|
||||||
(state) =>
|
const workspaceAuthority = getWorkspaceExecutionAuthority({
|
||||||
state.sessions[context.serverId]?.workspaces.get(context.workspaceId)?.workspaceDirectory ??
|
workspaces,
|
||||||
state.sessions[context.serverId]?.workspaces.get(context.workspaceId)?.projectRootPath ??
|
workspaceId: context.workspaceId,
|
||||||
null,
|
});
|
||||||
);
|
const workspaceDirectory = workspaceAuthority.ok
|
||||||
|
? workspaceAuthority.authority.workspaceDirectory
|
||||||
|
: null;
|
||||||
const terminalsQuery = useQuery({
|
const terminalsQuery = useQuery({
|
||||||
queryKey: ["terminals", context.serverId, workspaceDirectory] as const,
|
queryKey: ["terminals", context.serverId, workspaceDirectory] as const,
|
||||||
enabled: Boolean(client && workspaceDirectory),
|
enabled: Boolean(client && workspaceDirectory),
|
||||||
queryFn: async (): Promise<ListTerminalsPayload> => {
|
queryFn: async (): Promise<ListTerminalsPayload> => {
|
||||||
if (!client || !workspaceDirectory) {
|
if (!client || !workspaceDirectory) {
|
||||||
return { cwd: workspaceDirectory ?? "", terminals: [], requestId: "missing-client" };
|
throw new Error(
|
||||||
|
workspaceAuthority.ok ? "Workspace execution directory not found" : workspaceAuthority.message,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return client.listTerminals(workspaceDirectory);
|
return client.listTerminals(workspaceDirectory);
|
||||||
},
|
},
|
||||||
@@ -56,18 +61,27 @@ function useTerminalPanelDescriptor(
|
|||||||
function TerminalPanel() {
|
function TerminalPanel() {
|
||||||
const isFocused = useIsFocused();
|
const isFocused = useIsFocused();
|
||||||
const { serverId, workspaceId, target, isPaneFocused } = usePaneContext();
|
const { serverId, workspaceId, target, isPaneFocused } = usePaneContext();
|
||||||
const workspaceDirectory = useSessionStore(
|
const workspaces = useSessionStore((state) => state.sessions[serverId]?.workspaces);
|
||||||
(state) =>
|
const workspaceAuthority = getWorkspaceExecutionAuthority({ workspaces, workspaceId });
|
||||||
state.sessions[serverId]?.workspaces.get(workspaceId)?.workspaceDirectory ??
|
const workspaceDirectory = workspaceAuthority.ok
|
||||||
state.sessions[serverId]?.workspaces.get(workspaceId)?.projectRootPath ??
|
? workspaceAuthority.authority.workspaceDirectory
|
||||||
null,
|
: null;
|
||||||
);
|
|
||||||
invariant(target.kind === "terminal", "TerminalPanel requires terminal target");
|
invariant(target.kind === "terminal", "TerminalPanel requires terminal target");
|
||||||
|
|
||||||
if (!isFocused || !workspaceDirectory) {
|
if (!isFocused) {
|
||||||
return <View style={{ flex: 1 }} />;
|
return <View style={{ flex: 1 }} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!workspaceDirectory) {
|
||||||
|
return (
|
||||||
|
<View style={{ flex: 1, alignItems: "center", justifyContent: "center", padding: 16 }}>
|
||||||
|
<Text>
|
||||||
|
{workspaceAuthority.ok ? "Workspace execution directory not found." : workspaceAuthority.message}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TerminalPane
|
<TerminalPane
|
||||||
serverId={serverId}
|
serverId={serverId}
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ import type {
|
|||||||
AgentSessionConfig,
|
AgentSessionConfig,
|
||||||
} from "@server/server/agent/agent-sdk-types";
|
} from "@server/server/agent/agent-sdk-types";
|
||||||
import { AGENT_PROVIDER_DEFINITIONS } from "@server/server/agent/provider-manifest";
|
import { AGENT_PROVIDER_DEFINITIONS } from "@server/server/agent/provider-manifest";
|
||||||
import { resolveHydratedWorkspaceId } from "@/utils/resolve-hydrated-workspace-id";
|
import { resolveWorkspaceIdByExecutionDirectory } from "@/utils/workspace-execution";
|
||||||
import { prepareWorkspaceTab } from "@/utils/workspace-navigation";
|
import { prepareWorkspaceTab } from "@/utils/workspace-navigation";
|
||||||
import { useDesktopDragHandlers } from "@/utils/desktop-window";
|
import { useDesktopDragHandlers } from "@/utils/desktop-window";
|
||||||
import { useKeyboardShiftStyle } from "@/hooks/use-keyboard-shift-style";
|
import { useKeyboardShiftStyle } from "@/hooks/use-keyboard-shift-style";
|
||||||
@@ -634,6 +634,16 @@ function DraftAgentScreenContent({
|
|||||||
isGit: isAttachWorktree && selectedWorktreePath ? true : checkout?.isGit === true,
|
isGit: isAttachWorktree && selectedWorktreePath ? true : checkout?.isGit === true,
|
||||||
};
|
};
|
||||||
}, [selectedServerId, explorerCwd, isAttachWorktree, selectedWorktreePath, checkout?.isGit]);
|
}, [selectedServerId, explorerCwd, isAttachWorktree, selectedWorktreePath, checkout?.isGit]);
|
||||||
|
const draftExplorerWorkspaceId = useSessionStore(
|
||||||
|
useCallback(
|
||||||
|
(state) =>
|
||||||
|
resolveWorkspaceIdByExecutionDirectory({
|
||||||
|
workspaces: selectedServerId ? state.sessions[selectedServerId]?.workspaces?.values() : null,
|
||||||
|
workspaceDirectory: explorerCwd,
|
||||||
|
}),
|
||||||
|
[explorerCwd, selectedServerId],
|
||||||
|
),
|
||||||
|
);
|
||||||
const canOpenExplorer = draftExplorerCheckout !== null;
|
const canOpenExplorer = draftExplorerCheckout !== null;
|
||||||
const openExplorerForDraftCheckout = useCallback(() => {
|
const openExplorerForDraftCheckout = useCallback(() => {
|
||||||
if (!draftExplorerCheckout) {
|
if (!draftExplorerCheckout) {
|
||||||
@@ -910,9 +920,9 @@ function DraftAgentScreenContent({
|
|||||||
|
|
||||||
const createdWorkingDir = typeof result.cwd === "string" ? result.cwd.trim() : "";
|
const createdWorkingDir = typeof result.cwd === "string" ? result.cwd.trim() : "";
|
||||||
const configuredWorkingDir = config.cwd.trim();
|
const configuredWorkingDir = config.cwd.trim();
|
||||||
const workspaceId = resolveHydratedWorkspaceId({
|
const workspaceId = resolveWorkspaceIdByExecutionDirectory({
|
||||||
workspaces: useSessionStore.getState().sessions[selectedServerId]?.workspaces?.values(),
|
workspaces: useSessionStore.getState().sessions[selectedServerId]?.workspaces?.values(),
|
||||||
path: createdWorkingDir.length > 0 ? createdWorkingDir : configuredWorkingDir,
|
workspaceDirectory: createdWorkingDir.length > 0 ? createdWorkingDir : configuredWorkingDir,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -1218,7 +1228,7 @@ function DraftAgentScreenContent({
|
|||||||
{!isMobile && isExplorerOpen && explorerServerId && draftExplorerCheckout ? (
|
{!isMobile && isExplorerOpen && explorerServerId && draftExplorerCheckout ? (
|
||||||
<ExplorerSidebar
|
<ExplorerSidebar
|
||||||
serverId={explorerServerId}
|
serverId={explorerServerId}
|
||||||
workspaceId={draftExplorerCheckout.cwd}
|
workspaceId={draftExplorerWorkspaceId}
|
||||||
workspaceRoot={draftExplorerCheckout.cwd}
|
workspaceRoot={draftExplorerCheckout.cwd}
|
||||||
isGit={explorerIsGit}
|
isGit={explorerIsGit}
|
||||||
/>
|
/>
|
||||||
@@ -1241,7 +1251,7 @@ function DraftAgentScreenContent({
|
|||||||
{isMobile && explorerServerId && draftExplorerCheckout ? (
|
{isMobile && explorerServerId && draftExplorerCheckout ? (
|
||||||
<ExplorerSidebar
|
<ExplorerSidebar
|
||||||
serverId={explorerServerId}
|
serverId={explorerServerId}
|
||||||
workspaceId={draftExplorerCheckout.cwd}
|
workspaceId={draftExplorerWorkspaceId}
|
||||||
workspaceRoot={draftExplorerCheckout.cwd}
|
workspaceRoot={draftExplorerCheckout.cwd}
|
||||||
isGit={explorerIsGit}
|
isGit={explorerIsGit}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ import { buildWorkspaceDraftAgentConfig } from "@/screens/workspace/workspace-dr
|
|||||||
import { buildDraftStoreKey } from "@/stores/draft-keys";
|
import { buildDraftStoreKey } from "@/stores/draft-keys";
|
||||||
import { type Agent, useSessionStore } from "@/stores/session-store";
|
import { type Agent, useSessionStore } from "@/stores/session-store";
|
||||||
import { encodeImages } from "@/utils/encode-images";
|
import { encodeImages } from "@/utils/encode-images";
|
||||||
|
import {
|
||||||
|
getWorkspaceExecutionAuthority,
|
||||||
|
requireWorkspaceRecordId,
|
||||||
|
} from "@/utils/workspace-execution";
|
||||||
import { shouldAutoFocusWorkspaceDraftComposer } from "@/screens/workspace/workspace-draft-pane-focus";
|
import { shouldAutoFocusWorkspaceDraftComposer } from "@/screens/workspace/workspace-draft-pane-focus";
|
||||||
import type { AgentCapabilityFlags } from "@server/server/agent/agent-sdk-types";
|
import type { AgentCapabilityFlags } from "@server/server/agent/agent-sdk-types";
|
||||||
import type { AgentSnapshotPayload } from "@server/shared/messages";
|
import type { AgentSnapshotPayload } from "@server/shared/messages";
|
||||||
@@ -49,11 +53,10 @@ export function WorkspaceDraftAgentTab({
|
|||||||
}: WorkspaceDraftAgentTabProps) {
|
}: WorkspaceDraftAgentTabProps) {
|
||||||
const client = useHostRuntimeClient(serverId);
|
const client = useHostRuntimeClient(serverId);
|
||||||
const isConnected = useHostRuntimeIsConnected(serverId);
|
const isConnected = useHostRuntimeIsConnected(serverId);
|
||||||
const workspaceDirectory = useSessionStore(
|
const workspaces = useSessionStore((state) => state.sessions[serverId]?.workspaces);
|
||||||
(state) =>
|
const workspaceAuthority = getWorkspaceExecutionAuthority({ workspaces, workspaceId });
|
||||||
state.sessions[serverId]?.workspaces.get(workspaceId)?.workspaceDirectory ??
|
const workspaceExecutionAuthority = workspaceAuthority.ok ? workspaceAuthority.authority : null;
|
||||||
state.sessions[serverId]?.workspaces.get(workspaceId)?.projectRootPath,
|
const workspaceDirectory = workspaceExecutionAuthority?.workspaceDirectory ?? null;
|
||||||
);
|
|
||||||
const addImagesRef = useRef<((images: ImageAttachment[]) => void) | null>(null);
|
const addImagesRef = useRef<((images: ImageAttachment[]) => void) | null>(null);
|
||||||
const draftStoreKey = useMemo(
|
const draftStoreKey = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -69,10 +72,10 @@ export function WorkspaceDraftAgentTab({
|
|||||||
draftKey: draftStoreKey,
|
draftKey: draftStoreKey,
|
||||||
composer: {
|
composer: {
|
||||||
initialServerId: serverId,
|
initialServerId: serverId,
|
||||||
initialValues: { workingDir: workspaceDirectory },
|
initialValues: workspaceDirectory ? { workingDir: workspaceDirectory } : undefined,
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
onlineServerIds: isConnected ? [serverId] : [],
|
onlineServerIds: isConnected ? [serverId] : [],
|
||||||
lockedWorkingDir: workspaceDirectory,
|
lockedWorkingDir: workspaceDirectory ?? undefined,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -152,6 +155,7 @@ export function WorkspaceDraftAgentTab({
|
|||||||
},
|
},
|
||||||
createRequest: async ({ attempt, text, images }) => {
|
createRequest: async ({ attempt, text, images }) => {
|
||||||
invariant(workspaceDirectory, "Workspace directory is required");
|
invariant(workspaceDirectory, "Workspace directory is required");
|
||||||
|
invariant(workspaceExecutionAuthority, "Workspace authority is required");
|
||||||
if (!client) {
|
if (!client) {
|
||||||
throw new Error("Host is not connected");
|
throw new Error("Host is not connected");
|
||||||
}
|
}
|
||||||
@@ -169,6 +173,7 @@ export function WorkspaceDraftAgentTab({
|
|||||||
const imagesData = await encodeImages(images);
|
const imagesData = await encodeImages(images);
|
||||||
const result = await client.createAgent({
|
const result = await client.createAgent({
|
||||||
config,
|
config,
|
||||||
|
workspaceId: requireWorkspaceRecordId(workspaceExecutionAuthority.workspaceId),
|
||||||
...(text ? { initialPrompt: text } : {}),
|
...(text ? { initialPrompt: text } : {}),
|
||||||
clientMessageId: attempt.clientMessageId,
|
clientMessageId: attempt.clientMessageId,
|
||||||
...(imagesData && imagesData.length > 0 ? { images: imagesData } : {}),
|
...(imagesData && imagesData.length > 0 ? { images: imagesData } : {}),
|
||||||
|
|||||||
@@ -59,7 +59,6 @@ import { useKeyboardActionHandler } from "@/hooks/use-keyboard-action-handler";
|
|||||||
import type { KeyboardActionDefinition } from "@/keyboard/keyboard-action-dispatcher";
|
import type { KeyboardActionDefinition } from "@/keyboard/keyboard-action-dispatcher";
|
||||||
import { useCreateFlowStore } from "@/stores/create-flow-store";
|
import { useCreateFlowStore } from "@/stores/create-flow-store";
|
||||||
import { decodeWorkspaceIdFromPathSegment } from "@/utils/host-routes";
|
import { decodeWorkspaceIdFromPathSegment } from "@/utils/host-routes";
|
||||||
import { normalizeWorkspaceIdentity } from "@/utils/workspace-identity";
|
|
||||||
import {
|
import {
|
||||||
normalizeWorkspaceTabTarget,
|
normalizeWorkspaceTabTarget,
|
||||||
workspaceTabTargetsEqual,
|
workspaceTabTargetsEqual,
|
||||||
@@ -77,6 +76,10 @@ import { useArchiveAgent } from "@/hooks/use-archive-agent";
|
|||||||
import { useStableEvent } from "@/hooks/use-stable-event";
|
import { useStableEvent } from "@/hooks/use-stable-event";
|
||||||
import { buildProviderCommand } from "@/utils/provider-command-templates";
|
import { buildProviderCommand } from "@/utils/provider-command-templates";
|
||||||
import { generateDraftId } from "@/stores/draft-keys";
|
import { generateDraftId } from "@/stores/draft-keys";
|
||||||
|
import {
|
||||||
|
resolveWorkspaceExecutionAuthority,
|
||||||
|
resolveWorkspaceRouteId,
|
||||||
|
} from "@/utils/workspace-execution";
|
||||||
import {
|
import {
|
||||||
WorkspaceTabPresentationResolver,
|
WorkspaceTabPresentationResolver,
|
||||||
WorkspaceTabIcon,
|
WorkspaceTabIcon,
|
||||||
@@ -554,23 +557,13 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
|
|||||||
const isFocusModeEnabled = usePanelStore((state) => state.desktop.focusModeEnabled);
|
const isFocusModeEnabled = usePanelStore((state) => state.desktop.focusModeEnabled);
|
||||||
|
|
||||||
const normalizedServerId = trimNonEmpty(decodeSegment(serverId)) ?? "";
|
const normalizedServerId = trimNonEmpty(decodeSegment(serverId)) ?? "";
|
||||||
const rawWorkspaceIdentifier =
|
const normalizedWorkspaceId =
|
||||||
normalizeWorkspaceIdentity(decodeWorkspaceIdFromPathSegment(workspaceId)) ?? "";
|
resolveWorkspaceRouteId({
|
||||||
|
routeWorkspaceId: decodeWorkspaceIdFromPathSegment(workspaceId),
|
||||||
// Resolve the workspace ID: first try direct map key, then fall back to path-based lookup.
|
}) ?? "";
|
||||||
// This lets URLs that encode a filesystem path (e.g. from deep links or older bookmarks)
|
const sessionWorkspaces = useSessionStore(
|
||||||
// resolve correctly even though the map is keyed by numeric DB IDs.
|
(state) => state.sessions[normalizedServerId]?.workspaces,
|
||||||
const normalizedWorkspaceId = useSessionStore((state) => {
|
);
|
||||||
if (!normalizedServerId || !rawWorkspaceIdentifier) return "";
|
|
||||||
const workspaces = state.sessions[normalizedServerId]?.workspaces;
|
|
||||||
if (!workspaces) return rawWorkspaceIdentifier;
|
|
||||||
if (workspaces.has(rawWorkspaceIdentifier)) return rawWorkspaceIdentifier;
|
|
||||||
for (const [id, ws] of workspaces.entries()) {
|
|
||||||
if (normalizeWorkspaceIdentity(ws.workspaceDirectory) === rawWorkspaceIdentifier) return id;
|
|
||||||
if (normalizeWorkspaceIdentity(ws.projectRootPath) === rawWorkspaceIdentifier) return id;
|
|
||||||
}
|
|
||||||
return rawWorkspaceIdentifier;
|
|
||||||
});
|
|
||||||
|
|
||||||
const workspaceTerminalScopeKey =
|
const workspaceTerminalScopeKey =
|
||||||
normalizedServerId && normalizedWorkspaceId
|
normalizedServerId && normalizedWorkspaceId
|
||||||
@@ -583,11 +576,17 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const client = useHostRuntimeClient(normalizedServerId);
|
const client = useHostRuntimeClient(normalizedServerId);
|
||||||
const isConnected = useHostRuntimeIsConnected(normalizedServerId);
|
const isConnected = useHostRuntimeIsConnected(normalizedServerId);
|
||||||
const workspaceDescriptor = useSessionStore(
|
const workspaceDescriptor = sessionWorkspaces?.get(normalizedWorkspaceId) ?? null;
|
||||||
(state) => state.sessions[normalizedServerId]?.workspaces.get(normalizedWorkspaceId) ?? null,
|
const workspaceAuthority = useMemo(
|
||||||
|
() =>
|
||||||
|
resolveWorkspaceExecutionAuthority({
|
||||||
|
workspaces: sessionWorkspaces,
|
||||||
|
workspaceId: normalizedWorkspaceId,
|
||||||
|
}),
|
||||||
|
[normalizedWorkspaceId, sessionWorkspaces],
|
||||||
);
|
);
|
||||||
const workspaceDirectory =
|
const workspaceDirectory = workspaceAuthority?.workspaceDirectory ?? null;
|
||||||
workspaceDescriptor?.workspaceDirectory ?? workspaceDescriptor?.projectRootPath ?? null;
|
const isMissingWorkspaceExecutionAuthority = Boolean(workspaceDescriptor && !workspaceAuthority);
|
||||||
|
|
||||||
const workspaceAgentVisibility = useStoreWithEqualityFn(
|
const workspaceAgentVisibility = useStoreWithEqualityFn(
|
||||||
useSessionStore,
|
useSessionStore,
|
||||||
@@ -633,7 +632,7 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
|
|||||||
terminals: current?.terminals ?? [],
|
terminals: current?.terminals ?? [],
|
||||||
terminal: createdTerminal,
|
terminal: createdTerminal,
|
||||||
});
|
});
|
||||||
const cwd = current?.cwd ?? workspaceDirectory ?? undefined;
|
const cwd = current?.cwd ?? workspaceDirectory;
|
||||||
return {
|
return {
|
||||||
...(cwd ? { cwd } : {}),
|
...(cwd ? { cwd } : {}),
|
||||||
terminals: nextTerminals,
|
terminals: nextTerminals,
|
||||||
@@ -706,7 +705,10 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
|
|||||||
}, [client, isConnected, queryClient, terminalsQueryKey, workspaceDirectory]);
|
}, [client, isConnected, queryClient, terminalsQueryKey, workspaceDirectory]);
|
||||||
|
|
||||||
const checkoutQuery = useQuery({
|
const checkoutQuery = useQuery({
|
||||||
queryKey: checkoutStatusQueryKey(normalizedServerId, workspaceDirectory ?? ""),
|
queryKey: checkoutStatusQueryKey(
|
||||||
|
normalizedServerId,
|
||||||
|
workspaceDirectory ?? `missing-workspace-directory:${normalizedWorkspaceId}`,
|
||||||
|
),
|
||||||
enabled:
|
enabled:
|
||||||
Boolean(client && isConnected) &&
|
Boolean(client && isConnected) &&
|
||||||
Boolean(workspaceDirectory),
|
Boolean(workspaceDirectory),
|
||||||
@@ -1749,6 +1751,12 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
|
|||||||
<View style={styles.emptyState}>
|
<View style={styles.emptyState}>
|
||||||
<ActivityIndicator color={theme.colors.foregroundMuted} />
|
<ActivityIndicator color={theme.colors.foregroundMuted} />
|
||||||
</View>
|
</View>
|
||||||
|
) : isMissingWorkspaceExecutionAuthority ? (
|
||||||
|
<View style={styles.emptyState}>
|
||||||
|
<Text style={styles.emptyStateText}>
|
||||||
|
Workspace execution directory is missing. Reload workspace data before opening tabs.
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
) : !activeTabDescriptor ? (
|
) : !activeTabDescriptor ? (
|
||||||
!hasHydratedAgents ? (
|
!hasHydratedAgents ? (
|
||||||
<View style={styles.emptyState}>
|
<View style={styles.emptyState}>
|
||||||
@@ -1966,10 +1974,12 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
|
|||||||
<View style={styles.headerRight}>
|
<View style={styles.headerRight}>
|
||||||
{!isMobile && isGitCheckout ? (
|
{!isMobile && isGitCheckout ? (
|
||||||
<>
|
<>
|
||||||
<WorkspaceGitActions
|
{workspaceDirectory ? (
|
||||||
serverId={normalizedServerId}
|
<WorkspaceGitActions
|
||||||
cwd={workspaceDirectory ?? ""}
|
serverId={normalizedServerId}
|
||||||
/>
|
cwd={workspaceDirectory}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
|
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Pressable
|
<Pressable
|
||||||
@@ -2148,13 +2158,15 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
{(!isFocusModeEnabled || isMobile) && (
|
{(!isFocusModeEnabled || isMobile) && (
|
||||||
<ExplorerSidebar
|
workspaceDirectory ? (
|
||||||
serverId={normalizedServerId}
|
<ExplorerSidebar
|
||||||
workspaceId={normalizedWorkspaceId}
|
serverId={normalizedServerId}
|
||||||
workspaceRoot={workspaceDirectory ?? ""}
|
workspaceId={normalizedWorkspaceId}
|
||||||
isGit={isGitCheckout}
|
workspaceRoot={workspaceDirectory}
|
||||||
onOpenFile={handleOpenFileFromExplorer}
|
isGit={isGitCheckout}
|
||||||
/>
|
onOpenFile={handleOpenFileFromExplorer}
|
||||||
|
/>
|
||||||
|
) : null
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -6,19 +6,21 @@ describe("workspace-setup-store", () => {
|
|||||||
useWorkspaceSetupStore.setState({ pendingWorkspaceSetup: null });
|
useWorkspaceSetupStore.setState({ pendingWorkspaceSetup: null });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("tracks deferred project setup by path instead of a created workspace", () => {
|
it("tracks deferred workspace setup by source directory and optional workspace id", () => {
|
||||||
useWorkspaceSetupStore.getState().beginWorkspaceSetup({
|
useWorkspaceSetupStore.getState().beginWorkspaceSetup({
|
||||||
serverId: "server-1",
|
serverId: "server-1",
|
||||||
projectPath: "/Users/test/project",
|
sourceDirectory: "/Users/test/project",
|
||||||
projectName: "project",
|
sourceWorkspaceId: "42",
|
||||||
|
displayName: "project",
|
||||||
creationMethod: "open_project",
|
creationMethod: "open_project",
|
||||||
navigationMethod: "replace",
|
navigationMethod: "replace",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(useWorkspaceSetupStore.getState().pendingWorkspaceSetup).toEqual({
|
expect(useWorkspaceSetupStore.getState().pendingWorkspaceSetup).toEqual({
|
||||||
serverId: "server-1",
|
serverId: "server-1",
|
||||||
projectPath: "/Users/test/project",
|
sourceDirectory: "/Users/test/project",
|
||||||
projectName: "project",
|
sourceWorkspaceId: "42",
|
||||||
|
displayName: "project",
|
||||||
creationMethod: "open_project",
|
creationMethod: "open_project",
|
||||||
navigationMethod: "replace",
|
navigationMethod: "replace",
|
||||||
});
|
});
|
||||||
@@ -27,7 +29,7 @@ describe("workspace-setup-store", () => {
|
|||||||
it("clears pending setup state", () => {
|
it("clears pending setup state", () => {
|
||||||
useWorkspaceSetupStore.getState().beginWorkspaceSetup({
|
useWorkspaceSetupStore.getState().beginWorkspaceSetup({
|
||||||
serverId: "server-1",
|
serverId: "server-1",
|
||||||
projectPath: "/Users/test/project",
|
sourceDirectory: "/Users/test/project",
|
||||||
creationMethod: "create_worktree",
|
creationMethod: "create_worktree",
|
||||||
navigationMethod: "navigate",
|
navigationMethod: "navigate",
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,8 +5,9 @@ export type WorkspaceCreationMethod = "open_project" | "create_worktree";
|
|||||||
|
|
||||||
export interface PendingWorkspaceSetup {
|
export interface PendingWorkspaceSetup {
|
||||||
serverId: string;
|
serverId: string;
|
||||||
projectPath: string;
|
sourceDirectory: string;
|
||||||
projectName?: string;
|
sourceWorkspaceId?: string;
|
||||||
|
displayName?: string;
|
||||||
creationMethod: WorkspaceCreationMethod;
|
creationMethod: WorkspaceCreationMethod;
|
||||||
navigationMethod: WorkspaceSetupNavigationMethod;
|
navigationMethod: WorkspaceSetupNavigationMethod;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,20 @@ describe("resolveNotificationTarget", () => {
|
|||||||
workspaceId: null,
|
workspaceId: null,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not treat cwd as a workspace id alias", () => {
|
||||||
|
expect(
|
||||||
|
resolveNotificationTarget({
|
||||||
|
serverId: "srv-1",
|
||||||
|
agentId: "agent-1",
|
||||||
|
cwd: "/tmp/repo",
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
serverId: "srv-1",
|
||||||
|
agentId: "agent-1",
|
||||||
|
workspaceId: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("buildNotificationRoute", () => {
|
describe("buildNotificationRoute", () => {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export function resolveNotificationTarget(data: NotificationData): {
|
|||||||
return {
|
return {
|
||||||
serverId: readNonEmptyString(data, "serverId"),
|
serverId: readNonEmptyString(data, "serverId"),
|
||||||
agentId: readNonEmptyString(data, "agentId"),
|
agentId: readNonEmptyString(data, "agentId"),
|
||||||
workspaceId: readNonEmptyString(data, "workspaceId") ?? readNonEmptyString(data, "cwd"),
|
workspaceId: readNonEmptyString(data, "workspaceId"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
import type { WorkspaceDescriptor } from "@/stores/session-store";
|
|
||||||
import { normalizeWorkspaceIdentity } from "@/utils/workspace-identity";
|
|
||||||
|
|
||||||
export function resolveHydratedWorkspaceId(input: {
|
|
||||||
workspaces: Iterable<WorkspaceDescriptor> | null | undefined;
|
|
||||||
path: string | null | undefined;
|
|
||||||
}): string | null {
|
|
||||||
const normalizedPath = normalizeWorkspaceIdentity(input.path);
|
|
||||||
if (!normalizedPath) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const workspace of input.workspaces ?? []) {
|
|
||||||
if (normalizeWorkspaceIdentity(workspace.id) === normalizedPath) {
|
|
||||||
return workspace.id;
|
|
||||||
}
|
|
||||||
if (normalizeWorkspaceIdentity(workspace.workspaceDirectory) === normalizedPath) {
|
|
||||||
return workspace.id;
|
|
||||||
}
|
|
||||||
if (normalizeWorkspaceIdentity(workspace.projectRootPath) === normalizedPath) {
|
|
||||||
return workspace.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
@@ -17,7 +17,7 @@ function workspace(
|
|||||||
projectKind: input.projectKind ?? "git",
|
projectKind: input.projectKind ?? "git",
|
||||||
workspaceKind: input.workspaceKind ?? "worktree",
|
workspaceKind: input.workspaceKind ?? "worktree",
|
||||||
name: input.name ?? input.id,
|
name: input.name ?? input.id,
|
||||||
status: input.status ?? "done",
|
status: input.status ?? "running",
|
||||||
activityAt: input.activityAt ?? null,
|
activityAt: input.activityAt ?? null,
|
||||||
diffStat: input.diffStat ?? null,
|
diffStat: input.diffStat ?? null,
|
||||||
};
|
};
|
||||||
@@ -38,7 +38,7 @@ describe("resolveWorkspaceArchiveRedirectWorkspaceId", () => {
|
|||||||
).toBe("/repo");
|
).toBe("/repo");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("falls back to the project root path when the root checkout is not in the visible workspace list", () => {
|
it("falls back to the host root route when no sibling workspace target exists", () => {
|
||||||
const workspaces = [
|
const workspaces = [
|
||||||
workspace({
|
workspace({
|
||||||
id: "/repo/.paseo/worktrees/feature",
|
id: "/repo/.paseo/worktrees/feature",
|
||||||
@@ -48,11 +48,12 @@ describe("resolveWorkspaceArchiveRedirectWorkspaceId", () => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
resolveWorkspaceArchiveRedirectWorkspaceId({
|
buildWorkspaceArchiveRedirectRoute({
|
||||||
|
serverId: "server-1",
|
||||||
archivedWorkspaceId: "/repo/.paseo/worktrees/feature",
|
archivedWorkspaceId: "/repo/.paseo/worktrees/feature",
|
||||||
workspaces,
|
workspaces,
|
||||||
}),
|
}),
|
||||||
).toBe("/repo");
|
).toBe("/h/server-1");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("falls back to the host root route when no alternate workspace target exists", () => {
|
it("falls back to the host root route when no alternate workspace target exists", () => {
|
||||||
|
|||||||
@@ -1,20 +1,14 @@
|
|||||||
import type { WorkspaceDescriptor } from "@/stores/session-store";
|
import type { WorkspaceDescriptor } from "@/stores/session-store";
|
||||||
import { buildHostRootRoute, buildHostWorkspaceRoute } from "@/utils/host-routes";
|
import { buildHostRootRoute, buildHostWorkspaceRoute } from "@/utils/host-routes";
|
||||||
|
import { resolveWorkspaceRouteId } from "@/utils/workspace-execution";
|
||||||
function trimNonEmpty(value: string | null | undefined): string | null {
|
|
||||||
if (typeof value !== "string") {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const trimmed = value.trim();
|
|
||||||
return trimmed.length > 0 ? trimmed : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resolveWorkspaceArchiveRedirectWorkspaceId(input: {
|
export function resolveWorkspaceArchiveRedirectWorkspaceId(input: {
|
||||||
archivedWorkspaceId: string;
|
archivedWorkspaceId: string;
|
||||||
workspaces: Iterable<WorkspaceDescriptor>;
|
workspaces: Iterable<WorkspaceDescriptor>;
|
||||||
}): string | null {
|
}): string | null {
|
||||||
const archivedWorkspaceId = trimNonEmpty(input.archivedWorkspaceId);
|
const archivedWorkspaceId = resolveWorkspaceRouteId({
|
||||||
|
routeWorkspaceId: input.archivedWorkspaceId,
|
||||||
|
});
|
||||||
if (!archivedWorkspaceId) {
|
if (!archivedWorkspaceId) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -38,11 +32,6 @@ export function resolveWorkspaceArchiveRedirectWorkspaceId(input: {
|
|||||||
return rootCheckoutWorkspace.id;
|
return rootCheckoutWorkspace.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fallbackProjectRootPath = trimNonEmpty(archivedWorkspace.projectRootPath);
|
|
||||||
if (fallbackProjectRootPath && fallbackProjectRootPath !== archivedWorkspace.id) {
|
|
||||||
return fallbackProjectRootPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
const siblingWorkspace =
|
const siblingWorkspace =
|
||||||
sameProjectWorkspaces.find((workspace) => workspace.id !== archivedWorkspace.id) ?? null;
|
sameProjectWorkspaces.find((workspace) => workspace.id !== archivedWorkspace.id) ?? null;
|
||||||
return siblingWorkspace?.id ?? null;
|
return siblingWorkspace?.id ?? null;
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import type { WorkspaceDescriptor } from "@/stores/session-store";
|
||||||
|
import {
|
||||||
|
getWorkspaceExecutionAuthority,
|
||||||
|
requireWorkspaceExecutionAuthority,
|
||||||
|
resolveWorkspaceIdByExecutionDirectory,
|
||||||
|
resolveWorkspaceRouteId,
|
||||||
|
} from "./workspace-execution";
|
||||||
|
|
||||||
|
function createWorkspace(
|
||||||
|
input: Partial<WorkspaceDescriptor> & Pick<WorkspaceDescriptor, "id">,
|
||||||
|
): WorkspaceDescriptor {
|
||||||
|
return {
|
||||||
|
id: input.id,
|
||||||
|
projectId: input.projectId ?? "project-1",
|
||||||
|
projectDisplayName: input.projectDisplayName ?? "Project",
|
||||||
|
projectRootPath: input.projectRootPath ?? "/repo",
|
||||||
|
workspaceDirectory: input.workspaceDirectory ?? "/repo",
|
||||||
|
projectKind: input.projectKind ?? "git",
|
||||||
|
workspaceKind: input.workspaceKind ?? "checkout",
|
||||||
|
name: input.name ?? "main",
|
||||||
|
status: input.status ?? "running",
|
||||||
|
activityAt: input.activityAt ?? null,
|
||||||
|
diffStat: input.diffStat ?? null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("resolveWorkspaceRouteId", () => {
|
||||||
|
it("normalizes route workspace ids", () => {
|
||||||
|
expect(resolveWorkspaceRouteId({ routeWorkspaceId: " /tmp/repo/ " })).toBe("/tmp/repo");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns null for empty values", () => {
|
||||||
|
expect(resolveWorkspaceRouteId({ routeWorkspaceId: " " })).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("resolveWorkspaceIdByExecutionDirectory", () => {
|
||||||
|
it("matches workspace directories", () => {
|
||||||
|
const workspaces = [
|
||||||
|
createWorkspace({
|
||||||
|
id: "workspace-1",
|
||||||
|
projectRootPath: "/repo",
|
||||||
|
workspaceDirectory: "/repo/.paseo/worktrees/feature",
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(
|
||||||
|
resolveWorkspaceIdByExecutionDirectory({
|
||||||
|
workspaces,
|
||||||
|
workspaceDirectory: "/repo/.paseo/worktrees/feature",
|
||||||
|
}),
|
||||||
|
).toBe("workspace-1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not match project root metadata", () => {
|
||||||
|
const workspaces = [
|
||||||
|
createWorkspace({
|
||||||
|
id: "workspace-1",
|
||||||
|
projectRootPath: "/repo",
|
||||||
|
workspaceDirectory: "/repo/.paseo/worktrees/feature",
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(
|
||||||
|
resolveWorkspaceIdByExecutionDirectory({
|
||||||
|
workspaces,
|
||||||
|
workspaceDirectory: "/repo",
|
||||||
|
}),
|
||||||
|
).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("workspace execution authority", () => {
|
||||||
|
it("returns an explicit failure when workspace id is missing", () => {
|
||||||
|
expect(
|
||||||
|
getWorkspaceExecutionAuthority({
|
||||||
|
workspaces: new Map(),
|
||||||
|
workspaceId: null,
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
ok: false,
|
||||||
|
reason: "workspace_id_missing",
|
||||||
|
message: "Workspace id is required.",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns an explicit failure when workspace directory is missing", () => {
|
||||||
|
const workspaces = new Map<string, WorkspaceDescriptor>([
|
||||||
|
[
|
||||||
|
"workspace-1",
|
||||||
|
createWorkspace({
|
||||||
|
id: "workspace-1",
|
||||||
|
workspaceDirectory: " ",
|
||||||
|
projectRootPath: "/repo",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
getWorkspaceExecutionAuthority({
|
||||||
|
workspaces,
|
||||||
|
workspaceId: "workspace-1",
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
ok: false,
|
||||||
|
reason: "workspace_directory_missing",
|
||||||
|
message: "Workspace directory is missing for workspace workspace-1",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("never falls back to project root metadata", () => {
|
||||||
|
const workspaces = new Map<string, WorkspaceDescriptor>([
|
||||||
|
[
|
||||||
|
"workspace-1",
|
||||||
|
createWorkspace({
|
||||||
|
id: "workspace-1",
|
||||||
|
projectRootPath: "/repo",
|
||||||
|
workspaceDirectory: "/repo/.paseo/worktrees/feature",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
requireWorkspaceExecutionAuthority({
|
||||||
|
workspaces,
|
||||||
|
workspaceId: "workspace-1",
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
workspaceId: "workspace-1",
|
||||||
|
workspaceDirectory: "/repo/.paseo/worktrees/feature",
|
||||||
|
workspace: workspaces.get("workspace-1"),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
import type { WorkspaceDescriptor } from "@/stores/session-store";
|
||||||
|
import { normalizeWorkspaceIdentity } from "@/utils/workspace-identity";
|
||||||
|
|
||||||
|
export type WorkspaceAuthorityResult = {
|
||||||
|
workspaceId: string;
|
||||||
|
workspaceDirectory: string;
|
||||||
|
workspace: WorkspaceDescriptor;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WorkspaceExecutionAuthorityFailureReason =
|
||||||
|
| "workspace_id_missing"
|
||||||
|
| "workspace_missing"
|
||||||
|
| "workspace_directory_missing";
|
||||||
|
|
||||||
|
export type WorkspaceExecutionAuthorityResult =
|
||||||
|
| { ok: true; authority: WorkspaceAuthorityResult }
|
||||||
|
| {
|
||||||
|
ok: false;
|
||||||
|
reason: WorkspaceExecutionAuthorityFailureReason;
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function resolveWorkspaceRouteId(input: {
|
||||||
|
routeWorkspaceId: string | null | undefined;
|
||||||
|
}): string | null {
|
||||||
|
return normalizeWorkspaceIdentity(input.routeWorkspaceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveWorkspaceIdByExecutionDirectory(input: {
|
||||||
|
workspaces: Iterable<WorkspaceDescriptor> | null | undefined;
|
||||||
|
workspaceDirectory: string | null | undefined;
|
||||||
|
}): string | null {
|
||||||
|
const normalizedWorkspaceDirectory = normalizeWorkspaceIdentity(input.workspaceDirectory);
|
||||||
|
if (!normalizedWorkspaceDirectory) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const workspace of input.workspaces ?? []) {
|
||||||
|
if (normalizeWorkspaceIdentity(workspace.workspaceDirectory) === normalizedWorkspaceDirectory) {
|
||||||
|
return workspace.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getWorkspaceExecutionAuthority(
|
||||||
|
input:
|
||||||
|
| {
|
||||||
|
workspace: WorkspaceDescriptor | null | undefined;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
workspaces: Map<string, WorkspaceDescriptor> | undefined;
|
||||||
|
workspaceId: string | null | undefined;
|
||||||
|
},
|
||||||
|
): WorkspaceExecutionAuthorityResult {
|
||||||
|
const workspace =
|
||||||
|
"workspace" in input
|
||||||
|
? input.workspace
|
||||||
|
: (() => {
|
||||||
|
const normalizedWorkspaceId = normalizeWorkspaceIdentity(input.workspaceId);
|
||||||
|
if (!normalizedWorkspaceId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return input.workspaces?.get(normalizedWorkspaceId) ?? null;
|
||||||
|
})();
|
||||||
|
|
||||||
|
if ("workspaces" in input) {
|
||||||
|
const normalizedWorkspaceId = normalizeWorkspaceIdentity(input.workspaceId);
|
||||||
|
if (!normalizedWorkspaceId) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
reason: "workspace_id_missing",
|
||||||
|
message: "Workspace id is required.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!workspace) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
reason: "workspace_missing",
|
||||||
|
message:
|
||||||
|
"workspaces" in input
|
||||||
|
? `Workspace not found: ${String(input.workspaceId ?? "")}`
|
||||||
|
: "Workspace not found.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const workspaceDirectory = normalizeWorkspaceIdentity(workspace.workspaceDirectory);
|
||||||
|
if (!workspaceDirectory) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
reason: "workspace_directory_missing",
|
||||||
|
message: `Workspace directory is missing for workspace ${workspace.id}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
authority: {
|
||||||
|
workspaceId: workspace.id,
|
||||||
|
workspaceDirectory,
|
||||||
|
workspace,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function requireWorkspaceExecutionAuthority(
|
||||||
|
input:
|
||||||
|
| {
|
||||||
|
workspace: WorkspaceDescriptor | null | undefined;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
workspaces: Map<string, WorkspaceDescriptor> | undefined;
|
||||||
|
workspaceId: string | null | undefined;
|
||||||
|
},
|
||||||
|
): WorkspaceAuthorityResult {
|
||||||
|
const result = getWorkspaceExecutionAuthority(input);
|
||||||
|
if (!result.ok) {
|
||||||
|
throw new Error(result.message);
|
||||||
|
}
|
||||||
|
return result.authority;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function requireWorkspaceRecordId(workspaceId: string): number {
|
||||||
|
const normalizedWorkspaceId = normalizeWorkspaceIdentity(workspaceId);
|
||||||
|
if (!normalizedWorkspaceId) {
|
||||||
|
throw new Error("Workspace ID is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedWorkspaceId = Number(normalizedWorkspaceId);
|
||||||
|
if (!Number.isInteger(parsedWorkspaceId)) {
|
||||||
|
throw new Error(`Workspace ID is not a persisted record ID: ${workspaceId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedWorkspaceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveWorkspaceExecutionDirectory(input: {
|
||||||
|
workspaceDirectory: string | null | undefined;
|
||||||
|
}): string | null {
|
||||||
|
return normalizeWorkspaceIdentity(input.workspaceDirectory);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function requireWorkspaceExecutionDirectory(input: {
|
||||||
|
workspaceId?: string;
|
||||||
|
workspaceDirectory: string | null | undefined;
|
||||||
|
}): string {
|
||||||
|
const workspaceDirectory = resolveWorkspaceExecutionDirectory({
|
||||||
|
workspaceDirectory: input.workspaceDirectory,
|
||||||
|
});
|
||||||
|
if (!workspaceDirectory) {
|
||||||
|
throw new Error(
|
||||||
|
input.workspaceId
|
||||||
|
? `Workspace directory is missing for workspace ${input.workspaceId}`
|
||||||
|
: "Workspace directory is missing.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return workspaceDirectory;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveWorkspaceExecutionAuthority(
|
||||||
|
input:
|
||||||
|
| {
|
||||||
|
workspace: WorkspaceDescriptor | null | undefined;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
workspaces: Map<string, WorkspaceDescriptor> | undefined;
|
||||||
|
workspaceId: string | null | undefined;
|
||||||
|
},
|
||||||
|
): WorkspaceAuthorityResult | null {
|
||||||
|
const result = getWorkspaceExecutionAuthority(input);
|
||||||
|
return result.ok ? result.authority : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const parseWorkspaceRecordId = requireWorkspaceRecordId;
|
||||||
@@ -180,6 +180,7 @@ export type CreateAgentRequestOptions = {
|
|||||||
config?: AgentSessionConfig;
|
config?: AgentSessionConfig;
|
||||||
provider?: AgentProvider;
|
provider?: AgentProvider;
|
||||||
cwd?: string;
|
cwd?: string;
|
||||||
|
workspaceId?: number;
|
||||||
initialPrompt?: string;
|
initialPrompt?: string;
|
||||||
clientMessageId?: string;
|
clientMessageId?: string;
|
||||||
outputSchema?: Record<string, unknown>;
|
outputSchema?: Record<string, unknown>;
|
||||||
@@ -1383,6 +1384,7 @@ export class DaemonClient {
|
|||||||
type: "create_agent_request",
|
type: "create_agent_request",
|
||||||
requestId,
|
requestId,
|
||||||
config,
|
config,
|
||||||
|
...(typeof options.workspaceId === "number" ? { workspaceId: options.workspaceId } : {}),
|
||||||
...(options.initialPrompt ? { initialPrompt: options.initialPrompt } : {}),
|
...(options.initialPrompt ? { initialPrompt: options.initialPrompt } : {}),
|
||||||
...(options.clientMessageId ? { clientMessageId: options.clientMessageId } : {}),
|
...(options.clientMessageId ? { clientMessageId: options.clientMessageId } : {}),
|
||||||
...(options.outputSchema ? { outputSchema: options.outputSchema } : {}),
|
...(options.outputSchema ? { outputSchema: options.outputSchema } : {}),
|
||||||
@@ -3633,6 +3635,7 @@ function resolveAgentConfig(options: CreateAgentRequestOptions): AgentSessionCon
|
|||||||
config,
|
config,
|
||||||
provider,
|
provider,
|
||||||
cwd,
|
cwd,
|
||||||
|
workspaceId: _workspaceId,
|
||||||
initialPrompt: _initialPrompt,
|
initialPrompt: _initialPrompt,
|
||||||
images: _images,
|
images: _images,
|
||||||
git: _git,
|
git: _git,
|
||||||
|
|||||||
@@ -2522,7 +2522,7 @@ export class Session {
|
|||||||
);
|
);
|
||||||
await this.forwardAgentUpdate(snapshot);
|
await this.forwardAgentUpdate(snapshot);
|
||||||
if (sessionConfig.terminal) {
|
if (sessionConfig.terminal) {
|
||||||
void this.emitInitialTerminalsChangedSnapshot(sessionConfig.cwd);
|
void this.emitInitialTerminalsChangedSnapshot(resolvedWorkspace.directory);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (requestId) {
|
if (requestId) {
|
||||||
|
|||||||
@@ -630,6 +630,108 @@ describe("workspace aggregation", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("create_agent_request uses workspaceId as the execution authority", async () => {
|
||||||
|
const { session, emitted, projects, workspaces } = createSessionForWorkspaceTests();
|
||||||
|
seedProject({
|
||||||
|
projects,
|
||||||
|
id: 5,
|
||||||
|
directory: "/tmp/repo",
|
||||||
|
displayName: "repo",
|
||||||
|
kind: "git",
|
||||||
|
});
|
||||||
|
seedWorkspace({
|
||||||
|
workspaces,
|
||||||
|
id: 50,
|
||||||
|
projectId: 5,
|
||||||
|
directory: "/tmp/repo/.paseo/worktrees/feature",
|
||||||
|
displayName: "feature",
|
||||||
|
kind: "worktree",
|
||||||
|
});
|
||||||
|
|
||||||
|
const createdAgent = makeAgent({
|
||||||
|
id: "agent-1",
|
||||||
|
cwd: "/tmp/repo/.paseo/worktrees/feature",
|
||||||
|
status: "idle",
|
||||||
|
updatedAt: "2026-03-01T12:00:00.000Z",
|
||||||
|
});
|
||||||
|
const createAgent = vi.fn(async () => createdAgent as any);
|
||||||
|
|
||||||
|
(session as any).agentManager = {
|
||||||
|
createAgent,
|
||||||
|
getAgent: vi.fn(() => createdAgent as any),
|
||||||
|
};
|
||||||
|
(session as any).forwardAgentUpdate = vi.fn(async () => undefined);
|
||||||
|
(session as any).getAgentPayloadById = vi.fn(async () => createdAgent);
|
||||||
|
(session as any).buildAgentSessionConfig = vi.fn(async (config: any) => ({
|
||||||
|
sessionConfig: config,
|
||||||
|
worktreeConfig: null,
|
||||||
|
}));
|
||||||
|
|
||||||
|
await (session as any).handleCreateAgentRequest({
|
||||||
|
type: "create_agent_request",
|
||||||
|
requestId: "req-create-agent",
|
||||||
|
workspaceId: 50,
|
||||||
|
config: {
|
||||||
|
provider: "codex",
|
||||||
|
cwd: "/tmp/repo",
|
||||||
|
modeId: "default",
|
||||||
|
},
|
||||||
|
labels: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(createAgent).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
cwd: "/tmp/repo/.paseo/worktrees/feature",
|
||||||
|
}),
|
||||||
|
undefined,
|
||||||
|
expect.objectContaining({
|
||||||
|
workspaceId: 50,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const response = emitted.find((message) => message.type === "status") as any;
|
||||||
|
expect(response?.payload).toMatchObject({
|
||||||
|
status: "agent_created",
|
||||||
|
requestId: "req-create-agent",
|
||||||
|
agent: {
|
||||||
|
cwd: "/tmp/repo/.paseo/worktrees/feature",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("create_agent_request fails for an unknown workspaceId", async () => {
|
||||||
|
const { session, emitted } = createSessionForWorkspaceTests();
|
||||||
|
const createAgent = vi.fn();
|
||||||
|
|
||||||
|
(session as any).agentManager = {
|
||||||
|
createAgent,
|
||||||
|
getAgent: vi.fn(() => null),
|
||||||
|
};
|
||||||
|
(session as any).buildAgentSessionConfig = vi.fn(async (config: any) => ({
|
||||||
|
sessionConfig: config,
|
||||||
|
worktreeConfig: null,
|
||||||
|
}));
|
||||||
|
|
||||||
|
await (session as any).handleCreateAgentRequest({
|
||||||
|
type: "create_agent_request",
|
||||||
|
requestId: "req-create-agent-fail",
|
||||||
|
workspaceId: 999,
|
||||||
|
config: {
|
||||||
|
provider: "codex",
|
||||||
|
cwd: "/tmp/repo",
|
||||||
|
modeId: "default",
|
||||||
|
},
|
||||||
|
labels: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(createAgent).not.toHaveBeenCalled();
|
||||||
|
const response = emitted.find((message) => message.type === "status") as any;
|
||||||
|
expect(response?.payload).toMatchObject({
|
||||||
|
status: "agent_create_failed",
|
||||||
|
requestId: "req-create-agent-fail",
|
||||||
|
});
|
||||||
|
expect((response?.payload as any)?.error).toContain("Workspace not found: 999");
|
||||||
|
});
|
||||||
|
|
||||||
test("open_project_request creates git projects with GitHub owner/repo and branch names", async () => {
|
test("open_project_request creates git projects with GitHub owner/repo and branch names", async () => {
|
||||||
const { session, emitted, projects, workspaces } = createSessionForWorkspaceTests();
|
const { session, emitted, projects, workspaces } = createSessionForWorkspaceTests();
|
||||||
const { tempDir, repoDir } = createTempGitRepo({
|
const { tempDir, repoDir } = createTempGitRepo({
|
||||||
|
|||||||
@@ -573,6 +573,13 @@ export async function handleCreatePaseoWorktreeRequest(
|
|||||||
worktreePath,
|
worktreePath,
|
||||||
branchName: normalizedSlug,
|
branchName: normalizedSlug,
|
||||||
});
|
});
|
||||||
|
await createAgentWorktree({
|
||||||
|
cwd: repoRoot,
|
||||||
|
branchName: normalizedSlug,
|
||||||
|
baseBranch,
|
||||||
|
worktreeSlug: normalizedSlug,
|
||||||
|
paseoHome: dependencies.paseoHome,
|
||||||
|
});
|
||||||
const descriptor = await dependencies.describeWorkspaceRecord(workspace);
|
const descriptor = await dependencies.describeWorkspaceRecord(workspace);
|
||||||
dependencies.emit({
|
dependencies.emit({
|
||||||
type: "create_paseo_worktree_response",
|
type: "create_paseo_worktree_response",
|
||||||
@@ -622,14 +629,6 @@ export async function createPaseoWorktreeInBackground(
|
|||||||
let setupTerminalId: string | null = null;
|
let setupTerminalId: string | null = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await createAgentWorktree({
|
|
||||||
cwd: options.repoRoot,
|
|
||||||
branchName: options.slug,
|
|
||||||
baseBranch: options.baseBranch,
|
|
||||||
worktreeSlug: options.slug,
|
|
||||||
paseoHome: dependencies.paseoHome,
|
|
||||||
});
|
|
||||||
|
|
||||||
const setupCommands = getWorktreeSetupCommands(options.worktreePath);
|
const setupCommands = getWorktreeSetupCommands(options.worktreePath);
|
||||||
if (setupCommands.length > 0 && dependencies.terminalManager) {
|
if (setupCommands.length > 0 && dependencies.terminalManager) {
|
||||||
const runtimeEnv = await resolveWorktreeRuntimeEnv({
|
const runtimeEnv = await resolveWorktreeRuntimeEnv({
|
||||||
|
|||||||
Reference in New Issue
Block a user