Skip to content

Conversation

@sestinj
Copy link
Contributor

@sestinj sestinj commented Sep 1, 2025

Description

Image pasting support in cn via ctrl+V


Summary by cubic

Add image pasting to the CLI chat via Ctrl+V. Pasted images appear as [Image #n] in the input, are sent as inline data URLs, and display as placeholders in history.

  • New Features

    • Ctrl+V detects clipboard images (macOS, Windows, Linux) and inserts [Image #n] placeholders.
    • TextBuffer tracks image buffers; images are passed on submit and cleared afterward.
    • Message formatting builds MessagePart[] with text and imageUrl entries; 10MB cap with clear skip message.
    • Optional Sharp processing resizes to 1024x1024 and converts to JPEG for smaller payloads; falls back if unavailable.
    • Display shows images as [Image #n] in MemoizedMessage for readable history.
    • Remote mode sends a string with [Image] markers to the server.
    • Tests added for image paste flow, message formatting, and display.
  • Dependencies

    • Added optional @img/sharp-* platform packages; feature works without them but images won’t be resized/compressed.
    • Linux requires xclip for clipboard image capture.

@sestinj sestinj requested a review from a team as a code owner September 1, 2025 01:00
@sestinj sestinj requested review from RomneyDa and removed request for a team September 1, 2025 01:00
@dosubot dosubot bot added the size:XL This PR changes 500-999 lines, ignoring generated files. label Sep 1, 2025
@dosubot dosubot bot added size:XXL This PR changes 1000+ lines, ignoring generated files. and removed size:XL This PR changes 500-999 lines, ignoring generated files. labels Sep 1, 2025
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

7 issues found across 11 files

React with 👍 or 👎 to teach cubic. You can also tag @cubic-dev-ai to give feedback, ask questions, or re-run the review.

if (part.type === "text") {
return part.text;
} else if (part.type === "imageUrl") {
return "[Image]";
Copy link
Contributor

Choose a reason for hiding this comment

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

Images are reduced to “[Image]” and no binary or URL is transmitted in remote mode, so pasted images won’t be available to the remote server.

Prompt for AI agents
Address the following comment on extensions/cli/src/ui/hooks/useChat.ts at line 343:

<comment>Images are reduced to “[Image]” and no binary or URL is transmitted in remote mode, so pasted images won’t be available to the remote server.</comment>

<file context>
@@ -308,20 +311,45 @@ export function useChat({
+            if (part.type === &quot;text&quot;) {
+              return part.text;
+            } else if (part.type === &quot;imageUrl&quot;) {
+              return &quot;[Image]&quot;;
+            }
+            return &quot;&quot;;
</file context>

hasImages: !!(imageMap && imageMap.size > 0),
imageCount: imageMap?.size || 0,
});
const newUserMessage = await formatMessageWithFiles(
Copy link
Contributor

Choose a reason for hiding this comment

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

Images are processed (resize/base64) even in remote mode where only a text placeholder is sent; avoid this upfront work by skipping image processing in remote mode or moving formatting after the remote-mode branch.

Prompt for AI agents
Address the following comment on extensions/cli/src/ui/hooks/useChat.ts at line 319:

<comment>Images are processed (resize/base64) even in remote mode where only a text placeholder is sent; avoid this upfront work by skipping image processing in remote mode or moving formatting after the remote-mode branch.</comment>

<file context>
@@ -308,20 +311,45 @@ export function useChat({
+      hasImages: !!(imageMap &amp;&amp; imageMap.size &gt; 0),
+      imageCount: imageMap?.size || 0,
+    });
+    const newUserMessage = await formatMessageWithFiles(
       message,
       attachedFiles,
</file context>

);

const output = lastFrame();
expect(output).toContain("Start");
Copy link
Contributor

Choose a reason for hiding this comment

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

Assertion is too weak to verify whitespace preservation; assert the expected trailing/leading spaces to prevent regressions.

Prompt for AI agents
Address the following comment on extensions/cli/src/ui/components/MemoizedMessage.formatDisplay.test.tsx at line 129:

<comment>Assertion is too weak to verify whitespace preservation; assert the expected trailing/leading spaces to prevent regressions.</comment>

<file context>
@@ -0,0 +1,144 @@
+    );
+
+    const output = lastFrame();
+    expect(output).toContain(&quot;Start&quot;);
+    expect(output).toContain(&quot;end&quot;);
+    expect(output).toContain(&#39;{&quot;type&quot;:&quot;unknown&quot;,&quot;data&quot;:{&quot;some&quot;:&quot;data&quot;}}&#39;);
</file context>

}

// WebP: RIFF + WEBP at offset 8
if (signature[0] === 0x52 && signature[1] === 0x49 && buffer.length >= 12) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Tighten the RIFF signature check for WebP detection to validate all four bytes (RIFF) before checking for WEBP at offset 8.

Prompt for AI agents
Address the following comment on extensions/cli/src/ui/hooks/useChat.helpers.ts at line 110:

<comment>Tighten the RIFF signature check for WebP detection to validate all four bytes (RIFF) before checking for WEBP at offset 8.</comment>

<file context>
@@ -19,6 +19,194 @@ import { shouldAutoCompact } from &quot;../../util/tokenizer.js&quot;;
+  }
+
+  // WebP: RIFF + WEBP at offset 8
+  if (signature[0] === 0x52 &amp;&amp; signature[1] === 0x49 &amp;&amp; buffer.length &gt;= 12) {
+    const webpSig = buffer.subarray(8, 12);
+    if (
</file context>
Suggested change
if (signature[0] === 0x52 && signature[1] === 0x49 && buffer.length >= 12) {
if (signature[0] === 0x52 && signature[1] === 0x49 && signature[2] === 0x46 && signature[3] === 0x46 && buffer.length >= 12) {

addImage(imageBuffer: Buffer): string {
this._imageCounter++;
const placeholder = `[Image #${this._imageCounter}]`;
this._imageMap.set(placeholder, imageBuffer);
Copy link
Contributor

Choose a reason for hiding this comment

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

Stores raw image buffers without validating size/count, risking excessive memory usage; enforce reasonable limits before storing.

Prompt for AI agents
Address the following comment on extensions/cli/src/ui/TextBuffer.ts at line 200:

<comment>Stores raw image buffers without validating size/count, risking excessive memory usage; enforce reasonable limits before storing.</comment>

<file context>
@@ -189,6 +193,26 @@ export class TextBuffer {
+  addImage(imageBuffer: Buffer): string {
+    this._imageCounter++;
+    const placeholder = `[Image #${this._imageCounter}]`;
+    this._imageMap.set(placeholder, imageBuffer);
+    this.insertText(placeholder);
+    return placeholder;
</file context>

} else if (platform === "win32") {
// Windows: Use PowerShell to save clipboard image
await execAsync(
`powershell -command "Get-Clipboard -Format Image | Set-Content -Path '${tempImagePath}' -Encoding Byte"`,
Copy link
Contributor

Choose a reason for hiding this comment

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

Windows image save uses Set-Content on an Image object, producing invalid output; use Image.Save(...) to write a real PNG instead.

Prompt for AI agents
Address the following comment on extensions/cli/src/ui/hooks/useUserInput.ts at line 86:

<comment>Windows image save uses Set-Content on an Image object, producing invalid output; use Image.Save(...) to write a real PNG instead.</comment>

<file context>
@@ -10,6 +11,102 @@ interface ControlKeysOptions {
+    } else if (platform === &quot;win32&quot;) {
+      // Windows: Use PowerShell to save clipboard image
+      await execAsync(
+        `powershell -command &quot;Get-Clipboard -Format Image | Set-Content -Path &#39;${tempImagePath}&#39; -Encoding Byte&quot;`,
+      );
+    } else if (platform === &quot;linux&quot;) {
</file context>
Suggested change
`powershell -command "Get-Clipboard -Format Image | Set-Content -Path '${tempImagePath}' -Encoding Byte"`,
`powershell -command "$img=Get-Clipboard -Format Image; if ($img) { $img.Save('${tempImagePath.replace(/'/g, "''")}', [System.Drawing.Imaging.ImageFormat]::Png) }"`,

package.json Outdated
"prettier-plugin-tailwindcss": "^0.6.8",
"typescript": "^5.6.3"
},
"optionalDependencies": {
Copy link
Contributor

Choose a reason for hiding this comment

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

Adds multiple platform-specific sharp packages as root optionalDependencies, but no code imports or uses sharp; this adds install time/size and maintenance overhead. Prefer adding 'sharp' only in the specific package that needs it (which will pull the correct platform binaries) or defer until the code actually uses it.

Prompt for AI agents
Address the following comment on package.json at line 20:

<comment>Adds multiple platform-specific sharp packages as root optionalDependencies, but no code imports or uses sharp; this adds install time/size and maintenance overhead. Prefer adding &#39;sharp&#39; only in the specific package that needs it (which will pull the correct platform binaries) or defer until the code actually uses it.</comment>

<file context>
@@ -16,5 +16,13 @@
     &quot;prettier-plugin-tailwindcss&quot;: &quot;^0.6.8&quot;,
     &quot;typescript&quot;: &quot;^5.6.3&quot;
+  },
+  &quot;optionalDependencies&quot;: {
+    &quot;@img/sharp-darwin-arm64&quot;: &quot;^0.33.5&quot;,
+    &quot;@img/sharp-darwin-x64&quot;: &quot;^0.33.5&quot;,
</file context>

@sestinj sestinj merged commit 608e93a into main Sep 1, 2025
55 checks passed
@sestinj sestinj deleted the paste-images branch September 1, 2025 18:14
@github-project-automation github-project-automation bot moved this from Todo to Done in Issues and PRs Sep 1, 2025
@github-actions github-actions bot locked and limited conversation to collaborators Sep 1, 2025
@github-actions github-actions bot added the tier 1 Big feature that took multiple weeks to launch and represents a big milestone for the product label Sep 1, 2025
@sestinj
Copy link
Contributor Author

sestinj commented Sep 1, 2025

🎉 This PR is included in version 1.12.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

@sestinj
Copy link
Contributor Author

sestinj commented Sep 3, 2025

🎉 This PR is included in version 1.12.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

released size:XXL This PR changes 1000+ lines, ignoring generated files. tier 1 Big feature that took multiple weeks to launch and represents a big milestone for the product

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants