fix(cli): update docs, help text, and remove transcript dead code (#8077)

This commit is contained in:
Qwen Code Autofix 2026-07-30 10:30:52 +00:00
parent 33a32108b5
commit f07b027b88
23 changed files with 23 additions and 278 deletions

View file

@ -10,7 +10,7 @@ This document lists the available keyboard shortcuts in Qwen Code.
| `Ctrl+C` | Cancel the ongoing request and clear the input. Press twice to exit the application. |
| `Ctrl+D` | Exit the application if the input is empty. Press twice to confirm. |
| `Ctrl+L` | Clear the screen. |
| `Ctrl+O` | Open/close the full-detail transcript view (a scrollable, frozen snapshot showing every tool's complete output and full thinking). Press again, or `Esc`/`q`, to close. |
| `Ctrl+O` | Toggle expanded detail mode: expand or collapse all thinking blocks and tool outputs inline. Press again to collapse. |
| `Ctrl+S` | Stashes non-empty input for the current project and restores it on the next launch. With empty input, allows long responses to print fully, disabling truncation. Use your terminal's scrollback to view the entire output. |
| `Ctrl+T` | Toggle the display of tool descriptions. |
| `Ctrl+B` | While a foreground shell command is running: promote it to a background task. The child keeps running, the agent's turn unblocks, and the shell appears in `/tasks` + the Background tasks dialog. No-op when no shell is executing — Ctrl+B then falls through to its prompt-area binding (cursor-left). |

View file

@ -82,9 +82,6 @@ export enum Command {
// Thinking expansion
TOGGLE_THINKING_EXPANDED = 'toggleThinkingExpanded',
// Transcript full-detail screen (Ctrl+O)
TOGGLE_TRANSCRIPT = 'toggleTranscript',
// Scroll commands
SCROLL_UP = 'scrollUp',
SCROLL_DOWN = 'scrollDown',
@ -267,9 +264,6 @@ export const defaultKeyBindings: KeyBindingConfig = {
{ key: 't', meta: true },
],
// Transcript overlay — unbound (replaced by inline thinking expansion)
[Command.TOGGLE_TRANSCRIPT]: [],
// Scroll commands
[Command.SCROLL_UP]: [{ key: 'up', shift: true }],
[Command.SCROLL_DOWN]: [{ key: 'down', shift: true }],

View file

@ -58,7 +58,7 @@ export default {
'to search history': "per cercar a l'historial",
'to paste images': 'per enganxar imatges',
'for external editor': 'per a editor extern',
'to expand thinking': 'per expandir el pensament',
'to expand details': 'per expandir els detalls',
'Jump through words in the input': "Salta entre paraules a l'entrada",
'Close dialogs, cancel requests, or quit application':
"Tanca els diàlegs, cancel·la les peticions o surt de l'aplicació",
@ -254,7 +254,6 @@ export default {
Transcript: 'Transcripció',
'to close': 'per tancar',
'to scroll': 'per desplaçar',
'Failed to render transcript.': 'Error en renderitzar la transcripció.',
'Read {{count}} file': 'Ha llegit {{count}} fitxer',
'Read {{count}} files': 'Ha llegit {{count}} fitxers',
'Reading {{count}} file': 'Llegint {{count}} fitxer',

View file

@ -234,7 +234,6 @@ export default {
Transcript: 'Transkript',
'to close': 'zum Schließen',
'to scroll': 'zum Scrollen',
'Failed to render transcript.': 'Transkript konnte nicht gerendert werden.',
'Read {{count}} file': '{{count}} Datei gelesen',
'Read {{count}} files': '{{count}} Dateien gelesen',
'Reading {{count}} file': 'Lese {{count}} Datei',
@ -1911,7 +1910,7 @@ export default {
'Raw-Modus nicht verfügbar. Bitte in einem interaktiven Terminal ausführen.',
'(Use ↑ ↓ arrows to navigate, Enter to select, Ctrl+C to exit)\n':
'(↑ ↓ Pfeiltasten zum Navigieren, Enter zum Auswählen, Ctrl+C zum Beenden)\n',
'to expand thinking': 'zum Erweitern der Gedanken',
'to expand details': 'zum Erweitern der Details',
'Switch to plan mode or exit plan mode':
'In den Plan-Modus wechseln oder den Plan-Modus verlassen',
'Set how hard reasoning-capable models think ({{tiers}}); mapped and clamped per provider.':

View file

@ -276,7 +276,7 @@ export default {
'to search history': 'to search history',
'to paste images': 'to paste images',
'for external editor': 'for external editor',
'to expand thinking': 'to expand thinking',
'to expand details': 'to expand details',
'Jump through words in the input': 'Jump through words in the input',
'Close dialogs, cancel requests, or quit application':
'Close dialogs, cancel requests, or quit application',
@ -508,7 +508,6 @@ export default {
Transcript: 'Transcript',
'to close': 'to close',
'to scroll': 'to scroll',
'Failed to render transcript.': 'Failed to render transcript.',
'Read {{count}} file': 'Read {{count}} file',
'Read {{count}} files': 'Read {{count}} files',
'Reading {{count}} file': 'Reading {{count}} file',

View file

@ -258,7 +258,6 @@ export default {
Transcript: 'Transcription',
'to close': 'pour fermer',
'to scroll': 'pour défiler',
'Failed to render transcript.': 'Échec du rendu de la transcription.',
'Read {{count}} file': 'Lu {{count}} fichier',
'Read {{count}} files': 'Lu {{count}} fichiers',
'Reading {{count}} file': 'Lecture de {{count}} fichier',
@ -2045,7 +2044,7 @@ export default {
'Afficher le détail de lutilisation du contexte par élément.',
// === Missing key backfill ===
'to expand thinking': 'pour développer la réflexion',
'to expand details': 'pour développer les détails',
'The name of the extension to update.':
"Le nom de l'extension à mettre à jour.",
'Session (temporary)': 'Session (temporaire)',

View file

@ -210,7 +210,6 @@ export default {
Transcript: 'トランスクリプト',
'to close': '閉じる',
'to scroll': 'スクロール',
'Failed to render transcript.': 'トランスクリプトの描画に失敗しました。',
'Read {{count}} file': '{{count}} 件のファイルを読み込みました',
'Read {{count}} files': '{{count}} 件のファイルを読み込みました',
'Reading {{count}} file': '{{count}} 件のファイルを読み込み中',
@ -1449,7 +1448,7 @@ export default {
'Rawモードが利用できません。インタラクティブターミナルで実行してください。',
'(Use ↑ ↓ arrows to navigate, Enter to select, Ctrl+C to exit)\n':
'(↑ ↓ 矢印キーで移動、Enter で選択、Ctrl+C で終了)\n',
'to expand thinking': '思考を展開',
'to expand details': '詳細を展開',
'Switch to plan mode or exit plan mode':
'プランモードに切り替えるか、プランモードを終了する',
'Set how hard reasoning-capable models think ({{tiers}}); mapped and clamped per provider.':

View file

@ -54,7 +54,7 @@ export default {
'to search history': 'para pesquisar no histórico',
'to paste images': 'para colar imagens',
'for external editor': 'para editor externo',
'to expand thinking': 'para expandir o pensamento',
'to expand details': 'para expandir os detalhes',
'Jump through words in the input': 'Pular palavras na entrada',
'Close dialogs, cancel requests, or quit application':
'Fechar diálogos, cancelar solicitações ou sair do aplicativo',
@ -250,7 +250,6 @@ export default {
Transcript: 'Transcrição',
'to close': 'para fechar',
'to scroll': 'para rolar',
'Failed to render transcript.': 'Falha ao renderizar a transcrição.',
'Read {{count}} file': 'Leu {{count}} arquivo',
'Read {{count}} files': 'Leu {{count}} arquivos',
'Reading {{count}} file': 'Lendo {{count}} arquivo',

View file

@ -89,7 +89,7 @@ export default {
'to search history': 'поиск в истории',
'to paste images': 'вставить изображения',
'for external editor': 'внешний редактор',
'to expand thinking': 'развернуть размышления',
'to expand details': 'развернуть детали',
// ============================================================================
// Поля системной информации
@ -257,7 +257,6 @@ export default {
Transcript: 'Транскрипт',
'to close': 'закрыть',
'to scroll': 'прокрутить',
'Failed to render transcript.': 'Не удалось отобразить транскрипт.',
'Read {{count}} file': 'Прочитано файлов: {{count}}',
'Read {{count}} files': 'Прочитано файлов: {{count}}',
'Reading {{count}} file': 'Чтение файлов: {{count}}',

View file

@ -261,7 +261,7 @@ export default {
'to search history': '搜索歷史',
'to paste images': '粘貼圖片',
'for external editor': '外部編輯器',
'to expand thinking': '展開思考',
'to expand details': '展開詳情',
'Jump through words in the input': '在輸入中按單詞跳轉',
'Close dialogs, cancel requests, or quit application':
'關閉對話框、取消請求或退出應用程序',
@ -466,7 +466,6 @@ export default {
Transcript: '完整記錄',
'to close': '關閉',
'to scroll': '捲動',
'Failed to render transcript.': '無法呈現完整記錄。',
'Read {{count}} file': '讀取了 {{count}} 個檔案',
'Read {{count}} files': '讀取了 {{count}} 個檔案',
'Reading {{count}} file': '正在讀取 {{count}} 個檔案',

View file

@ -266,7 +266,7 @@ export default {
'to search history': '搜索历史',
'to paste images': '粘贴图片',
'for external editor': '外部编辑器',
'to expand thinking': '展开思考',
'to expand details': '展开详情',
'Jump through words in the input': '在输入中按单词跳转',
'Close dialogs, cancel requests, or quit application':
'关闭对话框、取消请求或退出应用程序',
@ -491,7 +491,6 @@ export default {
Transcript: '完整记录',
'to close': '关闭',
'to scroll': '滚动',
'Failed to render transcript.': '无法渲染完整记录。',
'Read {{count}} file': '读取了 {{count}} 个文件',
'Read {{count}} files': '读取了 {{count}} 个文件',
'Reading {{count}} file': '正在读取 {{count}} 个文件',

View file

@ -1,75 +0,0 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, afterEach } from 'vitest';
import { render } from 'ink-testing-library';
import { Text } from 'ink';
import { AlternateScreen } from './AlternateScreen.js';
const writeRaw = vi.fn();
vi.mock('../contexts/TerminalOutputContext.js', () => ({
useTerminalOutput: () => writeRaw,
}));
vi.mock('../hooks/useTerminalSize.js', () => ({
useTerminalSize: () => ({ rows: 24, columns: 80 }),
}));
const ENTER_ALT_SCREEN = '\x1b[?1049h';
const EXIT_ALT_SCREEN = '\x1b[?1049l';
describe('<AlternateScreen />', () => {
const origIsTTY = process.stdout.isTTY;
const setTTY = (value: boolean) =>
Object.defineProperty(process.stdout, 'isTTY', {
value,
configurable: true,
});
afterEach(() => {
writeRaw.mockClear();
setTTY(origIsTTY);
});
it('enters on mount and exits on unmount when stdout is a TTY', () => {
setTTY(true);
const { unmount } = render(
<AlternateScreen>
<Text>x</Text>
</AlternateScreen>,
);
expect(writeRaw).toHaveBeenCalledWith(
expect.stringContaining(ENTER_ALT_SCREEN),
);
writeRaw.mockClear();
unmount();
expect(writeRaw).toHaveBeenCalledWith(
expect.stringContaining(EXIT_ALT_SCREEN),
);
});
it('skips escape writes when disabled (VP mode owns the alt screen)', () => {
setTTY(true);
const { unmount } = render(
<AlternateScreen disabled>
<Text>x</Text>
</AlternateScreen>,
);
expect(writeRaw).not.toHaveBeenCalled();
unmount();
});
it('skips escape writes when stdout is not a TTY (piped/CI)', () => {
setTTY(false);
const { unmount } = render(
<AlternateScreen>
<Text>x</Text>
</AlternateScreen>,
);
expect(writeRaw).not.toHaveBeenCalled();
unmount();
});
});

View file

@ -1,65 +0,0 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import type { FC, ReactNode } from 'react';
import { useEffect } from 'react';
import { Box } from 'ink';
import { useTerminalOutput } from '../contexts/TerminalOutputContext.js';
import { useTerminalSize } from '../hooks/useTerminalSize.js';
const ENTER_ALT_SCREEN = '\x1b[?1049h';
const EXIT_ALT_SCREEN = '\x1b[?1049l';
const CLEAR_SCREEN = '\x1b[2J\x1b[H';
const HIDE_CURSOR = '\x1b[?25l';
const SHOW_CURSOR = '\x1b[?25h';
interface AlternateScreenProps {
children: ReactNode;
/** Skip escape writes when the root Ink renderer already owns the alt screen (VP mode). */
disabled?: boolean;
}
export const AlternateScreen: FC<AlternateScreenProps> = ({
children,
disabled,
}) => {
const writeRaw = useTerminalOutput();
const { rows } = useTerminalSize();
useEffect(() => {
// Skip when the root Ink renderer already owns the alt screen (VP mode),
// or when stdout is not a TTY (piped/redirected/CI): writing alt-screen
// escapes to a non-terminal would just emit garbage bytes. Mirrors the
// repo convention of guarding terminal-control writes on `isTTY`
// (see startInteractiveUI.tsx / notificationService.ts). On non-TTY the
// transcript degrades to in-buffer rendering (no full-screen takeover).
if (disabled || !process.stdout.isTTY) return;
// Guard the raw writes: stdout can throw synchronously (EPIPE when the
// terminal closes mid-render, EAGAIN under backpressure). An uncaught throw
// from this effect / its cleanup would crash the app or leave the terminal
// in a corrupt state; swallow it — a failed escape write is best-effort.
const safeWrite = (data: string) => {
try {
writeRaw(data);
} catch {
// best-effort terminal control; ignore transient I/O errors
}
};
safeWrite(ENTER_ALT_SCREEN + CLEAR_SCREEN + HIDE_CURSOR);
const onExit = () => safeWrite(SHOW_CURSOR + EXIT_ALT_SCREEN);
process.on('exit', onExit);
return () => {
process.removeListener('exit', onExit);
safeWrite(SHOW_CURSOR + EXIT_ALT_SCREEN);
};
}, [writeRaw, disabled]);
return (
<Box flexDirection="column" height={rows}>
{children}
</Box>
);
};

View file

@ -86,7 +86,7 @@ interface HistoryItemDisplayProps {
/** Force thinking blocks expanded (e.g. in SessionPreview). */
thoughtExpanded?: boolean;
/**
* Transcript full-detail mode (Ctrl+O). When true, collapse is lifted:
* Full-detail mode (Ctrl+O). When true, collapse is lifted:
* thinking blocks render expanded and tool groups force `forceExpandAll`
* + `forceShowResult` (every tool with its full, untruncated result).
* Default false (main view stays at the #5661 partition baseline).

View file

@ -38,7 +38,7 @@ const getShortcuts = (): Shortcut[] => [
{ key: 'ctrl+c', description: t('to quit') },
{ key: getNewlineKey(), description: t('for newline') + ' ⏎' },
{ key: 'ctrl+l', description: t('to clear screen') },
{ key: 'ctrl+o', description: t('to expand thinking') },
{ key: 'ctrl+o', description: t('to expand details') },
{ key: 'ctrl+r', description: t('to search history') },
{ key: 'ctrl+y', description: t('to retry last request') },
{ key: 'ctrl+q', description: t('to queue for the next turn') },

View file

@ -523,7 +523,7 @@ describe('<ToolGroupMessage />', () => {
});
});
// Transcript full-detail mode must NOT be short-circuited by the
// Full-detail mode must NOT be short-circuited by the
// memory-only / pure-parallel-agent early returns (which run before the
// forceExpandAll computation). Each tool must render in full.
describe('fullDetail bypasses compact early returns', () => {

View file

@ -150,7 +150,7 @@ interface ToolGroupMessageProps {
memoryReadCount?: number;
isUserInitiated?: boolean;
/**
* Transcript full-detail mode (Ctrl+O). When true, force `forceExpandAll`
* Full-detail mode (Ctrl+O). When true, force `forceExpandAll`
* (skip the type-based partition so every tool renders individually), pass
* `forceShowResult=true` to each `ToolMessage`, and lift the per-tool
* terminal-height truncation. Default false (main view keeps the #5661
@ -283,7 +283,7 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
// header's "N · done/N" honest, and `availableTerminalHeight` is a hard cap
// backstop for degenerate cases (many agents finishing at once).
//
// Skipped in transcript full-detail mode (fullDetail) so every agent
// Skipped in full-detail mode (fullDetail) so every agent
// falls through to its own full ToolMessage instead of the dense panel.
if (
!fullDetail &&
@ -332,7 +332,7 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
// Memory-only groups get their own compact rendering with read/write
// counts. Check BEFORE the partition logic so they aren't routed through
// the collapsible/non-collapsible split. Skipped in transcript full-detail
// the collapsible/non-collapsible split. Skipped in full-detail
// mode (fullDetail) so each memory op renders as its own full ToolMessage
// rather than collapsing to the "Recalled/Wrote N memories" badge.
const allMemOpsComplete =
@ -367,7 +367,7 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
// Force-expand ALL tools individually when the user must interact or
// must see full details: confirmation prompts, errors, user-initiated
// batches, focused shells, terminal subagents. Transcript full-detail
// batches, focused shells, terminal subagents. Full-detail
// mode (fullDetail) also forces it so every tool renders individually
// instead of collapsing read/search into a partition summary.
const hasTerminalSubagent = inlineToolCalls.some(isTerminalSubagentTool);
@ -454,7 +454,7 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
}
const countOneLineToolCalls =
nonCollapsibleTools.length - countToolCallsWithResults;
// In transcript full-detail mode, lift the per-tool height truncation so
// In full-detail mode, lift the per-tool height truncation so
// each tool's output renders in full (combined with forceShowResult below).
const availableTerminalHeightPerToolMessage = fullDetail
? undefined

View file

@ -803,7 +803,7 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
renderOutputAsMarkdown = false;
}
// §4.9: in transcript full-detail mode, collapsible tools (read/search/list)
// §4.9: in full-detail mode, collapsible tools (read/search/list)
// swap the summary `resultDisplay` for the complete `detailedDisplay` derived
// from the persisted functionResponse. Only a non-empty string detail
// qualifies; everything else (and all main-view rendering) keeps the summary.

View file

@ -7,7 +7,7 @@
import { createContext, useContext } from 'react';
export interface ThoughtExpandedValue {
/** Alt+T global toggle — expands every thinking block at once. */
/** Ctrl+O / Alt+T global toggle — expands every thinking block and tool output at once. */
allExpanded: boolean;
/**
* Head ids of thoughts the user expanded individually (by clicking the

View file

@ -161,9 +161,9 @@ export function useMouseEvents(
// Never write SGR mouse-mode escapes (?1002h ?1006h) unless stdout is a TTY.
// `isRawModeSupported` only reflects stdin; with stdout piped/redirected
// (`qwen | tee log`) an active, raw-mode-capable surface — e.g. the non-TTY
// transcript's focused ScrollableList (`bypassVpGate`) — would otherwise emit
// raw control bytes into the captured output. Mirrors AlternateScreen's
// `process.stdout.isTTY` guard so the non-TTY fallback stays byte-clean.
// focused ScrollableList (`bypassVpGate`) — would otherwise emit
// raw control bytes into the captured output. Mirrors the repo-wide
// `process.stdout.isTTY` convention so the non-TTY fallback stays byte-clean.
const enabled =
isActive && isRawModeSupported && vpGateOpen && Boolean(stdout.isTTY);

View file

@ -99,7 +99,6 @@ describe('keyMatchers', () => {
[Command.SCROLL_END]: (key: Key) => key.ctrl && key.name === 'end',
[Command.TOGGLE_THINKING_EXPANDED]: (key: Key) =>
(key.ctrl && key.name === 'o') || (key.meta && key.name === 't'),
[Command.TOGGLE_TRANSCRIPT]: (_key: Key) => false,
};
// Test data for each command with positive and negative test cases
@ -461,11 +460,6 @@ describe('keyMatchers', () => {
createKey('o'),
],
},
{
command: Command.TOGGLE_TRANSCRIPT,
positive: [],
negative: [createKey('o', { ctrl: true }), createKey('o')],
},
];
describe('Data-driven key binding matches original logic', () => {

View file

@ -16,7 +16,6 @@ import {
sanitizeSensitiveText,
sliceTextByVisualHeight,
truncateToWidth,
wrapToVisualLines,
} from './textUtils.js';
describe('textUtils', () => {
@ -398,49 +397,3 @@ describe('textUtils', () => {
});
});
});
describe('visual row counting agrees between wrap and slice', () => {
// Both functions are documented as measuring visual rows at a given width,
// and callers mix them (scroll offsets, pending-render height). They
// disagreed on anything `string-width` reports as zero width, because only
// one of them clamped the per-character width to 1.
// `hiddenLinesCount + visible` only recovers the true row count when the
// text actually overflows `visible`, so every case below is chosen to.
const rowsFromSlice = (text: string, width: number): number => {
const visible = 3;
return (
sliceTextByVisualHeight(text, visible, width).hiddenLinesCount + visible
);
};
it.each([
['tabs', '\t'.repeat(50)],
['combining marks', '́'.repeat(50)],
['zero-width joiners', ''.repeat(50)],
['a letter then combining marks', 'e' + '́'.repeat(49)],
])('agrees on a run of %s', (_label, text) => {
expect(wrapToVisualLines(text, 10).length).toBe(rowsFromSlice(text, 10));
});
// Guards against over-correcting: ordinary and wide characters were always
// consistent and must stay so. These pass before and after.
it.each([
['ascii', 'a'.repeat(50), 10],
['wide CJK', '漢'.repeat(25), 10],
['mixed', 'ab漢cd'.repeat(10), 10],
])('still agrees on %s', (_label, text, width) => {
expect(wrapToVisualLines(text, width).length).toBe(
rowsFromSlice(text, width),
);
});
it('still wraps a string shorter than the width to one row', () => {
expect(wrapToVisualLines('abc', 10)).toEqual(['abc']);
});
it('counts a run of tabs as more than one row', () => {
// The concrete regression: 50 zero-width characters at width 10 used to
// wrap to a single row.
expect(wrapToVisualLines('\t'.repeat(50), 10).length).toBe(5);
});
});

View file

@ -275,53 +275,6 @@ export function sliceTextByVisualHeight(
};
}
/**
* Wrap text into the visual rows it occupies at `width` columns, accounting
* for both explicit newlines and code-point-width-aware soft wrapping. Unlike
* `sliceTextByVisualHeight` (which keeps only a head/tail window), this returns
* every visual row, so callers that scroll an arbitrary offset can slice the
* rows the user actually sees.
*/
export function wrapToVisualLines(text: string, width: number): string[] {
if (width <= 0) {
return [''];
}
const visualLines: string[] = [];
for (const logicalLine of text.split('\n')) {
if (logicalLine === '') {
visualLines.push('');
continue;
}
let currentLine = '';
let currentWidth = 0;
for (const char of logicalLine) {
// Clamped to 1, matching sliceTextByVisualHeight. `string-width` reports
// 0 for TAB, ZWJ and combining marks, so without this a run of them was
// charged nothing and the whole run counted as a single row: 50 tabs at
// width 10 came back as 1 row here and 5 there, for the same input. Any
// caller mixing the two -- scroll offsets, pending-render height -- then
// disagreed with itself. Erring high is the safe direction for a
// terminal: reserving a row too many costs a blank line, while counting
// one too few overflows the region and pushes content off screen.
const charWidth = Math.max(getCachedStringWidth(char), 1);
if (currentWidth + charWidth > width && currentWidth > 0) {
visualLines.push(currentLine);
currentLine = '';
currentWidth = 0;
}
currentLine += char;
currentWidth += charWidth;
}
if (currentLine) {
visualLines.push(currentLine);
}
}
if (visualLines.length === 0) {
visualLines.push('');
}
return visualLines;
}
/**
* Clear the string width cache
*/