Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
346 changes: 346 additions & 0 deletions docs/design/vp-native-mouse-parity.md

Large diffs are not rendered by default.

68 changes: 34 additions & 34 deletions docs/users/configuration/settings.md

Large diffs are not rendered by default.

30 changes: 19 additions & 11 deletions docs/users/reference/keyboard-shortcuts.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,17 +102,25 @@ Press `Ctrl+R` to search prompt history, or shell history while shell mode is ac

Active when `ui.useTerminalBuffer` is enabled (Settings → UI → Virtualized History), screen reader mode is off, and Qwen Code is running in a compatible interactive terminal (`stdout` is a TTY, CI is inactive, and `TERM` is not `dumb`), which is the default for ordinary non-screen-reader sessions. In that mode conversation history is rendered inside an in-app viewport instead of the host terminal scrollback, so the keys below replace the terminal's native scroll.

| Shortcut | Description |
| --------------- | ------------------------------------------------------------------------------- |
| `Shift+Up` | Scroll history up one line. |
| `Shift+Down` | Scroll history down one line. |
| `PgUp` | Scroll history up one page (viewport height). |
| `PgDn` | Scroll history down one page (viewport height). |
| `Ctrl+Home` | Jump to the top of the conversation. |
| `Ctrl+End` | Jump to the bottom (and re-engage live auto-follow). |
| **Mouse wheel** | Scroll history (3 lines per tick). Requires `ui.mouseTracking` (on by default). |

When `ui.useTerminalBuffer` is on and `ui.mouseTracking` is enabled (the default), the terminal forwards mouse events to qwen-code so the wheel can drive the in-app viewport. As a side effect, native click-and-drag text selection is consumed by the program, so qwen-code provides its own: **drag to select text in the history viewport, double-click to select a word, triple-click to select a line.** The selection is highlighted and copied to the clipboard when you release the mouse (works locally, over SSH via OSC 52, and inside tmux). A single click clears the selection; scrolling or new output clears it too. Selection is limited to the visible viewport for now. You can still fall back to the terminal's own selection by holding `Shift` (or `Option` on macOS Terminal / iTerm) while dragging. Set `ui.mouseTracking` to `false` to stop qwen-code from capturing the mouse entirely; that restores the terminal's native right-click menu, OSC 8 hyperlink clicks, and click-and-drag selection, but the in-app viewport no longer responds to the mouse, so use the keyboard shortcuts above to scroll.
| Shortcut | Description |
| --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Shift+Up` | Scroll history up one line. |
| `Shift+Down` | Scroll history down one line. |
| `PgUp` | Scroll history up one page (viewport height). |
| `PgDn` | Scroll history down one page (viewport height). |
| `Ctrl+Home` | Jump to the top of the conversation. |
| `Ctrl+End` | Jump to the bottom (and re-engage live auto-follow). |
| **Mouse wheel** | Scroll history (3 lines per tick). Requires `ui.mouseTracking` (on by default). |
| **Ctrl+click** | Open an http(s) hyperlink under the pointer in your browser (other link schemes are copied to the clipboard instead). Requires `ui.mouseTracking`. |
| **Right-click** | Over a link or a text selection, open an in-app context menu (Open Link / Copy Link Address / Copy Selection). Requires `ui.mouseTracking`. |

When `ui.useTerminalBuffer` is on and `ui.mouseTracking` is enabled (the default), the terminal forwards mouse events to qwen-code so the wheel can drive the in-app viewport. As a side effect, native mouse behaviors are consumed by the program, so qwen-code provides its own equivalents:

- **Text selection:** drag to select text in the history viewport, double-click to select a word, triple-click to select a line. The selection is highlighted and copied to the clipboard when you release the mouse (works locally, over SSH via OSC 52, and inside tmux). A single click clears the selection; scrolling or new output clears it too. Selection is limited to the visible viewport for now.
- **Hyperlinks:** `Ctrl+click` a link to open it in your browser (on terminals such as iTerm2 that intercept `⌘+click` themselves, that native gesture is expected to keep working). `Ctrl` is used because `Shift`/`Option` stay reserved for the terminal-selection bypass below, and plain clicks keep their existing behavior. Only http(s) links open in the browser; other link schemes (mailto:, ftp:, ssh:, …) are copied to the clipboard instead.
- **Right-click menu:** right-click over a hyperlink or an active text selection opens a small context menu (right-click on plain text is a deliberate no-op). Over a hyperlink it offers **Open Link** and **Copy Link Address**; over an active text selection it offers **Copy Selection**. Navigate it with the mouse or `↑`/`↓` + `Enter`, and dismiss it with `Esc`, a click elsewhere, or scrolling.

You can still fall back to the terminal's own selection by holding `Shift` (or `Option` on macOS Terminal / iTerm) while dragging. Set `ui.mouseTracking` to `false` to stop qwen-code from capturing the mouse entirely and hand every mouse gesture back to the terminal (native right-click menu, link clicks, and click-and-drag selection); the in-app viewport then no longer responds to the mouse, so use the keyboard shortcuts above to scroll.

### tmux trackpad scrolling

Expand Down
8 changes: 4 additions & 4 deletions docs/users/support/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,10 +102,10 @@ This guide provides solutions to common issues and debugging tips, including top
- **Cause:** tmux can translate wheel gestures into plain arrow-key sequences. Those sequences are indistinguishable from real arrow-key presses by the time qwen-code receives them.
- **Solution:** If screen reader mode is disabled, make sure `ui.useTerminalBuffer` is enabled; then use `Shift+Up` / `Shift+Down`, or the mouse wheel when tmux forwards wheel events to the app (requires `ui.mouseTracking`). If you prefer host scrollback, adjust your tmux mouse bindings for wheel events.

- **Right-click does nothing, links do not open, or text cannot be selected in the terminal**
- **Issue:** Native right-click context menus, OSC 8 hyperlink clicks (Ctrl+Click or plain click on URLs), and terminal-native text selection stop working while Qwen Code is running.
- **Cause:** When `ui.mouseTracking` is enabled (the default), Qwen Code captures all mouse events via SGR mouse tracking to power in-app text selection, click-to-position, row hover, and viewport scrolling. The terminal forwards every mouse event to the app instead of handling it natively.
- **Solution:** Set `"ui.mouseTracking": false` in your `settings.json` to restore native right-click menus and clickable URL links. This turns off all in-app mouse interaction. In Virtualized History (`ui.useTerminalBuffer: true`, the default), the wheel will no longer scroll the transcript — use `Shift+↑/↓`, `PgUp/PgDn`, or `Ctrl+Home/End` instead. To also restore native terminal scrollback, set `"ui.useTerminalBuffer": false`. Requires restart.
- **Right-click, link clicks, or text selection behave differently in the terminal**
- **Issue:** The terminal's own right-click menu, link clicks, and click-and-drag selection seem to stop working while Qwen Code is running.
- **Cause:** When `ui.mouseTracking` is enabled (the default), Qwen Code captures mouse events via SGR mouse tracking to power in-app text selection, click-to-position, row hover, and viewport scrolling, so the terminal can no longer handle those gestures itself. Qwen Code supplies in-app equivalents: **Ctrl+click** opens an http(s) hyperlink under the pointer (other link schemes are copied to the clipboard), **right-click** over a link or a text selection opens an in-app context menu (Open Link / Copy Link Address / Copy Selection), and dragging selects text (copied on release).
- **Solution:** Use the in-app gestures above. If you prefer the terminal to handle the mouse natively instead, set `"ui.mouseTracking": false` in your `settings.json`. This turns off all in-app mouse interaction; in Virtualized History (`ui.useTerminalBuffer: true`, the default) the wheel will no longer scroll the transcript — use `Shift+↑/↓`, `PgUp/PgDn`, or `Ctrl+Home/End` instead. To also restore native terminal scrollback, set `"ui.useTerminalBuffer": false`. Requires restart.

## IDE Companion not connecting

Expand Down
57 changes: 57 additions & 0 deletions integration-tests/terminal-capture/scenarios/vp-context-menu.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/**
* Visual evidence for PR #8439 (VP native mouse parity).
*
* Runs the bundled CLI against a mock model that always replies with a
* markdown link, then injects raw SGR mouse bytes (the app is in VP mode with
* mouse tracking on): a right-click on the link label must raise the in-app
* context menu; Escape dismisses it.
*
* Prereq: the mock server must be listening, e.g.
* PORT=8795 node /tmp/vpm-final2/mock-openai-server.js
Comment thread
chiga0 marked this conversation as resolved.
*
* Label geometry verified by E2E on a 120x40 terminal: the `Example Domain`
* label renders at row 13, cols 9–22 (mid-label col 16).
*/
import type { ScenarioConfig } from '../scenario-runner.js';

const MOCK_PORT = 8795;

// SGR right-button press/release at col 16, row 13 (1-based).
const RIGHT_PRESS = '\u001b[<2;16;13M';
const RIGHT_RELEASE = '\u001b[<2;16;13m';

export default {
name: 'vp-context-menu',
Comment thread
chiga0 marked this conversation as resolved.
spawn: [
'bash',
'-c',
`TMUX= CI= CONTINUOUS_INTEGRATION= FORCE_HYPERLINK=1 BROWSER=echo ` +
`node dist/cli.js ` +
`--auth-type openai ` +
`--openai-base-url http://127.0.0.1:${MOCK_PORT}/v1 ` +
`--openai-api-key sk-mock -m mock-model --approval-mode yolo`,
],
terminal: {
cols: 120,
rows: 40,
title: 'qwen-code — VP context menu',
cwd: '../../..',
},
flow: [
// Dismiss the startup "Built-in Provider Update" dialog if it appears
// (Escape = "Remind me later"). Harmless when no dialog is shown.
{ sleep: 6000, key: 'Escape' },
// A following `key` step disables auto-Enter, so submit explicitly.
{
type: 'reply with exactly one line: See [Example Domain](https://example.com/) for details.',
},
{ sleep: 1000, key: 'Enter' },
// Right-click on the link label → context menu appears.
{ sleep: 500, key: [RIGHT_PRESS, RIGHT_RELEASE] },
{ capture: '02-menu-open.png' },
// Escape dismisses the menu.
{ key: 'Escape' },
{ capture: '03-menu-closed.png' },
],
gif: false,
} satisfies ScenarioConfig;
4 changes: 2 additions & 2 deletions packages/cli/src/config/settingsSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1082,7 +1082,7 @@ const SETTINGS_SCHEMA = {
requiresRestart: true,
default: true,
description:
'Render conversation history in an in-app scrollable viewport instead of the terminal scrollback buffer. Enabled by default in compatible interactive terminals to avoid flicker, scroll-storm, and interface freeze on long sessions, after Ctrl+O, after Ctrl+E / Ctrl+F (expand), after window resize, or when alt-tabbing back. Screen reader mode and non-interactive output such as piped stdout or CI use append-only terminal output instead. Scroll with Shift+↑/↓ (line), PgUp/PgDn (page), Ctrl+Home/End (top/bottom), or the mouse wheel. Also enables mouse interactions: click an option in a menu/dialog to select it, hover to highlight it, and click in the prompt to position the cursor. Does NOT use the host terminal scrollback while enabled. Drag to select text in the viewport (double/triple click selects a word/line), copied on release. To use the terminal’s own selection instead, hold Shift (or Option on macOS) while dragging. These mouse interactions are controlled by ui.mouseTracking; disable that setting to restore native right-click and OSC 8 hyperlink clicks.',
'Render conversation history in an in-app scrollable viewport instead of the terminal scrollback buffer. Enabled by default in compatible interactive terminals to avoid flicker, scroll-storm, and interface freeze on long sessions, after Ctrl+O, after Ctrl+E / Ctrl+F (expand), after window resize, or when alt-tabbing back. Screen reader mode and non-interactive output such as piped stdout or CI use append-only terminal output instead. Scroll with Shift+↑/↓ (line), PgUp/PgDn (page), Ctrl+Home/End (top/bottom), or the mouse wheel. Also enables mouse interactions: click an option in a menu/dialog to select it, hover to highlight it, and click in the prompt to position the cursor. Does NOT use the host terminal scrollback while enabled. Drag to select text in the viewport (double/triple click selects a word/line), copied on release. To use the terminal’s own selection instead, hold Shift (or Option on macOS) while dragging. Ctrl+click opens an http(s) hyperlink under the pointer (other link schemes are copied to the clipboard), and right-click over a link or a text selection opens an in-app context menu. These mouse interactions are controlled by ui.mouseTracking; disable that setting to hand the mouse fully back to the terminal.',
showInDialog: true,
},
showScrollbar: {
Expand All @@ -1102,7 +1102,7 @@ const SETTINGS_SCHEMA = {
requiresRestart: true,
default: true,
description:
'Enable in-app SGR mouse tracking. While enabled, Qwen Code captures mouse events for text selection, click-to-position in text inputs, row hover, history-item toggling, and viewport scrolling. Because the terminal forwards all mouse events to the app, it cannot show native right-click context menus or open OSC 8 hyperlink clicks. Disable to restore native right-click and clickable URL links; this turns off all in-app mouse interaction, and in Virtualized History the wheel no longer scrolls the transcript — use Shift+↑/↓, PgUp/PgDn, or Ctrl+Home/End instead (pair with ui.useTerminalBuffer: false to restore native terminal scrollback).',
'Enable in-app SGR mouse tracking. While enabled, Qwen Code captures mouse events for text selection, click-to-position in text inputs, row hover, history-item toggling, and viewport scrolling. Because the terminal forwards all mouse events to the app, Qwen Code supplies its own equivalents for what the terminal can no longer do natively: Ctrl+click opens an http(s) hyperlink under the pointer (other link schemes are copied to the clipboard; or ⌘+click where your terminal intercepts it), and right-click over a link or a text selection opens an in-app context menu with Open Link / Copy Link Address / Copy Selection. Disable to hand the mouse fully back to the terminal (native right-click menu and link clicks); this turns off all in-app mouse interaction, and in Virtualized History the wheel no longer scrolls the transcript — use Shift+↑/↓, PgUp/PgDn, or Ctrl+Home/End instead (pair with ui.useTerminalBuffer: false to restore native terminal scrollback).',
showInDialog: true,
},
shellOutputMaxLines: {
Expand Down
22 changes: 21 additions & 1 deletion packages/cli/src/ui/AppContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ import {
} from './hooks/useExtensionUpdates.js';
import { useProviderUpdates } from './hooks/useProviderUpdates.js';
import { ShellFocusContext } from './contexts/ShellFocusContext.js';
import { ContextMenuProvider } from './context-menu/ContextMenuContext.js';
import {
RenderModeProvider,
type RenderMode,
Expand Down Expand Up @@ -3191,6 +3192,14 @@ export const AppContainer = (props: AppContainerProps) => {
const ctrlDTimerRef = useRef<NodeJS.Timeout | null>(null);
const [escapePressedOnce, setEscapePressedOnce] = useState(false);
const escapeTimerRef = useRef<NodeJS.Timeout | null>(null);

// Mirror of the context-menu open state: the provider wraps the app below
// this component's own always-active keypress handler, so the handler
// cannot call useContextMenu() — the provider reports changes here instead.
const contextMenuOpenRef = useRef(false);
const handleContextMenuChange = useCallback((open: boolean) => {
contextMenuOpenRef.current = open;
}, []);
const dialogsVisibleRef = useRef(false);
const [isRewindSelectorOpen, setIsRewindSelectorOpen] = useState(false);
const [rewindEscPending, setRewindEscPending] = useState(false);
Expand Down Expand Up @@ -3972,6 +3981,12 @@ export const AppContainer = (props: AppContainerProps) => {
handleExit(ctrlDPressedOnce, setCtrlDPressedOnce, ctrlDTimerRef);
return;
} else if (keyMatchers[Command.ESCAPE](key)) {
// While the context menu is open its overlay owns Esc (closing the
// menu); the global branches below must not also fire on the same
// key — cancelling the stream, arming double-Esc, or cancelling btw.
if (contextMenuOpenRef.current) {
return;
}
// In vim INSERT mode, let vim's own handler (in InputPrompt) consume
// the Esc to switch to NORMAL mode. Without this guard, both handlers
// fire on the same keypress — vim switches mode AND AppContainer
Expand Down Expand Up @@ -4054,6 +4069,7 @@ export const AppContainer = (props: AppContainerProps) => {
btwItem &&
!btwItem.btw.isPending &&
!dialogsVisibleRef.current &&
!contextMenuOpenRef.current &&
buffer.text.length === 0
) {
if (key.name === 'return' || key.sequence === ' ') {
Expand Down Expand Up @@ -4768,7 +4784,11 @@ export const AppContainer = (props: AppContainerProps) => {
<RenderModeProvider value={renderModeValue}>
<TerminalOutputProvider value={writeRaw}>
<ShellFocusContext.Provider value={isFocused}>
<App />
<ContextMenuProvider
onMenuChange={handleContextMenuChange}
>
<App />
</ContextMenuProvider>
</ShellFocusContext.Provider>
</TerminalOutputProvider>
</RenderModeProvider>
Expand Down
10 changes: 10 additions & 0 deletions packages/cli/src/ui/components/HistoryItemDisplay.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -737,6 +737,16 @@ describe('<HistoryItemDisplay />', () => {
expect(toggle).not.toHaveBeenCalled();
});

it('does not toggle on Ctrl+click (reserved for the link gesture)', () => {
const toggle = vi.fn();
const handler = renderThoughtWithToggle(toggle);

handler?.({ ...mouseEvent('left-press', 5), ctrl: true });
handler?.({ ...mouseEvent('left-release', 5), ctrl: true });

expect(toggle).not.toHaveBeenCalled();
});

it('hides the click hint when ui.mouseTracking is false despite VP being on', () => {
const settingsNoMouse = new LoadedSettings(
{ path: '', settings: {}, originalSettings: {} },
Expand Down
Loading