Add image attachments to agent creation

This commit is contained in:
Mohamed Boudra
2025-12-29 09:07:54 +07:00
parent 5d24e0eee8
commit d5f5351d37
6 changed files with 51 additions and 38 deletions

View File

@@ -573,12 +573,6 @@ export default function DraftAgentScreen() {
...(modeId ? { modeId } : {}), ...(modeId ? { modeId } : {}),
...(trimmedModel ? { model: trimmedModel } : {}), ...(trimmedModel ? { model: trimmedModel } : {}),
}; };
// TODO: Images in initial agent creation are not yet supported by the server API.
// For now we log a warning. Images can be sent after agent creation via sendAgentMessage.
if (images && images.length > 0) {
console.warn("[DraftAgentScreen] Image attachments on agent creation not yet supported");
}
const trimmedBaseBranch = baseBranch.trim(); const trimmedBaseBranch = baseBranch.trim();
const shouldIncludeBase = const shouldIncludeBase =
trimmedBaseBranch.length > 0 || trimmedBaseBranch.length > 0 ||
@@ -608,6 +602,7 @@ export default function DraftAgentScreen() {
createAgent({ createAgent({
config, config,
initialPrompt: trimmedPrompt, initialPrompt: trimmedPrompt,
images,
git: gitOptions, git: gitOptions,
requestId, requestId,
}); });

View File

@@ -1284,6 +1284,34 @@ export function SessionProvider({ children, serverUrl, serverId }: SessionProvid
ws.send(msg); ws.send(msg);
}, [serverId, ws, setProviderModels]); }, [serverId, ws, setProviderModels]);
const encodeImages = useCallback(async (
images?: Array<{ uri: string; mimeType?: string }>
) => {
if (!images || images.length === 0) {
return undefined;
}
const encodedImages = await Promise.all(
images.map(async ({ uri, mimeType }) => {
try {
const data = await FileSystem.readAsStringAsync(uri, {
encoding: "base64",
});
return {
data,
mimeType: mimeType ?? "image/jpeg",
};
} catch (error) {
console.error("[Session] Failed to convert image:", error);
return null;
}
})
);
const validImages = encodedImages.filter(
(entry): entry is { data: string; mimeType: string } => entry !== null
);
return validImages.length > 0 ? validImages : undefined;
}, []);
const sendAgentMessage = useCallback(async ( const sendAgentMessage = useCallback(async (
agentId: string, agentId: string,
message: string, message: string,
@@ -1304,31 +1332,7 @@ export function SessionProvider({ children, serverUrl, serverId }: SessionProvid
return updated; return updated;
}); });
let imagesData: Array<{ data: string; mimeType: string }> | undefined; const imagesData = await encodeImages(images);
if (images && images.length > 0) {
const encodedImages = await Promise.all(
images.map(async ({ uri, mimeType }) => {
try {
const data = await FileSystem.readAsStringAsync(uri, {
encoding: "base64",
});
return {
data,
mimeType: mimeType ?? "image/jpeg",
};
} catch (error) {
console.error("[Session] Failed to convert image:", error);
return null;
}
})
);
const validImages = encodedImages.filter(
(entry): entry is { data: string; mimeType: string } => entry !== null
);
if (validImages.length > 0) {
imagesData = validImages;
}
}
const msg: WSInboundMessage = { const msg: WSInboundMessage = {
type: "session", type: "session",
@@ -1341,7 +1345,7 @@ export function SessionProvider({ children, serverUrl, serverId }: SessionProvid
}, },
}; };
ws.send(msg); ws.send(msg);
}, [serverId, ws, setAgentStreamState]); }, [encodeImages, serverId, ws, setAgentStreamState]);
const cancelAgentRun = useCallback((agentId: string) => { const cancelAgentRun = useCallback((agentId: string) => {
const msg: WSInboundMessage = { const msg: WSInboundMessage = {
@@ -1431,21 +1435,28 @@ export function SessionProvider({ children, serverUrl, serverId }: SessionProvid
} }
}, [ws]); }, [ws]);
const createAgent = useCallback(({ config, initialPrompt, git, worktreeName, requestId }: { config: any; initialPrompt: string; git?: any; worktreeName?: string; requestId?: string }) => { const createAgent = useCallback(async ({ config, initialPrompt, images, git, worktreeName, requestId }: { config: any; initialPrompt: string; images?: Array<{ uri: string; mimeType?: string }>; git?: any; worktreeName?: string; requestId?: string }) => {
const trimmedPrompt = initialPrompt.trim(); const trimmedPrompt = initialPrompt.trim();
let imagesData: Array<{ data: string; mimeType: string }> | undefined;
try {
imagesData = await encodeImages(images);
} catch (error) {
console.error("[Session] Failed to prepare images for agent creation:", error);
}
const msg: WSInboundMessage = { const msg: WSInboundMessage = {
type: "session", type: "session",
message: { message: {
type: "create_agent_request", type: "create_agent_request",
config, config,
...(trimmedPrompt ? { initialPrompt: trimmedPrompt } : {}), ...(trimmedPrompt ? { initialPrompt: trimmedPrompt } : {}),
...(imagesData && imagesData.length > 0 ? { images: imagesData } : {}),
...(git ? { git } : {}), ...(git ? { git } : {}),
...(worktreeName ? { worktreeName } : {}), ...(worktreeName ? { worktreeName } : {}),
...(requestId ? { requestId } : {}), ...(requestId ? { requestId } : {}),
}, },
}; };
ws.send(msg); ws.send(msg);
}, [ws]); }, [encodeImages, ws]);
const resumeAgent = useCallback(({ handle, overrides, requestId }: { handle: any; overrides?: any; requestId?: string }) => { const resumeAgent = useCallback(({ handle, overrides, requestId }: { handle: any; overrides?: any; requestId?: string }) => {
const msg: WSInboundMessage = { const msg: WSInboundMessage = {

View File

@@ -199,10 +199,11 @@ export interface SessionState {
createAgent: (options: { createAgent: (options: {
config: any; config: any;
initialPrompt: string; initialPrompt: string;
images?: Array<{ uri: string; mimeType?: string }>;
git?: any; git?: any;
worktreeName?: string; worktreeName?: string;
requestId?: string; requestId?: string;
}) => void; }) => Promise<void>;
resumeAgent: (options: { handle: any; overrides?: any; requestId?: string }) => void; resumeAgent: (options: { handle: any; overrides?: any; requestId?: string }) => void;
setAgentMode: (agentId: string, modeId: string) => void; setAgentMode: (agentId: string, modeId: string) => void;
respondToPermission: (agentId: string, requestId: string, response: any) => void; respondToPermission: (agentId: string, requestId: string, response: any) => void;

View File

@@ -365,6 +365,10 @@ export const CreateAgentRequestMessageSchema = z.object({
config: AgentSessionConfigSchema, config: AgentSessionConfigSchema,
worktreeName: z.string().optional(), worktreeName: z.string().optional(),
initialPrompt: z.string().optional(), initialPrompt: z.string().optional(),
images: z.array(z.object({
data: z.string(), // base64 encoded image
mimeType: z.string(), // e.g., "image/jpeg", "image/png"
})).optional(),
git: GitSetupOptionsSchema.optional(), git: GitSetupOptionsSchema.optional(),
requestId: z.string().optional(), requestId: z.string().optional(),
}); });

View File

@@ -1265,7 +1265,7 @@ export class Session {
private async handleCreateAgentRequest( private async handleCreateAgentRequest(
msg: Extract<SessionInboundMessage, { type: "create_agent_request" }> msg: Extract<SessionInboundMessage, { type: "create_agent_request" }>
): Promise<void> { ): Promise<void> {
const { config, worktreeName, requestId, initialPrompt, git } = msg; const { config, worktreeName, requestId, initialPrompt, git, images } = msg;
console.log( console.log(
`[Session ${this.clientId}] Creating agent in ${config.cwd} (${config.provider})${ `[Session ${this.clientId}] Creating agent in ${config.cwd} (${config.provider})${
worktreeName ? ` with worktree ${worktreeName}` : "" worktreeName ? ` with worktree ${worktreeName}` : ""
@@ -1306,7 +1306,8 @@ export class Session {
await this.handleSendAgentMessage( await this.handleSendAgentMessage(
snapshot.id, snapshot.id,
trimmedPrompt, trimmedPrompt,
uuidv4() uuidv4(),
images
); );
} catch (promptError) { } catch (promptError) {
console.error( console.error(

View File

@@ -134,10 +134,11 @@ Improvements to the new agent screen in the app.
- If broken, add fix tasks with debugging details. - If broken, add fix tasks with debugging details.
- **Done (2025-12-29 09:13)**: WHAT: Tested image attachments via Playwright MCP; traced code in `packages/app/src/app/agent/new.tsx:576-580`, `packages/app/src/contexts/session-context.tsx:1307-1331`, `packages/server/src/server/session.ts:316-334`. RESULT: Image attachments are BROKEN - three issues found: (1) Agent creation silently drops images with console warning, (2) Web platform fails to base64 encode images due to `expo-file-system.readAsStringAsync` not working, (3) Server `buildAgentPrompt` only creates text summary, never passes actual image bytes to LLM. EVIDENCE: Console logs `[WARNING] [DraftAgentScreen] Image attachments on agent creation not yet supported` and `[ERROR] [Session] Failed to convert image: Error: Method readAsStringAsync imported from "expo-file-system"...`; agent responses consistently state "I don't see any image attached". Full report in `REPORT-image-attachment-investigation.md`. - **Done (2025-12-29 09:13)**: WHAT: Tested image attachments via Playwright MCP; traced code in `packages/app/src/app/agent/new.tsx:576-580`, `packages/app/src/contexts/session-context.tsx:1307-1331`, `packages/server/src/server/session.ts:316-334`. RESULT: Image attachments are BROKEN - three issues found: (1) Agent creation silently drops images with console warning, (2) Web platform fails to base64 encode images due to `expo-file-system.readAsStringAsync` not working, (3) Server `buildAgentPrompt` only creates text summary, never passes actual image bytes to LLM. EVIDENCE: Console logs `[WARNING] [DraftAgentScreen] Image attachments on agent creation not yet supported` and `[ERROR] [Session] Failed to convert image: Error: Method readAsStringAsync imported from "expo-file-system"...`; agent responses consistently state "I don't see any image attached". Full report in `REPORT-image-attachment-investigation.md`.
- [ ] **Fix**: Add image support to agent creation API - [x] **Fix**: Add image support to agent creation API
- Extend `create_agent_request` to accept images array - Extend `create_agent_request` to accept images array
- Pass images to `handleSendAgentMessage` after agent creation - Pass images to `handleSendAgentMessage` after agent creation
- Remove warning in `new.tsx:576-580` once implemented - Remove warning in `new.tsx:576-580` once implemented
- **Done (2025-12-29 09:07)**: WHAT: `packages/server/src/server/messages.ts:363-373` adds optional `images` to `create_agent_request`; `packages/server/src/server/session.ts:1268-1311` forwards images into `handleSendAgentMessage`; `packages/app/src/contexts/session-context.tsx:1287-1459` encodes image attachments and includes them in create-agent websocket payloads; `packages/app/src/stores/session-store.ts:175-206` updates createAgent type to accept images/async; `packages/app/src/app/agent/new.tsx:560-606` forwards images to createAgent and removes the warning. RESULT: agent creation requests now carry image attachments through to the initial prompt path. EVIDENCE: Not run (not requested).
- [ ] **Fix**: Fix web platform image base64 encoding - [ ] **Fix**: Fix web platform image base64 encoding
- Replace `FileSystem.readAsStringAsync` in `session-context.tsx:1312` with cross-platform solution - Replace `FileSystem.readAsStringAsync` in `session-context.tsx:1312` with cross-platform solution