fix(server): rename worktree branches from prompt + attachments

Replace top-level `attachments` and `nameContext` on `create_paseo_worktree_request` with `firstAgentContext: { prompt?, attachments? }`, the single signal driving auto-rename. Drop the `worktreeSlug || …` short-circuits so a random placeholder slug no longer blocks renaming, drop the attachment-as-PR fallback in favor of explicit `githubPrNumber`, and reshape the MCP `create_worktree` tool around a `target` discriminated union (`branch-off` / `checkout-branch` / `checkout-pr`) with concise field descriptions so callers stop misreading `prompt` as a name field.
This commit is contained in:
Mohamed Boudra
2026-05-02 12:12:44 +07:00
parent 8e9134d255
commit 2f177dde7a
19 changed files with 312 additions and 534 deletions
@@ -14,11 +14,7 @@ import { normalizeAgentSnapshot } from "@/utils/agent-snapshots";
import { encodeImages } from "@/utils/encode-images";
import { toErrorMessage } from "@/utils/error-messages";
import { splitComposerAttachmentsForSubmit } from "@/components/composer-attachments";
import type {
CreateAgentRequestOptions,
CreatePaseoWorktreeInput,
DaemonClient,
} from "@server/client/daemon-client";
import type { CreateAgentRequestOptions, DaemonClient } from "@server/client/daemon-client";
import { projectIconPlaceholderLabelFromDisplayName } from "@/utils/project-display-name";
import { requireWorkspaceExecutionAuthority } from "@/utils/workspace-execution";
import { navigateToPreparedWorkspaceTab } from "@/utils/workspace-navigation";
@@ -73,24 +69,19 @@ function buildChatDraftComposerArgs({
};
}
type WorkspaceCreationAttachments = NonNullable<CreatePaseoWorktreeInput["attachments"]>;
async function callWorkspaceCreation({
creationMethod,
connectedClient,
input,
wirePayload,
}: {
creationMethod: "create_worktree" | "open_project";
connectedClient: DaemonClient;
input: { cwd: string };
wirePayload: { attachments: WorkspaceCreationAttachments };
}) {
if (creationMethod === "create_worktree") {
return connectedClient.createPaseoWorktree({
cwd: input.cwd,
worktreeSlug: createNameId(),
...(wirePayload.attachments.length > 0 ? { attachments: wirePayload.attachments } : {}),
});
}
return connectedClient.openProject(input.cwd);
@@ -228,12 +219,10 @@ export function WorkspaceSetupDialog() {
}
const connectedClient = withConnectedClient();
const wirePayload = splitComposerAttachmentsForSubmit(input.attachments);
const payload = await callWorkspaceCreation({
creationMethod: pendingWorkspaceSetup.creationMethod,
connectedClient,
input,
wirePayload,
});
if (payload.error || !payload.workspace) {
@@ -699,6 +699,7 @@ describe("NewWorkspaceScreen picker payload", () => {
await flush();
expect(mockClient.createPaseoWorktree).toHaveBeenCalledTimes(1);
expect(firstCreateWorktreeCall().firstAgentContext?.prompt).toBe("please review this change");
expect(mockClient.createAgent).not.toHaveBeenCalled();
expect(saveDraftInputMock).toHaveBeenCalledWith({
draftKey: "draft:server:draft-new-workspace",
@@ -876,7 +877,7 @@ describe("NewWorkspaceScreen picker payload", () => {
await flush();
const call = firstCreateWorktreeCall();
const attachments = call.attachments ?? [];
const attachments = call.firstAgentContext?.attachments ?? [];
expect(attachments).toHaveLength(1);
expect(attachments[0]).toMatchObject({ type: "github_pr", number: 202 });
});
@@ -907,8 +908,11 @@ describe("NewWorkspaceScreen picker payload", () => {
await flush();
const call = firstCreateWorktreeCall();
expect(call.attachments).toHaveLength(1);
expect(call.attachments?.[0]).toMatchObject({ type: "github_pr", number: 202 });
expect(call.firstAgentContext?.attachments).toHaveLength(1);
expect(call.firstAgentContext?.attachments?.[0]).toMatchObject({
type: "github_pr",
number: 202,
});
});
it("omits PR rows from the picker when GitHub features are disabled", async () => {
@@ -293,6 +293,7 @@ interface CreateChatAgentInput {
composerState: ReturnType<typeof useAgentInputDraft>["composerState"];
ensureWorkspace: (input: {
cwd: string;
prompt: string;
attachments: AgentAttachment[];
}) => Promise<ReturnType<typeof normalizeWorkspaceDescriptor>>;
serverId: string;
@@ -310,7 +311,11 @@ async function runCreateChatAgent(input: CreateChatAgentInput): Promise<void> {
throw new Error("Select a model");
}
const { attachments: reviewAttachments } = splitComposerAttachmentsForSubmit(attachments);
const ensuredWorkspace = await ensureWorkspace({ cwd, attachments: reviewAttachments });
const ensuredWorkspace = await ensureWorkspace({
cwd,
prompt: text,
attachments: reviewAttachments,
});
submitWorkspaceDraft({
serverId,
draftKey,
@@ -588,13 +593,22 @@ export function NewWorkspaceScreen({
}, []);
const buildCreateWorktreeInput = useCallback(
(input: { cwd: string; attachments: AgentAttachment[] }) => {
(input: { cwd: string; prompt: string; attachments: AgentAttachment[] }) => {
const checkoutRequest = pickerItemToCheckoutRequest(selectedItem);
const trimmedPrompt = input.prompt.trim();
const hasFirstAgentContext = trimmedPrompt.length > 0 || input.attachments.length > 0;
return {
cwd: input.cwd,
worktreeSlug: createNameId(),
...(input.attachments.length > 0 ? { attachments: input.attachments } : {}),
...(hasFirstAgentContext
? {
firstAgentContext: {
...(trimmedPrompt ? { prompt: trimmedPrompt } : {}),
...(input.attachments.length > 0 ? { attachments: input.attachments } : {}),
},
}
: {}),
...checkoutRequest,
};
},
@@ -602,7 +616,7 @@ export function NewWorkspaceScreen({
);
const ensureWorkspace = useCallback(
async (input: { cwd: string; attachments: AgentAttachment[] }) => {
async (input: { cwd: string; prompt: string; attachments: AgentAttachment[] }) => {
if (createdWorkspace) {
return createdWorkspace;
}
+3 -4
View File
@@ -251,7 +251,7 @@ export interface CreateAgentRequestOptions extends AgentConfigOverrides {
export interface CreatePaseoWorktreeInput extends Pick<
CreatePaseoWorktreeRequest,
"cwd" | "worktreeSlug" | "nameContext" | "attachments" | "refName" | "action" | "githubPrNumber"
"cwd" | "worktreeSlug" | "firstAgentContext" | "refName" | "action" | "githubPrNumber"
> {}
type CheckoutStatusPayload = CheckoutStatusResponse["payload"];
@@ -2773,9 +2773,8 @@ export class DaemonClient {
type: "create_paseo_worktree_request",
cwd: input.cwd,
worktreeSlug: input.worktreeSlug,
...(input.nameContext !== undefined ? { nameContext: input.nameContext } : {}),
...(input.attachments && input.attachments.length > 0
? { attachments: input.attachments }
...(input.firstAgentContext !== undefined
? { firstAgentContext: input.firstAgentContext }
: {}),
...(input.refName !== undefined ? { refName: input.refName } : {}),
...(input.action !== undefined ? { action: input.action } : {}),
@@ -478,42 +478,28 @@ describe("create_agent MCP tool", () => {
expect(parsed.success).toBe(true);
});
it("accepts optional worktree intent fields in create_worktree input validation", async () => {
it("accepts each create_worktree target mode", async () => {
const { agentManager, agentStorage } = createTestDeps();
const server = await createAgentMcpServer({ agentManager, agentStorage, logger });
const tool = registeredTool(server, "create_worktree");
const parsed = await tool.inputSchema.safeParseAsync({
cwd: existingCwd,
action: "checkout",
refName: "head-ref",
githubPrNumber: 42,
});
expect(parsed.success).toBe(true);
for (const target of [
{ mode: "branch-off", newBranch: "feature-x", base: "main" },
{ mode: "checkout-branch", branch: "head-ref" },
{ mode: "checkout-pr", prNumber: 42 },
] as const) {
const parsed = await tool.inputSchema.safeParseAsync({ cwd: existingCwd, target });
expect(parsed.success).toBe(true);
}
});
it("accepts optional name context in create_worktree input validation", async () => {
it("rejects create_worktree without a target", async () => {
const { agentManager, agentStorage } = createTestDeps();
const server = await createAgentMcpServer({ agentManager, agentStorage, logger });
const tool = registeredTool(server, "create_worktree");
const parsed = await tool.inputSchema.safeParseAsync({
cwd: existingCwd,
nameContext: "Fix workspace creation naming",
});
expect(parsed.success).toBe(true);
});
it("rejects create_worktree without a branch name or checkout intent", async () => {
const { agentManager, agentStorage } = createTestDeps();
const server = await createAgentMcpServer({ agentManager, agentStorage, logger });
const tool = registeredTool(server, "create_worktree");
await expect(tool.callback({})).rejects.toThrow(
"create_worktree requires branchName, nameContext, refName, or githubPrNumber",
);
const parsed = await tool.inputSchema.safeParseAsync({});
expect(parsed.success).toBe(false);
});
it("surfaces createAgent validation failures", async () => {
@@ -913,7 +899,7 @@ describe("create_agent MCP tool", () => {
expect(createPaseoWorktree).toHaveBeenCalledWith(
expect.objectContaining({
githubPrNumber: 123,
nameContext: "Rename this PR branch from prompt",
firstAgentContext: { prompt: "Rename this PR branch from prompt" },
}),
expect.objectContaining({
setupContinuation: expect.objectContaining({ kind: "agent" }),
@@ -968,8 +954,7 @@ describe("create_agent MCP tool", () => {
const tool = registeredTool(server, "create_worktree");
const response = await tool.callback({
cwd: repoDir,
branchName: "tool-worktree",
baseBranch: "main",
target: { mode: "branch-off", newBranch: "tool-worktree", base: "main" },
});
expect(response.structuredContent.branchName).toBe("tool-worktree");
@@ -983,106 +968,6 @@ describe("create_agent MCP tool", () => {
}
});
it("auto-names a standalone branch-off worktree from create_worktree name context", async () => {
const { agentManager, agentStorage, spies } = createTestDeps();
const tempDir = await mkdtemp(join(tmpdir(), "paseo-mcp-create-worktree-name-context-"));
const repoDir = join(tempDir, "repo");
const paseoHome = join(tempDir, ".paseo");
const broadcasts: string[] = [];
try {
execSync(`git init ${JSON.stringify(repoDir)}`, { stdio: "pipe" });
execSync("git config user.email test@example.com", { cwd: repoDir, stdio: "pipe" });
execSync("git config user.name Test", { cwd: repoDir, stdio: "pipe" });
execSync("git config commit.gpgsign false", { cwd: repoDir, stdio: "pipe" });
await writeFile(join(repoDir, "README.md"), "hello\n");
execSync("git add README.md", { cwd: repoDir, stdio: "pipe" });
execSync("git commit -m init", { cwd: repoDir, stdio: "pipe" });
execSync("git branch -M main", { cwd: repoDir, stdio: "pipe" });
const server = await createAgentMcpServer({
agentManager,
agentStorage,
paseoHome,
createPaseoWorktree: createPaseoWorktreeForMcpTest({ paseoHome, broadcasts }),
logger,
});
const tool = registeredTool(server, "create_worktree");
const response = await tool.callback({
cwd: repoDir,
nameContext: "Fix workspace creation naming",
baseBranch: "main",
});
expect(response.structuredContent.branchName).toBe("fix-workspace-creation-naming");
expect(
execSync("git branch --show-current", {
cwd: response.structuredContent.worktreePath as string,
stdio: "pipe",
})
.toString()
.trim(),
).toBe("fix-workspace-creation-naming");
expect(spies.agentManager.createAgent).not.toHaveBeenCalled();
expect(broadcasts).toHaveLength(1);
} finally {
await rm(tempDir, { recursive: true, force: true });
}
});
it("does not rename checkout-created worktrees from create_worktree name context", async () => {
const { agentManager, agentStorage, spies } = createTestDeps();
const tempDir = await mkdtemp(join(tmpdir(), "paseo-mcp-checkout-worktree-name-context-"));
const repoDir = join(tempDir, "repo");
const paseoHome = join(tempDir, ".paseo");
const broadcasts: string[] = [];
try {
execSync(`git init ${JSON.stringify(repoDir)}`, { stdio: "pipe" });
execSync("git config user.email test@example.com", { cwd: repoDir, stdio: "pipe" });
execSync("git config user.name Test", { cwd: repoDir, stdio: "pipe" });
execSync("git config commit.gpgsign false", { cwd: repoDir, stdio: "pipe" });
await writeFile(join(repoDir, "README.md"), "hello\n");
execSync("git add README.md", { cwd: repoDir, stdio: "pipe" });
execSync("git commit -m init", { cwd: repoDir, stdio: "pipe" });
execSync("git branch -M main", { cwd: repoDir, stdio: "pipe" });
execSync("git checkout -b existing-feature", { cwd: repoDir, stdio: "pipe" });
await writeFile(join(repoDir, "feature.txt"), "feature\n");
execSync("git add feature.txt", { cwd: repoDir, stdio: "pipe" });
execSync("git commit -m feature", { cwd: repoDir, stdio: "pipe" });
execSync("git checkout main", { cwd: repoDir, stdio: "pipe" });
const server = await createAgentMcpServer({
agentManager,
agentStorage,
paseoHome,
createPaseoWorktree: createPaseoWorktreeForMcpTest({ paseoHome, broadcasts }),
logger,
});
const tool = registeredTool(server, "create_worktree");
const response = await tool.callback({
cwd: repoDir,
action: "checkout",
refName: "existing-feature",
nameContext: "Should Not Rename Checkout",
});
expect(response.structuredContent.branchName).toBe("existing-feature");
expect(
execSync("git branch --show-current", {
cwd: response.structuredContent.worktreePath as string,
stdio: "pipe",
})
.toString()
.trim(),
).toBe("existing-feature");
expect(spies.agentManager.createAgent).not.toHaveBeenCalled();
expect(broadcasts).toHaveLength(1);
} finally {
await rm(tempDir, { recursive: true, force: true });
}
});
it("forces a workspace git snapshot refresh when archive_worktree deletes a worktree", async () => {
const { agentManager, agentStorage } = createTestDeps();
const tempDir = await mkdtemp(join(tmpdir(), "paseo-mcp-archive-worktree-"));
@@ -1128,8 +1013,7 @@ describe("create_agent MCP tool", () => {
const archiveTool = registeredTool(server, "archive_worktree");
const created = await createTool.callback({
cwd: repoDir,
branchName: "archive-tool-worktree",
baseBranch: "main",
target: { mode: "branch-off", newBranch: "archive-tool-worktree", base: "main" },
});
workspaceGitService.getSnapshot.mockClear();
+69 -29
View File
@@ -686,7 +686,9 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
refName,
action,
githubPrNumber,
nameContext: topLevelArgs.initialPrompt,
...(topLevelArgs.initialPrompt
? { firstAgentContext: { prompt: topLevelArgs.initialPrompt } }
: {}),
runSetup: false,
paseoHome: options.paseoHome,
},
@@ -1761,44 +1763,50 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
"create_worktree",
{
title: "Create worktree",
description: "Create a Paseo-managed git worktree.",
description:
"Create a Paseo-managed git worktree. Branch off a new branch, check out an existing branch, or check out a GitHub PR.",
inputSchema: {
cwd: z
.string()
.optional()
.describe("Optional repository cwd. Defaults to the caller agent cwd."),
branchName: z.string().optional(),
nameContext: z.string().optional(),
baseBranch: z.string().optional(),
refName: z.string().min(1).optional(),
action: z.enum(["branch-off", "checkout"]).optional(),
githubPrNumber: z.number().int().positive().optional(),
cwd: z.string().optional().describe("Repository directory. Defaults to the agent's cwd."),
target: z
.discriminatedUnion("mode", [
z
.object({
mode: z.literal("branch-off"),
newBranch: z.string().min(1).describe("Name for the new branch."),
base: z
.string()
.min(1)
.optional()
.describe("Base ref. Defaults to the repo's default branch."),
})
.describe("Create a new branch off a base."),
z
.object({
mode: z.literal("checkout-branch"),
branch: z.string().min(1).describe("Existing branch to check out."),
})
.describe("Check out an existing branch."),
z
.object({
mode: z.literal("checkout-pr"),
prNumber: z.number().int().positive().describe("Pull request number."),
})
.describe("Check out a GitHub pull request."),
])
.describe("What the worktree should contain."),
},
outputSchema: {
branchName: z.string(),
worktreePath: z.string(),
},
},
async ({ cwd, branchName, nameContext, baseBranch, refName, action, githubPrNumber }) => {
if (!branchName && !nameContext && !refName && githubPrNumber === undefined) {
throw new Error(
"create_worktree requires branchName, nameContext, refName, or githubPrNumber",
);
}
async ({ cwd, target }) => {
const repoRoot = resolveScopedCwd(cwd, { required: true });
const mcpInput = mcpCreateWorktreeInput(repoRoot, target, options.paseoHome);
const createdWorktree = await createMcpWorktree({
input: {
cwd: repoRoot,
worktreeSlug: branchName,
nameContext,
refName,
action,
githubPrNumber,
runSetup: false,
paseoHome: options.paseoHome,
},
input: mcpInput.input,
createPaseoWorktree: options.createPaseoWorktree,
resolveDefaultBranch: baseBranch ? async () => baseBranch : undefined,
resolveDefaultBranch: mcpInput.resolveDefaultBranch,
});
const { worktree } = createdWorktree;
@@ -2039,6 +2047,38 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
return server;
}
type McpCreateWorktreeTarget =
| { mode: "branch-off"; newBranch: string; base?: string }
| { mode: "checkout-branch"; branch: string }
| { mode: "checkout-pr"; prNumber: number };
function mcpCreateWorktreeInput(
repoRoot: string,
target: McpCreateWorktreeTarget,
paseoHome: string | undefined,
): { input: CreatePaseoWorktreeInput; resolveDefaultBranch?: (root: string) => Promise<string> } {
const base = { cwd: repoRoot, runSetup: false, paseoHome } as const;
switch (target.mode) {
case "branch-off":
return {
input: {
...base,
worktreeSlug: target.newBranch,
action: "branch-off",
...(target.base ? { refName: target.base } : {}),
},
};
case "checkout-branch":
return {
input: { ...base, action: "checkout", refName: target.branch },
};
case "checkout-pr":
return {
input: { ...base, action: "checkout", githubPrNumber: target.prNumber },
};
}
}
interface CreateMcpWorktreeOptions {
input: CreatePaseoWorktreeInput;
createPaseoWorktree: CreatePaseoWorktreeWorkflowFn | undefined;
@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
import { findGitHubPrAttachment, renderPromptAttachmentAsText } from "./prompt-attachments.js";
import { buildAgentBranchNameSeed, renderPromptAttachmentAsText } from "./prompt-attachments.js";
describe("prompt attachments", () => {
it("renders github_pr attachments as readable text", () => {
@@ -85,30 +85,31 @@ describe("prompt attachments", () => {
).toContain("GitHub Issue #55: Issue");
});
it("finds the first github_pr attachment", () => {
it("returns undefined when firstAgentContext is empty", () => {
expect(buildAgentBranchNameSeed(undefined)).toBeUndefined();
expect(buildAgentBranchNameSeed({})).toBeUndefined();
expect(buildAgentBranchNameSeed({ prompt: " " })).toBeUndefined();
expect(buildAgentBranchNameSeed({ attachments: [] })).toBeUndefined();
});
it("joins prompt and rendered attachments into a single seed", () => {
expect(
findGitHubPrAttachment([
{
type: "github_issue",
mimeType: "application/github-issue",
number: 55,
title: "Issue",
url: "https://github.com/getpaseo/paseo/issues/55",
},
{
type: "github_pr",
mimeType: "application/github-pr",
number: 123,
title: "PR",
url: "https://github.com/getpaseo/paseo/pull/123",
},
]),
).toEqual({
type: "github_pr",
mimeType: "application/github-pr",
number: 123,
title: "PR",
url: "https://github.com/getpaseo/paseo/pull/123",
});
buildAgentBranchNameSeed({
prompt: "Investigate flaky test",
attachments: [
{
type: "github_pr",
mimeType: "application/github-pr",
number: 123,
title: "Fix worktree naming",
url: "https://github.com/getpaseo/paseo/pull/123",
baseRefName: "main",
headRefName: "fix/worktree-naming",
},
],
}),
).toBe(
"Investigate flaky test\n\nGitHub PR #123: Fix worktree naming\nhttps://github.com/getpaseo/paseo/pull/123\nBase: main\nHead: fix/worktree-naming",
);
});
});
@@ -58,16 +58,22 @@ function padLineNumber(lineNumber: number | null): string {
return (lineNumber?.toString() ?? "-").padStart(2);
}
export function findGitHubPrAttachment(
attachments: readonly AgentAttachment[] | undefined,
): Extract<AgentAttachment, { type: "github_pr" }> | null {
if (!attachments) {
return null;
export function buildAgentBranchNameSeed(
firstAgentContext: { prompt?: string; attachments?: readonly AgentAttachment[] } | undefined,
): string | undefined {
if (!firstAgentContext) {
return undefined;
}
return (
attachments.find(
(attachment): attachment is Extract<AgentAttachment, { type: "github_pr" }> =>
attachment.type === "github_pr",
) ?? null
);
const parts: string[] = [];
const prompt = firstAgentContext.prompt?.trim();
if (prompt) {
parts.push(prompt);
}
for (const attachment of firstAgentContext.attachments ?? []) {
const rendered = renderPromptAttachmentAsText(attachment).trim();
if (rendered) {
parts.push(rendered);
}
}
return parts.length > 0 ? parts.join("\n\n") : undefined;
}
@@ -129,7 +129,7 @@ test("renames an eligible unnamed branch-off worktree once on first agent contex
const first = await attemptFirstAgentBranchAutoName({
cwd: created.worktree.worktreePath,
nameContext: "Build the agent context name",
firstAgentContext: { prompt: "Build the agent context name" },
generateBranchName: deps.generateBranchName,
});
const branchAfterFirst = execSync("git branch --show-current", {
@@ -155,7 +155,7 @@ test("renames an eligible unnamed branch-off worktree once on first agent contex
const second = await attemptFirstAgentBranchAutoName({
cwd: created.worktree.worktreePath,
nameContext: "Try another name",
firstAgentContext: { prompt: "Try another name" },
generateBranchName: () => "second-agent-name",
});
const branchAfterSecond = execSync("git branch --show-current", {
@@ -169,6 +169,80 @@ test("renames an eligible unnamed branch-off worktree once on first agent contex
expect(branchAfterSecond).toBe("renamed-from-agent-context");
});
test("renames the branch even when the app supplies a random placeholder slug", async () => {
const { repoDir, tempDir } = createGitRepo();
cleanupPaths.push(tempDir);
const deps = createDeps({
generateBranchName: (seed) =>
seed === "Investigate the failing login flow" ? "renamed-from-prompt" : (seed ?? "fallback"),
});
const created = await createPaseoWorktree(
{
cwd: repoDir,
worktreeSlug: "dazzling-yak",
firstAgentContext: { prompt: "Investigate the failing login flow" },
runSetup: false,
paseoHome: path.join(tempDir, ".paseo"),
},
deps,
);
const branchAfter = execSync("git branch --show-current", {
cwd: created.worktree.worktreePath,
stdio: "pipe",
})
.toString()
.trim();
expect(created.worktree.branchName).toBe("renamed-from-prompt");
expect(branchAfter).toBe("renamed-from-prompt");
expect(created.workspace.displayName).toBe("renamed-from-prompt");
});
test("renames the branch from a github_pr attachment when no prompt is supplied", async () => {
const { repoDir, tempDir } = createGitRepo();
cleanupPaths.push(tempDir);
const deps = createDeps({
generateBranchName: (seed) =>
seed?.includes("Investigate flaky checkout test")
? "renamed-from-pr-attachment"
: (seed ?? "fallback"),
});
const created = await createPaseoWorktree(
{
cwd: repoDir,
worktreeSlug: "dazzling-yak",
firstAgentContext: {
attachments: [
{
type: "github_pr",
mimeType: "application/github-pr",
number: 42,
title: "Investigate flaky checkout test",
url: "https://github.com/acme/repo/pull/42",
},
],
},
runSetup: false,
paseoHome: path.join(tempDir, ".paseo"),
},
deps,
);
const branchAfter = execSync("git branch --show-current", {
cwd: created.worktree.worktreePath,
stdio: "pipe",
})
.toString()
.trim();
expect(created.worktree.branchName).toBe("renamed-from-pr-attachment");
expect(branchAfter).toBe("renamed-from-pr-attachment");
expect(created.workspace.displayName).toBe("renamed-from-pr-attachment");
});
test("does not mark checkout branch worktrees as eligible for first-agent rename", async () => {
const { repoDir, tempDir } = createGitRepo();
cleanupPaths.push(tempDir);
@@ -196,7 +270,7 @@ test("does not mark checkout branch worktrees as eligible for first-agent rename
await expect(
attemptFirstAgentBranchAutoName({
cwd: created.worktree.worktreePath,
nameContext: "Rename checkout branch",
firstAgentContext: { prompt: "Rename checkout branch" },
generateBranchName: () => "must-not-rename",
}),
).resolves.toEqual({ attempted: false, renamed: false, branchName: null });
@@ -232,7 +306,7 @@ test("does not mark GitHub PR checkout worktrees as eligible for first-agent ren
await expect(
attemptFirstAgentBranchAutoName({
cwd: created.worktree.worktreePath,
nameContext: "Rename PR checkout",
firstAgentContext: { prompt: "Rename PR checkout" },
generateBranchName: () => "must-not-rename",
}),
).resolves.toEqual({ attempted: false, renamed: false, branchName: null });
@@ -1,5 +1,3 @@
import { basename } from "node:path";
import type { WorkspaceGitService } from "./workspace-git-service.js";
import {
type PersistedWorkspaceRecord,
@@ -23,6 +21,8 @@ import {
writePaseoWorktreeFirstAgentBranchAutoNameMetadata,
} from "../utils/worktree-metadata.js";
import type { WorktreeCreationIntent } from "./resolve-worktree-creation-intent.js";
import { buildAgentBranchNameSeed } from "./agent/prompt-attachments.js";
import type { FirstAgentContext } from "../shared/messages.js";
export interface CreatePaseoWorktreeInput extends CreateWorktreeCoreInput {}
@@ -58,7 +58,9 @@ export async function createPaseoWorktree(
deps: CreatePaseoWorktreeDeps,
): Promise<CreatePaseoWorktreeResult> {
const createdWorktree = await createWorktreeCore(input, deps);
maybeMarkFirstAgentBranchAutoNameEligible({ input, createdWorktree });
if (!buildAgentBranchNameSeed(input.firstAgentContext)) {
maybeMarkFirstAgentBranchAutoNameEligible({ createdWorktree });
}
const worktree = await maybeAutoNameCreatedWorktree({
input,
createdWorktree,
@@ -84,12 +86,12 @@ export async function createPaseoWorktree(
export async function attemptFirstAgentBranchAutoName(options: {
cwd: string;
nameContext?: string;
firstAgentContext: FirstAgentContext | undefined;
generateBranchName: (seed: string | undefined) => string;
renameCurrentBranch?: typeof renameCurrentBranch;
}): Promise<AttemptFirstAgentBranchAutoNameResult> {
const nameContext = options.nameContext?.trim();
if (!nameContext) {
const seed = buildAgentBranchNameSeed(options.firstAgentContext);
if (!seed) {
return { attempted: false, renamed: false, branchName: null };
}
@@ -109,7 +111,7 @@ export async function attemptFirstAgentBranchAutoName(options: {
markPaseoWorktreeFirstAgentBranchAutoNameAttempted(options.cwd);
const branchName = options.generateBranchName(nameContext);
const branchName = options.generateBranchName(seed);
const validation = validateBranchSlug(branchName);
if (!validation.valid || branchName === metadata.firstAgentBranchAutoName.placeholderBranchName) {
return { attempted: true, renamed: false, branchName: null };
@@ -125,15 +127,10 @@ export async function attemptFirstAgentBranchAutoName(options: {
}
function maybeMarkFirstAgentBranchAutoNameEligible(options: {
input: CreatePaseoWorktreeInput;
createdWorktree: Awaited<ReturnType<typeof createWorktreeCore>>;
}): void {
const { input, createdWorktree } = options;
if (
!createdWorktree.created ||
input.worktreeSlug ||
createdWorktree.intent.kind !== "branch-off"
) {
const { createdWorktree } = options;
if (!createdWorktree.created || createdWorktree.intent.kind !== "branch-off") {
return;
}
@@ -148,27 +145,12 @@ async function maybeAutoNameCreatedWorktree(options: {
deps: Pick<CreatePaseoWorktreeDeps, "generateBranchName">;
}): Promise<WorktreeConfig> {
const { input, createdWorktree, deps } = options;
const nameContext = input.nameContext?.trim();
if (
!nameContext ||
input.worktreeSlug ||
!createdWorktree.created ||
createdWorktree.intent.kind !== "branch-off"
) {
const seed = buildAgentBranchNameSeed(input.firstAgentContext);
if (!seed || !createdWorktree.created || createdWorktree.intent.kind !== "branch-off") {
return createdWorktree.worktree;
}
const generatedPlaceholderName = basename(createdWorktree.worktree.worktreePath);
if (
!generatedPlaceholderName ||
createdWorktree.worktree.branchName !== generatedPlaceholderName
) {
return createdWorktree.worktree;
}
markPaseoWorktreeFirstAgentBranchAutoNameAttempted(createdWorktree.worktree.worktreePath);
const branchName = deps.generateBranchName(nameContext);
const branchName = deps.generateBranchName(seed);
const validation = validateBranchSlug(branchName);
if (!validation.valid || branchName === createdWorktree.worktree.branchName) {
return createdWorktree.worktree;
@@ -1,9 +1,7 @@
import { describe, expect, test } from "vitest";
import type { GitHubService } from "../services/github-service.js";
import type { AgentAttachment } from "./messages.js";
import {
ConflictingGitHubPullRequestIntentError,
MissingCheckoutTargetError,
resolveWorktreeCreationIntent,
} from "./resolve-worktree-creation-intent.js";
@@ -57,22 +55,6 @@ function createResolverHarness(): ResolverHarness {
};
}
function createGitHubPrAttachment(params: {
number: number;
headRefName?: string;
baseRefName?: string;
}): AgentAttachment {
return {
type: "github_pr",
mimeType: "application/github-pr",
number: params.number,
title: `PR ${params.number}`,
url: `https://github.com/acme/repo/pull/${params.number}`,
...(params.headRefName ? { headRefName: params.headRefName } : {}),
...(params.baseRefName ? { baseRefName: params.baseRefName } : {}),
};
}
describe("resolveWorktreeCreationIntent", () => {
const repoRoot = "/tmp/repo";
@@ -155,80 +137,4 @@ describe("resolveWorktreeCreationIntent", () => {
resolveWorktreeCreationIntent({ action: "checkout" }, repoRoot, deps),
).rejects.toBeInstanceOf(MissingCheckoutTargetError);
});
test("preserves the legacy GitHub PR attachment checkout path", async () => {
const deps = createResolverHarness();
await expect(
resolveWorktreeCreationIntent(
{ attachments: [createGitHubPrAttachment({ number: 5, headRefName: "pr-five" })] },
repoRoot,
deps,
),
).resolves.toEqual({
kind: "checkout-github-pr",
githubPrNumber: 5,
headRef: "pr-five",
baseRefName: "main",
});
expect(deps.headRefLookups).toEqual([]);
});
test("accepts matching explicit and attachment GitHub PR numbers", async () => {
const deps = createResolverHarness();
await expect(
resolveWorktreeCreationIntent(
{
action: "checkout",
githubPrNumber: 5,
attachments: [createGitHubPrAttachment({ number: 5, headRefName: "pr-five" })],
},
repoRoot,
deps,
),
).resolves.toEqual({
kind: "checkout-github-pr",
githubPrNumber: 5,
headRef: "pr-five",
baseRefName: "main",
});
expect(deps.headRefLookups).toEqual([]);
});
test("rejects conflicting explicit and attachment GitHub PR numbers", async () => {
const deps = createResolverHarness();
await expect(
resolveWorktreeCreationIntent(
{
action: "checkout",
githubPrNumber: 5,
attachments: [createGitHubPrAttachment({ number: 7, headRefName: "pr-seven" })],
},
repoRoot,
deps,
),
).rejects.toBeInstanceOf(ConflictingGitHubPullRequestIntentError);
});
test("ignores a GitHub PR attachment for explicit branch-off", async () => {
const deps = createResolverHarness();
await expect(
resolveWorktreeCreationIntent(
{
action: "branch-off",
attachments: [createGitHubPrAttachment({ number: 5, headRefName: "pr-five" })],
},
repoRoot,
deps,
),
).resolves.toEqual({
kind: "branch-off",
baseBranch: "main",
newBranchName: "generated-worktree",
});
expect(deps.headRefLookups).toEqual([]);
});
});
@@ -1,6 +1,4 @@
import type { GitHubPullRequestCheckoutTarget, GitHubService } from "../services/github-service.js";
import type { AgentAttachment } from "./messages.js";
import { findGitHubPrAttachment } from "./agent/prompt-attachments.js";
import type { WorktreeSource } from "../utils/worktree.js";
export type WorktreeCreationIntent = WorktreeSource;
@@ -10,7 +8,6 @@ export interface ResolveWorktreeCreationIntentInput {
refName?: string;
action?: "branch-off" | "checkout";
githubPrNumber?: number;
attachments?: AgentAttachment[];
}
export interface ResolveWorktreeCreationIntentDeps {
@@ -28,31 +25,11 @@ export class MissingCheckoutTargetError extends Error {
}
}
export class ConflictingGitHubPullRequestIntentError extends Error {
readonly explicitGitHubPrNumber: number;
readonly attachmentGitHubPrNumber: number;
constructor(params: { explicitGitHubPrNumber: number; attachmentGitHubPrNumber: number }) {
super(
`Conflicting GitHub PR intent: explicit PR #${params.explicitGitHubPrNumber} does not match attachment PR #${params.attachmentGitHubPrNumber}`,
);
this.name = "ConflictingGitHubPullRequestIntentError";
this.explicitGitHubPrNumber = params.explicitGitHubPrNumber;
this.attachmentGitHubPrNumber = params.attachmentGitHubPrNumber;
}
}
export async function resolveWorktreeCreationIntent(
input: ResolveWorktreeCreationIntentInput,
repoRoot: string,
deps: ResolveWorktreeCreationIntentDeps,
): Promise<WorktreeCreationIntent> {
const githubPrAttachment = findGitHubPrAttachment(input.attachments);
assertGitHubPrIntentAgreesWithAttachment({
githubPrNumber: input.githubPrNumber,
githubPrAttachment,
});
if (input.action === "branch-off") {
return {
kind: "branch-off",
@@ -66,7 +43,6 @@ export async function resolveWorktreeCreationIntent(
return resolveGitHubPrCheckoutIntent({
refName: input.refName,
githubPrNumber: input.githubPrNumber,
githubPrAttachment,
repoRoot,
deps,
});
@@ -87,7 +63,6 @@ export async function resolveWorktreeCreationIntent(
return resolveGitHubPrCheckoutIntent({
refName: input.refName,
githubPrNumber: input.githubPrNumber,
githubPrAttachment,
repoRoot,
deps,
});
@@ -101,15 +76,6 @@ export async function resolveWorktreeCreationIntent(
};
}
if (githubPrAttachment) {
return resolveGitHubPrCheckoutIntent({
githubPrNumber: githubPrAttachment.number,
githubPrAttachment,
repoRoot,
deps,
});
}
return {
kind: "branch-off",
baseBranch: await resolveDefaultBranch(repoRoot, deps),
@@ -120,7 +86,6 @@ export async function resolveWorktreeCreationIntent(
async function resolveGitHubPrCheckoutIntent(params: {
refName?: string;
githubPrNumber: number;
githubPrAttachment: Extract<AgentAttachment, { type: "github_pr" }> | null;
repoRoot: string;
deps: ResolveWorktreeCreationIntentDeps;
}): Promise<Extract<WorktreeCreationIntent, { kind: "checkout-github-pr" }>> {
@@ -128,14 +93,13 @@ async function resolveGitHubPrCheckoutIntent(params: {
const headRef = await resolveGitHubPrHeadRef({
refName: params.refName,
githubPrNumber: params.githubPrNumber,
githubPrAttachment: params.githubPrAttachment,
checkoutTarget,
repoRoot: params.repoRoot,
deps: params.deps,
});
const baseRefName =
checkoutTarget?.baseRefName?.trim() ||
(await resolveGitHubPrBaseRefName(params.githubPrAttachment, params.repoRoot, params.deps));
(await resolveDefaultBranch(params.repoRoot, params.deps));
const localBranchName = buildGitHubPrLocalBranchName({ headRef, checkoutTarget });
const pushRemoteUrl = checkoutTarget
? checkoutTarget.headRepositorySshUrl || checkoutTarget.headRepositoryUrl || undefined
@@ -176,26 +140,9 @@ async function resolveDefaultBranch(
return baseBranch;
}
function assertGitHubPrIntentAgreesWithAttachment(params: {
githubPrNumber?: number;
githubPrAttachment: Extract<AgentAttachment, { type: "github_pr" }> | null;
}): void {
if (
params.githubPrNumber !== undefined &&
params.githubPrAttachment &&
params.githubPrNumber !== params.githubPrAttachment.number
) {
throw new ConflictingGitHubPullRequestIntentError({
explicitGitHubPrNumber: params.githubPrNumber,
attachmentGitHubPrNumber: params.githubPrAttachment.number,
});
}
}
async function resolveGitHubPrHeadRef(params: {
refName?: string;
githubPrNumber: number;
githubPrAttachment: Extract<AgentAttachment, { type: "github_pr" }> | null;
checkoutTarget?: GitHubPullRequestCheckoutTarget | null;
repoRoot: string;
deps: ResolveWorktreeCreationIntentDeps;
@@ -208,12 +155,6 @@ async function resolveGitHubPrHeadRef(params: {
if (checkoutTargetHeadRef) {
return checkoutTargetHeadRef;
}
if (params.githubPrAttachment) {
const attachmentHeadRef = params.githubPrAttachment.headRefName?.trim();
if (attachmentHeadRef) {
return attachmentHeadRef;
}
}
return params.deps.github.getPullRequestHeadRef({
cwd: params.repoRoot,
number: params.githubPrNumber,
@@ -234,11 +175,3 @@ function normalizeGitHubOwnerForBranch(owner: string | null): string | null {
const normalized = owner?.trim().toLowerCase() ?? "";
return /^[a-z0-9-]+$/.test(normalized) ? normalized : null;
}
async function resolveGitHubPrBaseRefName(
attachment: Extract<AgentAttachment, { type: "github_pr" }> | null,
repoRoot: string,
deps: ResolveWorktreeCreationIntentDeps,
): Promise<string> {
return attachment?.baseRefName?.trim() || (await resolveDefaultBranch(repoRoot, deps));
}
+11 -14
View File
@@ -13,6 +13,7 @@ import {
serializeAgentStreamEvent,
type AgentSnapshotPayload,
type AgentAttachment,
type FirstAgentContext,
type SessionInboundMessage,
type SessionOutboundMessage,
type FileExplorerRequest,
@@ -200,7 +201,6 @@ import { createWorktreeCoreDeps } from "./worktree-core.js";
import {
assertSafeGitRef as assertWorktreeSafeGitRef,
buildAgentSessionConfig as buildWorktreeAgentSessionConfig,
buildAgentWorktreeNameContext,
createPaseoWorktreeWorkflow as createWorktreeWorkflow,
type CreatePaseoWorktreeSetupContinuationInput,
type CreatePaseoWorktreeWorkflowResult,
@@ -2984,16 +2984,15 @@ export class Session {
...(provisionalTitle ? { title: provisionalTitle } : {}),
};
const agentNameContext = buildAgentWorktreeNameContext({
initialPrompt: trimmedPrompt,
attachments,
});
const firstAgentContext: FirstAgentContext = {
...(trimmedPrompt ? { prompt: trimmedPrompt } : {}),
...(attachments && attachments.length > 0 ? { attachments } : {}),
};
const { sessionConfig, setupContinuation } = await this.buildAgentSessionConfig(
resolvedConfig,
git,
worktreeName,
attachments,
agentNameContext,
firstAgentContext,
);
let resolvedWorkspace = msg.workspaceId
? await this.workspaceRegistry.get(msg.workspaceId)
@@ -3004,7 +3003,7 @@ export class Session {
}
resolvedWorkspace = await this.maybeAutoNameWorkspaceBranchForFirstAgent({
workspace: resolvedWorkspace,
nameContext: agentNameContext,
firstAgentContext,
});
const snapshot = await this.agentManager.createAgent(
{
@@ -3352,8 +3351,7 @@ export class Session {
config: AgentSessionConfig,
gitOptions?: GitSetupOptions,
legacyWorktreeName?: string,
attachments?: AgentAttachment[],
nameContext?: string,
firstAgentContext?: FirstAgentContext,
): Promise<{
sessionConfig: AgentSessionConfig;
setupContinuation?: CreatePaseoWorktreeWorkflowResult["setupContinuation"];
@@ -3391,19 +3389,18 @@ export class Session {
config,
gitOptions,
legacyWorktreeName,
attachments,
nameContext,
firstAgentContext,
);
}
private async maybeAutoNameWorkspaceBranchForFirstAgent(input: {
workspace: PersistedWorkspaceRecord;
nameContext?: string;
firstAgentContext: FirstAgentContext;
}): Promise<PersistedWorkspaceRecord> {
const coreDeps = createWorktreeCoreDeps(this.github);
const result = await attemptFirstAgentBranchAutoName({
cwd: input.workspace.cwd,
nameContext: input.nameContext,
firstAgentContext: input.firstAgentContext,
generateBranchName: coreDeps.generateBranchName,
});
if (!result.renamed || !result.branchName) {
+2 -1
View File
@@ -13,11 +13,12 @@ import {
type ResolveWorktreeCreationIntentInput,
type WorktreeCreationIntent,
} from "./resolve-worktree-creation-intent.js";
import type { FirstAgentContext } from "../shared/messages.js";
import type { WorkspaceGitService } from "./workspace-git-service.js";
export interface CreateWorktreeCoreInput extends ResolveWorktreeCreationIntentInput {
cwd: string;
nameContext?: string;
firstAgentContext?: FirstAgentContext;
paseoHome?: string;
runSetup?: boolean;
}
@@ -1,13 +1,9 @@
import {
ConflictingGitHubPullRequestIntentError,
MissingCheckoutTargetError,
} from "./resolve-worktree-creation-intent.js";
import { MissingCheckoutTargetError } from "./resolve-worktree-creation-intent.js";
import { BranchAlreadyCheckedOutError, UnknownBranchError } from "../utils/worktree.js";
export type WorktreeWireErrorCode =
| "branch_already_checked_out"
| "missing_checkout_target"
| "conflicting_github_pull_request_intent"
| "unknown_branch"
| "unknown";
@@ -33,9 +29,6 @@ export function toWorktreeWireError(error: unknown): WorktreeWireError {
if (error instanceof MissingCheckoutTargetError) {
return { code: "missing_checkout_target", message: error.message };
}
if (error instanceof ConflictingGitHubPullRequestIntentError) {
return { code: "conflicting_github_pull_request_intent", message: error.message };
}
if (error instanceof UnknownBranchError) {
return { code: "unknown_branch", message: error.message };
}
@@ -16,7 +16,6 @@ import type { SessionOutboundMessage, WorkspaceDescriptorPayload } from "./messa
import { archivePaseoWorktree } from "./paseo-worktree-archive-service.js";
import {
buildAgentSessionConfig,
buildAgentWorktreeNameContext,
createPaseoWorktreeWorkflow,
handlePaseoWorktreeArchiveRequest,
handlePaseoWorktreeListRequest,
@@ -1158,7 +1157,7 @@ describe("handleCreatePaseoWorktreeRequest", () => {
}
});
test("checks out the GitHub PR branch when a github_pr attachment is present", async () => {
test("checks out the GitHub PR branch when githubPrNumber is supplied", async () => {
const { tempDir, repoDir } = createGitHubPrRemoteRepo();
cleanupPaths.push(tempDir);
@@ -1180,17 +1179,9 @@ describe("handleCreatePaseoWorktreeRequest", () => {
requestId: "req-pr-worktree",
cwd: repoDir,
worktreeSlug: "review-pr-123",
attachments: [
{
type: "github_pr",
mimeType: "application/github-pr",
number: 123,
title: "Review branch",
url: "https://github.com/getpaseo/paseo/pull/123",
baseRefName: "main",
headRefName: "feature/review-pr",
},
],
action: "checkout",
githubPrNumber: 123,
refName: "feature/review-pr",
},
);
@@ -1228,6 +1219,10 @@ describe("handleCreatePaseoWorktreeRequest", () => {
{
paseoHome: path.join(tempDir, ".paseo"),
sessionLogger: createLogger(),
workspaceGitService: {
resolveRepoRoot: vi.fn(async () => repoDir),
resolveDefaultBranch: vi.fn(async () => "main"),
} as unknown as WorkspaceGitService,
createPaseoWorktree: createPaseoWorktreeForTest({
paseoHome: path.join(tempDir, ".paseo"),
events,
@@ -1246,19 +1241,10 @@ describe("handleCreatePaseoWorktreeRequest", () => {
{
createWorktree: true,
worktreeSlug: "agent-review-pr-123",
action: "checkout",
githubPrNumber: 123,
refName: "feature/review-pr",
},
undefined,
[
{
type: "github_pr",
mimeType: "application/github-pr",
number: 123,
title: "Review branch",
url: "https://github.com/getpaseo/paseo/pull/123",
baseRefName: "main",
headRefName: "feature/review-pr",
},
],
);
expect(result.sessionConfig.cwd).toContain("agent-review-pr-123");
@@ -1332,11 +1318,11 @@ describe("handleCreatePaseoWorktreeRequest", () => {
repoRoot: "/tmp/repo",
created: true,
}));
const nameContext = buildAgentWorktreeNameContext({
initialPrompt: "Create a worktree name from this prompt",
const firstAgentContext = {
prompt: "Create a worktree name from this prompt",
attachments: [
{
type: "github_pr",
type: "github_pr" as const,
mimeType: "application/github-pr",
number: 123,
title: "Fix worktree naming",
@@ -1345,7 +1331,7 @@ describe("handleCreatePaseoWorktreeRequest", () => {
headRefName: "fix/worktree-naming",
},
],
});
};
const result = await buildAgentSessionConfig(
{
@@ -1370,24 +1356,12 @@ describe("handleCreatePaseoWorktreeRequest", () => {
action: "branch-off",
},
undefined,
[
{
type: "github_pr",
mimeType: "application/github-pr",
number: 123,
title: "Fix worktree naming",
url: "https://github.com/getpaseo/paseo/pull/123",
baseRefName: "main",
headRefName: "fix/worktree-naming",
},
],
nameContext,
firstAgentContext,
);
expect(createPaseoWorktree).toHaveBeenCalledWith(
expect.objectContaining({
nameContext:
"Create a worktree name from this prompt\n\nGitHub PR #123: Fix worktree naming\nhttps://github.com/getpaseo/paseo/pull/123\nBase: main\nHead: fix/worktree-naming",
firstAgentContext,
}),
expect.anything(),
);
@@ -1641,7 +1615,6 @@ describe("handleCreatePaseoWorktreeRequest", () => {
type: "create_paseo_worktree_request",
cwd: repoDir,
action: "checkout",
attachments: [],
requestId: "req-missing-target",
},
);
@@ -1681,7 +1654,6 @@ describe("handleCreatePaseoWorktreeRequest", () => {
cwd: repoDir,
action: "checkout",
refName: "missing-branch",
attachments: [],
requestId: "req-unknown-branch",
},
);
+4 -26
View File
@@ -3,8 +3,8 @@ import { basename } from "node:path";
import type { AgentSessionConfig } from "./agent/agent-sdk-types.js";
import {
type AgentAttachment,
type GitSetupOptions,
type FirstAgentContext,
type SessionInboundMessage,
type SessionOutboundMessage,
type WorkspaceSetupSnapshot,
@@ -24,7 +24,6 @@ import type { ScriptRouteStore } from "./script-proxy.js";
import type { WorkspaceScriptRuntimeStore } from "./workspace-script-runtime-store.js";
import type { GitHubService } from "../services/github-service.js";
import type { CheckoutExistingBranchResult } from "../utils/checkout-git.js";
import { renderPromptAttachmentAsText } from "./agent/prompt-attachments.js";
import { expandTilde } from "../utils/path.js";
import {
getWorktreeSetupCommands,
@@ -169,8 +168,7 @@ export async function buildAgentSessionConfig(
config: AgentSessionConfig,
gitOptions?: GitSetupOptions,
legacyWorktreeName?: string,
attachments?: AgentAttachment[],
nameContext?: string,
firstAgentContext?: FirstAgentContext,
): Promise<{
sessionConfig: AgentSessionConfig;
setupContinuation?: AgentWorktreeSetupContinuation;
@@ -201,8 +199,7 @@ export async function buildAgentSessionConfig(
refName: normalized.refName,
action: normalized.action,
githubPrNumber: normalized.githubPrNumber,
nameContext,
attachments,
firstAgentContext,
runSetup: false,
paseoHome: dependencies.paseoHome,
},
@@ -247,24 +244,6 @@ export async function buildAgentSessionConfig(
};
}
export function buildAgentWorktreeNameContext(input: {
initialPrompt?: string | null;
attachments?: readonly AgentAttachment[];
}): string | undefined {
const parts: string[] = [];
const prompt = input.initialPrompt?.trim();
if (prompt) {
parts.push(prompt);
}
for (const attachment of input.attachments ?? []) {
const rendered = renderPromptAttachmentAsText(attachment).trim();
if (rendered) {
parts.push(rendered);
}
}
return parts.length > 0 ? parts.join("\n\n") : undefined;
}
interface ValidateNormalizedGitOptionsInput {
baseBranch: string | undefined;
createNewBranch: boolean;
@@ -513,11 +492,10 @@ export async function handleCreatePaseoWorktreeRequest(
const createdWorktree = await dependencies.createPaseoWorktreeWorkflow({
cwd: request.cwd,
worktreeSlug: request.worktreeSlug,
nameContext: request.nameContext,
firstAgentContext: request.firstAgentContext,
refName: request.refName,
action: request.action,
githubPrNumber: request.githubPrNumber,
attachments: request.attachments,
runSetup: false,
paseoHome: dependencies.paseoHome,
});
@@ -226,28 +226,31 @@ describe("shared messages attachments", () => {
]);
});
it("keeps known attachments and drops unknown worktree-create attachments", () => {
it("keeps known firstAgentContext attachments and drops unknown ones", () => {
const parsed = CreatePaseoWorktreeRequestSchema.parse({
type: "create_paseo_worktree_request",
requestId: "req-3",
cwd: "/tmp/repo",
attachments: [
{
type: "github_pr",
mimeType: "application/github-pr",
number: 99,
title: "Fork-safe PR checkout",
url: "https://github.com/getpaseo/paseo/pull/99",
},
{
type: "future_attachment",
mimeType: "application/future",
foo: "bar",
},
],
firstAgentContext: {
prompt: "Investigate flaky test",
attachments: [
{
type: "github_pr",
mimeType: "application/github-pr",
number: 99,
title: "Fork-safe PR checkout",
url: "https://github.com/getpaseo/paseo/pull/99",
},
{
type: "future_attachment",
mimeType: "application/future",
foo: "bar",
},
],
},
});
expect(parsed.attachments).toEqual([
expect(parsed.firstAgentContext?.attachments).toEqual([
{
type: "github_pr",
mimeType: "application/github-pr",
@@ -256,21 +259,20 @@ describe("shared messages attachments", () => {
url: "https://github.com/getpaseo/paseo/pull/99",
},
]);
expect(parsed.firstAgentContext?.prompt).toBe("Investigate flaky test");
});
it("parses old-shape create-worktree payloads without the new intent fields", () => {
it("parses worktree-create payloads without a firstAgentContext", () => {
const parsed = CreatePaseoWorktreeRequestSchema.parse({
type: "create_paseo_worktree_request",
requestId: "req-4",
cwd: "/tmp/repo",
attachments: [],
});
expect(parsed).toEqual({
type: "create_paseo_worktree_request",
requestId: "req-4",
cwd: "/tmp/repo",
attachments: [],
});
});
@@ -279,7 +281,6 @@ describe("shared messages attachments", () => {
type: "create_paseo_worktree_request",
requestId: "req-5",
cwd: "/tmp/repo",
attachments: [],
action: "checkout",
refName: "feature/ref-picker",
githubPrNumber: 42,
@@ -290,7 +291,6 @@ describe("shared messages attachments", () => {
type: "create_paseo_worktree_request",
requestId: "req-5",
cwd: "/tmp/repo",
attachments: [],
action: "checkout",
refName: "feature/ref-picker",
githubPrNumber: 42,
+7 -2
View File
@@ -1363,12 +1363,16 @@ export const PaseoWorktreeArchiveRequestSchema = z.object({
requestId: z.string(),
});
export const FirstAgentContextSchema = z.object({
prompt: z.string().optional(),
attachments: AgentAttachmentsSchema,
});
export const CreatePaseoWorktreeRequestSchema = z.object({
type: z.literal("create_paseo_worktree_request"),
cwd: z.string(),
worktreeSlug: z.string().optional(),
nameContext: z.string().optional(),
attachments: AgentAttachmentsSchema,
firstAgentContext: FirstAgentContextSchema.optional(),
refName: z.string().min(1).optional(),
action: z.enum(["branch-off", "checkout"]).optional(),
githubPrNumber: z.number().int().positive().optional(),
@@ -3484,6 +3488,7 @@ export type DictationStreamFinishMessage = z.infer<typeof DictationStreamFinishM
export type DictationStreamCancelMessage = z.infer<typeof DictationStreamCancelMessageSchema>;
export type CreateAgentRequestMessage = z.infer<typeof CreateAgentRequestMessageSchema>;
export type AgentAttachment = z.infer<typeof AgentAttachmentSchema>;
export type FirstAgentContext = z.infer<typeof FirstAgentContextSchema>;
export type ReviewAttachment = z.infer<typeof ReviewAttachmentSchema>;
export type ListProviderModelsRequestMessage = z.infer<
typeof ListProviderModelsRequestMessageSchema