|
1 | 1 | import * as acp from "@agentclientprotocol/sdk"; |
2 | 2 | import {RequestError, type SessionId, type SessionModeState} from "@agentclientprotocol/sdk"; |
| 3 | +import {readFile} from "node:fs/promises"; |
| 4 | +import {extname} from "node:path"; |
| 5 | +import {fileURLToPath, pathToFileURL} from "node:url"; |
3 | 6 | import {CodexEventHandler} from "./CodexEventHandler"; |
4 | 7 | import {CodexApprovalHandler} from "./CodexApprovalHandler"; |
5 | 8 | import {CodexElicitationHandler} from "./CodexElicitationHandler"; |
@@ -80,6 +83,7 @@ import { |
80 | 83 | createAgentTextMessageChunk, |
81 | 84 | createAgentTextThoughtChunk, |
82 | 85 | createUserMessageChunk, |
| 86 | + visibleUserMessageText, |
83 | 87 | } from "./ContentChunks"; |
84 | 88 | import { |
85 | 89 | sameThreadGoalSnapshot, |
@@ -1232,13 +1236,11 @@ export class CodexAcpServer { |
1232 | 1236 | } |
1233 | 1237 | } |
1234 | 1238 |
|
1235 | | - private createUserMessageUpdates(item: ThreadItem & { type: "userMessage" }): UpdateSessionEvent[] { |
| 1239 | + private async createUserMessageUpdates(item: ThreadItem & { type: "userMessage" }): Promise<UpdateSessionEvent[]> { |
1236 | 1240 | const updates: UpdateSessionEvent[] = []; |
1237 | | - const messageId = item.id; |
1238 | 1241 | for (const input of item.content) { |
1239 | | - const blocks = this.userInputToContentBlocks(input); |
1240 | | - for (const block of blocks) { |
1241 | | - updates.push(createUserMessageChunk(block, messageId)); |
| 1242 | + for (const block of await this.userInputToContentBlocks(input)) { |
| 1243 | + updates.push(createUserMessageChunk(block, item.id)); |
1242 | 1244 | } |
1243 | 1245 | } |
1244 | 1246 | return updates; |
@@ -1291,20 +1293,39 @@ export class CodexAcpServer { |
1291 | 1293 | }; |
1292 | 1294 | } |
1293 | 1295 |
|
1294 | | - private userInputToContentBlocks(input: UserInput): acp.ContentBlock[] { |
| 1296 | + private async userInputToContentBlocks(input: UserInput): Promise<acp.ContentBlock[]> { |
1295 | 1297 | switch (input.type) { |
1296 | | - case "text": |
1297 | | - return input.text.length > 0 ? [{ type: "text", text: input.text }] : []; |
1298 | | - case "image": |
| 1298 | + case "text": { |
| 1299 | + const visibleText = visibleUserMessageText(input.text); |
| 1300 | + return visibleText.length > 0 ? [{ type: "text", text: visibleText }] : []; |
| 1301 | + } |
| 1302 | + case "image": { |
| 1303 | + const match = /^data:(image\/[^;]+);base64,(.+)$/.exec(input.url); |
| 1304 | + const mimeType = match?.[1]; |
| 1305 | + const data = match?.[2]; |
| 1306 | + if (mimeType && data) { |
| 1307 | + return [{ type: "image", mimeType, data }]; |
| 1308 | + } |
1299 | 1309 | return [{ type: "text", text: this.formatUriAsLink("image", input.url) }]; |
| 1310 | + } |
1300 | 1311 | case "localImage": { |
1301 | | - const uri = input.path.startsWith("file://") ? input.path : `file://${input.path}`; |
| 1312 | + const path = input.path.startsWith("file://") ? fileURLToPath(input.path) : input.path; |
| 1313 | + const uri = pathToFileURL(path).href; |
| 1314 | + const extension = extname(path).slice(1).toLowerCase(); |
| 1315 | + const data = extension ? await readFile(path).catch(() => null) : null; |
| 1316 | + if (data) { |
| 1317 | + const mimeType = `image/${extension === "jpg" ? "jpeg" : extension}`; |
| 1318 | + return [{ type: "image", data: data.toString("base64"), mimeType, uri }]; |
| 1319 | + } |
1302 | 1320 | return [{ type: "text", text: this.formatUriAsLink(null, uri) }]; |
1303 | 1321 | } |
1304 | 1322 | case "skill": |
1305 | 1323 | return [{ type: "text", text: `skill:${input.name} (${input.path})` }]; |
| 1324 | + case "mention": { |
| 1325 | + const uri = input.path.startsWith("file://") ? input.path : pathToFileURL(input.path).href; |
| 1326 | + return [{ type: "resource_link", name: input.name, uri }]; |
| 1327 | + } |
1306 | 1328 | } |
1307 | | - return []; |
1308 | 1329 | } |
1309 | 1330 |
|
1310 | 1331 | private formatUriAsLink(name: string | null, uri: string): string { |
|
0 commit comments