Skip to content

feat(cli): queue input editing — pop queued messages for editing via ↑/ESC#2871

Merged
tanzhenxin merged 13 commits into
QwenLM:mainfrom
wenshao:feat/queue-input-editing
Apr 11, 2026
Merged

feat(cli): queue input editing — pop queued messages for editing via ↑/ESC#2871
tanzhenxin merged 13 commits into
QwenLM:mainfrom
wenshao:feat/queue-input-editing

Conversation

@wenshao

@wenshao wenshao commented Apr 4, 2026

Copy link
Copy Markdown
Collaborator

Problem

When users queue messages during AI streaming, typos or wrong instructions waste a full turn — the queued text is submitted as-is with no way to review or correct it before it's sent.

┌─────────────────────────────────────────────────────────┐
│ AI is responding...                                     │
│                                                         │
│  ❌ Before: queued messages are locked, sent blindly    │
│                                                         │
│   "refactor the auth modle"     ← typo, can't fix      │
│   "also update teh tests"       ← another typo          │
│                                                         │
│ > (user can only type new messages)                     │
└─────────────────────────────────────────────────────────┘

Solution

Allow users to pop queued messages back into the input for editing before they're sent:

┌─────────────────────────────────────────────────────────┐
│ AI is responding...                                     │
│                                                         │
│   "refactor the auth modle"                             │
│   "also update teh tests"                               │
│   Press ↑ to edit queued messages                       │
│                                                         │
│ > _                              ← press ↑ or ESC       │
└─────────────────────────────────────────────────────────┘

                        ↓ after pressing ↑ or ESC

┌─────────────────────────────────────────────────────────┐
│ AI is responding...                                     │
│                                                         │
│ > refactor the auth module       ← fixed! cursor here   │
│ >                                                       │
│ > also update the tests          ← fixed!               │
└─────────────────────────────────────────────────────────┘

Key Behaviors

Trigger Condition Action
↑ arrow Queue non-empty, cursor at top of input Pop all queued messages into input
ESC Queue non-empty, no search/shell/completion active Pop all queued messages into input
↑ arrow Queue empty Normal history navigation (no behavior change)
Ctrl+P Always History navigation (never intercepted)
ESC Queue empty Normal double-ESC clear behavior

Cursor preservation

When existing text is in the input, queued messages are prepended and the cursor stays at the user's original editing position:

Before pop:  > some new text|        (cursor at position 14)
After pop:   > queued msg 1
             >
             > queued msg 2
             > some new text|        (cursor preserved at original position)

Progressive hint

"Press ↑ to edit queued messages" is shown below queued messages. It automatically hides after being displayed 3 times (per session), so experienced users aren't distracted.

Implementation

Layer File Change
Hook useMessageQueue.ts Add popAllMessages() — atomic pop via queueRef to prevent duplicate pops from key auto-repeat
Context UIActionsContext.tsx Add popAllQueuedMessages to interface
Container AppContainer.tsx Wire popAllMessages → UIActions; use hook's drainQueue for mid-turn drain
Component InputPrompt.tsx popQueueIntoInput() helper shared by ↑ and ESC handlers; cursor offset via moveToOffset
Component QueuedMessageDisplay.tsx Progressive hint with session-level ref counter
i18n 6 locale files "Press ↑ to edit queued messages"
Tests 5 test files 12 new test cases covering pop, prepend, cursor, ESC, hint, and edge cases

15 files changed, +364 −18

Design Decisions & Claude Code Comparison

This feature aligns with Claude Code's queue editing behavior. Key differences are intentional:

Aspect This PR Claude Code Rationale
Queue type string[] QueuedCommand objects qwen-code only queues user text; no need for structured command types
Image support N/A Extracts images on pop qwen-code queue doesn't contain images (they go through attachment channel)
Meta filtering N/A Skips isMeta commands qwen-code has no system-generated queue messages
Hint persistence Session-level (ref) Cross-session (globalConfig) Session reset is acceptable; claude-code's counter has a bug (never incremented)
Separator \n\n between messages \n Double newline is more readable for multi-message editing

Test Plan

  • Press ↑ with non-empty queue → messages pop into input
  • Press ↑ with empty queue → normal history navigation
  • Press ESC with non-empty queue → messages pop into input
  • Press Ctrl+P with non-empty queue → history navigation (not intercepted)
  • Pop with existing input text → queued messages prepended, cursor preserved
  • Rapid key repeat → only one pop (atomic via queueRef)
  • Pop from empty queue (race) → returns null, falls through to default behavior
  • Hint shown 3 times then hidden
  • Single message pop → no separator
  • Multiple message pop → joined with \n\n

🤖 Generated with Claude Code

Allow users to edit queued messages by pressing the Up arrow key when
the cursor is at the top of the input. All queued messages are popped
into the input field for revision before resubmission, reducing wasted
turns from incorrect queued instructions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Apr 4, 2026

Copy link
Copy Markdown
Contributor

📋 Review Summary

This PR introduces a feature to allow users to edit queued messages by pressing the Up arrow key when the cursor is at the top of the input field. The implementation adds popLast() to AsyncMessageQueue, popAllMessages() to the useMessageQueue hook, and integrates this functionality into the InputPrompt component with appropriate UI hints in QueuedMessageDisplay. The code is well-structured with good test coverage, though there are a few areas that could be improved.

🔍 General Feedback

  • The implementation follows existing patterns in the codebase (similar to how other queue operations are handled)
  • Good separation of concerns: core queue logic in packages/core, UI hook in packages/cli, and component integration
  • Comprehensive test coverage with 26 unit tests mentioned
  • Proper i18n support across 6 languages (de, en, ja, pt, ru, zh)
  • The feature addresses a real UX pain point (reducing wasted turns from incorrect queued instructions)
  • Consistent with existing keyboard shortcut patterns (Esc for cancel, now Up for edit)

🎯 Specific Feedback

🟡 High

  • File: packages/cli/src/ui/components/InputPrompt.tsx:839-857 - The condition for triggering the queue pop is complex and may have edge cases. The logic checks both Command.HISTORY_UP and Command.NAVIGATION_UP with cursor position checks, but the interaction with attachment mode (isAttachmentMode) isn't explicitly handled. Consider adding a guard to ensure this doesn't interfere with attachment navigation:

    // Add !isAttachmentMode guard to prevent conflict
    if (
      !isAttachmentMode &&  // Add this check
      uiState.messageQueue.length > 0 &&
      ...
    )
  • File: packages/cli/src/ui/hooks/useMessageQueue.ts:58-62 - The popAllMessages function joins messages with single \n, but getQueuedMessagesText uses \n\n. This inconsistency could lead to confusing behavior when messages are re-submitted. Consider using the same separator (\n\n) for consistency with the auto-submit behavior:

    const allText = messageQueue.join('\n\n');  // Use double newline for consistency

🟢 Medium

  • File: packages/core/src/utils/asyncMessageQueue.ts:50-57 - The popLast() method is specific to this feature but may have broader utility. Consider adding a doc comment explaining the intended use case and behavior, similar to other methods in the class:

    /**
     * Remove and return the last item from the queue, or null if empty.
     * Useful for retrieving the most recently queued message for editing.
     */
    popLast(): T | null { ... }
  • File: packages/cli/src/ui/components/QueuedMessageDisplay.tsx:46-50 - The edit hint is always shown when there are messages in the queue, but it doesn't indicate when the feature is unavailable (e.g., during streaming). Consider adding a condition to only show when the feature is actually usable:

    // Only show hint when user can actually interact (e.g., not during streaming)
    {messageQueue.length > 0 && streamingState !== StreamingState.Responding && (
      <Box paddingLeft={2}>
        <Text dimColor italic>
          {t('Press ↑ to edit queued messages')}
        </Text>
      </Box>
    )}
  • File: packages/cli/src/ui/components/InputPrompt.tsx:839-857 - When popping messages and prepending to current buffer text, there's no validation of the resulting text length. If the user has a long message already in the input and pops multiple queued messages, it could create an unexpectedly large input. Consider adding a safeguard or at least a comment about this behavior.

🔵 Low

  • File: packages/cli/src/i18n/locales/de.js:1442 - The German translation "Drücken Sie ↑, um Nachrichten in der Warteschlange zu bearbeiten" is quite long compared to the English version. This might cause UI layout issues in the terminal. Consider shortening to "↑ drücken, um Nachrichten zu bearbeiten" (consistent with other German translations in the file that are more concise).

  • File: packages/cli/src/ui/components/QueuedMessageDisplay.tsx - The component doesn't receive streamingState as a prop, which would be needed to conditionally show the edit hint. This would require updating the component's interface and all call sites. This is a minor suggestion for future enhancement.

  • File: packages/cli/src/ui/hooks/useMessageQueue.test.ts:256 - The test "should pop all messages from queue" verifies the joined format, but doesn't test the edge case of a single message (which shouldn't have any separator). Consider adding a test case for a single message in the queue.

✅ Highlights

  • Test coverage is excellent: The PR includes tests for both AsyncMessageQueue.popLast() and useMessageQueue.popAllMessages(), including edge cases (empty queue returns null)
  • Clean API design: The popAllMessages() function returns string | null, making it easy for callers to handle the empty queue case without try-catch
  • Thoughtful UX consideration: The feature falls back gracefully to history navigation when the queue is empty, maintaining existing behavior
  • Proper TypeScript usage: All new functions have proper type annotations and return types
  • Good i18n coverage: All major supported languages have been updated with the new translation string
  • Non-breaking change: The feature is additive and doesn't modify existing behavior, only adds new functionality when the queue is non-empty

wenshao added a commit to wenshao/codeagents that referenced this pull request Apr 4, 2026
Qwen Code PR #2871 implements queue input editing via Up arrow key:
- popLast() on AsyncMessageQueue
- popAllMessages() on useMessageQueue hook
- "Press ↑ to edit" hint in QueuedMessageDisplay

Updated matrix (进展 column) and P2 detail section.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add popAllQueuedMessages mock and messageQueue to UIState/UIActions
  mocks in InputPrompt.test.tsx to fix 25 test failures
- Add !isAttachmentMode guard to prevent queue pop from conflicting
  with attachment navigation
- Add single-message popAllMessages test case

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@wenshao wenshao requested a review from Copilot April 4, 2026 04:14
wenshao added a commit to wenshao/codeagents that referenced this pull request Apr 4, 2026
#39)

Qwen Code PR #2871 implements queue input editing via Up arrow key:
- popLast() on AsyncMessageQueue
- popAllMessages() on useMessageQueue hook
- "Press ↑ to edit" hint in QueuedMessageDisplay

Updated matrix (进展 column) and P2 detail section.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@wenshao

wenshao commented Apr 4, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks for the thorough review! Addressed the feedback in 9a419ca:

🟡 High

  • !isAttachmentMode guard — ✅ Fixed. Added guard to prevent queue pop from conflicting with attachment navigation.
  • Join separator (\n vs \n\n) — Keeping \n intentionally. This matches upstream Claude Code behavior (popAllEditable uses \n). The \n\n separator in getQueuedMessagesText is for auto-submission where messages are combined into a single prompt, while \n is better for editing in the input buffer.

🟢 Medium

  • QueuedMessageDisplay hint during streaming — The hint is intentionally always shown when the queue has messages. Queued messages only appear during streaming (that's when users submit to the queue), so the hint is always actionable when visible. Adding streamingState as a prop would add unnecessary complexity.
  • Text length validation — No practical limit needed; the input buffer already handles large text.

🟢 Low

  • Single message test — ✅ Added test case for single message (no separator in output).
  • InputPrompt test failures — ✅ Fixed. Added missing popAllQueuedMessages mock and messageQueue: [] to UIState mock in InputPrompt.test.tsx, fixing 25 test failures.

Copilot AI left a comment

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.

Pull request overview

Adds a CLI UX improvement to let users pull queued messages back into the input for editing (via Up-arrow at top of input), plus a UI hint and supporting queue APIs.

Changes:

  • Add popLast() to AsyncMessageQueue (core) with unit tests.
  • Add popAllMessages() to the CLI useMessageQueue hook and surface it through UIActions to InputPrompt.
  • Show a localized “Press ↑ to edit queued messages” hint in QueuedMessageDisplay and add locale strings + tests.

Reviewed changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
packages/core/src/utils/asyncMessageQueue.ts Adds popLast() API on the queue
packages/core/src/utils/asyncMessageQueue.test.ts Tests for popLast()
packages/cli/src/ui/hooks/useMessageQueue.ts Adds popAllMessages() to clear+return queued messages
packages/cli/src/ui/hooks/useMessageQueue.test.ts Tests for popAllMessages()
packages/cli/src/ui/contexts/UIActionsContext.tsx Extends UIActions with popAllQueuedMessages()
packages/cli/src/ui/components/QueuedMessageDisplay.tsx Adds localized hint for editing queued messages
packages/cli/src/ui/components/QueuedMessageDisplay.test.tsx Tests hint rendering
packages/cli/src/ui/components/InputPrompt.tsx Handles Up-arrow to pop queued messages into the buffer
packages/cli/src/ui/components/InputPrompt.test.tsx Updates mocks for new UI state/action fields
packages/cli/src/ui/AppContainer.tsx Wires popAllMessages into UI actions/state
packages/cli/src/i18n/locales/en.js Adds new translation key
packages/cli/src/i18n/locales/de.js Adds new translation key
packages/cli/src/i18n/locales/ja.js Adds new translation key
packages/cli/src/i18n/locales/pt.js Adds new translation key
packages/cli/src/i18n/locales/ru.js Adds new translation key
packages/cli/src/i18n/locales/zh.js Adds new translation key

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/core/src/utils/asyncMessageQueue.ts Outdated
Comment thread packages/cli/src/ui/hooks/useMessageQueue.ts Outdated
Comment thread packages/cli/src/ui/components/InputPrompt.tsx Outdated
Comment thread packages/cli/src/ui/components/InputPrompt.tsx Outdated
… docs

- Only trigger queue pop on NAVIGATION_UP (arrow key), not HISTORY_UP
  (Ctrl+P), preserving existing Ctrl+P history navigation behavior
- Update AsyncMessageQueue class docs to describe popLast() LIFO semantics
- Add InputPrompt tests: Up arrow pops queue, Up arrow falls back to
  history when queue empty, Ctrl+P not intercepted by queue pop

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 16 out of 16 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/core/src/utils/asyncMessageQueue.ts Outdated
Comment thread packages/cli/src/ui/hooks/useMessageQueue.ts Outdated
- Update @fileoverview to describe FIFO+LIFO capability instead of
  "Simple FIFO queue"
- Use queueRef to make popAllMessages atomic, preventing duplicate
  pops from key auto-repeat before React re-renders

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 16 out of 16 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/cli/src/ui/hooks/useMessageQueue.ts Outdated
Comment thread packages/cli/src/ui/components/InputPrompt.tsx Outdated
- Update queueRef inside addMessage setter and clearQueue to keep ref
  in sync between renders, preventing stale reads after clearQueue
- When popAllQueuedMessages returns null (queue already cleared), fall
  through to normal history navigation instead of consuming the key

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 16 out of 16 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

packages/cli/src/ui/hooks/useMessageQueue.ts:86

  • useEffect auto-submission clears React state via setMessageQueue([]) but does not clear queueRef.current. In the window before the next render, popAllMessages() can still read/return the stale queued items from the ref (e.g., if the user presses ↑ right as streaming becomes Idle), leading to duplicate insertion/editing of messages that were just auto-submitted. Clear queueRef.current at the same time you clear state in the Idle auto-submit path (before calling submitQuery) so the ref remains the source of truth between renders.
  // Process queued messages when streaming becomes idle
  useEffect(() => {
    if (
      isConfigInitialized &&
      streamingState === StreamingState.Idle &&
      messageQueue.length > 0
    ) {
      // Combine all messages with double newlines for clarity
      const combinedMessage = messageQueue.join('\n\n');
      // Clear the queue and submit
      setMessageQueue([]);
      submitQuery(combinedMessage);
    }

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@wenshao wenshao added the DDAR DataWorks Data Agent Ready label Apr 4, 2026
wenshao and others added 3 commits April 8, 2026 18:58
Keep both popAllMessages (for Up-arrow queue editing) and drainQueue
(for atomic mid-turn drain). Adopt main's ref-first write pattern.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove unused AsyncMessageQueue.popLast() (no production callers)
- Change popAllMessages join separator from \n to \n\n for consistency
  with getQueuedMessagesText and auto-submit behavior

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…mption race

The midTurnDrainRef previously used a separate messageQueueRef (synced
from React state), while popAllMessages uses the hook's internal
queueRef. If a tool completed between popAllMessages clearing queueRef
and React re-rendering, midTurnDrainRef would read stale data and
consume the same messages a second time.

Switching to the hook's drainQueue makes both paths read from the same
synchronous ref, eliminating the window for double consumption.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@tanzhenxin tanzhenxin added the type/feature-request New feature or enhancement request label Apr 9, 2026
wenshao added 4 commits April 9, 2026 21:20
# Conflicts:
#	packages/cli/src/ui/AppContainer.tsx
Add popAllMessages to useMessageQueue mock in AppContainer tests.
Add test for prepending queued messages before existing input text.
- ESC pops queued messages before double-ESC clear logic
- Cursor stays at user's editing position after pop via moveToOffset
- Extract popQueueIntoInput helper to share logic between Up and ESC
- QueuedMessageDisplay hint hides after 3 empty→non-empty transitions
@wenshao wenshao changed the title feat(cli): add queue input editing via Up arrow key feat(cli): queue input editing — pop queued messages for editing via ↑/ESC Apr 9, 2026
Verify that when React state shows non-empty queue but the ref is
already drained (popAllQueuedMessages returns null), Up arrow falls
through to normal history navigation instead of getting stuck.

@tanzhenxin tanzhenxin left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Review

Clean implementation — all issues from the previous review have been addressed. The atomic ref pattern, shared popQueueIntoInput helper, and null-return fallthrough design are well done.

Verdict

APPROVE

@tanzhenxin tanzhenxin merged commit 61ad9db into QwenLM:main Apr 11, 2026
14 checks passed
xaelistic pushed a commit to xaelistic/qwen-code that referenced this pull request Jun 7, 2026
…↑/ESC (QwenLM#2871)

* feat(cli): add queue input editing via Up arrow key

Allow users to edit queued messages by pressing the Up arrow key when
the cursor is at the top of the input. All queued messages are popped
into the input field for revision before resubmission, reducing wasted
turns from incorrect queued instructions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add missing mocks for InputPrompt tests and attachment mode guard

- Add popAllQueuedMessages mock and messageQueue to UIState/UIActions
  mocks in InputPrompt.test.tsx to fix 25 test failures
- Add !isAttachmentMode guard to prevent queue pop from conflicting
  with attachment navigation
- Add single-message popAllMessages test case

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address Copilot review - restrict to Up arrow, add tests, update docs

- Only trigger queue pop on NAVIGATION_UP (arrow key), not HISTORY_UP
  (Ctrl+P), preserving existing Ctrl+P history navigation behavior
- Update AsyncMessageQueue class docs to describe popLast() LIFO semantics
- Add InputPrompt tests: Up arrow pops queue, Up arrow falls back to
  history when queue empty, Ctrl+P not intercepted by queue pop

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: update fileoverview docs and make popAllMessages atomic via ref

- Update @fileoverview to describe FIFO+LIFO capability instead of
  "Simple FIFO queue"
- Use queueRef to make popAllMessages atomic, preventing duplicate
  pops from key auto-repeat before React re-renders

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: sync queueRef in addMessage/clearQueue and fall through on null pop

- Update queueRef inside addMessage setter and clearQueue to keep ref
  in sync between renders, preventing stale reads after clearQueue
- When popAllQueuedMessages returns null (queue already cleared), fall
  through to normal history navigation instead of consuming the key

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: remove dead popLast() and align popAllMessages separator to \n\n

- Remove unused AsyncMessageQueue.popLast() (no production callers)
- Change popAllMessages join separator from \n to \n\n for consistency
  with getQueuedMessagesText and auto-submit behavior

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: use hook's drainQueue for mid-turn drain to prevent double-consumption race

The midTurnDrainRef previously used a separate messageQueueRef (synced
from React state), while popAllMessages uses the hook's internal
queueRef. If a tool completed between popAllMessages clearing queueRef
and React re-rendering, midTurnDrainRef would read stale data and
consume the same messages a second time.

Switching to the hook's drainQueue makes both paths read from the same
synchronous ref, eliminating the window for double consumption.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add missing popAllMessages mock and prepend branch test

Add popAllMessages to useMessageQueue mock in AppContainer tests.
Add test for prepending queued messages before existing input text.

* feat: add ESC trigger, cursor preservation, and progressive hint

- ESC pops queued messages before double-ESC clear logic
- Cursor stays at user's editing position after pop via moveToOffset
- Extract popQueueIntoInput helper to share logic between Up and ESC
- QueuedMessageDisplay hint hides after 3 empty→non-empty transitions

* test: add null-pop fallthrough test for queue race condition

Verify that when React state shows non-empty queue but the ref is
already drained (popAllQueuedMessages returns null), Up arrow falls
through to normal history navigation instead of getting stuck.

---------

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

Labels

DDAR DataWorks Data Agent Ready type/feature-request New feature or enhancement request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants