feat: add prompt attachment support across sessions
This commit is contained in:
@@ -13,6 +13,7 @@ import {
|
|||||||
} from "@/contexts/session-stream-reducers";
|
} from "@/contexts/session-stream-reducers";
|
||||||
import type {
|
import type {
|
||||||
ActivityLogPayload,
|
ActivityLogPayload,
|
||||||
|
AgentAttachment,
|
||||||
AgentStreamEventPayload,
|
AgentStreamEventPayload,
|
||||||
SessionOutboundMessage,
|
SessionOutboundMessage,
|
||||||
} from "@server/shared/messages";
|
} from "@server/shared/messages";
|
||||||
@@ -300,7 +301,12 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider
|
|||||||
|
|
||||||
const previousAgentStatusRef = useRef<Map<string, AgentLifecycleStatus>>(new Map());
|
const previousAgentStatusRef = useRef<Map<string, AgentLifecycleStatus>>(new Map());
|
||||||
const sendAgentMessageRef = useRef<
|
const sendAgentMessageRef = useRef<
|
||||||
((agentId: string, message: string, images?: AttachmentMetadata[]) => Promise<void>) | null
|
((
|
||||||
|
agentId: string,
|
||||||
|
message: string,
|
||||||
|
images?: AttachmentMetadata[],
|
||||||
|
attachments?: AgentAttachment[],
|
||||||
|
) => Promise<void>) | null
|
||||||
>(null);
|
>(null);
|
||||||
const sessionStateTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const sessionStateTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
const attentionNotifiedRef = useRef<Map<string, number>>(new Map());
|
const attentionNotifiedRef = useRef<Map<string, number>>(new Map());
|
||||||
@@ -1584,7 +1590,12 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const sendAgentMessage = useCallback(
|
const sendAgentMessage = useCallback(
|
||||||
async (agentId: string, message: string, images?: AttachmentMetadata[]) => {
|
async (
|
||||||
|
agentId: string,
|
||||||
|
message: string,
|
||||||
|
images?: AttachmentMetadata[],
|
||||||
|
attachments?: AgentAttachment[],
|
||||||
|
) => {
|
||||||
const messageId = generateMessageId();
|
const messageId = generateMessageId();
|
||||||
const userMessage: StreamItem = {
|
const userMessage: StreamItem = {
|
||||||
kind: "user_message",
|
kind: "user_message",
|
||||||
@@ -1624,6 +1635,7 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider
|
|||||||
.sendAgentMessage(agentId, message, {
|
.sendAgentMessage(agentId, message, {
|
||||||
messageId,
|
messageId,
|
||||||
...(imagesData && imagesData.length > 0 ? { images: imagesData } : {}),
|
...(imagesData && imagesData.length > 0 ? { images: imagesData } : {}),
|
||||||
|
...(attachments && attachments.length > 0 ? { attachments } : {}),
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error("[Session] Failed to send agent message:", error);
|
console.error("[Session] Failed to send agent message:", error);
|
||||||
@@ -1692,6 +1704,7 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider
|
|||||||
config,
|
config,
|
||||||
initialPrompt,
|
initialPrompt,
|
||||||
images,
|
images,
|
||||||
|
attachments,
|
||||||
git,
|
git,
|
||||||
worktreeName,
|
worktreeName,
|
||||||
requestId,
|
requestId,
|
||||||
@@ -1699,6 +1712,7 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider
|
|||||||
config: any;
|
config: any;
|
||||||
initialPrompt: string;
|
initialPrompt: string;
|
||||||
images?: AttachmentMetadata[];
|
images?: AttachmentMetadata[];
|
||||||
|
attachments?: AgentAttachment[];
|
||||||
git?: any;
|
git?: any;
|
||||||
worktreeName?: string;
|
worktreeName?: string;
|
||||||
requestId?: string;
|
requestId?: string;
|
||||||
@@ -1718,6 +1732,7 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider
|
|||||||
config,
|
config,
|
||||||
...(trimmedPrompt ? { initialPrompt: trimmedPrompt } : {}),
|
...(trimmedPrompt ? { initialPrompt: trimmedPrompt } : {}),
|
||||||
...(imagesData && imagesData.length > 0 ? { images: imagesData } : {}),
|
...(imagesData && imagesData.length > 0 ? { images: imagesData } : {}),
|
||||||
|
...(attachments && attachments.length > 0 ? { attachments } : {}),
|
||||||
...(git ? { git } : {}),
|
...(git ? { git } : {}),
|
||||||
...(worktreeName ? { worktreeName } : {}),
|
...(worktreeName ? { worktreeName } : {}),
|
||||||
...(requestId ? { requestId } : {}),
|
...(requestId ? { requestId } : {}),
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import { normalizeAgentSnapshot } from "@/utils/agent-snapshots";
|
|||||||
import { encodeImages } from "@/utils/encode-images";
|
import { encodeImages } from "@/utils/encode-images";
|
||||||
import { toErrorMessage } from "@/utils/error-messages";
|
import { toErrorMessage } from "@/utils/error-messages";
|
||||||
import { openExternalUrl } from "@/utils/open-external-url";
|
import { openExternalUrl } from "@/utils/open-external-url";
|
||||||
|
import { buildGitHubAttachmentFromSearchItem } from "@/utils/review-attachments";
|
||||||
import {
|
import {
|
||||||
requireWorkspaceExecutionAuthority,
|
requireWorkspaceExecutionAuthority,
|
||||||
} from "@/utils/workspace-execution";
|
} from "@/utils/workspace-execution";
|
||||||
@@ -32,23 +33,6 @@ import { navigateToPreparedWorkspaceTab } from "@/utils/workspace-navigation";
|
|||||||
import type { ImageAttachment, MessagePayload } from "@/components/message-input";
|
import type { ImageAttachment, MessagePayload } from "@/components/message-input";
|
||||||
import type { GitHubSearchItem } from "@server/shared/messages";
|
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 {
|
interface NewWorkspaceScreenProps {
|
||||||
serverId: string;
|
serverId: string;
|
||||||
sourceDirectory: string;
|
sourceDirectory: string;
|
||||||
@@ -176,9 +160,11 @@ export function NewWorkspaceScreen({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const connectedClient = withConnectedClient();
|
const connectedClient = withConnectedClient();
|
||||||
|
const reviewAttachment = buildGitHubAttachmentFromSearchItem(selectedGithubItem);
|
||||||
const payload = await connectedClient.createPaseoWorktree({
|
const payload = await connectedClient.createPaseoWorktree({
|
||||||
cwd: sourceDirectory,
|
cwd: sourceDirectory,
|
||||||
worktreeSlug: createNameId(),
|
worktreeSlug: createNameId(),
|
||||||
|
...(reviewAttachment ? { attachments: [reviewAttachment] } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (payload.error || !payload.workspace) {
|
if (payload.error || !payload.workspace) {
|
||||||
@@ -193,6 +179,7 @@ export function NewWorkspaceScreen({
|
|||||||
createdWorkspace,
|
createdWorkspace,
|
||||||
mergeWorkspaces,
|
mergeWorkspaces,
|
||||||
serverId,
|
serverId,
|
||||||
|
selectedGithubItem,
|
||||||
sourceDirectory,
|
sourceDirectory,
|
||||||
withConnectedClient,
|
withConnectedClient,
|
||||||
]);
|
]);
|
||||||
@@ -208,8 +195,9 @@ export function NewWorkspaceScreen({
|
|||||||
throw new Error("Composer state is required");
|
throw new Error("Composer state is required");
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialPrompt = buildInitialPrompt(text.trim(), selectedGithubItem);
|
const initialPrompt = text.trim();
|
||||||
const encodedImages = await encodeImages(images);
|
const encodedImages = await encodeImages(images);
|
||||||
|
const reviewAttachment = buildGitHubAttachmentFromSearchItem(selectedGithubItem);
|
||||||
const workspaceDirectory = requireWorkspaceExecutionAuthority({ workspace }).workspaceDirectory;
|
const workspaceDirectory = requireWorkspaceExecutionAuthority({ workspace }).workspaceDirectory;
|
||||||
const agent = await connectedClient.createAgent({
|
const agent = await connectedClient.createAgent({
|
||||||
provider: composerState.selectedProvider,
|
provider: composerState.selectedProvider,
|
||||||
@@ -224,6 +212,7 @@ export function NewWorkspaceScreen({
|
|||||||
: {}),
|
: {}),
|
||||||
...(initialPrompt ? { initialPrompt } : {}),
|
...(initialPrompt ? { initialPrompt } : {}),
|
||||||
...(encodedImages && encodedImages.length > 0 ? { images: encodedImages } : {}),
|
...(encodedImages && encodedImages.length > 0 ? { images: encodedImages } : {}),
|
||||||
|
...(reviewAttachment ? { attachments: [reviewAttachment] } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
setAgents(serverId, (previous) => {
|
setAgents(serverId, (previous) => {
|
||||||
|
|||||||
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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 } : {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -344,6 +344,145 @@ describe("DaemonClient", () => {
|
|||||||
await expect(createPromise).rejects.toThrow("compat test sentinel");
|
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 () => {
|
test("sends explicit shutdown_server_request via shutdownServer", async () => {
|
||||||
const logger = createMockLogger();
|
const logger = createMockLogger();
|
||||||
const mock = createMockTransport();
|
const mock = createMockTransport();
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import type {
|
|||||||
AgentSnapshotPayload,
|
AgentSnapshotPayload,
|
||||||
ProjectPlacementPayload,
|
ProjectPlacementPayload,
|
||||||
AgentPermissionResolvedMessage,
|
AgentPermissionResolvedMessage,
|
||||||
|
AgentAttachment,
|
||||||
CreateAgentRequestMessage,
|
CreateAgentRequestMessage,
|
||||||
FileDownloadTokenResponse,
|
FileDownloadTokenResponse,
|
||||||
FileExplorerResponse,
|
FileExplorerResponse,
|
||||||
@@ -63,6 +64,7 @@ import type {
|
|||||||
TerminalInput,
|
TerminalInput,
|
||||||
SessionInboundMessage,
|
SessionInboundMessage,
|
||||||
SessionOutboundMessage,
|
SessionOutboundMessage,
|
||||||
|
SendAgentMessageRequest,
|
||||||
EditorTargetId,
|
EditorTargetId,
|
||||||
} from "../shared/messages.js";
|
} from "../shared/messages.js";
|
||||||
import type {
|
import type {
|
||||||
@@ -198,6 +200,7 @@ export type DaemonClientConfig = {
|
|||||||
export type SendMessageOptions = {
|
export type SendMessageOptions = {
|
||||||
messageId?: string;
|
messageId?: string;
|
||||||
images?: Array<{ data: string; mimeType: string }>;
|
images?: Array<{ data: string; mimeType: string }>;
|
||||||
|
attachments?: SendAgentMessageRequest["attachments"];
|
||||||
};
|
};
|
||||||
|
|
||||||
type AgentConfigOverrides = Partial<Omit<AgentSessionConfig, "provider" | "cwd">>;
|
type AgentConfigOverrides = Partial<Omit<AgentSessionConfig, "provider" | "cwd">>;
|
||||||
@@ -211,6 +214,7 @@ export type CreateAgentRequestOptions = {
|
|||||||
clientMessageId?: string;
|
clientMessageId?: string;
|
||||||
outputSchema?: Record<string, unknown>;
|
outputSchema?: Record<string, unknown>;
|
||||||
images?: CreateAgentRequestMessage["images"];
|
images?: CreateAgentRequestMessage["images"];
|
||||||
|
attachments?: CreateAgentRequestMessage["attachments"];
|
||||||
git?: GitSetupOptions;
|
git?: GitSetupOptions;
|
||||||
worktreeName?: string;
|
worktreeName?: string;
|
||||||
requestId?: string;
|
requestId?: string;
|
||||||
@@ -1494,6 +1498,9 @@ export class DaemonClient {
|
|||||||
...(options.clientMessageId ? { clientMessageId: options.clientMessageId } : {}),
|
...(options.clientMessageId ? { clientMessageId: options.clientMessageId } : {}),
|
||||||
...(options.outputSchema ? { outputSchema: options.outputSchema } : {}),
|
...(options.outputSchema ? { outputSchema: options.outputSchema } : {}),
|
||||||
...(options.images && options.images.length > 0 ? { images: options.images } : {}),
|
...(options.images && options.images.length > 0 ? { images: options.images } : {}),
|
||||||
|
...(options.attachments && options.attachments.length > 0
|
||||||
|
? { attachments: options.attachments }
|
||||||
|
: {}),
|
||||||
...(options.git ? { git: options.git } : {}),
|
...(options.git ? { git: options.git } : {}),
|
||||||
...(options.worktreeName ? { worktreeName: options.worktreeName } : {}),
|
...(options.worktreeName ? { worktreeName: options.worktreeName } : {}),
|
||||||
...(options.labels && Object.keys(options.labels).length > 0
|
...(options.labels && Object.keys(options.labels).length > 0
|
||||||
@@ -1723,6 +1730,7 @@ export class DaemonClient {
|
|||||||
text,
|
text,
|
||||||
...(messageId ? { messageId } : {}),
|
...(messageId ? { messageId } : {}),
|
||||||
...(options?.images ? { images: options.images } : {}),
|
...(options?.images ? { images: options.images } : {}),
|
||||||
|
...(options?.attachments ? { attachments: options.attachments } : {}),
|
||||||
});
|
});
|
||||||
const payload = await this.sendRequest({
|
const payload = await this.sendRequest({
|
||||||
requestId,
|
requestId,
|
||||||
@@ -2544,7 +2552,11 @@ export class DaemonClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async createPaseoWorktree(
|
async createPaseoWorktree(
|
||||||
input: { cwd: string; worktreeSlug?: string },
|
input: {
|
||||||
|
cwd: string;
|
||||||
|
worktreeSlug?: string;
|
||||||
|
attachments?: AgentAttachment[];
|
||||||
|
},
|
||||||
requestId?: string,
|
requestId?: string,
|
||||||
): Promise<CreatePaseoWorktreePayload> {
|
): Promise<CreatePaseoWorktreePayload> {
|
||||||
return this.sendCorrelatedSessionRequest({
|
return this.sendCorrelatedSessionRequest({
|
||||||
@@ -2553,6 +2565,9 @@ export class DaemonClient {
|
|||||||
type: "create_paseo_worktree_request",
|
type: "create_paseo_worktree_request",
|
||||||
cwd: input.cwd,
|
cwd: input.cwd,
|
||||||
worktreeSlug: input.worktreeSlug,
|
worktreeSlug: input.worktreeSlug,
|
||||||
|
...(input.attachments && input.attachments.length > 0
|
||||||
|
? { attachments: input.attachments }
|
||||||
|
: {}),
|
||||||
},
|
},
|
||||||
responseType: "create_paseo_worktree_response",
|
responseType: "create_paseo_worktree_response",
|
||||||
timeout: 60000,
|
timeout: 60000,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { Options as ClaudeAgentOptions } from "@anthropic-ai/claude-agent-sdk";
|
import type { Options as ClaudeAgentOptions } from "@anthropic-ai/claude-agent-sdk";
|
||||||
|
import type { AgentAttachment } from "../../shared/messages.js";
|
||||||
|
|
||||||
export type AgentProvider = string;
|
export type AgentProvider = string;
|
||||||
|
|
||||||
@@ -117,7 +118,8 @@ export type AgentPersistenceHandle = {
|
|||||||
|
|
||||||
export type AgentPromptContentBlock =
|
export type AgentPromptContentBlock =
|
||||||
| { type: "text"; text: string }
|
| { type: "text"; text: string }
|
||||||
| { type: "image"; data: string; mimeType: string };
|
| { type: "image"; data: string; mimeType: string }
|
||||||
|
| AgentAttachment;
|
||||||
|
|
||||||
export type AgentPromptInput = string | AgentPromptContentBlock[];
|
export type AgentPromptInput = string | AgentPromptContentBlock[];
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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<AgentAttachment, { type: "github_pr" }> | null {
|
||||||
|
if (!attachments) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
attachments.find(
|
||||||
|
(attachment): attachment is Extract<AgentAttachment, { type: "github_pr" }> =>
|
||||||
|
attachment.type === "github_pr",
|
||||||
|
) ?? null
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -87,6 +87,7 @@ import {
|
|||||||
resolveProviderCommandPrefix,
|
resolveProviderCommandPrefix,
|
||||||
type ProviderRuntimeSettings,
|
type ProviderRuntimeSettings,
|
||||||
} from "../provider-launch-config.js";
|
} from "../provider-launch-config.js";
|
||||||
|
import { renderPromptAttachmentAsText } from "../prompt-attachments.js";
|
||||||
import { findExecutable } from "../../../utils/executable.js";
|
import { findExecutable } from "../../../utils/executable.js";
|
||||||
import { spawnProcess } from "../../../utils/spawn.js";
|
import { spawnProcess } from "../../../utils/spawn.js";
|
||||||
|
|
||||||
@@ -1739,6 +1740,9 @@ function toACPContentBlocks(prompt: AgentPromptInput): ContentBlock[] {
|
|||||||
if (block.type === "text") {
|
if (block.type === "text") {
|
||||||
return { type: "text", text: block.text };
|
return { type: "text", text: block.text };
|
||||||
}
|
}
|
||||||
|
if (block.type === "github_pr" || block.type === "github_issue") {
|
||||||
|
return { type: "text", text: renderPromptAttachmentAsText(block) };
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
type: "image",
|
type: "image",
|
||||||
data: block.data,
|
data: block.data,
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ import {
|
|||||||
formatProviderDiagnosticError,
|
formatProviderDiagnosticError,
|
||||||
toDiagnosticErrorMessage,
|
toDiagnosticErrorMessage,
|
||||||
} from "./diagnostic-utils.js";
|
} from "./diagnostic-utils.js";
|
||||||
|
import { renderPromptAttachmentAsText } from "../prompt-attachments.js";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
AgentPermissionAction,
|
AgentPermissionAction,
|
||||||
@@ -2186,6 +2187,8 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
data: chunk.data,
|
data: chunk.data,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
} else if (chunk.type === "github_pr" || chunk.type === "github_issue") {
|
||||||
|
content.push({ type: "text", text: renderPromptAttachmentAsText(chunk) });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -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", () => {
|
test("maps patch notifications with array-style changes and alias diff keys", () => {
|
||||||
const item = __codexAppServerInternals.mapCodexPatchNotificationToToolCall({
|
const item = __codexAppServerInternals.mapCodexPatchNotificationToToolCall({
|
||||||
callId: "patch-array-alias",
|
callId: "patch-array-alias",
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import path from "node:path";
|
|||||||
import readline from "node:readline";
|
import readline from "node:readline";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { loadCodexPersistedTimeline } from "./codex-rollout-timeline.js";
|
import { loadCodexPersistedTimeline } from "./codex-rollout-timeline.js";
|
||||||
|
import { renderPromptAttachmentAsText } from "../prompt-attachments.js";
|
||||||
import {
|
import {
|
||||||
mapCodexRolloutToolCall,
|
mapCodexRolloutToolCall,
|
||||||
mapCodexToolCallFromThreadItem,
|
mapCodexToolCallFromThreadItem,
|
||||||
@@ -2356,6 +2357,15 @@ export async function codexAppServerTurnInputFromPrompt(
|
|||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (record.type === "github_pr" || record.type === "github_issue") {
|
||||||
|
output.push({
|
||||||
|
type: "text",
|
||||||
|
text: renderPromptAttachmentAsText(
|
||||||
|
record as Extract<AgentPromptContentBlock, { type: "github_pr" | "github_issue" }>,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
output.push(block);
|
output.push(block);
|
||||||
}
|
}
|
||||||
return output;
|
return output;
|
||||||
|
|||||||
@@ -475,4 +475,24 @@ describe("OpenCode adapter context-window normalization", () => {
|
|||||||
|
|
||||||
expect(onAssistantModelContextWindowResolved).toHaveBeenCalledWith(400_000);
|
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"),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ import {
|
|||||||
resolveBinaryVersion,
|
resolveBinaryVersion,
|
||||||
toDiagnosticErrorMessage,
|
toDiagnosticErrorMessage,
|
||||||
} from "./diagnostic-utils.js";
|
} from "./diagnostic-utils.js";
|
||||||
|
import { renderPromptAttachmentAsText } from "../prompt-attachments.js";
|
||||||
|
|
||||||
const OPENCODE_CAPABILITIES: AgentCapabilityFlags = {
|
const OPENCODE_CAPABILITIES: AgentCapabilityFlags = {
|
||||||
supportsStreaming: true,
|
supportsStreaming: true,
|
||||||
@@ -540,6 +541,10 @@ function buildOpenCodePromptParts(
|
|||||||
output.push({ type: "text", text: part.text });
|
output.push({ type: "text", text: part.text });
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (part.type === "github_pr" || part.type === "github_issue") {
|
||||||
|
output.push({ type: "text", text: renderPromptAttachmentAsText(part) });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
attachmentOrdinal += 1;
|
attachmentOrdinal += 1;
|
||||||
const normalized = toOpenCodeDataUrl(part.mimeType, part.data);
|
const normalized = toOpenCodeDataUrl(part.mimeType, part.data);
|
||||||
output.push({
|
output.push({
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
isLegacyEditorTargetId,
|
isLegacyEditorTargetId,
|
||||||
serializeAgentStreamEvent,
|
serializeAgentStreamEvent,
|
||||||
type AgentSnapshotPayload,
|
type AgentSnapshotPayload,
|
||||||
|
type AgentAttachment,
|
||||||
type SessionInboundMessage,
|
type SessionInboundMessage,
|
||||||
type SessionOutboundMessage,
|
type SessionOutboundMessage,
|
||||||
type FileExplorerRequest,
|
type FileExplorerRequest,
|
||||||
@@ -836,18 +837,24 @@ export class Session {
|
|||||||
private buildAgentPrompt(
|
private buildAgentPrompt(
|
||||||
text: string,
|
text: string,
|
||||||
images?: Array<{ data: string; mimeType: string }>,
|
images?: Array<{ data: string; mimeType: string }>,
|
||||||
|
attachments?: AgentAttachment[],
|
||||||
): AgentPromptInput {
|
): AgentPromptInput {
|
||||||
const normalized = text?.trim() ?? "";
|
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;
|
return normalized;
|
||||||
}
|
}
|
||||||
const blocks: AgentPromptContentBlock[] = [];
|
const blocks: AgentPromptContentBlock[] = [];
|
||||||
if (normalized.length > 0) {
|
if (normalized.length > 0) {
|
||||||
blocks.push({ type: "text", text: normalized });
|
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 });
|
blocks.push({ type: "image", data: image.data, mimeType: image.mimeType });
|
||||||
}
|
}
|
||||||
|
for (const attachment of attachments ?? []) {
|
||||||
|
blocks.push(attachment);
|
||||||
|
}
|
||||||
return blocks;
|
return blocks;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2763,12 +2770,24 @@ export class Session {
|
|||||||
text: string,
|
text: string,
|
||||||
messageId?: string,
|
messageId?: string,
|
||||||
images?: Array<{ data: string; mimeType: string }>,
|
images?: Array<{ data: string; mimeType: string }>,
|
||||||
|
attachments?: AgentAttachment[],
|
||||||
runOptions?: AgentRunOptions,
|
runOptions?: AgentRunOptions,
|
||||||
options?: { spokenInput?: boolean },
|
options?: { spokenInput?: boolean },
|
||||||
): Promise<{ ok: true } | { ok: false; error: string }> {
|
): Promise<{ ok: true } | { ok: false; error: string }> {
|
||||||
this.sessionLogger.info(
|
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);
|
await this.unarchiveAgentState(agentId);
|
||||||
@@ -2796,7 +2815,7 @@ export class Session {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const promptText = options?.spokenInput ? wrapSpokenInput(text) : text;
|
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);
|
return this.startAgentStream(agentId, prompt, runOptions);
|
||||||
}
|
}
|
||||||
@@ -2816,6 +2835,7 @@ export class Session {
|
|||||||
outputSchema,
|
outputSchema,
|
||||||
git,
|
git,
|
||||||
images,
|
images,
|
||||||
|
attachments,
|
||||||
labels,
|
labels,
|
||||||
} = msg;
|
} = msg;
|
||||||
this.sessionLogger.info(
|
this.sessionLogger.info(
|
||||||
@@ -2840,7 +2860,7 @@ export class Session {
|
|||||||
resolvedConfig,
|
resolvedConfig,
|
||||||
git,
|
git,
|
||||||
worktreeName,
|
worktreeName,
|
||||||
labels,
|
attachments,
|
||||||
);
|
);
|
||||||
const resolvedWorkspace =
|
const resolvedWorkspace =
|
||||||
msg.workspaceId
|
msg.workspaceId
|
||||||
@@ -2864,7 +2884,7 @@ export class Session {
|
|||||||
);
|
);
|
||||||
await this.forwardAgentUpdate(snapshot);
|
await this.forwardAgentUpdate(snapshot);
|
||||||
|
|
||||||
if (trimmedPrompt) {
|
if (trimmedPrompt || (images?.length ?? 0) > 0 || (attachments?.length ?? 0) > 0) {
|
||||||
scheduleAgentMetadataGeneration({
|
scheduleAgentMetadataGeneration({
|
||||||
agentManager: this.agentManager,
|
agentManager: this.agentManager,
|
||||||
agentId: snapshot.id,
|
agentId: snapshot.id,
|
||||||
@@ -2877,9 +2897,10 @@ export class Session {
|
|||||||
|
|
||||||
const started = await this.handleSendAgentMessage(
|
const started = await this.handleSendAgentMessage(
|
||||||
snapshot.id,
|
snapshot.id,
|
||||||
trimmedPrompt,
|
trimmedPrompt ?? "",
|
||||||
resolveClientMessageId(clientMessageId),
|
resolveClientMessageId(clientMessageId),
|
||||||
images,
|
images,
|
||||||
|
attachments,
|
||||||
outputSchema ? { outputSchema } : undefined,
|
outputSchema ? { outputSchema } : undefined,
|
||||||
);
|
);
|
||||||
if (!started.ok) {
|
if (!started.ok) {
|
||||||
@@ -2903,7 +2924,7 @@ export class Session {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (trimmedPrompt) {
|
if (trimmedPrompt || (images?.length ?? 0) > 0 || (attachments?.length ?? 0) > 0) {
|
||||||
scheduleAgentMetadataGeneration({
|
scheduleAgentMetadataGeneration({
|
||||||
agentManager: this.agentManager,
|
agentManager: this.agentManager,
|
||||||
agentId: snapshot.id,
|
agentId: snapshot.id,
|
||||||
@@ -2916,9 +2937,10 @@ export class Session {
|
|||||||
|
|
||||||
void this.handleSendAgentMessage(
|
void this.handleSendAgentMessage(
|
||||||
snapshot.id,
|
snapshot.id,
|
||||||
trimmedPrompt,
|
trimmedPrompt ?? "",
|
||||||
resolveClientMessageId(clientMessageId),
|
resolveClientMessageId(clientMessageId),
|
||||||
images,
|
images,
|
||||||
|
attachments,
|
||||||
outputSchema ? { outputSchema } : undefined,
|
outputSchema ? { outputSchema } : undefined,
|
||||||
).catch((promptError) => {
|
).catch((promptError) => {
|
||||||
this.sessionLogger.error(
|
this.sessionLogger.error(
|
||||||
@@ -3100,7 +3122,7 @@ export class Session {
|
|||||||
config: AgentSessionConfig,
|
config: AgentSessionConfig,
|
||||||
gitOptions?: GitSetupOptions,
|
gitOptions?: GitSetupOptions,
|
||||||
legacyWorktreeName?: string,
|
legacyWorktreeName?: string,
|
||||||
_labels?: Record<string, string>,
|
attachments?: AgentAttachment[],
|
||||||
): Promise<{
|
): Promise<{
|
||||||
sessionConfig: AgentSessionConfig;
|
sessionConfig: AgentSessionConfig;
|
||||||
worktreeBootstrap?: { worktree: WorktreeConfig; shouldBootstrap: boolean };
|
worktreeBootstrap?: { worktree: WorktreeConfig; shouldBootstrap: boolean };
|
||||||
@@ -3115,7 +3137,7 @@ export class Session {
|
|||||||
config,
|
config,
|
||||||
gitOptions,
|
gitOptions,
|
||||||
legacyWorktreeName,
|
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(
|
this.sessionLogger.trace(
|
||||||
{ agentId, messageId: msg.messageId },
|
{ agentId, messageId: msg.messageId },
|
||||||
"send_agent_message_request: starting agent stream",
|
"send_agent_message_request: starting agent stream",
|
||||||
@@ -7387,6 +7409,7 @@ export class Session {
|
|||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
|
undefined,
|
||||||
{ spokenInput: true },
|
{ spokenInput: true },
|
||||||
);
|
);
|
||||||
await this.flushPendingAudioSegments("transcription complete");
|
await this.flushPendingAudioSegments("transcription complete");
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import type { SessionOutboundMessage } from "./messages.js";
|
|||||||
import { ScriptRouteStore } from "./script-proxy.js";
|
import { ScriptRouteStore } from "./script-proxy.js";
|
||||||
import * as worktreeBootstrap from "./worktree-bootstrap.js";
|
import * as worktreeBootstrap from "./worktree-bootstrap.js";
|
||||||
import {
|
import {
|
||||||
|
buildAgentSessionConfig,
|
||||||
createPaseoWorktreeInBackground,
|
createPaseoWorktreeInBackground,
|
||||||
handleCreatePaseoWorktreeRequest,
|
handleCreatePaseoWorktreeRequest,
|
||||||
handleWorkspaceSetupStatusRequest,
|
handleWorkspaceSetupStatusRequest,
|
||||||
@@ -93,6 +94,35 @@ function createGitRepo(options?: { paseoConfig?: Record<string, unknown> }) {
|
|||||||
return { tempDir, repoDir };
|
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", () => {
|
describe("createPaseoWorktreeInBackground", () => {
|
||||||
const cleanupPaths: string[] = [];
|
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<SessionOutboundMessage, { type: "create_paseo_worktree_response" }> =>
|
||||||
|
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", () => {
|
describe("handleCreatePaseoWorktreeRequest", () => {
|
||||||
test("invokes worktree creation once for a create request", async () => {
|
test("invokes worktree creation once for a create request", async () => {
|
||||||
const { tempDir, repoDir } = createGitRepo();
|
const { tempDir, repoDir } = createGitRepo();
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { exec } from "node:child_process";
|
import { exec, execFile } from "node:child_process";
|
||||||
import { promisify } from "node:util";
|
import { promisify } from "node:util";
|
||||||
import type { Logger } from "pino";
|
import type { Logger } from "pino";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
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 { AgentManager } from "./agent/agent-manager.js";
|
||||||
import type { AgentStorage } from "./agent/agent-storage.js";
|
import type { AgentStorage } from "./agent/agent-storage.js";
|
||||||
import {
|
import {
|
||||||
|
type AgentAttachment,
|
||||||
type GitSetupOptions,
|
type GitSetupOptions,
|
||||||
type ProjectPlacementPayload,
|
type ProjectPlacementPayload,
|
||||||
type SessionInboundMessage,
|
type SessionInboundMessage,
|
||||||
@@ -14,6 +15,7 @@ import {
|
|||||||
type WorkspaceSetupSnapshot,
|
type WorkspaceSetupSnapshot,
|
||||||
type WorkspaceDescriptorPayload,
|
type WorkspaceDescriptorPayload,
|
||||||
} from "./messages.js";
|
} from "./messages.js";
|
||||||
|
import { findGitHubPrAttachment } from "./agent/prompt-attachments.js";
|
||||||
import type {
|
import type {
|
||||||
PersistedWorkspaceRecord,
|
PersistedWorkspaceRecord,
|
||||||
ProjectRegistry,
|
ProjectRegistry,
|
||||||
@@ -48,9 +50,11 @@ import {
|
|||||||
type WorktreeSetupCommandResult,
|
type WorktreeSetupCommandResult,
|
||||||
WorktreeSetupError,
|
WorktreeSetupError,
|
||||||
} from "../utils/worktree.js";
|
} from "../utils/worktree.js";
|
||||||
|
import { writePaseoWorktreeMetadata } from "../utils/worktree-metadata.js";
|
||||||
import { READ_ONLY_GIT_ENV, toCheckoutError } from "./checkout-git-utils.js";
|
import { READ_ONLY_GIT_ENV, toCheckoutError } from "./checkout-git-utils.js";
|
||||||
|
|
||||||
const execAsync = promisify(exec);
|
const execAsync = promisify(exec);
|
||||||
|
const execFileAsync = promisify(execFile);
|
||||||
const SAFE_GIT_REF_PATTERN = /^[A-Za-z0-9._\/-]+$/;
|
const SAFE_GIT_REF_PATTERN = /^[A-Za-z0-9._\/-]+$/;
|
||||||
|
|
||||||
export type NormalizedGitOptions = {
|
export type NormalizedGitOptions = {
|
||||||
@@ -149,7 +153,7 @@ export async function buildAgentSessionConfig(
|
|||||||
config: AgentSessionConfig,
|
config: AgentSessionConfig,
|
||||||
gitOptions?: GitSetupOptions,
|
gitOptions?: GitSetupOptions,
|
||||||
legacyWorktreeName?: string,
|
legacyWorktreeName?: string,
|
||||||
_labels?: Record<string, string>,
|
attachments?: AgentAttachment[],
|
||||||
): Promise<{
|
): Promise<{
|
||||||
sessionConfig: AgentSessionConfig;
|
sessionConfig: AgentSessionConfig;
|
||||||
worktreeBootstrap?: { worktree: WorktreeConfig; shouldBootstrap: boolean };
|
worktreeBootstrap?: { worktree: WorktreeConfig; shouldBootstrap: boolean };
|
||||||
@@ -168,6 +172,35 @@ export async function buildAgentSessionConfig(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (normalized.createWorktree) {
|
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;
|
let targetBranch: string;
|
||||||
|
|
||||||
if (normalized.createNewBranch) {
|
if (normalized.createNewBranch) {
|
||||||
@@ -304,6 +337,56 @@ export async function resolveGitCreateBaseBranch(
|
|||||||
return baseBranch;
|
return baseBranch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveGitHubPrBranchName(
|
||||||
|
attachment: Extract<AgentAttachment, { type: "github_pr" }>,
|
||||||
|
): 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<void> {
|
||||||
|
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<WorktreeConfig> {
|
||||||
|
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(
|
export async function handlePaseoWorktreeListRequest(
|
||||||
dependencies: { emit: EmitSessionMessage; paseoHome?: string },
|
dependencies: { emit: EmitSessionMessage; paseoHome?: string },
|
||||||
msg: Extract<SessionInboundMessage, { type: "paseo_worktree_list_request" }>,
|
msg: Extract<SessionInboundMessage, { type: "paseo_worktree_list_request" }>,
|
||||||
@@ -588,14 +671,26 @@ export async function handleCreatePaseoWorktreeRequest(
|
|||||||
throw new Error(`Invalid worktree name: ${validation.error}`);
|
throw new Error(`Invalid worktree name: ${validation.error}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
await computeWorktreePath(repoRoot, normalizedSlug, dependencies.paseoHome);
|
const githubPrAttachment = findGitHubPrAttachment(request.attachments);
|
||||||
const createdWorktree = await createAgentWorktree({
|
const createdWorktree = githubPrAttachment
|
||||||
cwd: repoRoot,
|
? {
|
||||||
branchName: normalizedSlug,
|
worktree: await createGitHubPrWorktree({
|
||||||
baseBranch,
|
cwd: repoRoot,
|
||||||
worktreeSlug: normalizedSlug,
|
branchName: resolveGitHubPrBranchName(githubPrAttachment),
|
||||||
paseoHome: dependencies.paseoHome,
|
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({
|
const workspace = await dependencies.registerPendingWorktreeWorkspace({
|
||||||
repoRoot,
|
repoRoot,
|
||||||
worktreePath: createdWorktree.worktree.worktreePath,
|
worktreePath: createdWorktree.worktree.worktreePath,
|
||||||
|
|||||||
@@ -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",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -613,19 +613,59 @@ export const SetVoiceModeMessageSchema = z.object({
|
|||||||
requestId: z.string().optional(),
|
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({
|
export const SendAgentMessageSchema = z.object({
|
||||||
type: z.literal("send_agent_message"),
|
type: z.literal("send_agent_message"),
|
||||||
agentId: z.string(),
|
agentId: z.string(),
|
||||||
text: z.string(),
|
text: z.string(),
|
||||||
messageId: z.string().optional(), // Client-provided ID for deduplication
|
messageId: z.string().optional(), // Client-provided ID for deduplication
|
||||||
images: z
|
images: z.array(ImageAttachmentSchema).optional(),
|
||||||
.array(
|
attachments: AgentAttachmentsSchema,
|
||||||
z.object({
|
|
||||||
data: z.string(), // base64 encoded image
|
|
||||||
mimeType: z.string(), // e.g., "image/jpeg", "image/png"
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.optional(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -710,14 +750,8 @@ export const SendAgentMessageRequestSchema = z.object({
|
|||||||
agentId: z.string(),
|
agentId: z.string(),
|
||||||
text: z.string(),
|
text: z.string(),
|
||||||
messageId: z.string().optional(), // Client-provided ID for deduplication
|
messageId: z.string().optional(), // Client-provided ID for deduplication
|
||||||
images: z
|
images: z.array(ImageAttachmentSchema).optional(),
|
||||||
.array(
|
attachments: AgentAttachmentsSchema,
|
||||||
z.object({
|
|
||||||
data: z.string(), // base64 encoded image
|
|
||||||
mimeType: z.string(), // e.g., "image/jpeg", "image/png"
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.optional(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const WaitForFinishRequestSchema = z.object({
|
export const WaitForFinishRequestSchema = z.object({
|
||||||
@@ -775,14 +809,8 @@ export const CreateAgentRequestMessageSchema = z.object({
|
|||||||
initialPrompt: z.string().optional(),
|
initialPrompt: z.string().optional(),
|
||||||
clientMessageId: z.string().optional(),
|
clientMessageId: z.string().optional(),
|
||||||
outputSchema: z.record(z.unknown()).optional(),
|
outputSchema: z.record(z.unknown()).optional(),
|
||||||
images: z
|
images: z.array(ImageAttachmentSchema).optional(),
|
||||||
.array(
|
attachments: AgentAttachmentsSchema,
|
||||||
z.object({
|
|
||||||
data: z.string(), // base64 encoded image
|
|
||||||
mimeType: z.string(), // e.g., "image/jpeg", "image/png"
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.optional(),
|
|
||||||
git: GitSetupOptionsSchema.optional(),
|
git: GitSetupOptionsSchema.optional(),
|
||||||
labels: z.record(z.string()).default({}),
|
labels: z.record(z.string()).default({}),
|
||||||
requestId: z.string(),
|
requestId: z.string(),
|
||||||
@@ -1119,6 +1147,8 @@ export const GitHubSearchItemSchema = z.object({
|
|||||||
state: z.string(),
|
state: z.string(),
|
||||||
body: z.string().nullable(),
|
body: z.string().nullable(),
|
||||||
labels: z.array(z.string()),
|
labels: z.array(z.string()),
|
||||||
|
baseRefName: z.string().nullable().optional(),
|
||||||
|
headRefName: z.string().nullable().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const GitHubSearchRequestSchema = z.object({
|
export const GitHubSearchRequestSchema = z.object({
|
||||||
@@ -1158,6 +1188,7 @@ export const CreatePaseoWorktreeRequestSchema = z.object({
|
|||||||
type: z.literal("create_paseo_worktree_request"),
|
type: z.literal("create_paseo_worktree_request"),
|
||||||
cwd: z.string(),
|
cwd: z.string(),
|
||||||
worktreeSlug: z.string().optional(),
|
worktreeSlug: z.string().optional(),
|
||||||
|
attachments: AgentAttachmentsSchema,
|
||||||
requestId: z.string(),
|
requestId: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -3012,6 +3043,7 @@ export type DictationStreamChunkMessage = z.infer<typeof DictationStreamChunkMes
|
|||||||
export type DictationStreamFinishMessage = z.infer<typeof DictationStreamFinishMessageSchema>;
|
export type DictationStreamFinishMessage = z.infer<typeof DictationStreamFinishMessageSchema>;
|
||||||
export type DictationStreamCancelMessage = z.infer<typeof DictationStreamCancelMessageSchema>;
|
export type DictationStreamCancelMessage = z.infer<typeof DictationStreamCancelMessageSchema>;
|
||||||
export type CreateAgentRequestMessage = z.infer<typeof CreateAgentRequestMessageSchema>;
|
export type CreateAgentRequestMessage = z.infer<typeof CreateAgentRequestMessageSchema>;
|
||||||
|
export type AgentAttachment = z.infer<typeof AgentAttachmentSchema>;
|
||||||
export type ListProviderModelsRequestMessage = z.infer<
|
export type ListProviderModelsRequestMessage = z.infer<
|
||||||
typeof ListProviderModelsRequestMessageSchema
|
typeof ListProviderModelsRequestMessageSchema
|
||||||
>;
|
>;
|
||||||
@@ -3096,6 +3128,7 @@ export type BranchSuggestionsResponse = z.infer<typeof BranchSuggestionsResponse
|
|||||||
export type GitHubSearchItem = z.infer<typeof GitHubSearchItemSchema>;
|
export type GitHubSearchItem = z.infer<typeof GitHubSearchItemSchema>;
|
||||||
export type GitHubSearchRequest = z.infer<typeof GitHubSearchRequestSchema>;
|
export type GitHubSearchRequest = z.infer<typeof GitHubSearchRequestSchema>;
|
||||||
export type GitHubSearchResponse = z.infer<typeof GitHubSearchResponseSchema>;
|
export type GitHubSearchResponse = z.infer<typeof GitHubSearchResponseSchema>;
|
||||||
|
export type CreatePaseoWorktreeRequest = z.infer<typeof CreatePaseoWorktreeRequestSchema>;
|
||||||
export type DirectorySuggestionsRequest = z.infer<typeof DirectorySuggestionsRequestSchema>;
|
export type DirectorySuggestionsRequest = z.infer<typeof DirectorySuggestionsRequestSchema>;
|
||||||
export type DirectorySuggestionsResponse = z.infer<typeof DirectorySuggestionsResponseSchema>;
|
export type DirectorySuggestionsResponse = z.infer<typeof DirectorySuggestionsResponseSchema>;
|
||||||
export type PaseoWorktreeListRequest = z.infer<typeof PaseoWorktreeListRequestSchema>;
|
export type PaseoWorktreeListRequest = z.infer<typeof PaseoWorktreeListRequestSchema>;
|
||||||
|
|||||||
@@ -2387,6 +2387,8 @@ export interface GitHubSearchResult {
|
|||||||
state: string;
|
state: string;
|
||||||
body: string | null;
|
body: string | null;
|
||||||
labels: string[];
|
labels: string[];
|
||||||
|
baseRefName?: string | null;
|
||||||
|
headRefName?: string | null;
|
||||||
}>;
|
}>;
|
||||||
githubFeaturesEnabled: boolean;
|
githubFeaturesEnabled: boolean;
|
||||||
}
|
}
|
||||||
@@ -2428,7 +2430,7 @@ export async function searchGitHubIssuesAndPrs(
|
|||||||
"--search",
|
"--search",
|
||||||
query,
|
query,
|
||||||
"--json",
|
"--json",
|
||||||
"number,title,url,state,body,labels",
|
"number,title,url,state,body,labels,baseRefName,headRefName",
|
||||||
"--limit",
|
"--limit",
|
||||||
String(limit),
|
String(limit),
|
||||||
],
|
],
|
||||||
@@ -2452,6 +2454,8 @@ export async function searchGitHubIssuesAndPrs(
|
|||||||
labels: Array.isArray(item.labels)
|
labels: Array.isArray(item.labels)
|
||||||
? item.labels.map((l: { name?: string }) => l.name ?? "").filter(Boolean)
|
? item.labels.map((l: { name?: string }) => l.name ?? "").filter(Boolean)
|
||||||
: [],
|
: [],
|
||||||
|
baseRefName: item.baseRefName ?? null,
|
||||||
|
headRefName: item.headRefName ?? null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user