Skip to content

Image generation (Imagen + Gemini 2.5) + top-bar quick pickers - #9

Merged
aciderix merged 2 commits into
mainfrom
claude/image-generation
Apr 22, 2026
Merged

aciderix merged 2 commits into
mainfrom
claude/image-generation

Conversation

@aciderix

@aciderix aciderix commented Apr 22, 2026 •

Copy link
Copy Markdown
Owner

Summary

  1. Image generation (both paths, auto-enabled)

    • Imagen (tool-based): new generate_image tool that calls models/{imagen}:predict with the user's API key. The chat model invokes it automatically when asked to draw/illustrate. Bytes are persisted under <filesDir>/attachments/imagen-*.png and attached to the tool-result bubble as thumbnails.
    • Gemini 2.5 native multimodal output: when the selected chat model name contains -image (e.g. gemini-2.5-flash-image-preview), the request payload gets generationConfig.responseModalities = ["TEXT","IMAGE"]. The response parser now handles inlineData parts, decodes them, persists to disk, and attaches the paths to the live model bubble. Toggle is automatic per the user's choice — picking an image-capable model is enough.
    • Imagen model choice in Settings → Model: dropdown with imagen-3.0-generate-002 (default), imagen-3.0-fast-generate-001, imagen-4.0-generate-preview-06-06, plus a custom-ID field. Stored in plain prefs under imagen_model.
    • Shared persistence: single helper RestGeminiCore.persistAttachmentBytes(id, bytes, mime) used by both the response parser and the Imagen tool.
    • Model + tool-result bubbles now render AttachmentThumbnails (120-dp squares, sampled BitmapFactory), same as the user side.
  2. Top-bar quick pickers

    • Model name (top-right of chat) is now tappable → DropdownMenu listing the discovered models with the active one highlighted. Last entry is "More models…" which opens the full Settings sheet (so the user can still set a custom ID / refresh the list).
    • Folder name (below the model) is tappable → menu with:
      • Open folder — best-effort ACTION_VIEW with vnd.android.document/directory on the tree's document URI (falls back to a plain ACTION_VIEW on the URI, and to a toast when no Files app handles it).
      • Change folder — launches OpenDocumentTree directly from the chat screen (no need to go through Settings).

Files touched

  • core-bridge/.../RestGeminiCore.kt — tool registration, persistAttachmentBytes, inlineData parser branch, generationConfig.responseModalities gated on modelEmitsImages(), imagenModel()/setImagenModel(), new AVAILABLE_IMAGEN_MODELS + DEFAULT_IMAGEN_MODEL constants.
  • core-bridge/.../tools/GenerateImageTool.kt — new; POSTs to :predict, decodes predictions[].bytesBase64Encoded, persists + returns ToolCallResult(attachmentPaths=…).
  • core-bridge/.../workspace/Workspace.kt — expose rootUri(): Uri? for "Open folder".
  • core-bridge/.../storage/SecurePrefs.kt — new imagenModel plain pref.
  • domain/.../ToolTypes.kt — ToolCallResult.attachmentPaths field.
  • app/.../ui/chat/ChatScreen.kt — top-bar clickable model/folder with DropdownMenus, folder-picker launcher, openWorkspaceFolder helper, model bubble now renders AttachmentThumbnails.
  • app/.../ui/chat/ChatViewModel.kt — imagenModel state + setter, workspaceUri state.
  • app/.../ui/settings/SettingsSheet.kt — "Image generation model" picker subsection inside Model accordion (dropdown + custom ID input).

Test plan

  • With a non-image chat model, ask Gemini to "draw a red fox" — it should call generate_image, image appears as thumbnail in the tool-result bubble.
  • Switch Imagen model in Settings to imagen-3.0-fast-generate-001, repeat — new model name appears in the tool-result header.
  • Switch chat model to gemini-2.5-flash-image-preview, ask for "a landscape oil painting" — image appears directly in the model bubble alongside any text.
  • Tap model name in top bar → dropdown opens, shows the model list, picking one switches; "More models…" opens Settings.
  • Tap folder name → "Open folder" launches the system Files app pointed at the workspace; "Change folder" opens the SAF tree picker.
  • Save a chat with a generated image → reload from "Open…" → thumbnails still render.
  • CI: release + debug APK build both succeed.

https://claude.ai/code/session_015dVKN2jG34HKP9SeBpSvS5

Summary by Sourcery

Add automatic image generation support via Imagen and Gemini image-capable models, and introduce quick-access model and workspace folder pickers in the chat top bar.

New Features:

  • Introduce a generate_image tool that calls Imagen :predict, saves generated image bytes, and surfaces them as attachments in chat.
  • Enable native multimodal image output for image-capable Gemini models, persisting inline image data and displaying thumbnails in model/tool bubbles.
  • Add an Imagen model picker in settings with predefined options and support for custom Imagen model IDs.
  • Make the chat top-bar model name tappable to quickly switch between discovered models or open the full model settings sheet.
  • Make the workspace folder label tappable to open the folder in a files app or change the workspace directory directly from the chat screen.

Enhancements:

  • Persist model and tool-generated image bytes into app-owned attachments storage via a shared helper for reuse in chat UI.
  • Extend tool-result and model message rendering to show attachment thumbnails above text content in chat bubbles.
  • Expose the workspace root URI from the Workspace class so the UI can launch a file manager for the current project folder.

Summary by CodeRabbit

  • New Features
    • Added clickable dropdowns in the chat interface to switch between available AI models and manage workspace folders.
    • Added image generation model configuration in settings, supporting both preset and custom models.
    • Implemented automatic image generation with results displayed as attachments in conversations.
    • Enhanced message display to render image attachments from both AI-generated and user sources.

- Imagen integration: new `generate_image` tool that calls
  `models/{imagen}:predict` with the API key and the imagen model
  selected in Settings (default imagen-3.0-generate-002; picker lists
  3 built-in variants + custom ID). Decoded PNGs are persisted under
  <filesDir>/attachments/imagen-*.png and returned both as text
  (for the model turn) and as `attachmentPaths` on the ToolCallResult,
  so the tool-result bubble renders them as thumbnails.
- Gemini 2.5 native multimodal output: when the selected chat model's
  name contains "-image" (heuristic, auto-detects
  `gemini-2.5-flash-image-preview`), the request gets
  `generationConfig.responseModalities = [TEXT, IMAGE]`. Response
  parser now handles `inlineData` parts, persists the bytes, and
  attaches the paths to the live model bubble alongside streamed text.
- Model + tool-result bubbles now render `AttachmentThumbnails`, same
  120-dp thumbnails as the user side.
- Shared persistence: `RestGeminiCore.persistAttachmentBytes(id,bytes,mime)`
  is the single helper used by the response parser and by the Imagen
  tool.
- Top bar quick pickers: tapping the model name opens a DropdownMenu
  listing the discovered models (+ "More models…" shortcut to full
  settings). Tapping the workspace folder name opens a menu with
  "Open folder" (best-effort ACTION_VIEW on the tree/file URI,
  falls back to a toast) and "Change folder" (launches
  OpenDocumentTree).
@sourcery-ai

sourcery-ai Bot commented Apr 22, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

Adds end-to-end image generation support via a new Imagen-based tool and native Gemini 2.5 image outputs, including shared attachment persistence and thumbnail rendering, and introduces top-bar quick pickers for switching models and workspace folders directly from the chat UI.

Sequence diagram for generate_image tool-based image generation

sequenceDiagram
    actor User
    participant ChatScreen
    participant ChatViewModel
    participant RestGeminiCore
    participant GeminiModel
    participant GenerateImageTool
    participant ImagenAPI
    participant Storage as AttachmentsStorage

    User->>ChatScreen: Type prompt "draw a red fox"
    ChatScreen->>ChatViewModel: sendPrompt(prompt, currentModel)
    ChatViewModel->>RestGeminiCore: startChatTurn(prompt)

    RestGeminiCore->>GeminiModel: POST contents (non image model)
    GeminiModel-->>RestGeminiCore: Response with tool call generate_image

    RestGeminiCore->>GenerateImageTool: execute(ToolCall)
    GenerateImageTool->>GenerateImageTool: read apiKey via getApiKey()
    GenerateImageTool->>GenerateImageTool: read model via getModel()
    GenerateImageTool->>ImagenAPI: POST models/{imagenModel}:predict
    ImagenAPI-->>GenerateImageTool: predictions[].bytesBase64Encoded

    GenerateImageTool->>GenerateImageTool: decode Base64 to bytes
    loop for each image
        GenerateImageTool->>RestGeminiCore: persist(id, bytes, mime)
        RestGeminiCore->>AttachmentsStorage: persistAttachmentBytes(id, bytes, mime)
        AttachmentsStorage-->>RestGeminiCore: imagePath
        RestGeminiCore-->>GenerateImageTool: imagePath
    end

    GenerateImageTool-->>RestGeminiCore: ToolCallResult(attachmentPaths)

    RestGeminiCore->>RestGeminiCore: create GeminiMessage(role TOOL, attachmentPaths)
    RestGeminiCore-->>ChatViewModel: GeminiEvent.ToolCallCompleted
    ChatViewModel-->>ChatScreen: updated messages with tool-result message

    ChatScreen->>ChatScreen: MessageBubble(toolMessage)
    ChatScreen->>ChatScreen: AttachmentThumbnails(message.attachmentPaths)
    ChatScreen-->>User: Tool-result bubble with image thumbnails
Loading

Sequence diagram for inline Gemini 2.5 image output handling

sequenceDiagram
    actor User
    participant ChatScreen
    participant ChatViewModel
    participant RestGeminiCore
    participant GeminiModel as Gemini2_5_ImageModel
    participant AttachmentsStorage

    User->>ChatScreen: Select gemini-2.5-flash-image-preview
    ChatScreen->>ChatViewModel: setModel("gemini-2.5-flash-image-preview")
    ChatViewModel->>RestGeminiCore: setModel(name)

    User->>ChatScreen: Ask for "a landscape oil painting"
    ChatScreen->>ChatViewModel: sendPrompt(prompt)
    ChatViewModel->>RestGeminiCore: startChatTurn(prompt)

    RestGeminiCore->>RestGeminiCore: buildRequestBody()
    RestGeminiCore->>RestGeminiCore: modelEmitsImages(model) == true
    RestGeminiCore->>RestGeminiCore: add generationConfig.responseModalities [TEXT, IMAGE]

    RestGeminiCore->>GeminiModel: POST chat request with responseModalities
    GeminiModel-->>RestGeminiCore: Streaming response parts

    loop for each part
        alt part has inlineData
            RestGeminiCore->>RestGeminiCore: read inlineData.mimeType, data
            RestGeminiCore->>RestGeminiCore: Base64 decode bytes
            RestGeminiCore->>AttachmentsStorage: persistAttachmentBytes(id, bytes, mime)
            AttachmentsStorage-->>RestGeminiCore: imagePath
            alt first image and liveMessage is null
                RestGeminiCore->>RestGeminiCore: create liveMessage(role MODEL, attachmentPaths=[imagePath])
                RestGeminiCore-->>ChatViewModel: addUiMessage(liveMessage)
            else subsequent image
                RestGeminiCore->>RestGeminiCore: liveMessage.copy(attachmentPaths + imagePath)
                RestGeminiCore-->>ChatViewModel: replaceUiMessage(liveMessage)
            end
        else text part
            RestGeminiCore->>RestGeminiCore: append to accumulated text
        end
    end

    ChatViewModel-->>ChatScreen: updated model message with attachmentPaths
    ChatScreen->>ChatScreen: MessageBubble(modelMessage)
    ChatScreen->>ChatScreen: AttachmentThumbnails(message.attachmentPaths)
    ChatScreen-->>User: Model bubble showing text and image thumbnails
Loading

Class diagram for Imagen tool, core, and UI integration

classDiagram
    class RestGeminiCore {
        - Context appContext
        - SecurePrefs prefs
        - ToolRegistry registry
        + String imagenModel()
        + void setImagenModel(name String)
        + String~nullable~ persistAttachmentBytes(id String, bytes ByteArray, mime String)
        + String buildRequestBody()
        + void setModel(name String)
        + List~String~ listModels()
        + String currentModel()
        + Boolean isAutoCompressEnabled()
        + void setAutoCompressEnabled(enabled Boolean)
        + Float autoCompressThreshold()
        + void setAutoCompressThreshold(fraction Float)
        + void handleToolResult(result ToolCallResult)
        + void parseInlineData(part JSONObject)
        <<companion>>
        + String DEFAULT_MODEL
        + String DEFAULT_IMAGEN_MODEL
        + List~String~ AVAILABLE_MODELS
        + List~String~ AVAILABLE_IMAGEN_MODELS
        + Boolean modelEmitsImages(modelName String)
    }

    class GenerateImageTool {
        - () -> String getApiKey
        - () -> String getModel
        - (String, ByteArray, String) -> String~nullable~ persist
        + ToolSpec spec
        + ToolCallResult execute(call ToolCall)
    }

    class SecurePrefs {
        - SharedPreferences plain
        + String~nullable~ model
        + String~nullable~ imagenModel
        + String~nullable~ workspaceUri
        + Boolean autoApprove
        + Boolean autoCompressEnabled
        + Float autoCompressThreshold
    }

    class Workspace {
        - DocumentFile~nullable~ root
        - String label
        + void init()
        + String rootLabel()
        + Uri~nullable~ rootUri()
        + void setFile(file File)
        + void setTree(uri Uri)
        + String absolutePath()
        + String~nullable~ unreachableReason()
    }

    class ChatViewModel {
        - RestGeminiCore core
        - MutableStateFlow~String~ _model
        - MutableStateFlow~List~String~~ _availableModels
        - MutableStateFlow~String~ _imagenModel
        - MutableStateFlow~String~ _workspaceLabel
        - MutableStateFlow~String~ _workspacePath
        - MutableStateFlow~String~ _workspaceReason
        - MutableStateFlow~String~ _workspaceUri
        + StateFlow~String~ model
        + StateFlow~List~String~~ availableModels
        + StateFlow~String~ imagenModel
        + StateFlow~String~ workspaceLabel
        + StateFlow~String~ workspaceUri
        + void setModel(name String)
        + void setImagenModel(name String)
        + void setProjectFolder(uri String)
        + void refreshWorkspace()
    }

    class ToolCallResult {
        + String callId
        + Boolean ok
        + String output
        + Boolean truncated
        + List~String~ attachmentPaths
    }

    class GeminiMessage {
        + String id
        + String text
        + Boolean isUser
        + Long timestamp
        + MessageRole role
        + ToolCallResult~nullable~ toolResult
        + List~String~ attachmentPaths
    }

    class ChatScreen {
        + void ChatScreen(viewModel ChatViewModel, onEvent Function)
        + void openWorkspaceFolder(context Context, workspaceUri String~nullable~)
        + void AttachmentThumbnails(paths List~String~)
        + void MessageBubble(message GeminiMessage)
    }

    class SettingsSheet {
        + void SettingsSheet(viewModel ChatViewModel)
    }

    class ToolRegistry {
        + void register(tool Tool)
    }

    class Tool {
        <<interface>>
        + ToolSpec spec
        + ToolCallResult execute(call ToolCall)
    }

    RestGeminiCore --> SecurePrefs : uses
    RestGeminiCore --> Workspace : uses
    RestGeminiCore --> ToolRegistry : owns
    RestGeminiCore ..> GenerateImageTool : registers
    RestGeminiCore --> ToolCallResult : creates
    RestGeminiCore --> GeminiMessage : creates
    RestGeminiCore "1" --> "*" Tool : invokes

    GenerateImageTool ..|> Tool
    GenerateImageTool --> ToolCallResult : returns

    ChatViewModel --> RestGeminiCore : delegates
    ChatViewModel --> Workspace : reads rootUri

    ChatScreen --> ChatViewModel : observes state
    ChatScreen --> GeminiMessage : renders

    SettingsSheet --> ChatViewModel : reads and sets imagenModel

    SecurePrefs --> Workspace : stores workspaceUri

    GeminiMessage --> ToolCallResult : toolResult
    ToolRegistry --> Tool : holds
Loading

File-Level Changes

Change Details Files
Wire up Imagen image generation as a tool with persisted attachments and plumb attachment paths through tool results and messages.
  • Register a new GenerateImageTool in the tool registry with dynamic API key/model lookup and shared byte persistence.
  • Add imagenModel accessors and constants in core plus a helper to persist image bytes to an attachments directory.
  • Extend ToolCallResult and tool-completed message creation so tool-produced attachments are stored on GeminiMessage and available to the UI.
core-bridge/src/main/kotlin/com/gemini/bridge/RestGeminiCore.kt
core-bridge/src/main/kotlin/com/gemini/bridge/tools/GenerateImageTool.kt
domain/src/main/kotlin/com/gemini/domain/ToolTypes.kt
core-bridge/src/main/kotlin/com/gemini/bridge/storage/SecurePrefs.kt
Enable Gemini 2.5 models that emit images to request and handle inline image data, persisting it for display in model bubbles.
  • Gate request body generationConfig.responseModalities on a modelEmitsImages heuristic that looks for -image in the model name.
  • Parse inlineData parts in streaming responses, decode base64 image bytes, persist them via shared helper, and update or create the live model message with attachment paths.
core-bridge/src/main/kotlin/com/gemini/bridge/RestGeminiCore.kt
Expose Imagen model selection in settings and propagate the chosen model through core and view model state.
  • Add imagenModel state and setter to ChatViewModel, backed by new prefs key in SecurePrefs and RestGeminiCore accessors.
  • Introduce AVAILABLE_IMAGEN_MODELS and DEFAULT_IMAGEN_MODEL constants, and a settings-sheet section with an Imagen model dropdown plus custom ID field.
app/src/main/kotlin/com/gemini/app/ui/chat/ChatViewModel.kt
core-bridge/src/main/kotlin/com/gemini/bridge/RestGeminiCore.kt
core-bridge/src/main/kotlin/com/gemini/bridge/storage/SecurePrefs.kt
app/src/main/kotlin/com/gemini/app/ui/settings/SettingsSheet.kt
Render image attachments as thumbnails in model/tool bubbles and persist workspace URIs so the UI can open folders.
  • Update MessageBubble to show AttachmentThumbnails before text for messages with attachment paths, mirroring user attachments.
  • Expose Workspace.rootUri so the view model can surface the workspace URI, and wire workspaceUri through ChatViewModel state.
app/src/main/kotlin/com/gemini/app/ui/chat/ChatScreen.kt
core-bridge/src/main/kotlin/com/gemini/bridge/workspace/Workspace.kt
app/src/main/kotlin/com/gemini/app/ui/chat/ChatViewModel.kt
Add top-bar quick pickers for model and workspace folder with intents to open or change the folder.
  • Make the model label in the chat top bar clickable to open a DropdownMenu listing available models and a More models… entry that opens settings.
  • Make the workspace label clickable to open a dropdown with Open folder (using best-effort ACTION_VIEW handling for tree and file URIs) and Change folder (launching OpenDocumentTree).
app/src/main/kotlin/com/gemini/app/ui/chat/ChatScreen.kt

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented Apr 22, 2026 •

Copy link
Copy Markdown

Warning

Rate limit exceeded

@aciderix has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 54 minutes and 38 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 54 minutes and 38 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 24e3ab40-7459-447c-b094-751d2af4de82

📥 Commits

Reviewing files that changed from the base of the PR and between 1f14b93 and 55f2b47.

📒 Files selected for processing (1)
  • core-bridge/src/main/kotlin/com/gemini/bridge/RestGeminiCore.kt
📝 Walkthrough

Walkthrough

This PR implements end-to-end image generation support by introducing the GenerateImageTool, persisting generated images to the file system, exposing Imagen model selection in settings, rendering image attachments in chat, and adding workspace folder management capabilities.

Changes

Cohort / File(s) Summary
UI State & Reactive Bindings
app/src/main/kotlin/com/gemini/app/ui/chat/ChatViewModel.kt
Added observable state for workspaceUri and imagenModel, plus setImagenModel() method to update the selected Imagen model through core.
Chat Screen UI & Interactions
app/src/main/kotlin/com/gemini/app/ui/chat/ChatScreen.kt
Added dropdown menus for model and workspace folder selection, activity launcher for folder picker, attachment thumbnail rendering, and helper to open workspace folders via intent.
Settings & Configuration
app/src/main/kotlin/com/gemini/app/ui/settings/SettingsSheet.kt
Extended settings UI with "Image generation model (Imagen)" section offering dropdown model selection and custom model ID input.
Core Image Generation Tool
core-bridge/src/main/kotlin/com/gemini/bridge/tools/GenerateImageTool.kt
New tool implementing image generation via Gemini API with prompt, aspect ratio, and image count parameters; handles base64 decoding and attachment persistence.
Core Tool Integration & Persistence
core-bridge/src/main/kotlin/com/gemini/bridge/RestGeminiCore.kt
Integrated GenerateImageTool into tool registry, added persistAttachmentBytes() for image storage, updated SSE parsing to extract inline image data, added Imagen model preferences APIs, and extended request building for image-emitting models.
Preference Storage & Workspace
core-bridge/src/main/kotlin/com/gemini/bridge/storage/SecurePrefs.kt, core-bridge/src/main/kotlin/com/gemini/bridge/workspace/Workspace.kt
Added imagenModel preference storage and rootUri() method to expose workspace root URI.
Domain Type Extension
domain/src/main/kotlin/com/gemini/domain/ToolTypes.kt
Extended ToolCallResult data class with attachmentPaths field to carry image file paths from tool execution.

Sequence Diagram

sequenceDiagram
    participant User
    participant ChatScreen
    participant GenerateImageTool
    participant GeminiAPI as Gemini API
    participant Persistence
    participant ChatViewModel

    User->>ChatScreen: Sends message triggering image generation
    ChatScreen->>ChatViewModel: Captures message with generate_image intent
    ChatViewModel->>GenerateImageTool: Executes tool via RestGeminiCore
    GenerateImageTool->>GeminiAPI: POST /v1beta/models/$model:predict<br/>(prompt, aspectRatio, sampleCount)
    GeminiAPI-->>GenerateImageTool: Returns predictions with<br/>bytesBase64Encoded & mimeType
    GenerateImageTool->>Persistence: persistAttachmentBytes(id, bytes, mime)
    Persistence-->>GenerateImageTool: Returns file path(s)
    GenerateImageTool-->>ChatViewModel: Returns ToolCallResult<br/>with attachmentPaths
    ChatViewModel->>ChatScreen: Updates message with<br/>attachmentPaths
    ChatScreen->>ChatScreen: Renders AttachmentThumbnails
    ChatScreen-->>User: Displays generated images
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly Related PRs

  • PR #5: Implements complementary image-attachment support across the same persistence and message-rendering pipeline, with overlapping changes to RestGeminiCore, ChatViewModel, and ChatScreen.
  • PR #1: Directly related predecessor that establishes the foundation for Imagen model selection APIs, preferences storage, and image-generation tool wiring in RestGeminiCore and SecurePrefs.

Poem

🐰 ✨ A rabbit hops through bytes and dreams,
Generating images in API streams,
From base64 decoded to folder bright,
Attachments bloom—the chat's delight!
Models dropdown, workspace in sight—
Images painted, purely right! 🎨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 23.08% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the two main features added: image generation capabilities (Imagen + Gemini 2.5) and clickable top-bar quick pickers for model and folder selection.
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.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/image-generation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@sourcery-ai sourcery-ai 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.

Hey - I've found 2 issues, and left some high level feedback:

  • In persistAttachmentBytes, the mime-type checks use case-sensitive contains which may miss valid types like image/PNG; consider using contains("png", ignoreCase = true) (and similarly for the other types) or parsing the subtype more robustly before mapping to an extension.
  • In openWorkspaceFolder, the fallback Intent(Intent.ACTION_VIEW, target) does not carry FLAG_GRANT_READ_URI_PERMISSION, so opening a content:// URI may fail where the first attempt would have worked; propagate the read permission flag to the fallback intent as well.
  • The new top-bar model and folder pickers rely on clickable Text with a small padded area; for better accessibility/usability, consider wrapping them in a larger hit target (e.g. Modifier.sizeIn(minHeight = ...) or an IconButton-style container) so they are easier to tap.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `persistAttachmentBytes`, the mime-type checks use case-sensitive `contains` which may miss valid types like `image/PNG`; consider using `contains("png", ignoreCase = true)` (and similarly for the other types) or parsing the subtype more robustly before mapping to an extension.
- In `openWorkspaceFolder`, the fallback `Intent(Intent.ACTION_VIEW, target)` does not carry `FLAG_GRANT_READ_URI_PERMISSION`, so opening a `content://` URI may fail where the first attempt would have worked; propagate the read permission flag to the fallback intent as well.
- The new top-bar model and folder pickers rely on clickable `Text` with a small padded area; for better accessibility/usability, consider wrapping them in a larger hit target (e.g. `Modifier.sizeIn(minHeight = ...)` or an `IconButton`-style container) so they are easier to tap.

## Individual Comments

### Comment 1
<location path="app/src/main/kotlin/com/gemini/app/ui/chat/ChatScreen.kt" line_range="1218-1220" />
<code_context>
+    } else uri
+    intent.setDataAndType(target, "vnd.android.document/directory")
+    runCatching { context.startActivity(intent) }.onFailure {
+        // Retry without mime type — some Files apps only match on the URI.
+        val fallback = Intent(Intent.ACTION_VIEW, target)
+            .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+        runCatching { context.startActivity(fallback) }.onFailure {
+            Toast.makeText(
</code_context>
<issue_to_address>
**issue (bug_risk):** Fallback folder intent drops the read-permission flag, which can break `content://` URIs.

In `openWorkspaceFolder`, the fallback `Intent` created on retry omits `FLAG_GRANT_READ_URI_PERMISSION`, unlike the primary intent. For SAF `content://` URIs this means the target app may not be able to read the folder on the fallback path. Please mirror the permission flags from the primary intent on the fallback (at least `FLAG_GRANT_READ_URI_PERMISSION`) so both code paths behave consistently.
</issue_to_address>

### Comment 2
<location path="core-bridge/src/main/kotlin/com/gemini/bridge/RestGeminiCore.kt" line_range="193-206" />
<code_context>
+     * by the GenerateImageTool (for Imagen outputs). Returns the absolute path
+     * that can be stored in `GeminiMessage.attachmentPaths`.
+     */
+    fun persistAttachmentBytes(id: String, bytes: ByteArray, mime: String): String? = runCatching {
+        val ext = when {
+            mime.contains("png") -> "png"
+            mime.contains("webp") -> "webp"
+            mime.contains("gif") -> "gif"
+            mime.contains("heic") -> "heic"
+            mime.contains("heif") -> "heif"
+            else -> "jpg"
+        }
+        val dir = java.io.File(appContext.filesDir, "attachments").also { it.mkdirs() }
+        val file = java.io.File(dir, "$id.$ext")
+        file.writeBytes(bytes)
+        file.absolutePath
+    }.getOrNull()
</code_context>
<issue_to_address>
**suggestion (bug_risk):** MIME-type checks are case-sensitive and may misclassify some valid image types.

In `persistAttachmentBytes`, `mime.contains("png")` / `"webp"` / etc. are case-sensitive, so values like `image/PNG` or `IMAGE/WEBP` will incorrectly fall back to `jpg`. Normalize once (e.g., `val m = mime.lowercase()`) and run the `contains` checks on `m` to avoid this misclassification.

```suggestion
    fun persistAttachmentBytes(id: String, bytes: ByteArray, mime: String): String? = runCatching {
        val m = mime.lowercase()
        val ext = when {
            m.contains("png") -> "png"
            m.contains("webp") -> "webp"
            m.contains("gif") -> "gif"
            m.contains("heic") -> "heic"
            m.contains("heif") -> "heif"
            else -> "jpg"
        }
        val dir = java.io.File(appContext.filesDir, "attachments").also { it.mkdirs() }
        val file = java.io.File(dir, "$id.$ext")
        file.writeBytes(bytes)
        file.absolutePath
    }.getOrNull()
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +1218 to +1220
// Retry without mime type — some Files apps only match on the URI.
val fallback = Intent(Intent.ACTION_VIEW, target)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): Fallback folder intent drops the read-permission flag, which can break content:// URIs.

In openWorkspaceFolder, the fallback Intent created on retry omits FLAG_GRANT_READ_URI_PERMISSION, unlike the primary intent. For SAF content:// URIs this means the target app may not be able to read the folder on the fallback path. Please mirror the permission flags from the primary intent on the fallback (at least FLAG_GRANT_READ_URI_PERMISSION) so both code paths behave consistently.

Comment on lines +193 to +206
fun persistAttachmentBytes(id: String, bytes: ByteArray, mime: String): String? = runCatching {
val ext = when {
mime.contains("png") -> "png"
mime.contains("webp") -> "webp"
mime.contains("gif") -> "gif"
mime.contains("heic") -> "heic"
mime.contains("heif") -> "heif"
else -> "jpg"
}
val dir = java.io.File(appContext.filesDir, "attachments").also { it.mkdirs() }
val file = java.io.File(dir, "$id.$ext")
file.writeBytes(bytes)
file.absolutePath
}.getOrNull()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (bug_risk): MIME-type checks are case-sensitive and may misclassify some valid image types.

In persistAttachmentBytes, mime.contains("png") / "webp" / etc. are case-sensitive, so values like image/PNG or IMAGE/WEBP will incorrectly fall back to jpg. Normalize once (e.g., val m = mime.lowercase()) and run the contains checks on m to avoid this misclassification.

Suggested change
fun persistAttachmentBytes(id: String, bytes: ByteArray, mime: String): String? = runCatching {
val ext = when {
mime.contains("png") -> "png"
mime.contains("webp") -> "webp"
mime.contains("gif") -> "gif"
mime.contains("heic") -> "heic"
mime.contains("heif") -> "heif"
else -> "jpg"
}
val dir = java.io.File(appContext.filesDir, "attachments").also { it.mkdirs() }
val file = java.io.File(dir, "$id.$ext")
file.writeBytes(bytes)
file.absolutePath
}.getOrNull()
fun persistAttachmentBytes(id: String, bytes: ByteArray, mime: String): String? = runCatching {
val m = mime.lowercase()
val ext = when {
m.contains("png") -> "png"
m.contains("webp") -> "webp"
m.contains("gif") -> "gif"
m.contains("heic") -> "heic"
m.contains("heif") -> "heif"
else -> "jpg"
}
val dir = java.io.File(appContext.filesDir, "attachments").also { it.mkdirs() }
val file = java.io.File(dir, "$id.$ext")
file.writeBytes(bytes)
file.absolutePath
}.getOrNull()

@aciderix
aciderix merged commit 6a195de into main Apr 22, 2026
3 checks passed
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.

2 participants