Skip to content

Image attachment support in chat + pop-out state transfer fix - #63

Merged
ddutchie merged 3 commits into
mainfrom
ddutchie/imagesinchat
Jun 24, 2026
Merged

ddutchie merged 3 commits into
mainfrom
ddutchie/imagesinchat

Conversation

@ddutchie

@ddutchie ddutchie commented Jun 24, 2026 •

Copy link
Copy Markdown
Owner

What does this PR do?

Adds image attachment support to chat — paste clipboard images or use the attachment button, preview as thumbnails, send as base64 inline data URLs to vision-capable LLMs (GPT-4o, Claude Sonnet 4.6, Gemini 3.5 Flash). Also fixes the pop-out state transfer bug where { data: T } unwrapping prevented pop-out from working, and validates sender identity on the chat:requestPopIn handler.

Images are ephemeral — kept as inline base64 data URLs on ChatMessage.images, no persistent disk storage. Only the current message's images are sent to the LLM (not full history). When the model doesn't support vision, images are silently omitted with a text notice prepended to the message.

Type of change

  • Bug fix
  • New feature
  • Refactor / code quality
  • Docs / changelog
  • Tests

Screenshots / recording

N/A

Checklist

  • npm run type-check:all passes
  • npm run lint passes
  • npm test passes
  • npm run test:e2e passes
  • No hardcoded colours — CSS variables only
  • No text-[Npx] pixel font classes — rem equivalents only
  • New IPC handlers wrapped in handle() and return IpcResult<T>
  • New DB migrations appended (not edited) in schema.ts
  • New SQL goes in electron/db/queries.ts
  • New dependencies or devDependencies added to ROLE_MAP
  • New MCP tools registered in electron/mcp/tools/index.ts dispatch
  • If you added/removed a --external:<pkg> flag

Notes for reviewer

Vision API tested end-to-end against three endpoints: localhost:3042 (GPT-4o, Claude Sonnet 4.6) and localhost:3420 (Gemini 3.5 Flash). Uses standard OpenAI multi-part content array format — any vision-capable OpenAI-compatible endpoint should work. Image attach/remove via paste handler and attachment button in ChatInput, thumbnail preview strip with per-image remove. FileReader handles error/abort per file so one bad image doesn't block others. chat:requestPopIn handler now rejects non-main-window callers.

@coderabbitai

coderabbitai Bot commented Jun 24, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds image attachments to chat input, message storage, and chat streaming, renders attached images in message bubbles, and wraps chat pop-out IPC responses in { data: ... }. The changelog is updated with matching notes.

Changes

Chat image attachments

Layer / File(s) Summary
Image message and request shapes
electron/lib/tools.ts, src/hooks/useChatStream.ts, src/types/index.ts, src/store/slices/chat.ts
ChatRequest, ChatStreamRequest, and ChatMessage gain optional image arrays, and addMessage stores attached images when present.
ChatInput attachment controls
src/components/chat/ChatInput.tsx
ChatInput accepts attachment props, handles paste and file selection, renders pending image thumbnails, and allows send with pending images.
ChatPanel image send flow
src/components/chat/chat-panel/index.tsx
ChatPanel tracks pending images, converts selected files to data URLs, passes image data into message creation, and includes images in the stream request.
Electron chat stream multimodal request
electron/ipc/chat.ts
chat:stream filters unsupported images and builds a multimodal user message from req.images before continuing the stream loop.
Chat message image rendering
src/components/chat/chat-panel/ChatMessageBubble.tsx
Message bubbles render attached images as a wrapped image gallery when message.images is present.

Chat pop-out IPC response wrapping

Layer / File(s) Summary
Pop-out response wrappers
electron/chat-popout.ts
chat:popOut, chat:requestPopIn, chat:popoutReady, and chat:popIn now return { data: ... } payloads for success, failure, and state responses.

Sequence Diagram(s)

sequenceDiagram
  participant ChatInput
  participant ChatPanel
  participant chatStream as chat:stream
  participant ChatMessageBubble

  ChatInput->>ChatPanel: onAttachImages(files)
  ChatInput->>ChatPanel: onSend(text, pendingImages)
  ChatPanel->>ChatPanel: addMessage(message, images)
  ChatPanel->>chatStream: sendStream(message, images)
  chatStream->>chatStream: build multimodal userMessage
  ChatPanel->>ChatMessageBubble: render message.images
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • ddutchie/cairn#62: Introduces the chat pop-out IPC flow whose handler response shapes are wrapped in this PR.

Poem

I tucked bright pixels in my paws,
And sent them hopping without pause.
The chat now blooms with image cheer,
While pop-out doors reply clear.
🐇✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly captures the two main changes: chat image attachments and the pop-out state transfer fix.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ddutchie/imagesinchat

Comment @coderabbitai help to get the list of available commands.

- Paste/attach images in chat input with thumbnail preview strip
- Images kept inline as base64 data URLs (no persistent disk storage)
- Rendered as inline thumbnails in chat bubbles
- Sent to LLM as base64 multi-part content (OpenAI vision API format)
- Supported models: GPT-4o, Claude Sonnet 4.6, Gemini 3.5 Flash
- Fixed chat-popout handlers returning plain objects instead of { data: T }
@ddutchie
ddutchie force-pushed the ddutchie/imagesinchat branch from 8185484 to c595b2b Compare June 24, 2026 20:51

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (1)
src/components/chat/ChatInput.tsx (1)

287-292: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use a color token instead of raw text-white.

The remove button uses the raw Tailwind color text-white. As per coding guidelines, all colours must use CSS variables (e.g. var(--text-on-accent) / a danger-foreground token) and never raw Tailwind colour names.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/chat/ChatInput.tsx` around lines 287 - 292, The remove button
in ChatInput uses a raw Tailwind text color, which needs to be replaced with a
CSS variable token. Update the button class in ChatInput’s image remove control
(the onClick/remove-image button near the X icon) to use the appropriate
foreground token for the danger background instead of text-white, keeping the
existing danger background styling intact.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@changelogs/v2.2.4.md`:
- Line 5: Update the changelog entry for image attachments so it matches the
actual behavior implemented in the chat attachment flow. The current description
incorrectly says images remain only as base64 data URLs with no disk
persistence; revise it to reflect that attachments are stored in the attachments
storage, persisted on ChatMessage, and served via asset:// while still
mentioning inline thumbnail rendering and vision-capable model support.

In `@electron/chat-popout.ts`:
- Around line 158-162: The chat pop-in handler currently relays
`chat:requestPopIn` from any renderer, so update
`ipcMain.handle("chat:requestPopIn", ...)` to validate the caller first. Use the
handler’s `event.sender.id` and compare it against `mainWindowWebContentsId`,
only sending `popoutWindow.webContents.send("chat:requestPopIn")` when the
request comes from the main window; otherwise ignore or reject the request while
keeping the existing return shape.

In `@electron/ipc/chat.ts`:
- Around line 389-397: The user message assembly in chat.ts currently appends
image_url content whenever req.images is present, regardless of whether the
selected provider/model supports multimodal input. Update the logic around the
userMessage construction to check the active provider/model capability before
including images, and only build the image parts when vision is supported. If
images are not supported, either omit them and notify the caller or surface a
clear notice before sending the request.

In `@src/components/chat/chat-panel/index.tsx`:
- Around line 339-350: The FileReader flow in handleAttachImages only resolves
on reader.onload, so a failed or aborted read can leave the promise pending and
stall the loop. Update the FileReader promise to also handle reader.onerror and
reader.onabort by rejecting or otherwise settling with an error path, and make
handleAttachImages recover per file so one bad image does not block later files
or setPendingImages.

In `@src/types/index.ts`:
- Around line 203-204: The `images` field in `chatMessages` is currently
persisting inline `dataUrl` payloads via `persist`, while the database write
path omits `images`, so update the message model and storage flow to avoid
keeping base64 data in client state. In the relevant message/image handling code
that sets `images[].url` from `img.dataUrl` and the persistence logic around
`get().persist()`, convert images to workspace attachment references (such as an
`asset://` or attachment path) before storing them, or, if inline data URLs are
intentional, update the `images` JSDoc in `Message` to match the actual
behavior. Use the `images` property in `src/types/index.ts` and the chat message
persistence flow as the anchor points for the fix.

---

Nitpick comments:
In `@src/components/chat/ChatInput.tsx`:
- Around line 287-292: The remove button in ChatInput uses a raw Tailwind text
color, which needs to be replaced with a CSS variable token. Update the button
class in ChatInput’s image remove control (the onClick/remove-image button near
the X icon) to use the appropriate foreground token for the danger background
instead of text-white, keeping the existing danger background styling intact.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1f0b23e9-cdd5-4880-bba7-31373e8227f2

📥 Commits

Reviewing files that changed from the base of the PR and between b4a8592 and 71c0d36.

📒 Files selected for processing (10)
  • changelogs/v2.2.4.md
  • electron/chat-popout.ts
  • electron/ipc/chat.ts
  • electron/lib/tools.ts
  • src/components/chat/ChatInput.tsx
  • src/components/chat/chat-panel/ChatMessageBubble.tsx
  • src/components/chat/chat-panel/index.tsx
  • src/hooks/useChatStream.ts
  • src/store/slices/chat.ts
  • src/types/index.ts

Comment thread changelogs/v2.2.4.md
Comment thread electron/chat-popout.ts Outdated
Comment thread electron/ipc/chat.ts
Comment thread src/components/chat/chat-panel/index.tsx
Comment thread src/types/index.ts Outdated
@ddutchie
ddutchie merged commit a87ae95 into main Jun 24, 2026
6 checks passed
@ddutchie
ddutchie deleted the ddutchie/imagesinchat branch June 24, 2026 21:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant