diff --git a/pkg/clipboard/service.go b/pkg/clipboard/service.go index f084990e..0e4332fa 100644 --- a/pkg/clipboard/service.go +++ b/pkg/clipboard/service.go @@ -3,6 +3,7 @@ package clipboard import ( "context" + "encoding/base64" core "dappco.re/go/core" ) @@ -36,7 +37,7 @@ func (s *Service) OnStartup(_ context.Context) core.Result { if !ok { return core.Result{Value: false, OK: true} } - data, _ := opts.Get("data").Value.([]byte) + data := clipboardImageData(opts) if len(data) == 0 || len(data) > MaxImageBytes { return core.Result{Value: false, OK: true} } @@ -92,3 +93,20 @@ func (s *Service) handleQuery(_ *core.Core, q core.Query) core.Result { return core.Result{} } } + +// clipboardImageData normalizes clipboard image inputs from MCP, preload bridge, and WS callers. +// Use: bytes := clipboardImageData(core.NewOptions(core.Option{Key: "data", Value: "iVBORw0KGgo..."})) +func clipboardImageData(opts core.Options) []byte { + if raw, ok := opts.Get("data").Value.([]byte); ok && len(raw) > 0 { + return append([]byte(nil), raw...) + } + encoded := opts.String("data") + if encoded == "" { + return nil + } + data, err := base64.StdEncoding.DecodeString(encoded) + if err != nil { + return nil + } + return data +} diff --git a/pkg/display/display.go b/pkg/display/display.go index b45cd2c2..5aeb0620 100644 --- a/pkg/display/display.go +++ b/pkg/display/display.go @@ -545,6 +545,8 @@ func (s *Service) handleWSMessage(msg WSMessage) core.Result { return c.Action("gui.chat.conversations.export").Run(ctx, wsOptions(msg.Data)) case "chat:attach-image": return c.Action("gui.chat.attachImage").Run(ctx, wsOptions(msg.Data)) + case "chat:attach-image-file": + return c.Action("gui.chat.attachImageFile").Run(ctx, wsOptions(msg.Data)) case "chat:remove-image": return c.Action("gui.chat.removeImage").Run(ctx, wsOptions(msg.Data)) case "chat:thinking:start": diff --git a/ui/src/chat/chat-state.service.ts b/ui/src/chat/chat-state.service.ts index def9df56..fa84ec48 100644 --- a/ui/src/chat/chat-state.service.ts +++ b/ui/src/chat/chat-state.service.ts @@ -188,10 +188,13 @@ export class ChatStateService { continue; } const attachment = await this.fileToAttachment(file); - await this.invoke('gui.chat.attachImage', { + const queued = await this.invoke('gui.chat.attachImage', { conversation_id: this.activeConversation()?.id, ...attachment, }); + if (queued) { + this.upsertQueuedAttachment(queued); + } } } @@ -214,10 +217,13 @@ export class ChatStateService { return; } for (const path of paths) { - await this.invokeGUI('gui.chat.attachImageFile', { + const attachment = await this.invoke('gui.chat.attachImageFile', { conversation_id: this.activeConversation()?.id, path, }); + if (attachment) { + this.upsertQueuedAttachment(attachment); + } } } @@ -398,7 +404,7 @@ export class ChatStateService { if (!data.attachment || conversationID !== activeID && !(conversationID === 'draft' && !this.activeConversation()?.messages.length)) { return; } - this.queuedAttachments.update((items) => [...items, data.attachment!]); + this.upsertQueuedAttachment(data.attachment); }); } @@ -480,6 +486,23 @@ export class ChatStateService { return model?.supports_vision ?? false; } + private upsertQueuedAttachment(attachment: ImageAttachment): void { + this.queuedAttachments.update((items) => { + if (items.some((item) => this.sameAttachment(item, attachment))) { + return items; + } + return [...items, attachment]; + }); + } + + private sameAttachment(left: ImageAttachment, right: ImageAttachment): boolean { + return left.filename === right.filename && + left.mime_type === right.mime_type && + left.data === right.data && + left.width === right.width && + left.height === right.height; + } + private async fileToAttachment(file: File): Promise { const data = await this.readFileAsDataURL(file); const dimensions = await this.readImageDimensions(data); diff --git a/ui/src/generated/core-gui-chat.bindings.ts b/ui/src/generated/core-gui-chat.bindings.ts index 3e9d6d67..725a9ecf 100644 --- a/ui/src/generated/core-gui-chat.bindings.ts +++ b/ui/src/generated/core-gui-chat.bindings.ts @@ -42,6 +42,10 @@ export interface ChatRouteMap { request: ({ conversation_id?: string } & ImageAttachment); response: ImageAttachment; }; + 'gui.chat.attachImageFile': { + request: { conversation_id?: string; path: string }; + response: ImageAttachment; + }; 'gui.chat.removeImage': { request: { conversation_id?: string; index: number }; response: ImageAttachment;