Skip to content

Conversation

@anntnzrb
Copy link
Contributor

@anntnzrb anntnzrb commented Dec 28, 2025

Summary

Add the ability to edit and discard queued messages while the agent is working.
This allows users to fix typos, rephrase, or completely remove messages from
the queue before they are processed.

New keybinds:

  • <leader>i - Edit the last queued message
  • <leader>d - Discard the last queued message

Motivation

When iterating quickly with the AI assistant, users often queue multiple messages.
Currently, there's no way to modify or remove these queued messages once sent.
This leads to:

  • Wasted tokens on messages with typos
  • Duplicate/redundant instructions when user and agent solve the problem simultaneously
  • No way to "take back" a queued message

This feature addresses a common workflow need requested by multiple users.

Related Issues & PRs

Closes

Supersedes

Related

Screenshots

Queued message with hints

queued-message-with-hints

Editing a queued message

editing-queued-message

Command palette with queue commands

command-palette-queue

Demo

https://streamable.com/urjeww

The video demonstrates the complete flow:

  1. Sending a message while agent is working (queued)
  2. Editing the queued message with <leader>i
  3. Discarding a queued message with <leader>d
  4. The toast notification when a message is processed while being edited

Implementation

Why this PR exists (context on #5415)

This implementation started from PR #5415 which was stale and had several issues:

Problems with the original PR #5415

  1. Reused history_previous keybind - Hijacked the UP arrow when prompt was empty, conflicting with normal history navigation
  2. Lost file attachments - When loading a queued message for editing, only text parts were preserved; files and images were silently discarded
  3. No visual feedback - No indication that a message was being edited vs just queued
  4. No dedicated discard action - Could only edit, not quickly discard
  5. Missing API endpoint - Only had cancel endpoint, no way to fetch message without removing it
  6. Bug in dialog-command.tsx - Disabled commands could still be triggered via keybinds

What we fixed/improved

Issue Original #5415 This PR
Keybinds Reuses UP arrow Dedicated <leader>i / <leader>d
File preservation Lost on edit Preserved with extmarks
Visual feedback None QUEUED + EDITING badges
Discard action None Dedicated keybind
API 1 endpoint (cancel) 3 endpoints (list, get, cancel)
Disabled commands Could trigger via keybind Fixed to respect disabled state
Architecture Decisions

Why dedicated keybinds instead of UP arrow?

The original PR #5415 proposed reusing history_previous (UP arrow) when the prompt
is empty. We chose dedicated keybinds (<leader>i and <leader>d) because:

  1. No conflict with history navigation - Users can still navigate history normally
  2. Explicit intent - Editing a queued message is a distinct action from browsing history
  3. Consistency with OpenCode patterns - Other specialized actions use leader-prefix keybinds (e.g., <leader>h for tips, <leader>up for parent session)
  4. Discoverability - Commands appear in the command palette with clear names
  5. Future-proof - PR tui: align keybinds with standard terminal/readline/screen conventions #4268 proposes changing history keybinds, our approach avoids conflicts

Why separate Edit and Discard?

  • Edit (<leader>i): Loads the message into the prompt for modification
  • Discard (<leader>d): Removes the message entirely without loading it

This separation allows:

  • Quick discard without polluting the prompt
  • Discard while already editing (removes message, clears prompt)
  • Clear mental model: "i" for insert/edit, "d" for delete

Why only edit the last queued message?

Both OpenAI Codex and our implementation only allow editing the most recent queued message.
Rationale:

  • Simplifies UX - no need for message selection UI
  • Covers 99% of use cases - users typically want to fix what they just typed
  • If you need to edit something deeper in the queue, you probably made a larger mistake and should discard and re-queue

State Management

We track editingQueuedMessageID in the prompt store to:

  • Show the EDITING badge on the correct message
  • Know which message to cancel when submitting the edited version
  • Clear state appropriately when the message is processed mid-edit
  • Prevent editing multiple messages simultaneously

File/Image Preservation

When loading a queued message for editing, we preserve non-text parts by reconstructing them with virtual text representations (e.g., [File: filename] or [Image 1]) and creating extmarks for proper rendering. This ensures images and files aren't silently lost when editing.

Comparison with Other Tools

We studied how other AI coding assistants handle queued message editing:

OpenAI Codex (Rust TUI)

Source: codex-rs/tui/src/chatwidget.rs

Codex stores queued messages in a VecDeque<UserMessage> with text and image paths. It uses Alt+Up to pop the most recent queued message back into the composer, preserving image paths separately. A static hint "Alt+Up edit" is shown on queued messages.

What we learned from Codex:

  • Preserve image paths separately (we preserve all file parts with extmarks)
  • Show hint on queued messages (we show contextual hints that change based on state)
  • Only edit last message (we follow this pattern)

How we differ from Codex:

Aspect Codex OpenCode (this PR)
Keybind Alt+Up <leader>i
Discard action None <leader>d
Visual state No badge EDITING badge
Hints Static "Alt+Up edit" Contextual (changes when editing)
On interrupt Restores all to composer N/A (different architecture)

Claude Code

Claude Code allows editing queued messages via UP arrow when prompt is empty.
This is similar to the original #5415 approach, which we improved upon with
dedicated keybinds to avoid conflicts with history navigation.

Flow Diagram
                              QUEUED MESSAGE
    ┌──────────────────────────────────────────────────────────┐
    │                                                          │
    │   You  QUEUED  ctrl+x i edit · ctrl+x d discard          │
    │   > Your queued message text here...                     │
    │                                                          │
    └──────────────────────────────────────────────────────────┘
                    │                           │
                    │ <leader>i                 │ <leader>d
                    ▼                           ▼
    ┌───────────────────────────┐     ┌─────────────────────────┐
    │                           │     │                         │
    │  QUEUED   EDITING         │     │  Message removed from   │
    │                           │     │  queue and storage      │
    │  enter submit             │     │                         │
    │  ctrl+c cancel            │     └─────────────────────────┘
    │  ctrl+x d discard         │
    │                           │
    │  ┌─────────────────────┐  │
    │  │ Prompt: edited text │  │
    │  └─────────────────────┘  │
    └───────────────────────────┘
                    │
                    │ Enter
                    ▼
    ┌───────────────────────────┐
    │                           │
    │  1. Original message      │
    │     cancelled             │
    │                           │
    │  2. New message sent      │
    │     with edited content   │
    │                           │
    └───────────────────────────┘
API Endpoints Added
Method Endpoint Description
GET /session/:sessionID/queue List queued message IDs
GET /session/:sessionID/queue/:messageID Get queued message (without removing)
DELETE /session/:sessionID/queue/:messageID Cancel and remove queued message

Changes

Backend (packages/opencode/src/)

File Changes
session/prompt.ts Add queued(), getQueued(), cancelQueued() functions; track messageID in callbacks
server/server.ts Add 3 REST endpoints for queue operations
config/config.ts Add queue_edit and queue_discard keybind defaults

Frontend (packages/opencode/src/cli/cmd/tui/)

File Changes
component/prompt/index.tsx Add queue edit/discard commands; track editingQueuedMessageID state; preserve file parts with extmarks; handle edge cases (message processed mid-edit, cancel with ctrl+c, clear with ctrl+u)
component/dialog-command.tsx Bug fix: prevent disabled commands from triggering via keybinds
routes/session/index.tsx Show QUEUED/EDITING badges with contextual hints

SDK (auto-generated)

File Changes
packages/sdk/js/src/v2/gen/* Types for new endpoints
packages/sdk/openapi.json API schema
Bug fix: disabled commands triggering via keybinds

Found and fixed a bug in dialog-command.tsx where disabled commands could still be triggered via their keybinds:

- if (option.keybind && keybind.match(option.keybind, evt)) {
+ if (option.keybind && !option.disabled && keybind.match(option.keybind, evt)) {

This affected all commands with a disabled property, not just the queue commands.

Edge Cases

Scenario Behavior
Message processed while editing Toast "Queued message was processed", prompt cleared, editing state reset
Edit message with file attachments Files preserved as [File: filename] with extmarks
Cancel edit (ctrl+c) Prompt cleared, returns to normal state, original message stays queued
Clear prompt (ctrl+u) Editing state also cleared
Discard while editing Message removed, prompt cleared
No queued messages Commands disabled in palette, keybinds have no effect
Multiple queued messages Only last message can be edited (consistent with Codex behavior)

How to Test

git checkout feat/edit-queued-messages
bun install
cd packages/opencode && bun dev
  1. Start a conversation with the agent
  2. While the agent is working, send another message (it will show QUEUED badge)
  3. Press ctrl+x i to edit the queued message
  4. Modify the text and press Enter to submit, or ctrl+c to cancel
  5. Alternatively, press ctrl+x d to discard without editing

Testing

test/session/queue.test.ts (new file)

  • SessionPrompt.queued() - returns empty array for non-existent session
  • SessionPrompt.getQueued() - returns undefined for non-existent session/message
  • SessionPrompt.cancelQueued() - returns undefined for non-existent session/message

test/config/config.test.ts (additions)

  • queue_edit defaults to <leader>i
  • queue_discard defaults to <leader>d
  • Both keybinds can be customized via config

Breaking Changes

None. This is an additive feature with new keybinds that don't conflict with existing ones.

Add the ability to edit and discard messages that are queued while the
agent is working. This allows users to fix typos or completely remove
queued messages before they are processed.

Changes:
- Add queue_edit (<leader>i) and queue_discard (<leader>d) keybinds
- Add REST endpoints: GET/DELETE /session/:sessionID/queue/:messageID
- Show QUEUED badge with contextual hints on queued messages
- Show EDITING badge when a queued message is being edited
- Preserve file attachments when loading queued message for editing
- Clear editing state with toast when message is processed mid-edit
- Fix: prevent disabled commands from triggering via keybinds
@anntnzrb
Copy link
Contributor Author

I'm genuinely excited about this contribution. This is a considerably large implementation, and I fully understand it won't be accepted without thorough review.

I'm very open to feedback and suggestions from maintainers and the community. I really hope this change can be incorporated, as I believe it's a truly useful feature that addresses a common pain point.

I apologize for the lengthy PR description, but I've put significant thought into it with the goal of being as detailed as possible, explaining the decisions I made. I went through a considerable research phase (studying how Codex and other tools handle this) before implementing, so I wanted to document that reasoning.

Thank you for your time and understanding.

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