Skip to content

feat(repl): Ctrl+V image paste in graff#19

Merged
justrach merged 7 commits into
mainfrom
feat/graff-image-paste
May 5, 2026
Merged

feat(repl): Ctrl+V image paste in graff#19
justrach merged 7 commits into
mainfrom
feat/graff-image-paste

Conversation

@justrach

@justrach justrach commented May 4, 2026

Copy link
Copy Markdown
Owner

Summary

Closes #16.

  • Pressing Ctrl+V in the graff REPL now captures any clipboard image, encodes it as PNG to a unique temp file, and inserts @[<temp.png>] into the input buffer — the existing attachment pipeline turns that into a multimodal content block at send time.
  • Falls back to plain text paste when no image is on the clipboard, so the standard Ctrl+V experience is preserved.
  • New module crates/forge_main/src/clipboard.rs (~80 LoC) and a small extension to ForgeEditMode::parse_event in editor.rs — same interception pattern the bracketed-paste handler already uses.
  • image added to [workspace.dependencies] with PNG-only feature to keep binary impact minimal.

Why Ctrl+V (not Cmd+V)

On macOS, Cmd+V is intercepted by the terminal before the app sees it; the app only receives whatever the terminal forwards (usually text or filename, never raw image bytes). Ctrl+V reaches the app directly and is the only binding that gives reliable in-process clipboard access without per-terminal config.

Test plan

  • Take a screenshot to clipboard (Cmd+Shift+Ctrl+4 on macOS) → run graff → press Ctrl+V → verify @[/var/folders/.../graff-clipboard-<uuid>.png] is inserted.
  • Send the message and confirm the model sees the image.
  • Copy plain text to clipboard → run graff → press Ctrl+V → verify text is inserted as normal paste.
  • Verify Ctrl+V with empty clipboard is a no-op (no crash, no garbage in buffer).
  • Confirm bracketed-paste behaviour for drag-and-drop file paths is unaffected.

🤖 Generated with Claude Code

justrach and others added 5 commits May 5, 2026 01:51
The zsh-setup visual check and `graff zsh doctor` still rendered the
legacy `󱙺 FORGE 33.0k` line; replace with ` GRAFF 33.0k` to match the
rest of the rebrand. Cosmetic only — internal symbols, env vars, and
crate names are unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The right-hand prompt was rendering `󱙺 FORGE` (anvil + UpperSnake of
the agent ID) — the anvil glyph plus the visible brand. Swap the
agent symbol from `\u{f167a}` (nf-md-anvil) to `\u{f013}`
(nf-fa-cog) and special-case the `forge` agent ID to render as
`GRAFF` instead of `FORGE`. The internal agent ID stays `forge` so
`:forge` slash commands and config keep working.

Test fixtures updated; all 20 rprompt tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Every forge_* crate hardcoded `version = "0.1.1"` in its Cargo.toml,
so even though the v0.1.2 tag and `workspace.package.version`
both said 0.1.2, the displayed `forge_tracker::VERSION` (which
expands `CARGO_PKG_VERSION` of the per-crate manifest) stayed at
0.1.1. Switch all 25 crates to `version.workspace = true` so they
inherit the workspace version. Future bumps now only need to edit
the root Cargo.toml.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The REPL prompt rendered by reedline (`forge_main::prompt::ForgePrompt`)
had its own copy of the anvil `AGENT_SYMBOL` and UpperSnake-of-agent-id
formatting. The earlier sweep only fixed the zsh rprompt path, so
launching `graff` interactively still showed `󱙺 FORGE` on the right
side of the REPL prompt. Apply the same swap here: gear glyph plus
`forge` agent ID rendered as `GRAFF` (internal ID stays `forge`).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes #16.

When the user presses Ctrl+V in the graff REPL, the system clipboard is
inspected. If it holds an image, the RGBA pixel buffer is encoded as PNG
to a unique file under the system temp directory and `@[<temp.png>]` is
inserted into the input buffer. The existing `forge_domain::Attachment`
pipeline resolves that reference into a multimodal content block at
message-send time, so the model sees the image with no further plumbing.

If the clipboard does not hold an image, Ctrl+V falls back to plain text
paste (replicating the standard binding) so users do not lose normal
paste behaviour. If clipboard access fails entirely, the binding is a
no-op.

Why Ctrl+V (not Cmd+V): on macOS, Cmd+V is intercepted by the terminal
itself; the app receives whatever the terminal forwards (usually text or
filename, never raw image bytes). Ctrl+V reaches the app directly. This
is the only binding that gives reliable in-process clipboard access
without per-terminal configuration.

Implementation:
- New module `crates/forge_main/src/clipboard.rs` wraps the arboard
  clipboard API and writes PNGs via the `image` crate.
- `ForgeEditMode::parse_event` (`crates/forge_main/src/editor.rs`)
  intercepts the Ctrl+V key event before delegating to the inner Emacs
  edit mode — the same pattern already used for bracketed paste.
- `image` added to `[workspace.dependencies]` with PNG-only feature to
  keep binary size impact minimal.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added the type: feature Brand new functionality, features, pages, workflows, endpoints, etc. label May 4, 2026
Save clipboard PNGs under `~/forge/clipboard/<8hex>.png` instead of
the system temp directory. The resulting `@[...]` reference inserted
into the input buffer drops from ~109 chars (`/var/folders/t3/...`)
to ~50 chars, so the user's typed message is not visually swamped by
the path.

Falls back to the system temp dir when `~/forge/` cannot be reached
(sandboxed builds, CI). Uses an 8-character UUID prefix so paths stay
readable but unique within the directory.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the long `@[/Users/.../forge/clipboard/<8hex>.png]` insertion
with a tidy `[Image 1]`, `[Image 2]`, ... chip. The chip is rewritten
to the canonical `@[<absolute-path>]` syntax at message-submit time,
so the existing `forge_domain::Attachment` pipeline sees no change.

Implementation:
- A process-level `LazyLock<Mutex<Vec<PathBuf>>>` registry maps slot
  numbers to on-disk PNG paths. Lifetime is the process — counters
  do not reset on `:new` for v1, simplest correct behavior.
- `capture_clipboard_image()` returns `CapturedImage { slot }`; the
  Ctrl+V handler in `ForgeEditMode::parse_event` inserts
  `[Image {slot}]` instead of the path.
- `expand_image_chips(buffer)` rewrites every `[Image N]` whose `N`
  is a registered slot into `@[<absolute-path>]`. Out-of-range or
  malformed chips (`[image 1]`, `[Image  1]`, `[Image 0]`) are left
  as literal text so users can type them organically.
- `From<Signal> for ReadResult` calls the expander on the trimmed
  buffer before handing it to the agent pipeline.
- 8 unit tests cover single chip, multiple chips, out-of-range,
  zero index, lowercase, extra-space, empty registry, unicode.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@justrach justrach merged commit 3d54839 into main May 5, 2026
6 of 7 checks passed
@justrach justrach deleted the feat/graff-image-paste branch May 5, 2026 03:52
justrach added a commit that referenced this pull request May 5, 2026
Captures the slash-palette + tier 1-7 dispatcher work merged in #21,
#23 (via #25), and #25, plus the rebrand and image-paste work from
#19. All forge_* and codegraff crates inherit via
\`version.workspace = true\`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type: feature Brand new functionality, features, pages, workflows, endpoints, etc.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add Ctrl+V image-paste support to graff REPL

1 participant