Skip to content
This repository was archived by the owner on Mar 30, 2026. It is now read-only.

feat: add image generation support (gemini-3.1-flash-image)#522

Closed
bentcc wants to merge 1 commit intoNoeFabris:devfrom
bentcc:dev
Closed

feat: add image generation support (gemini-3.1-flash-image)#522
bentcc wants to merge 1 commit intoNoeFabris:devfrom
bentcc:dev

Conversation

@bentcc
Copy link
Copy Markdown
Contributor

@bentcc bentcc commented Mar 1, 2026

Summary

  • Add antigravity-gemini-3.1-flash-image model definition with per-model validation for aspect ratios and image sizes
  • Implement --resolution and --aspect-ratio prompt flags that are parsed from user input and stripped before sending to Gemini
  • Change image output directory from ~/.opencode/generated-images/ to ./nanobanana/ (relative to process.cwd())
  • Remove all gemini-3-pro-image references (model was removed by Google from Antigravity — returns 404)

Details

New model: antigravity-gemini-3.1-flash-image

  • Context: 131,072 tokens, output: 32,768 tokens
  • Input modalities: text, image, pdf; output: text, image
  • Thinking support: minimal (default) and high variants
  • Extended aspect ratios (4:1, 8:1) and image size 0.5K (flash-only)

Prompt flags

Users can inline --resolution=4K and --aspect-ratio=16:9 in their prompt text. Flags are extracted, validated per-model, and stripped before the prompt reaches Gemini. Priority: prompt flags > env vars (OPENCODE_IMAGE_SIZE, OPENCODE_IMAGE_ASPECT_RATIO) > defaults.

Per-model validation

  • Flash image models get extended aspect ratios (4:1, 8:1) and 0.5K image size
  • Non-flash image models get the base set
  • Invalid values log a warning and fall back to defaults

Files changed (14 files, +805 / -53)

  • models.ts — flash-image model definition added, pro-image removed
  • gemini.tsbuildImageGenerationConfig() now accepts model + overrides, per-model validation via getValidAspectRatios() / getValidImageSizes()
  • prompt-flags.ts (new) — parses --resolution and --aspect-ratio from prompt text
  • model-resolver.ts — flash-image thinking support (minimal/high), pro-image cleanup
  • request.ts — integrates prompt flag parsing into image pipeline, flash-image thinking config
  • image-saver.ts — output dir changed to ./nanobanana/
  • index.ts — barrel exports for new modules
  • README.md — model table, JSON config, usage examples with flag documentation

Test Plan

  • 975 tests pass (0 failures), including 27 new tests for prompt flag parsing
  • npm run build compiles cleanly
  • npm run typecheck passes

Attribution

All code in this PR was written by Claude Opus 4.6 (Anthropic), pair-programmed with a human operator via OpenCode.

…lags, and nanobanana output

- Add antigravity-gemini-3-pro-image and antigravity-gemini-3.1-flash-image model definitions
- Per-model aspect ratio validation (flash supports 1:4, 4:1, 1:8, 8:1; pro does not)
- Per-model imageSize validation (flash supports 0.5K; pro does not)
- Parse --resolution and --aspect-ratio flags from prompt text (stripped before sending to Gemini)
- Prompt flag overrides take priority over env vars (OPENCODE_IMAGE_SIZE, OPENCODE_IMAGE_ASPECT_RATIO)
- Change image output directory from ~/.opencode/generated-images/ to ./nanobanana/
- Flash-image thinking support (minimal/high)
- Update README with image model config and usage docs
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 1, 2026

Walkthrough

This pull request introduces a new image-generation model called antigravity-gemini-3.1-flash-image with thinking capabilities. It adds a prompt flag parser to extract --resolution and --aspect-ratio parameters from user prompts, implements model-aware image validation (with extended support for flash-image variants), updates the request pipeline to parse and apply these flags to image configuration, modifies the image output directory to use the current working directory instead of the home directory, and updates model definitions, test expectations, and model resolution logic to recognize and handle flash image models with thinking support.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main addition: image generation support for the gemini-3.1-flash-image model, which aligns with the primary changeset focus.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, covering new model definition, prompt flags, directory changes, and removed references with supporting details and test results.
Docstring Coverage ✅ Passed Docstring coverage is 90.00% which is sufficient. The required threshold is 80.00%.

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


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.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/plugin/transform/prompt-flags.ts (1)

45-56: Prefer last-occurrence precedence when the same flag appears multiple times.

Current extraction captures only the first --resolution / --aspect-ratio. If users append a later override, it is ignored.

♻️ Suggested change
-  const resolutionMatch = RESOLUTION_PATTERN.exec(prompt)
-  if (resolutionMatch?.[1]) {
-    result.resolution = resolutionMatch[1]
-  }
+  let resolutionMatch: RegExpExecArray | null
+  while ((resolutionMatch = RESOLUTION_PATTERN.exec(prompt)) !== null) {
+    if (resolutionMatch[1]) {
+      result.resolution = resolutionMatch[1]
+    }
+  }
   // Reset lastIndex for global regex
   RESOLUTION_PATTERN.lastIndex = 0

   // Extract --aspect-ratio
-  const aspectRatioMatch = ASPECT_RATIO_PATTERN.exec(prompt)
-  if (aspectRatioMatch?.[1]) {
-    result.aspectRatio = aspectRatioMatch[1]
-  }
+  let aspectRatioMatch: RegExpExecArray | null
+  while ((aspectRatioMatch = ASPECT_RATIO_PATTERN.exec(prompt)) !== null) {
+    if (aspectRatioMatch[1]) {
+      result.aspectRatio = aspectRatioMatch[1]
+    }
+  }
   ASPECT_RATIO_PATTERN.lastIndex = 0
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/plugin/transform/prompt-flags.ts` around lines 45 - 56, Current code uses
RESOLUTION_PATTERN.exec(prompt) and ASPECT_RATIO_PATTERN.exec(prompt) which only
return the first match; change each extraction to iterate over all matches and
assign the capture group on each iteration so the last occurrence wins (e.g.,
use a while loop with RESOLUTION_PATTERN.exec(prompt) and set result.resolution
= match[1] each iteration, then reset RESOLUTION_PATTERN.lastIndex = 0; do the
same for ASPECT_RATIO_PATTERN/result.aspectRatio), or use prompt.matchAll(...)
and take the last match; ensure you still reset lastIndex for global regexes
after scanning.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/plugin/transform/gemini.test.ts`:
- Around line 648-651: The test "returns false for gemini-2.5-flash-image"
currently asserts true, contradicting the test intent and flash-model gating;
update the assertion in the test (in src/plugin/transform/gemini.test.ts) to
expect(false) for the input "gemini-2.5-flash-image" so it aligns with the
isFlashImageModel(model: string) behavior and the regex that only matches
gemini-3(-3.1)-flash-image variants.

In `@src/plugin/transform/model-resolver.ts`:
- Around line 221-228: The returned object leaves the original tier (e.g.,
"low"/"medium") while coercing thinkingLevel to flashImageThinkingLevel ("high"
or "minimal"), causing inconsistent state; update the returned tier to the
effective flash-image level by replacing the spread that conditionally returns
tier with one that sets tier: flashImageThinkingLevel (or only include tier when
it matches the effective level), so in the function that constructs the result
(references: resolvedModel, flashImageThinkingLevel, thinkingLevel, tier,
quotaPreference) ensure the tier property reflects flashImageThinkingLevel
instead of the original tier value.

---

Nitpick comments:
In `@src/plugin/transform/prompt-flags.ts`:
- Around line 45-56: Current code uses RESOLUTION_PATTERN.exec(prompt) and
ASPECT_RATIO_PATTERN.exec(prompt) which only return the first match; change each
extraction to iterate over all matches and assign the capture group on each
iteration so the last occurrence wins (e.g., use a while loop with
RESOLUTION_PATTERN.exec(prompt) and set result.resolution = match[1] each
iteration, then reset RESOLUTION_PATTERN.lastIndex = 0; do the same for
ASPECT_RATIO_PATTERN/result.aspectRatio), or use prompt.matchAll(...) and take
the last match; ensure you still reset lastIndex for global regexes after
scanning.

ℹ️ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between 610bbcc and 22ad11b.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (13)
  • README.md
  • src/plugin/accounts.test.ts
  • src/plugin/config/models.test.ts
  • src/plugin/config/models.ts
  • src/plugin/image-saver.ts
  • src/plugin/request.ts
  • src/plugin/transform/gemini.test.ts
  • src/plugin/transform/gemini.ts
  • src/plugin/transform/index.ts
  • src/plugin/transform/model-resolver.test.ts
  • src/plugin/transform/model-resolver.ts
  • src/plugin/transform/prompt-flags.test.ts
  • src/plugin/transform/prompt-flags.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Greptile Review
  • GitHub Check: Greptile Review
🧰 Additional context used
🧬 Code graph analysis (4)
src/plugin/transform/prompt-flags.ts (1)
src/plugin/transform/index.ts (3)
  • ParsedPromptFlags (69-69)
  • parsePromptFlags (66-66)
  • extractLastUserPrompt (67-67)
src/plugin/transform/prompt-flags.test.ts (1)
src/plugin/transform/prompt-flags.ts (2)
  • parsePromptFlags (39-73)
  • extractLastUserPrompt (82-109)
src/plugin/transform/gemini.test.ts (1)
src/plugin/transform/gemini.ts (5)
  • isImageGenerationModel (152-158)
  • isFlashImageModel (245-247)
  • buildImageGenerationConfig (271-302)
  • getValidAspectRatios (225-230)
  • getValidImageSizes (235-240)
src/plugin/transform/model-resolver.test.ts (2)
src/plugin/transform/index.ts (1)
  • resolveModelWithTier (22-22)
src/plugin/transform/model-resolver.ts (1)
  • resolveModelWithTier (169-296)
🔇 Additional comments (17)
src/plugin/accounts.test.ts (1)

1166-1177: Model name migration in strict-header wait-time tests looks correct.

This update keeps coverage intact while moving the test to the flash-image model key.

src/plugin/image-saver.ts (1)

13-16: Output directory migration to cwd-relative nanobanana is cleanly implemented.

The path change and doc updates are consistent, and directory bootstrapping remains intact.

src/plugin/config/models.ts (1)

70-81: New flash-image model definition is well-structured and consistent with the catalog.

The added entry cleanly expresses limits, modalities, and variant thinking levels.

src/plugin/transform/prompt-flags.ts (1)

82-109: Backward scan for the last user text prompt is solid.

The function correctly prioritizes the most recent actionable user text and returns stable indices for in-place mutation.

src/plugin/transform/gemini.ts (1)

225-302: Model-aware image config validation and override precedence are implemented cleanly.

The helper split (getValidAspectRatios, getValidImageSizes, buildImageGenerationConfig) makes behavior explicit and testable.

src/plugin/transform/index.ts (1)

53-69: Transform index exports are updated coherently for the new image/prompt utilities.

The re-export surface now cleanly exposes the new flash-image and prompt-flag capabilities.

src/plugin/request.ts (2)

976-1010: Prompt-flag extraction + imageConfig override wiring is well integrated.

This segment cleanly parses inline image flags, removes them from user text, and applies model-aware config with correct flash-image thinking handling.


1787-1801: Image-size rejection annotation is a useful recovery hook.

Setting x-antigravity-image-error=imagesize_unsupported here is a good signal for downstream fallback behavior.

README.md (3)

125-125: Model table addition looks correct.

Line 125 cleanly introduces the new image model and its variants in the reference list.


152-175: Image-generation usage docs are clear and actionable.

The examples and precedence explanation are easy to follow and match the expected UX for overrides and defaults.


231-239: Full configuration block is consistent with the new model contract.

Good addition for copy-paste onboarding.

src/plugin/config/models.test.ts (1)

22-22: Model definition coverage update is correct.

Line 22 appropriately extends the expected key list for the new model.

src/plugin/transform/gemini.test.ts (2)

688-838: Great coverage for image config precedence and validation.

These tests thoroughly cover override priority, normalization, and model-specific acceptance/rejection paths.


841-879: Helper API tests are well-scoped and valuable.

Nice addition of direct assertions for base vs flash-specific option sets.

src/plugin/transform/prompt-flags.test.ts (1)

4-196: Comprehensive parser/extractor test coverage.

The scenarios cover practical prompt shapes and edge cases well, especially multi-part user content traversal.

src/plugin/transform/model-resolver.test.ts (2)

82-85: CLI-first routing expectation for image models is correctly enforced.

Good guard that image models keep antigravity quota semantics.


158-190: Flash-image resolver behavior is well covered.

These assertions meaningfully lock in tier-to-thinking mapping for the new model variants.

Comment on lines +648 to +651
it("returns false for gemini-2.5-flash-image", () => {
// 2.5 flash image is not a gemini-3 flash image model
expect(isFlashImageModel("gemini-2.5-flash-image")).toBe(true);
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Align the assertion with the test intent and flash-model gating.

Line 650 currently asserts true, but the test title/comment says false. This contradiction can hide incorrect classification behavior for non-3.1 models.

🔧 Proposed fix
-    it("returns false for gemini-2.5-flash-image", () => {
-      // 2.5 flash image is not a gemini-3 flash image model
-      expect(isFlashImageModel("gemini-2.5-flash-image")).toBe(true);
-    });
+    it("returns false for gemini-2.5-flash-image", () => {
+      expect(isFlashImageModel("gemini-2.5-flash-image")).toBe(false);
+    });
// src/plugin/transform/gemini.ts (outside this line range)
export function isFlashImageModel(model: string): boolean {
  return /gemini-3(?:\.1)?-flash-image(?:-preview)?/i.test(model);
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it("returns false for gemini-2.5-flash-image", () => {
// 2.5 flash image is not a gemini-3 flash image model
expect(isFlashImageModel("gemini-2.5-flash-image")).toBe(true);
});
it("returns false for gemini-2.5-flash-image", () => {
expect(isFlashImageModel("gemini-2.5-flash-image")).toBe(false);
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/plugin/transform/gemini.test.ts` around lines 648 - 651, The test
"returns false for gemini-2.5-flash-image" currently asserts true, contradicting
the test intent and flash-model gating; update the assertion in the test (in
src/plugin/transform/gemini.test.ts) to expect(false) for the input
"gemini-2.5-flash-image" so it aligns with the isFlashImageModel(model: string)
behavior and the regex that only matches gemini-3(-3.1)-flash-image variants.

Comment on lines +221 to +228
const flashImageThinkingLevel = tier === "high" ? "high" : "minimal";
return {
actualModel: resolvedModel,
isThinkingModel: true,
isImageModel: true,
thinkingLevel: flashImageThinkingLevel,
...(tier ? { tier } : {}),
quotaPreference,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Normalize returned tier to the effective flash-image level.

In this branch, tier="low" / tier="medium" is coerced to thinkingLevel="minimal", but the original tier is still returned. That creates inconsistent resolved state.

🔧 Suggested fix
-      const flashImageThinkingLevel = tier === "high" ? "high" : "minimal";
+      const flashImageThinkingLevel = tier === "high" ? "high" : "minimal";
+      const normalizedTier: ThinkingTier | undefined =
+        tier ? (flashImageThinkingLevel === "high" ? "high" : "minimal") : undefined;
       return {
         actualModel: resolvedModel,
         isThinkingModel: true,
         isImageModel: true,
         thinkingLevel: flashImageThinkingLevel,
-        ...(tier ? { tier } : {}),
+        ...(normalizedTier ? { tier: normalizedTier } : {}),
         quotaPreference,
         explicitQuota,
       };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const flashImageThinkingLevel = tier === "high" ? "high" : "minimal";
return {
actualModel: resolvedModel,
isThinkingModel: true,
isImageModel: true,
thinkingLevel: flashImageThinkingLevel,
...(tier ? { tier } : {}),
quotaPreference,
const flashImageThinkingLevel = tier === "high" ? "high" : "minimal";
const normalizedTier: ThinkingTier | undefined =
tier ? (flashImageThinkingLevel === "high" ? "high" : "minimal") : undefined;
return {
actualModel: resolvedModel,
isThinkingModel: true,
isImageModel: true,
thinkingLevel: flashImageThinkingLevel,
...(normalizedTier ? { tier: normalizedTier } : {}),
quotaPreference,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/plugin/transform/model-resolver.ts` around lines 221 - 228, The returned
object leaves the original tier (e.g., "low"/"medium") while coercing
thinkingLevel to flashImageThinkingLevel ("high" or "minimal"), causing
inconsistent state; update the returned tier to the effective flash-image level
by replacing the spread that conditionally returns tier with one that sets tier:
flashImageThinkingLevel (or only include tier when it matches the effective
level), so in the function that constructs the result (references:
resolvedModel, flashImageThinkingLevel, thinkingLevel, tier, quotaPreference)
ensure the tier property reflects flashImageThinkingLevel instead of the
original tier value.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Mar 1, 2026

Greptile Summary

This PR adds image generation support via a new antigravity-gemini-3.1-flash-image model, introducing inline prompt flags (--resolution, --aspect-ratio), per-model validation of image parameters, and removes the defunct gemini-3-pro-image model. The core pipeline logic is well-structured and the 27 new tests provide good coverage, but two issues in image-saver.ts need attention before merging.

Key changes:

  • New antigravity-gemini-3.1-flash-image model definition with minimal/high thinking variants and flash-exclusive 0.5K size and 4:1/8:1 aspect ratios
  • prompt-flags.ts parses and strips --resolution / --aspect-ratio from the last user message before it reaches Gemini
  • buildImageGenerationConfig() now accepts a model name and prompt-flag overrides for per-model validation with a priority chain (flags > env vars > defaults)
  • Breaking change: image output directory changed from ~/.opencode/generated-images/ (stable absolute path) to ./nanobanana/ (CWD-relative), which will scatter images to different locations depending on where the CLI is invoked — this deserves reconsideration
  • The open "${filePath}" hint returned in image markdown is macOS-only and will be confusing on Linux/Windows
  • Module-level global regex constants in prompt-flags.ts carry mutable lastIndex state that is manually reset; this works today but is fragile for future maintenance

Confidence Score: 3/5

  • Safe to merge functionally, but the CWD-relative output directory is a UX regression that should be addressed first.
  • The image generation pipeline logic, per-model validation, and thinking-tier resolution are all correct and well-tested. The main concern blocking a clean merge is the change to a CWD-relative output directory (./nanobanana/), which breaks the predictability of where images are saved and is a behavioural regression from the previous absolute path. The macOS-only open hint and the mutable global regex state are lower-severity issues but still worth fixing.
  • Pay close attention to src/plugin/image-saver.ts (output directory path and platform hint) and src/plugin/transform/prompt-flags.ts (module-level mutable regex state).

Important Files Changed

Filename Overview
src/plugin/transform/prompt-flags.ts New module that parses --resolution and --aspect-ratio flags from prompt text. Logic is correct, but module-level global regex constants share mutable lastIndex state across calls, which is fragile.
src/plugin/image-saver.ts Output directory changed to CWD-relative ./nanobanana/, which scatters images depending on invocation directory; macOS-only open hint is embedded in returned markdown.
src/plugin/transform/gemini.ts Adds per-model validation for aspect ratios and image sizes, flash model detection, and buildImageGenerationConfig with priority override chain. Logic is sound.
src/plugin/transform/model-resolver.ts Adds flash-image thinking tier resolution (minimal/high) and removes pro-image references. Routing logic is correct; image models correctly force Antigravity quota via explicitQuota.
src/plugin/config/models.ts Adds antigravity-gemini-3.1-flash-image model definition with correct context/output limits, modalities, and minimal/high thinking variants. Removes deprecated pro-image entry.
src/plugin/request.ts Integrates prompt-flag parsing into the image pipeline; correctly mutates the last user prompt part in-place before sending. Flash-image thinking config and safety settings are properly handled.
src/plugin/transform/index.ts Barrel exports updated to expose new prompt-flag helpers and flash-image gemini utilities. No issues.

Sequence Diagram

sequenceDiagram
    participant Client
    participant request.ts
    participant prompt-flags.ts
    participant gemini.ts
    participant model-resolver.ts
    participant Antigravity API
    participant image-saver.ts

    Client->>request.ts: POST /v1/messages (image model)
    request.ts->>model-resolver.ts: resolveModelWithTier(model)
    model-resolver.ts-->>request.ts: ResolvedModel { isImageModel, thinkingLevel }

    request.ts->>prompt-flags.ts: extractLastUserPrompt(contents)
    prompt-flags.ts-->>request.ts: { text, contentIndex, partIndex }
    request.ts->>prompt-flags.ts: parsePromptFlags(text)
    prompt-flags.ts-->>request.ts: { cleanedPrompt, resolution, aspectRatio }
    request.ts->>request.ts: mutate contents[i].parts[j].text = cleanedPrompt

    request.ts->>gemini.ts: buildImageGenerationConfig(model, overrides)
    note over gemini.ts: Priority: flags > env vars > default
    gemini.ts->>gemini.ts: getValidAspectRatios / getValidImageSizes
    gemini.ts-->>request.ts: ImageConfig { aspectRatio, imageSize }

    request.ts->>request.ts: generationConfig.imageConfig = imageConfig
    request.ts->>Antigravity API: POST with imageConfig + thinkingConfig

    Antigravity API-->>request.ts: SSE stream with inlineData (base64 image)
    request.ts->>image-saver.ts: processImageData(inlineData)
    image-saver.ts->>image-saver.ts: saveImageToDisk → ./nanobanana/
    image-saver.ts-->>request.ts: markdown with file path
    request.ts-->>Client: transformed SSE response
Loading

Last reviewed commit: 22ad11b

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

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

14 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

Comment thread src/plugin/image-saver.ts
function getImageOutputDir(): string {
const homeDir = os.homedir();
const outputDir = path.join(homeDir, '.opencode', 'generated-images');
const outputDir = path.join(process.cwd(), 'nanobanana');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

CWD-relative output path causes inconsistent image locations

path.join(process.cwd(), 'nanobanana') means images are saved relative to wherever the user runs the CLI. If users invoke the CLI from their home directory, a project subdirectory, or a script, images land in a different place each time — making them hard to find. The previous path (~/.opencode/generated-images/) was always the same regardless of CWD.

Consider using an absolute, stable path instead:

Suggested change
const outputDir = path.join(process.cwd(), 'nanobanana');
const outputDir = path.join(require('os').homedir(), '.opencode', 'generated-images');

or at minimum document prominently that the directory is CWD-relative, and expose a config option so users can override it.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/plugin/image-saver.ts
Line: 16

Comment:
**CWD-relative output path causes inconsistent image locations**

`path.join(process.cwd(), 'nanobanana')` means images are saved relative to wherever the user runs the CLI. If users invoke the CLI from their home directory, a project subdirectory, or a script, images land in a different place each time — making them hard to find. The previous path (`~/.opencode/generated-images/`) was always the same regardless of CWD.

Consider using an absolute, stable path instead:

```suggestion
  const outputDir = path.join(require('os').homedir(), '.opencode', 'generated-images');
```

or at minimum document prominently that the directory is CWD-relative, and expose a config option so users can override it.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +30 to +31
const RESOLUTION_PATTERN = /--resolution[=\s]+["']?([^\s"']+)["']?/gi
const ASPECT_RATIO_PATTERN = /--aspect-ratio[=\s]+["']?([^\s"']+)["']?/gi
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Module-level global regex constants carry mutable lastIndex state across calls

RESOLUTION_PATTERN and ASPECT_RATIO_PATTERN are module-level singletons with the g flag. Every call to exec() advances the shared lastIndex. The manual lastIndex = 0 resets placed throughout the function guard against this today, but the pattern is fragile: any future refactoring that forgets a reset, or an early return/exception before a reset fires, will leave the regex in a dirty state and cause the next call to miss its match.

A safer approach for the exec() step is to create a fresh, non-global regex (or a scoped copy) so lastIndex is never an issue, while keeping the g flag only for the .replace() step:

// Use a scoped copy for exec (no shared state)
const resolutionMatch = new RegExp(RESOLUTION_PATTERN.source, "i").exec(prompt)

Alternatively, split the constants into a non-global extractor and a global replacer.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/plugin/transform/prompt-flags.ts
Line: 30-31

Comment:
**Module-level global regex constants carry mutable `lastIndex` state across calls**

`RESOLUTION_PATTERN` and `ASPECT_RATIO_PATTERN` are module-level singletons with the `g` flag. Every call to `exec()` advances the shared `lastIndex`. The manual `lastIndex = 0` resets placed throughout the function guard against this today, but the pattern is fragile: any future refactoring that forgets a reset, or an early return/exception before a reset fires, will leave the regex in a dirty state and cause the *next* call to miss its match.

A safer approach for the `exec()` step is to create a fresh, non-global regex (or a scoped copy) so `lastIndex` is never an issue, while keeping the `g` flag only for the `.replace()` step:

```ts
// Use a scoped copy for exec (no shared state)
const resolutionMatch = new RegExp(RESOLUTION_PATTERN.source, "i").exec(prompt)
```

Alternatively, split the constants into a non-global extractor and a global replacer.

How can I resolve this? If you propose a fix, please make it concise.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Mar 1, 2026

Additional Comments (1)

src/plugin/image-saver.ts
macOS-only open command embedded in returned markdown

open "${filePath}" only works on macOS. On Linux the equivalent is xdg-open, on Windows it is start. Embedding a platform-specific command in the returned markdown will be confusing or broken for non-macOS users.

    return `![Generated Image](${filePath})\n\nImage saved to: \`${filePath}\``;

(Drop the platform-specific hint, or detect the platform and choose the right command.)

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/plugin/image-saver.ts
Line: 91

Comment:
**macOS-only `open` command embedded in returned markdown**

`open "${filePath}"` only works on macOS. On Linux the equivalent is `xdg-open`, on Windows it is `start`. Embedding a platform-specific command in the returned markdown will be confusing or broken for non-macOS users.

```suggestion
    return `![Generated Image](${filePath})\n\nImage saved to: \`${filePath}\``;
```

(Drop the platform-specific hint, or detect the platform and choose the right command.)

How can I resolve this? If you propose a fix, please make it concise.

@bentcc bentcc closed this by deleting the head repository Mar 7, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant