feat(webchat): support image attachments

This commit is contained in:
William Valentin
2026-02-13 15:03:48 -08:00
parent 955b9e28e0
commit cc54b3a10c
7 changed files with 707 additions and 31 deletions
+15 -8
View File
@@ -21,11 +21,18 @@ export function createAgentHandlers(deps: AgentHandlerDeps) {
return {
'agent.send': async (request: GatewayRequest, send: SendFn): Promise<OutboundMessage | void> => {
const params = request.params as { message?: string; connectionId?: string; attachments?: GatewayAttachment[]; metadata?: { isCommand?: boolean; command?: string; commandArgs?: string } } | undefined;
if (!params?.message && !params?.metadata?.isCommand) {
return makeError(request.id, ErrorCode.InvalidRequest, 'message is required');
if (!params) {
return makeError(request.id, ErrorCode.InvalidRequest, 'params are required');
}
const connectionId = params.connectionId as string;
const safeParams = params;
const hasMessage = Boolean(safeParams.message && safeParams.message.trim());
const hasAttachments = Boolean(safeParams.attachments && safeParams.attachments.length > 0);
if (!hasMessage && !hasAttachments && !safeParams.metadata?.isCommand) {
return makeError(request.id, ErrorCode.InvalidRequest, 'message or attachments are required');
}
const connectionId = safeParams.connectionId as string;
if (!connectionId) {
return makeError(request.id, ErrorCode.InvalidRequest, 'connectionId is required (set by server)');
}
@@ -48,9 +55,9 @@ export function createAgentHandlers(deps: AgentHandlerDeps) {
return deps.laneQueue.enqueue(laneId, async () => {
deps.sessionBridge.setBusy(connectionId, true);
const commandInput = params.metadata?.isCommand && typeof params.metadata.command === 'string'
? `/${params.metadata.command}${params.metadata.commandArgs ? ` ${params.metadata.commandArgs}` : ''}`
: params.message;
const commandInput = safeParams.metadata?.isCommand && typeof safeParams.metadata.command === 'string'
? `/${safeParams.metadata.command}${safeParams.metadata.commandArgs ? ` ${safeParams.metadata.commandArgs}` : ''}`
: (safeParams.message ?? '');
if (commandInput && deps.commandRegistry?.isCommand(commandInput)) {
const sessionId = deps.sessionBridge.getSessionId(connectionId);
@@ -160,14 +167,14 @@ export function createAgentHandlers(deps: AgentHandlerDeps) {
try {
// Convert gateway attachments to channel attachments
const attachments: Attachment[] | undefined = params.attachments?.map(a => ({
const attachments: Attachment[] | undefined = safeParams.attachments?.map(a => ({
mimeType: a.mimeType,
data: a.data,
url: a.url,
filename: a.filename,
}));
const response = await agent.process(params.message!, attachments);
const response = await agent.process(safeParams.message ?? '', attachments);
deps.metrics?.incrementMessages();
send(makeEvent(request.id, 'done', { content: response }));
} catch (err) {
+22 -1
View File
@@ -323,7 +323,28 @@ describe('agent handlers', () => {
expect(sent).toHaveLength(1);
});
it('agent.send requires message', async () => {
it('agent.send accepts attachment-only requests', async () => {
const req: GatewayRequest = {
id: 12,
method: 'agent.send',
params: {
connectionId: 'conn-1',
attachments: [{ mimeType: 'image/png', data: 'iVBOR...' }],
},
};
const sent: OutboundMessage[] = [];
const send = vi.fn((msg: OutboundMessage) => sent.push(msg));
await handlers['agent.send'](req, send);
expect(mockAgent.process).toHaveBeenCalledWith('', [
{ mimeType: 'image/png', data: 'iVBOR...', url: undefined, filename: undefined },
]);
expect(sent).toHaveLength(1);
expect((sent[0] as GatewayEvent).event).toBe('done');
});
it('agent.send requires message or attachments', async () => {
const req: GatewayRequest = { id: 2, method: 'agent.send', params: { connectionId: 'conn-1' } };
const send = vi.fn();
const result = await handlers['agent.send'](req, send) as GatewayError;