diff --git a/packages/app/src/contexts/session-context.tsx b/packages/app/src/contexts/session-context.tsx index 174704e77..79c30c209 100644 --- a/packages/app/src/contexts/session-context.tsx +++ b/packages/app/src/contexts/session-context.tsx @@ -13,6 +13,7 @@ import { } from "@/contexts/session-stream-reducers"; import type { ActivityLogPayload, + AgentAttachment, AgentStreamEventPayload, SessionOutboundMessage, } from "@server/shared/messages"; @@ -300,7 +301,12 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider const previousAgentStatusRef = useRef>(new Map()); const sendAgentMessageRef = useRef< - ((agentId: string, message: string, images?: AttachmentMetadata[]) => Promise) | null + (( + agentId: string, + message: string, + images?: AttachmentMetadata[], + attachments?: AgentAttachment[], + ) => Promise) | null >(null); const sessionStateTimeoutRef = useRef | null>(null); const attentionNotifiedRef = useRef>(new Map()); @@ -1584,7 +1590,12 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider ]); const sendAgentMessage = useCallback( - async (agentId: string, message: string, images?: AttachmentMetadata[]) => { + async ( + agentId: string, + message: string, + images?: AttachmentMetadata[], + attachments?: AgentAttachment[], + ) => { const messageId = generateMessageId(); const userMessage: StreamItem = { kind: "user_message", @@ -1624,6 +1635,7 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider .sendAgentMessage(agentId, message, { messageId, ...(imagesData && imagesData.length > 0 ? { images: imagesData } : {}), + ...(attachments && attachments.length > 0 ? { attachments } : {}), }) .catch((error) => { console.error("[Session] Failed to send agent message:", error); @@ -1692,6 +1704,7 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider config, initialPrompt, images, + attachments, git, worktreeName, requestId, @@ -1699,6 +1712,7 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider config: any; initialPrompt: string; images?: AttachmentMetadata[]; + attachments?: AgentAttachment[]; git?: any; worktreeName?: string; requestId?: string; @@ -1718,6 +1732,7 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider config, ...(trimmedPrompt ? { initialPrompt: trimmedPrompt } : {}), ...(imagesData && imagesData.length > 0 ? { images: imagesData } : {}), + ...(attachments && attachments.length > 0 ? { attachments } : {}), ...(git ? { git } : {}), ...(worktreeName ? { worktreeName } : {}), ...(requestId ? { requestId } : {}), diff --git a/packages/app/src/screens/new-workspace-screen.tsx b/packages/app/src/screens/new-workspace-screen.tsx index 6c53b4cb2..2d4034f48 100644 --- a/packages/app/src/screens/new-workspace-screen.tsx +++ b/packages/app/src/screens/new-workspace-screen.tsx @@ -25,6 +25,7 @@ import { normalizeAgentSnapshot } from "@/utils/agent-snapshots"; import { encodeImages } from "@/utils/encode-images"; import { toErrorMessage } from "@/utils/error-messages"; import { openExternalUrl } from "@/utils/open-external-url"; +import { buildGitHubAttachmentFromSearchItem } from "@/utils/review-attachments"; import { requireWorkspaceExecutionAuthority, } from "@/utils/workspace-execution"; @@ -32,23 +33,6 @@ import { navigateToPreparedWorkspaceTab } from "@/utils/workspace-navigation"; import type { ImageAttachment, MessagePayload } from "@/components/message-input"; import type { GitHubSearchItem } from "@server/shared/messages"; -function buildInitialPrompt(userText: string, githubItem: GitHubSearchItem | null): string { - const parts: string[] = []; - - if (githubItem) { - const kind = githubItem.kind === "pr" ? "Pull Request" : "Issue"; - const header = `GitHub ${kind} #${githubItem.number}: ${githubItem.title}`; - const body = githubItem.body?.trim(); - parts.push(body ? `${header}\n\n${body}` : header); - } - - if (userText) { - parts.push(userText); - } - - return parts.join("\n\n---\n\n"); -} - interface NewWorkspaceScreenProps { serverId: string; sourceDirectory: string; @@ -176,9 +160,11 @@ export function NewWorkspaceScreen({ } const connectedClient = withConnectedClient(); + const reviewAttachment = buildGitHubAttachmentFromSearchItem(selectedGithubItem); const payload = await connectedClient.createPaseoWorktree({ cwd: sourceDirectory, worktreeSlug: createNameId(), + ...(reviewAttachment ? { attachments: [reviewAttachment] } : {}), }); if (payload.error || !payload.workspace) { @@ -193,6 +179,7 @@ export function NewWorkspaceScreen({ createdWorkspace, mergeWorkspaces, serverId, + selectedGithubItem, sourceDirectory, withConnectedClient, ]); @@ -208,8 +195,9 @@ export function NewWorkspaceScreen({ throw new Error("Composer state is required"); } - const initialPrompt = buildInitialPrompt(text.trim(), selectedGithubItem); + const initialPrompt = text.trim(); const encodedImages = await encodeImages(images); + const reviewAttachment = buildGitHubAttachmentFromSearchItem(selectedGithubItem); const workspaceDirectory = requireWorkspaceExecutionAuthority({ workspace }).workspaceDirectory; const agent = await connectedClient.createAgent({ provider: composerState.selectedProvider, @@ -224,6 +212,7 @@ export function NewWorkspaceScreen({ : {}), ...(initialPrompt ? { initialPrompt } : {}), ...(encodedImages && encodedImages.length > 0 ? { images: encodedImages } : {}), + ...(reviewAttachment ? { attachments: [reviewAttachment] } : {}), }); setAgents(serverId, (previous) => { diff --git a/packages/app/src/utils/review-attachments.test.ts b/packages/app/src/utils/review-attachments.test.ts new file mode 100644 index 000000000..50e45a0e0 --- /dev/null +++ b/packages/app/src/utils/review-attachments.test.ts @@ -0,0 +1,55 @@ +import { describe, expect, it } from "vitest"; + +import { buildGitHubAttachmentFromSearchItem } from "./review-attachments"; + +describe("buildGitHubAttachmentFromSearchItem", () => { + it("builds a github_pr attachment for pull requests", () => { + const attachment = buildGitHubAttachmentFromSearchItem({ + kind: "pr", + number: 123, + title: "Fix race in worktree setup", + url: "https://github.com/getpaseo/paseo/pull/123", + state: "OPEN", + body: "PR body", + labels: ["bug"], + baseRefName: "main", + headRefName: "fix/worktree-race", + }); + + expect(attachment).toEqual({ + type: "github_pr", + mimeType: "application/github-pr", + number: 123, + title: "Fix race in worktree setup", + url: "https://github.com/getpaseo/paseo/pull/123", + body: "PR body", + baseRefName: "main", + headRefName: "fix/worktree-race", + }); + }); + + it("builds a github_issue attachment for issues", () => { + const attachment = buildGitHubAttachmentFromSearchItem({ + kind: "issue", + number: 55, + title: "Improve startup error details", + url: "https://github.com/getpaseo/paseo/issues/55", + state: "OPEN", + body: "Issue body", + labels: ["bug"], + }); + + expect(attachment).toEqual({ + type: "github_issue", + mimeType: "application/github-issue", + number: 55, + title: "Improve startup error details", + url: "https://github.com/getpaseo/paseo/issues/55", + body: "Issue body", + }); + }); + + it("returns null when no item is selected", () => { + expect(buildGitHubAttachmentFromSearchItem(null)).toBeNull(); + }); +}); diff --git a/packages/app/src/utils/review-attachments.ts b/packages/app/src/utils/review-attachments.ts new file mode 100644 index 000000000..60592c6d8 --- /dev/null +++ b/packages/app/src/utils/review-attachments.ts @@ -0,0 +1,31 @@ +import type { AgentAttachment, GitHubSearchItem } from "@server/shared/messages"; + +export function buildGitHubAttachmentFromSearchItem( + item: GitHubSearchItem | null, +): AgentAttachment | null { + if (!item) { + return null; + } + + if (item.kind === "pr") { + return { + type: "github_pr", + mimeType: "application/github-pr", + number: item.number, + title: item.title, + url: item.url, + ...(item.body ? { body: item.body } : {}), + ...(item.baseRefName ? { baseRefName: item.baseRefName } : {}), + ...(item.headRefName ? { headRefName: item.headRefName } : {}), + }; + } + + return { + type: "github_issue", + mimeType: "application/github-issue", + number: item.number, + title: item.title, + url: item.url, + ...(item.body ? { body: item.body } : {}), + }; +} diff --git a/packages/server/src/client/daemon-client.test.ts b/packages/server/src/client/daemon-client.test.ts index 7369fae1e..eb73a8538 100644 --- a/packages/server/src/client/daemon-client.test.ts +++ b/packages/server/src/client/daemon-client.test.ts @@ -344,6 +344,145 @@ describe("DaemonClient", () => { await expect(createPromise).rejects.toThrow("compat test sentinel"); }); + test("sends structured attachments with create_agent_request", async () => { + const logger = createMockLogger(); + const mock = createMockTransport(); + + const client = new DaemonClient({ + url: "ws://test", + clientId: "clsk_unit_test", + logger, + reconnect: { enabled: false }, + transportFactory: () => mock.transport, + }); + clients.push(client); + + const connectPromise = client.connect(); + mock.triggerOpen(); + await connectPromise; + + const createPromise = client.createAgent({ + provider: "codex", + cwd: "/tmp/project", + initialPrompt: "Review this PR", + attachments: [ + { + type: "github_pr", + mimeType: "application/github-pr", + number: 123, + title: "Fix race in worktree setup", + url: "https://github.com/getpaseo/paseo/pull/123", + baseRefName: "main", + headRefName: "fix/worktree-race", + }, + ], + }); + + expect(mock.sent).toHaveLength(1); + const request = JSON.parse(String(mock.sent[0])) as { + type: "session"; + message: { + type: "create_agent_request"; + requestId: string; + attachments: Array<{ type: string; mimeType: string; number: number }>; + }; + }; + expect(request.message.attachments).toEqual([ + { + type: "github_pr", + mimeType: "application/github-pr", + number: 123, + title: "Fix race in worktree setup", + url: "https://github.com/getpaseo/paseo/pull/123", + baseRefName: "main", + headRefName: "fix/worktree-race", + }, + ]); + + mock.triggerMessage( + wrapSessionMessage({ + type: "status", + payload: { + status: "agent_create_failed", + requestId: request.message.requestId, + error: "attachment test sentinel", + }, + }), + ); + + await expect(createPromise).rejects.toThrow("attachment test sentinel"); + }); + + test("sends structured attachments with create_paseo_worktree_request", async () => { + const logger = createMockLogger(); + const mock = createMockTransport(); + + const client = new DaemonClient({ + url: "ws://test", + clientId: "clsk_unit_test", + logger, + reconnect: { enabled: false }, + transportFactory: () => mock.transport, + }); + clients.push(client); + + const connectPromise = client.connect(); + mock.triggerOpen(); + await connectPromise; + + const createPromise = client.createPaseoWorktree({ + cwd: "/tmp/project", + worktreeSlug: "review-pr-123", + attachments: [ + { + type: "github_pr", + mimeType: "application/github-pr", + number: 123, + title: "Fix race in worktree setup", + url: "https://github.com/getpaseo/paseo/pull/123", + }, + ], + }); + + expect(mock.sent).toHaveLength(1); + const request = JSON.parse(String(mock.sent[0])) as { + type: "session"; + message: { + type: "create_paseo_worktree_request"; + requestId: string; + attachments: Array<{ type: string; mimeType: string; number: number }>; + }; + }; + expect(request.message.attachments).toEqual([ + { + type: "github_pr", + mimeType: "application/github-pr", + number: 123, + title: "Fix race in worktree setup", + url: "https://github.com/getpaseo/paseo/pull/123", + }, + ]); + + mock.triggerMessage( + wrapSessionMessage({ + type: "create_paseo_worktree_response", + payload: { + requestId: request.message.requestId, + workspace: null, + error: "worktree attachment sentinel", + setupTerminalId: null, + }, + }), + ); + + await expect(createPromise).resolves.toEqual({ + requestId: request.message.requestId, + workspace: null, + error: "worktree attachment sentinel", + setupTerminalId: null, + }); + }); + test("sends explicit shutdown_server_request via shutdownServer", async () => { const logger = createMockLogger(); const mock = createMockTransport(); diff --git a/packages/server/src/client/daemon-client.ts b/packages/server/src/client/daemon-client.ts index ff95d9cb6..08a4723d8 100644 --- a/packages/server/src/client/daemon-client.ts +++ b/packages/server/src/client/daemon-client.ts @@ -16,6 +16,7 @@ import type { AgentSnapshotPayload, ProjectPlacementPayload, AgentPermissionResolvedMessage, + AgentAttachment, CreateAgentRequestMessage, FileDownloadTokenResponse, FileExplorerResponse, @@ -63,6 +64,7 @@ import type { TerminalInput, SessionInboundMessage, SessionOutboundMessage, + SendAgentMessageRequest, EditorTargetId, } from "../shared/messages.js"; import type { @@ -198,6 +200,7 @@ export type DaemonClientConfig = { export type SendMessageOptions = { messageId?: string; images?: Array<{ data: string; mimeType: string }>; + attachments?: SendAgentMessageRequest["attachments"]; }; type AgentConfigOverrides = Partial>; @@ -211,6 +214,7 @@ export type CreateAgentRequestOptions = { clientMessageId?: string; outputSchema?: Record; images?: CreateAgentRequestMessage["images"]; + attachments?: CreateAgentRequestMessage["attachments"]; git?: GitSetupOptions; worktreeName?: string; requestId?: string; @@ -1494,6 +1498,9 @@ export class DaemonClient { ...(options.clientMessageId ? { clientMessageId: options.clientMessageId } : {}), ...(options.outputSchema ? { outputSchema: options.outputSchema } : {}), ...(options.images && options.images.length > 0 ? { images: options.images } : {}), + ...(options.attachments && options.attachments.length > 0 + ? { attachments: options.attachments } + : {}), ...(options.git ? { git: options.git } : {}), ...(options.worktreeName ? { worktreeName: options.worktreeName } : {}), ...(options.labels && Object.keys(options.labels).length > 0 @@ -1723,6 +1730,7 @@ export class DaemonClient { text, ...(messageId ? { messageId } : {}), ...(options?.images ? { images: options.images } : {}), + ...(options?.attachments ? { attachments: options.attachments } : {}), }); const payload = await this.sendRequest({ requestId, @@ -2544,7 +2552,11 @@ export class DaemonClient { } async createPaseoWorktree( - input: { cwd: string; worktreeSlug?: string }, + input: { + cwd: string; + worktreeSlug?: string; + attachments?: AgentAttachment[]; + }, requestId?: string, ): Promise { return this.sendCorrelatedSessionRequest({ @@ -2553,6 +2565,9 @@ export class DaemonClient { type: "create_paseo_worktree_request", cwd: input.cwd, worktreeSlug: input.worktreeSlug, + ...(input.attachments && input.attachments.length > 0 + ? { attachments: input.attachments } + : {}), }, responseType: "create_paseo_worktree_response", timeout: 60000, diff --git a/packages/server/src/server/agent/agent-sdk-types.ts b/packages/server/src/server/agent/agent-sdk-types.ts index 139920d47..b4bd27730 100644 --- a/packages/server/src/server/agent/agent-sdk-types.ts +++ b/packages/server/src/server/agent/agent-sdk-types.ts @@ -1,4 +1,5 @@ import type { Options as ClaudeAgentOptions } from "@anthropic-ai/claude-agent-sdk"; +import type { AgentAttachment } from "../../shared/messages.js"; export type AgentProvider = string; @@ -117,7 +118,8 @@ export type AgentPersistenceHandle = { export type AgentPromptContentBlock = | { type: "text"; text: string } - | { type: "image"; data: string; mimeType: string }; + | { type: "image"; data: string; mimeType: string } + | AgentAttachment; export type AgentPromptInput = string | AgentPromptContentBlock[]; diff --git a/packages/server/src/server/agent/prompt-attachments.test.ts b/packages/server/src/server/agent/prompt-attachments.test.ts new file mode 100644 index 000000000..a76c60392 --- /dev/null +++ b/packages/server/src/server/agent/prompt-attachments.test.ts @@ -0,0 +1,47 @@ +import { describe, expect, it } from "vitest"; + +import { findGitHubPrAttachment, renderPromptAttachmentAsText } from "./prompt-attachments.js"; + +describe("prompt attachments", () => { + it("renders github_pr attachments as readable text", () => { + expect( + renderPromptAttachmentAsText({ + type: "github_pr", + mimeType: "application/github-pr", + number: 123, + title: "Fix race in worktree setup", + url: "https://github.com/getpaseo/paseo/pull/123", + body: "PR body", + baseRefName: "main", + headRefName: "fix/worktree-race", + }), + ).toContain("GitHub PR #123: Fix race in worktree setup"); + }); + + it("finds the first github_pr attachment", () => { + 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", + }); + }); +}); diff --git a/packages/server/src/server/agent/prompt-attachments.ts b/packages/server/src/server/agent/prompt-attachments.ts new file mode 100644 index 000000000..07417ae08 --- /dev/null +++ b/packages/server/src/server/agent/prompt-attachments.ts @@ -0,0 +1,40 @@ +import type { AgentAttachment } from "../../shared/messages.js"; + +export function renderPromptAttachmentAsText(attachment: AgentAttachment): string { + switch (attachment.type) { + case "github_pr": { + const lines = [`GitHub PR #${attachment.number}: ${attachment.title}`, attachment.url]; + if (attachment.baseRefName) { + lines.push(`Base: ${attachment.baseRefName}`); + } + if (attachment.headRefName) { + lines.push(`Head: ${attachment.headRefName}`); + } + if (attachment.body) { + lines.push("", attachment.body); + } + return lines.join("\n"); + } + case "github_issue": { + const lines = [`GitHub Issue #${attachment.number}: ${attachment.title}`, attachment.url]; + if (attachment.body) { + lines.push("", attachment.body); + } + return lines.join("\n"); + } + } +} + +export function findGitHubPrAttachment( + attachments: readonly AgentAttachment[] | undefined, +): Extract | null { + if (!attachments) { + return null; + } + return ( + attachments.find( + (attachment): attachment is Extract => + attachment.type === "github_pr", + ) ?? null + ); +} diff --git a/packages/server/src/server/agent/providers/acp-agent.ts b/packages/server/src/server/agent/providers/acp-agent.ts index 83802b2b3..43fc4e370 100644 --- a/packages/server/src/server/agent/providers/acp-agent.ts +++ b/packages/server/src/server/agent/providers/acp-agent.ts @@ -87,6 +87,7 @@ import { resolveProviderCommandPrefix, type ProviderRuntimeSettings, } from "../provider-launch-config.js"; +import { renderPromptAttachmentAsText } from "../prompt-attachments.js"; import { findExecutable } from "../../../utils/executable.js"; import { spawnProcess } from "../../../utils/spawn.js"; @@ -1739,6 +1740,9 @@ function toACPContentBlocks(prompt: AgentPromptInput): ContentBlock[] { if (block.type === "text") { return { type: "text", text: block.text }; } + if (block.type === "github_pr" || block.type === "github_issue") { + return { type: "text", text: renderPromptAttachmentAsText(block) }; + } return { type: "image", data: block.data, diff --git a/packages/server/src/server/agent/providers/claude-agent.ts b/packages/server/src/server/agent/providers/claude-agent.ts index 7cf590d96..06b2efdfa 100644 --- a/packages/server/src/server/agent/providers/claude-agent.ts +++ b/packages/server/src/server/agent/providers/claude-agent.ts @@ -47,6 +47,7 @@ import { formatProviderDiagnosticError, toDiagnosticErrorMessage, } from "./diagnostic-utils.js"; +import { renderPromptAttachmentAsText } from "../prompt-attachments.js"; import type { AgentPermissionAction, @@ -2186,6 +2187,8 @@ class ClaudeAgentSession implements AgentSession { data: chunk.data, }, }); + } else if (chunk.type === "github_pr" || chunk.type === "github_issue") { + content.push({ type: "text", text: renderPromptAttachmentAsText(chunk) }); } } } else { diff --git a/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts b/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts index 66a72c7b8..f175d177a 100644 --- a/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts +++ b/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts @@ -235,6 +235,31 @@ describe("Codex app-server provider", () => { } }); + test("maps github_pr prompt attachments to Codex text input", async () => { + const input = await codexAppServerTurnInputFromPrompt( + [ + { + type: "github_pr", + mimeType: "application/github-pr", + number: 123, + title: "Fix race in worktree setup", + url: "https://github.com/getpaseo/paseo/pull/123", + body: "Review body", + baseRefName: "main", + headRefName: "fix/worktree-race", + }, + ], + logger, + ); + + expect(input).toEqual([ + { + type: "text", + text: expect.stringContaining("GitHub PR #123: Fix race in worktree setup"), + }, + ]); + }); + test("maps patch notifications with array-style changes and alias diff keys", () => { const item = __codexAppServerInternals.mapCodexPatchNotificationToToolCall({ callId: "patch-array-alias", diff --git a/packages/server/src/server/agent/providers/codex-app-server-agent.ts b/packages/server/src/server/agent/providers/codex-app-server-agent.ts index 2e3072e60..2fc50ca03 100644 --- a/packages/server/src/server/agent/providers/codex-app-server-agent.ts +++ b/packages/server/src/server/agent/providers/codex-app-server-agent.ts @@ -38,6 +38,7 @@ import path from "node:path"; import readline from "node:readline"; import { z } from "zod"; import { loadCodexPersistedTimeline } from "./codex-rollout-timeline.js"; +import { renderPromptAttachmentAsText } from "../prompt-attachments.js"; import { mapCodexRolloutToolCall, mapCodexToolCallFromThreadItem, @@ -2356,6 +2357,15 @@ export async function codexAppServerTurnInputFromPrompt( } continue; } + if (record.type === "github_pr" || record.type === "github_issue") { + output.push({ + type: "text", + text: renderPromptAttachmentAsText( + record as Extract, + ), + }); + continue; + } output.push(block); } return output; diff --git a/packages/server/src/server/agent/providers/opencode-agent.test.ts b/packages/server/src/server/agent/providers/opencode-agent.test.ts index d31648607..8a39152dc 100644 --- a/packages/server/src/server/agent/providers/opencode-agent.test.ts +++ b/packages/server/src/server/agent/providers/opencode-agent.test.ts @@ -475,4 +475,24 @@ describe("OpenCode adapter context-window normalization", () => { expect(onAssistantModelContextWindowResolved).toHaveBeenCalledWith(400_000); }); + + test("renders github issue attachments as text prompt parts", () => { + const parts = __openCodeInternals.buildOpenCodePromptParts([ + { + type: "github_issue", + mimeType: "application/github-issue", + number: 55, + title: "Improve startup error details", + url: "https://github.com/getpaseo/paseo/issues/55", + body: "Issue body", + }, + ]); + + expect(parts).toEqual([ + { + type: "text", + text: expect.stringContaining("GitHub Issue #55: Improve startup error details"), + }, + ]); + }); }); diff --git a/packages/server/src/server/agent/providers/opencode-agent.ts b/packages/server/src/server/agent/providers/opencode-agent.ts index 4cae22707..1c0194981 100644 --- a/packages/server/src/server/agent/providers/opencode-agent.ts +++ b/packages/server/src/server/agent/providers/opencode-agent.ts @@ -54,6 +54,7 @@ import { resolveBinaryVersion, toDiagnosticErrorMessage, } from "./diagnostic-utils.js"; +import { renderPromptAttachmentAsText } from "../prompt-attachments.js"; const OPENCODE_CAPABILITIES: AgentCapabilityFlags = { supportsStreaming: true, @@ -540,6 +541,10 @@ function buildOpenCodePromptParts( output.push({ type: "text", text: part.text }); continue; } + if (part.type === "github_pr" || part.type === "github_issue") { + output.push({ type: "text", text: renderPromptAttachmentAsText(part) }); + continue; + } attachmentOrdinal += 1; const normalized = toOpenCodeDataUrl(part.mimeType, part.data); output.push({ diff --git a/packages/server/src/server/session.ts b/packages/server/src/server/session.ts index 73cf0d0d8..7e2a95ae4 100644 --- a/packages/server/src/server/session.ts +++ b/packages/server/src/server/session.ts @@ -11,6 +11,7 @@ import { isLegacyEditorTargetId, serializeAgentStreamEvent, type AgentSnapshotPayload, + type AgentAttachment, type SessionInboundMessage, type SessionOutboundMessage, type FileExplorerRequest, @@ -836,18 +837,24 @@ export class Session { private buildAgentPrompt( text: string, images?: Array<{ data: string; mimeType: string }>, + attachments?: AgentAttachment[], ): AgentPromptInput { const normalized = text?.trim() ?? ""; - if (!images || images.length === 0) { + const hasImages = Boolean(images && images.length > 0); + const hasAttachments = Boolean(attachments && attachments.length > 0); + if (!hasImages && !hasAttachments) { return normalized; } const blocks: AgentPromptContentBlock[] = []; if (normalized.length > 0) { blocks.push({ type: "text", text: normalized }); } - for (const image of images) { + for (const image of images ?? []) { blocks.push({ type: "image", data: image.data, mimeType: image.mimeType }); } + for (const attachment of attachments ?? []) { + blocks.push(attachment); + } return blocks; } @@ -2763,12 +2770,24 @@ export class Session { text: string, messageId?: string, images?: Array<{ data: string; mimeType: string }>, + attachments?: AgentAttachment[], runOptions?: AgentRunOptions, options?: { spokenInput?: boolean }, ): Promise<{ ok: true } | { ok: false; error: string }> { this.sessionLogger.info( - { agentId, textPreview: text.substring(0, 50), imageCount: images?.length ?? 0 }, - `Sending text to agent ${agentId}${images && images.length > 0 ? ` with ${images.length} image attachment(s)` : ""}`, + { + agentId, + textPreview: text.substring(0, 50), + imageCount: images?.length ?? 0, + attachmentCount: attachments?.length ?? 0, + }, + `Sending text to agent ${agentId}${ + images && images.length > 0 ? ` with ${images.length} image attachment(s)` : "" + }${ + attachments && attachments.length > 0 + ? ` and ${attachments.length} structured attachment(s)` + : "" + }`, ); await this.unarchiveAgentState(agentId); @@ -2796,7 +2815,7 @@ export class Session { } const promptText = options?.spokenInput ? wrapSpokenInput(text) : text; - const prompt = this.buildAgentPrompt(promptText, images); + const prompt = this.buildAgentPrompt(promptText, images, attachments); return this.startAgentStream(agentId, prompt, runOptions); } @@ -2816,6 +2835,7 @@ export class Session { outputSchema, git, images, + attachments, labels, } = msg; this.sessionLogger.info( @@ -2840,7 +2860,7 @@ export class Session { resolvedConfig, git, worktreeName, - labels, + attachments, ); const resolvedWorkspace = msg.workspaceId @@ -2864,7 +2884,7 @@ export class Session { ); await this.forwardAgentUpdate(snapshot); - if (trimmedPrompt) { + if (trimmedPrompt || (images?.length ?? 0) > 0 || (attachments?.length ?? 0) > 0) { scheduleAgentMetadataGeneration({ agentManager: this.agentManager, agentId: snapshot.id, @@ -2877,9 +2897,10 @@ export class Session { const started = await this.handleSendAgentMessage( snapshot.id, - trimmedPrompt, + trimmedPrompt ?? "", resolveClientMessageId(clientMessageId), images, + attachments, outputSchema ? { outputSchema } : undefined, ); if (!started.ok) { @@ -2903,7 +2924,7 @@ export class Session { }); } - if (trimmedPrompt) { + if (trimmedPrompt || (images?.length ?? 0) > 0 || (attachments?.length ?? 0) > 0) { scheduleAgentMetadataGeneration({ agentManager: this.agentManager, agentId: snapshot.id, @@ -2916,9 +2937,10 @@ export class Session { void this.handleSendAgentMessage( snapshot.id, - trimmedPrompt, + trimmedPrompt ?? "", resolveClientMessageId(clientMessageId), images, + attachments, outputSchema ? { outputSchema } : undefined, ).catch((promptError) => { this.sessionLogger.error( @@ -3100,7 +3122,7 @@ export class Session { config: AgentSessionConfig, gitOptions?: GitSetupOptions, legacyWorktreeName?: string, - _labels?: Record, + attachments?: AgentAttachment[], ): Promise<{ sessionConfig: AgentSessionConfig; worktreeBootstrap?: { worktree: WorktreeConfig; shouldBootstrap: boolean }; @@ -3115,7 +3137,7 @@ export class Session { config, gitOptions, legacyWorktreeName, - _labels, + attachments, ); } @@ -6824,7 +6846,7 @@ export class Session { ); } - const prompt = this.buildAgentPrompt(msg.text, msg.images); + const prompt = this.buildAgentPrompt(msg.text, msg.images, msg.attachments); this.sessionLogger.trace( { agentId, messageId: msg.messageId }, "send_agent_message_request: starting agent stream", @@ -7387,6 +7409,7 @@ export class Session { undefined, undefined, undefined, + undefined, { spokenInput: true }, ); await this.flushPendingAudioSegments("transcription complete"); diff --git a/packages/server/src/server/worktree-session.test.ts b/packages/server/src/server/worktree-session.test.ts index 0f8cf7ba0..8ad5e31a2 100644 --- a/packages/server/src/server/worktree-session.test.ts +++ b/packages/server/src/server/worktree-session.test.ts @@ -8,6 +8,7 @@ import type { SessionOutboundMessage } from "./messages.js"; import { ScriptRouteStore } from "./script-proxy.js"; import * as worktreeBootstrap from "./worktree-bootstrap.js"; import { + buildAgentSessionConfig, createPaseoWorktreeInBackground, handleCreatePaseoWorktreeRequest, handleWorkspaceSetupStatusRequest, @@ -93,6 +94,35 @@ function createGitRepo(options?: { paseoConfig?: Record }) { return { tempDir, repoDir }; } +function createGitHubPrRemoteRepo() { + const { tempDir, repoDir } = createGitRepo(); + const featureBranch = "feature/review-pr"; + execSync(`git checkout -b ${JSON.stringify(featureBranch)}`, { cwd: repoDir, stdio: "pipe" }); + writeFileSync(path.join(repoDir, "README.md"), "review branch\n"); + execSync("git add README.md", { cwd: repoDir, stdio: "pipe" }); + execSync("git -c commit.gpgsign=false commit -m 'review branch'", { + cwd: repoDir, + stdio: "pipe", + }); + const featureSha = execSync("git rev-parse HEAD", { cwd: repoDir, stdio: "pipe" }) + .toString() + .trim(); + execSync("git checkout main", { cwd: repoDir, stdio: "pipe" }); + execSync(`git branch -D ${JSON.stringify(featureBranch)}`, { cwd: repoDir, stdio: "pipe" }); + + const remoteDir = path.join(tempDir, "remote.git"); + execSync(`git clone --bare ${JSON.stringify(repoDir)} ${JSON.stringify(remoteDir)}`, { + stdio: "pipe", + }); + execSync(`git --git-dir=${JSON.stringify(remoteDir)} update-ref refs/pull/123/head ${featureSha}`, { + stdio: "pipe", + }); + execSync(`git remote add origin ${JSON.stringify(remoteDir)}`, { cwd: repoDir, stdio: "pipe" }); + execSync("git fetch origin", { cwd: repoDir, stdio: "pipe" }); + + return { tempDir, repoDir }; +} + describe("createPaseoWorktreeInBackground", () => { const cleanupPaths: string[] = []; @@ -687,6 +717,148 @@ describe("createPaseoWorktreeInBackground", () => { }); +describe("handleCreatePaseoWorktreeRequest", () => { + const cleanupPaths: string[] = []; + + afterEach(() => { + for (const target of cleanupPaths.splice(0)) { + rmSync(target, { recursive: true, force: true }); + } + }); + + test("checks out the GitHub PR branch when a github_pr attachment is present", async () => { + const { tempDir, repoDir } = createGitHubPrRemoteRepo(); + cleanupPaths.push(tempDir); + + const emitted: SessionOutboundMessage[] = []; + const logger = createLogger(); + const paseoHome = path.join(tempDir, ".paseo"); + + await handleCreatePaseoWorktreeRequest( + { + paseoHome, + describeWorkspaceRecord: async (workspace) => + ({ + id: String(workspace.id), + projectId: String(workspace.projectId), + projectDisplayName: "repo", + projectRootPath: repoDir, + workspaceDirectory: workspace.directory, + workspaceKind: "worktree", + projectKind: "git", + name: workspace.displayName, + status: "done", + activityAt: null, + diffStat: null, + scripts: [], + }) as any, + emit: (message) => emitted.push(message), + registerPendingWorktreeWorkspace: async ({ worktreePath, branchName }) => + ({ + id: 1, + projectId: 1, + directory: worktreePath, + displayName: branchName, + kind: "worktree", + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + archivedAt: null, + }) as any, + sessionLogger: logger, + createPaseoWorktreeInBackground: async () => {}, + }, + { + type: "create_paseo_worktree_request", + 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", + }, + ], + }, + ); + + const response = emitted.find( + (message): message is Extract => + message.type === "create_paseo_worktree_response", + ); + + expect(response?.payload.error).toBeNull(); + expect(response?.payload.workspace?.workspaceDirectory).toBeTruthy(); + + const worktreePath = response?.payload.workspace?.workspaceDirectory; + expect(worktreePath).toBeTruthy(); + if (!worktreePath) { + return; + } + + const branch = execSync("git branch --show-current", { cwd: worktreePath, stdio: "pipe" }) + .toString() + .trim(); + expect(branch).toBe("feature/review-pr"); + + const readme = readFileSync(path.join(worktreePath, "README.md"), "utf8"); + expect(readme).toContain("review branch"); + }); + + test("buildAgentSessionConfig checks out the GitHub PR branch for agent worktrees", async () => { + const { tempDir, repoDir } = createGitHubPrRemoteRepo(); + cleanupPaths.push(tempDir); + + const result = await buildAgentSessionConfig( + { + paseoHome: path.join(tempDir, ".paseo"), + sessionLogger: createLogger(), + checkoutExistingBranch: async () => { + throw new Error("should not checkout existing branch"); + }, + createBranchFromBase: async () => { + throw new Error("should not create a new branch from base"); + }, + }, + { + provider: "codex", + cwd: repoDir, + }, + { + createWorktree: true, + worktreeSlug: "agent-review-pr-123", + }, + 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.worktreeBootstrap?.worktree.branchName).toBe("feature/review-pr"); + expect(result.worktreeBootstrap?.worktree.worktreePath).toContain("agent-review-pr-123"); + + const branch = execSync("git branch --show-current", { + cwd: result.sessionConfig.cwd, + stdio: "pipe", + }) + .toString() + .trim(); + expect(branch).toBe("feature/review-pr"); + }); +}); + describe("handleCreatePaseoWorktreeRequest", () => { test("invokes worktree creation once for a create request", async () => { const { tempDir, repoDir } = createGitRepo(); diff --git a/packages/server/src/server/worktree-session.ts b/packages/server/src/server/worktree-session.ts index 6510f83e6..025b9628e 100644 --- a/packages/server/src/server/worktree-session.ts +++ b/packages/server/src/server/worktree-session.ts @@ -1,4 +1,4 @@ -import { exec } from "node:child_process"; +import { exec, execFile } from "node:child_process"; import { promisify } from "node:util"; import type { Logger } from "pino"; import { v4 as uuidv4 } from "uuid"; @@ -7,6 +7,7 @@ import type { AgentSessionConfig } from "./agent/agent-sdk-types.js"; import type { AgentManager } from "./agent/agent-manager.js"; import type { AgentStorage } from "./agent/agent-storage.js"; import { + type AgentAttachment, type GitSetupOptions, type ProjectPlacementPayload, type SessionInboundMessage, @@ -14,6 +15,7 @@ import { type WorkspaceSetupSnapshot, type WorkspaceDescriptorPayload, } from "./messages.js"; +import { findGitHubPrAttachment } from "./agent/prompt-attachments.js"; import type { PersistedWorkspaceRecord, ProjectRegistry, @@ -48,9 +50,11 @@ import { type WorktreeSetupCommandResult, WorktreeSetupError, } from "../utils/worktree.js"; +import { writePaseoWorktreeMetadata } from "../utils/worktree-metadata.js"; import { READ_ONLY_GIT_ENV, toCheckoutError } from "./checkout-git-utils.js"; const execAsync = promisify(exec); +const execFileAsync = promisify(execFile); const SAFE_GIT_REF_PATTERN = /^[A-Za-z0-9._\/-]+$/; export type NormalizedGitOptions = { @@ -149,7 +153,7 @@ export async function buildAgentSessionConfig( config: AgentSessionConfig, gitOptions?: GitSetupOptions, legacyWorktreeName?: string, - _labels?: Record, + attachments?: AgentAttachment[], ): Promise<{ sessionConfig: AgentSessionConfig; worktreeBootstrap?: { worktree: WorktreeConfig; shouldBootstrap: boolean }; @@ -168,6 +172,35 @@ export async function buildAgentSessionConfig( } if (normalized.createWorktree) { + const githubPrAttachment = findGitHubPrAttachment(attachments); + if (githubPrAttachment) { + const baseBranch = + githubPrAttachment.baseRefName?.trim() || + normalized.baseBranch || + (await resolveGitCreateBaseBranch(cwd, dependencies.paseoHome)); + const worktreeSlug = normalized.worktreeSlug ?? slugify(resolveGitHubPrBranchName(githubPrAttachment)); + const createdWorktree = await createGitHubPrWorktree({ + cwd, + branchName: resolveGitHubPrBranchName(githubPrAttachment), + baseBranch, + prNumber: githubPrAttachment.number, + worktreeSlug, + paseoHome: dependencies.paseoHome, + }); + cwd = createdWorktree.worktreePath; + worktreeBootstrap = { + worktree: createdWorktree, + shouldBootstrap: true, + }; + return { + sessionConfig: { + ...config, + cwd, + }, + worktreeBootstrap, + }; + } + let targetBranch: string; if (normalized.createNewBranch) { @@ -304,6 +337,56 @@ export async function resolveGitCreateBaseBranch( return baseBranch; } +function resolveGitHubPrBranchName( + attachment: Extract, +): string { + const trimmed = attachment.headRefName?.trim(); + return trimmed && trimmed.length > 0 ? trimmed : `pr-${attachment.number}`; +} + +async function fetchGitHubPrBranch(params: { + cwd: string; + branchName: string; + prNumber: number; +}): Promise { + await execFileAsync( + "git", + [ + "fetch", + "origin", + `refs/pull/${params.prNumber}/head:refs/heads/${params.branchName}`, + "--force", + ], + { cwd: params.cwd }, + ); +} + +async function createGitHubPrWorktree(params: { + cwd: string; + branchName: string; + baseBranch: string; + prNumber: number; + worktreeSlug: string; + paseoHome?: string; +}): Promise { + assertSafeGitRef(params.branchName, "github pr branch"); + await computeWorktreePath(params.cwd, params.worktreeSlug, params.paseoHome); + await fetchGitHubPrBranch({ + cwd: params.cwd, + branchName: params.branchName, + prNumber: params.prNumber, + }); + const worktreePath = await computeWorktreePath(params.cwd, params.worktreeSlug, params.paseoHome); + await execFileAsync("git", ["worktree", "add", worktreePath, params.branchName], { + cwd: params.cwd, + }); + writePaseoWorktreeMetadata(worktreePath, { baseRefName: params.baseBranch }); + return { + branchName: params.branchName, + worktreePath, + }; +} + export async function handlePaseoWorktreeListRequest( dependencies: { emit: EmitSessionMessage; paseoHome?: string }, msg: Extract, @@ -588,14 +671,26 @@ export async function handleCreatePaseoWorktreeRequest( throw new Error(`Invalid worktree name: ${validation.error}`); } - await computeWorktreePath(repoRoot, normalizedSlug, dependencies.paseoHome); - const createdWorktree = await createAgentWorktree({ - cwd: repoRoot, - branchName: normalizedSlug, - baseBranch, - worktreeSlug: normalizedSlug, - paseoHome: dependencies.paseoHome, - }); + const githubPrAttachment = findGitHubPrAttachment(request.attachments); + const createdWorktree = githubPrAttachment + ? { + worktree: await createGitHubPrWorktree({ + cwd: repoRoot, + branchName: resolveGitHubPrBranchName(githubPrAttachment), + baseBranch: githubPrAttachment.baseRefName?.trim() || baseBranch, + prNumber: githubPrAttachment.number, + worktreeSlug: normalizedSlug, + paseoHome: dependencies.paseoHome, + }), + shouldBootstrap: true, + } + : await createAgentWorktree({ + cwd: repoRoot, + branchName: normalizedSlug, + baseBranch, + worktreeSlug: normalizedSlug, + paseoHome: dependencies.paseoHome, + }); const workspace = await dependencies.registerPendingWorktreeWorkspace({ repoRoot, worktreePath: createdWorktree.worktree.worktreePath, diff --git a/packages/server/src/shared/messages.attachments.test.ts b/packages/server/src/shared/messages.attachments.test.ts new file mode 100644 index 000000000..693a98815 --- /dev/null +++ b/packages/server/src/shared/messages.attachments.test.ts @@ -0,0 +1,118 @@ +import { describe, expect, it } from "vitest"; + +import { + CreateAgentRequestMessageSchema, + CreatePaseoWorktreeRequestSchema, + SendAgentMessageRequestSchema, +} from "./messages.js"; + +describe("shared messages attachments", () => { + it("keeps known attachments and drops unknown create-agent attachments", () => { + const parsed = CreateAgentRequestMessageSchema.parse({ + type: "create_agent_request", + requestId: "req-1", + config: { + provider: "codex", + cwd: "/tmp/repo", + }, + initialPrompt: "Review this PR", + attachments: [ + { + type: "github_pr", + mimeType: "application/github-pr", + number: 123, + title: "Fix race in worktree setup", + url: "https://github.com/getpaseo/paseo/pull/123", + body: "Body", + baseRefName: "main", + headRefName: "fix/worktree-race", + }, + { + type: "future_attachment", + mimeType: "application/future", + foo: "bar", + }, + ], + }); + + expect(parsed.attachments).toEqual([ + { + type: "github_pr", + mimeType: "application/github-pr", + number: 123, + title: "Fix race in worktree setup", + url: "https://github.com/getpaseo/paseo/pull/123", + body: "Body", + baseRefName: "main", + headRefName: "fix/worktree-race", + }, + ]); + }); + + it("keeps known attachments and drops unknown send-message attachments", () => { + const parsed = SendAgentMessageRequestSchema.parse({ + type: "send_agent_message_request", + requestId: "req-2", + agentId: "agent-1", + text: "Review", + attachments: [ + { + type: "github_issue", + mimeType: "application/github-issue", + number: 55, + title: "Improve startup error details", + url: "https://github.com/getpaseo/paseo/issues/55", + body: "Body", + }, + { + type: "future_attachment", + mimeType: "application/future", + foo: "bar", + }, + ], + }); + + expect(parsed.attachments).toEqual([ + { + type: "github_issue", + mimeType: "application/github-issue", + number: 55, + title: "Improve startup error details", + url: "https://github.com/getpaseo/paseo/issues/55", + body: "Body", + }, + ]); + }); + + it("keeps known attachments and drops unknown worktree-create attachments", () => { + 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", + }, + ], + }); + + expect(parsed.attachments).toEqual([ + { + type: "github_pr", + mimeType: "application/github-pr", + number: 99, + title: "Fork-safe PR checkout", + url: "https://github.com/getpaseo/paseo/pull/99", + }, + ]); + }); +}); diff --git a/packages/server/src/shared/messages.ts b/packages/server/src/shared/messages.ts index 4c728d669..539f8ec35 100644 --- a/packages/server/src/shared/messages.ts +++ b/packages/server/src/shared/messages.ts @@ -613,19 +613,59 @@ export const SetVoiceModeMessageSchema = z.object({ requestId: z.string().optional(), }); +export const GitHubPrAttachmentSchema = z.object({ + type: z.literal("github_pr"), + mimeType: z.literal("application/github-pr"), + number: z.number().int().positive(), + title: z.string(), + url: z.string(), + body: z.string().nullable().optional(), + baseRefName: z.string().nullable().optional(), + headRefName: z.string().nullable().optional(), +}); + +export const GitHubIssueAttachmentSchema = z.object({ + type: z.literal("github_issue"), + mimeType: z.literal("application/github-issue"), + number: z.number().int().positive(), + title: z.string(), + url: z.string(), + body: z.string().nullable().optional(), +}); + +export const AgentAttachmentSchema = z.discriminatedUnion("type", [ + GitHubPrAttachmentSchema, + GitHubIssueAttachmentSchema, +]); + +function normalizeAgentAttachments(input: unknown): AgentAttachment[] { + if (!Array.isArray(input)) { + return []; + } + const normalized: AgentAttachment[] = []; + for (const item of input) { + const parsed = AgentAttachmentSchema.safeParse(item); + if (parsed.success) { + normalized.push(parsed.data); + } + } + return normalized; +} + +const AgentAttachmentsSchema = z.unknown().transform(normalizeAgentAttachments).optional(); + +const ImageAttachmentSchema = z.object({ + data: z.string(), // base64 encoded image + mimeType: z.string(), // e.g., "image/jpeg", "image/png" +}); + export const SendAgentMessageSchema = z.object({ type: z.literal("send_agent_message"), agentId: z.string(), text: z.string(), messageId: z.string().optional(), // Client-provided ID for deduplication - images: z - .array( - z.object({ - data: z.string(), // base64 encoded image - mimeType: z.string(), // e.g., "image/jpeg", "image/png" - }), - ) - .optional(), + images: z.array(ImageAttachmentSchema).optional(), + attachments: AgentAttachmentsSchema, }); // ============================================================================ @@ -710,14 +750,8 @@ export const SendAgentMessageRequestSchema = z.object({ agentId: z.string(), text: z.string(), messageId: z.string().optional(), // Client-provided ID for deduplication - images: z - .array( - z.object({ - data: z.string(), // base64 encoded image - mimeType: z.string(), // e.g., "image/jpeg", "image/png" - }), - ) - .optional(), + images: z.array(ImageAttachmentSchema).optional(), + attachments: AgentAttachmentsSchema, }); export const WaitForFinishRequestSchema = z.object({ @@ -775,14 +809,8 @@ export const CreateAgentRequestMessageSchema = z.object({ initialPrompt: z.string().optional(), clientMessageId: z.string().optional(), outputSchema: z.record(z.unknown()).optional(), - images: z - .array( - z.object({ - data: z.string(), // base64 encoded image - mimeType: z.string(), // e.g., "image/jpeg", "image/png" - }), - ) - .optional(), + images: z.array(ImageAttachmentSchema).optional(), + attachments: AgentAttachmentsSchema, git: GitSetupOptionsSchema.optional(), labels: z.record(z.string()).default({}), requestId: z.string(), @@ -1119,6 +1147,8 @@ export const GitHubSearchItemSchema = z.object({ state: z.string(), body: z.string().nullable(), labels: z.array(z.string()), + baseRefName: z.string().nullable().optional(), + headRefName: z.string().nullable().optional(), }); export const GitHubSearchRequestSchema = z.object({ @@ -1158,6 +1188,7 @@ export const CreatePaseoWorktreeRequestSchema = z.object({ type: z.literal("create_paseo_worktree_request"), cwd: z.string(), worktreeSlug: z.string().optional(), + attachments: AgentAttachmentsSchema, requestId: z.string(), }); @@ -3012,6 +3043,7 @@ export type DictationStreamChunkMessage = z.infer; export type DictationStreamCancelMessage = z.infer; export type CreateAgentRequestMessage = z.infer; +export type AgentAttachment = z.infer; export type ListProviderModelsRequestMessage = z.infer< typeof ListProviderModelsRequestMessageSchema >; @@ -3096,6 +3128,7 @@ export type BranchSuggestionsResponse = z.infer; export type GitHubSearchRequest = z.infer; export type GitHubSearchResponse = z.infer; +export type CreatePaseoWorktreeRequest = z.infer; export type DirectorySuggestionsRequest = z.infer; export type DirectorySuggestionsResponse = z.infer; export type PaseoWorktreeListRequest = z.infer; diff --git a/packages/server/src/utils/checkout-git.ts b/packages/server/src/utils/checkout-git.ts index c4a453da9..f77212421 100644 --- a/packages/server/src/utils/checkout-git.ts +++ b/packages/server/src/utils/checkout-git.ts @@ -2387,6 +2387,8 @@ export interface GitHubSearchResult { state: string; body: string | null; labels: string[]; + baseRefName?: string | null; + headRefName?: string | null; }>; githubFeaturesEnabled: boolean; } @@ -2428,7 +2430,7 @@ export async function searchGitHubIssuesAndPrs( "--search", query, "--json", - "number,title,url,state,body,labels", + "number,title,url,state,body,labels,baseRefName,headRefName", "--limit", String(limit), ], @@ -2452,6 +2454,8 @@ export async function searchGitHubIssuesAndPrs( labels: Array.isArray(item.labels) ? item.labels.map((l: { name?: string }) => l.name ?? "").filter(Boolean) : [], + baseRefName: item.baseRefName ?? null, + headRefName: item.headRefName ?? null, }); } }