Commit graph

21 commits

Author SHA1 Message Date
ChiGao
0e229be76e
feat(tui): Ctrl+O frozen transcript view and unified tool output rendering (#5666)
* feat(tui): remove tool group borders and collapse completed tool results

Remove round borders from ToolGroupMessage, CompactToolGroupDisplay, and
InlineParallelAgentsDisplay. Completed tools now default to a single
collapsed header line with dimColor styling. Executing/error/confirming
tools continue to show their full result block.

Part of #4588 (Track 3: Simplify tool-call rendering).

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(tui): gate collapse on compact mode and fix innerWidth calculation

- Only collapse completed tool results in compact mode, preserving
  full visibility in non-compact mode
- Subtract 2 from innerWidth to account for ToolMessage paddingX={1}
- Update snapshots to reflect removed borders

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(tui): address review feedback on collapse and visual alignment

- Gate isDim on compact mode so non-compact tools stay fully styled
- Add paddingX={1} to CompactToolGroupDisplay for left-edge alignment
- Delete Border Color Logic test block (borders removed)
- Add compact-mode test coverage for Error/Executing/Pending/forceShowResult
- Clean up stale border references in comments

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* feat(tui): unify tool output with semantic summaries

Replace the dual compact/normal mode tool output with a single unified
mode. Completed tools always show a semantic overview line
("Read 3 files, edited 2 files") instead of dumping full results.

- Add buildToolSummary() for category-based semantic summaries
- Remove compactMode gate from shouldCollapse and isDim in ToolMessage
- Make all-completed tool groups use CompactToolGroupDisplay
- Remove unused useCompactMode hook calls from ToolMessage

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* test(tui): add buildToolSummary unit tests and fix stale comment

- Add 10 dedicated unit tests for buildToolSummary covering edge cases
- Fix stale comment referencing old compactMode gate logic

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(tui): address audit findings for unified tool output

- Add Canceled status to allComplete check in ToolGroupMessage
- Move memory-only group rendering before showCompact to prevent
  them being swallowed by CompactToolGroupDisplay
- Fix LLM summary duplication: absorbedCallIds now tracks completed
  groups in non-compact mode; HistoryItemDisplay no longer bypasses
  summaryAbsorbed when !compactMode
- Update StandaloneSessionPicker test for new compact rendering
- Fix design doc category order example and add missing rendering rules

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(tui): address inline review findings

- Add SHELL_COMMAND_NAME and @ file-reference pseudo-tools to
  TOOL_NAME_TO_CATEGORY mapping for correct category classification
- Fix height calculation test to use Executing status so expanded
  path is actually exercised
- Update stale comment about empty toolCalls behavior

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(tui): remove unused compactMode import in HistoryItemDisplay

Fixes CI build failure caused by TS6133 (noUnusedLocals) — the
compactMode destructure became dead code after the summary gating
was moved to summaryAbsorbed.

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* ci: trigger re-run with updated merge ref

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* docs(tui): design — remove global compact mode, add Ctrl+O transcript + mouse click-to-expand

Design-only. Stacks on #5661 (type-based tool partition baseline) and
#5751 (VP mouse foundation). Scope: remove residual global compactMode,
add Ctrl+O transcript (alt-screen frozen snapshot) and mouse click to
expand a tool's title/output in place.

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* feat(tui): remove global compact mode toggle (on top of #5661 partition baseline)

Builds on #5661's type-based tool partition. Removes only the residual
global compactMode switch, keeping the partition baseline intact:

- ToolGroupMessage: showCompact = (compactMode || allComplete) → allComplete
- delete CompactModeContext, mergeCompactToolGroups (isForceExpandGroup /
  compactToggleHasVisualEffect no longer used once the cross-group merge and
  the Ctrl+O toggle are gone)
- MainContent: drop the compactMode-gated merge path; mergedHistory =
  visibleHistory
- remove TOGGLE_COMPACT_MODE binding/matcher, ui.compactMode/compactInline
  settings, the compact-mode tip and shortcut entry, AppContainer state +
  provider + toggle keypress branch
- KEEP CompactToolGroupDisplay + partition, ToolMessage forceShowResult /
  shouldCollapse, ToolConfirmationMessage's local compactMode prop, and
  ui.compactMode in WEB_SHELL_SETTINGS (web shell is a separate surface)

typecheck + affected suites green (224 tests). Ctrl+O is a temporary no-op
until the TranscriptView lands.

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* feat(tui): Ctrl+O opens a frozen alt-screen transcript full-detail view

Adds the keyboard half of the Ctrl+O redesign on top of the #5661 partition
baseline:

- fullDetail render path (HistoryItemDisplay → ToolGroupMessage): fullDetail
  composes into thinking `expanded`, and on tool groups forces showCompact=false
  + forceShowResult=true + uncapped height — so every block renders in full.
- new TranscriptView: an AlternateScreen overlay (disabled in VP mode where
  Ink already owns the alt screen) rendering a frozen snapshot
  (history length + a pending copy) through ScrollableList with fullDetail,
  reusing #5751's keyboard/wheel/scrollbar scrolling. Adaptive
  estimatedItemHeight for the taller full-detail rows.
- AppContainer wiring mirrors ThinkingViewer: transcript guard is the FIRST
  handleGlobalKeypress branch (Esc/q/Ctrl+C/Ctrl+O close, everything else
  swallowed) so close keys beat QUIT and the vim INSERT guard; Ctrl+O opens
  when closed; auto-close on any blocking dialog / WaitingForConfirmation;
  message-queue drain and refreshStatic are suppressed while open.
- Command.TOGGLE_TRANSCRIPT bound to Ctrl+O.

typecheck + 8 suites (268 tests) green. Mouse click-to-expand (per-tool)
follows in a later commit. Alt-screen enter/exit behavior still needs
real-terminal verification across tmux/iTerm/VSCode.

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(tui): repaint normal buffer when transcript closes (no duplicate scrollback)

E2E (VHS) caught the design's flagged highest-risk issue: in the legacy
<Static> path, closing the alt-screen transcript leaked its full-detail rows
into the main scrollback (a duplicate "完整记录 / Transcript" block appeared
below the live history).

Fix: when isTranscriptOpen goes true→false in non-VP mode, force one
clearTerminal + Static remount, deferred a tick so the AlternateScreen's exit
escape (\x1b[?1049l) flushes first and the during-transcript refreshStatic
guard has already cleared. VP mode keeps its own scrollback via the React tree
and is unaffected.

Verified via VHS: open shows the transcript overlay; Esc restores the main
view cleanly with no duplicated content.

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* docs(tui): rebase ctrl-o design doc to #5661's type-based partition

The design doc was written against an early state-based snapshot of #5661
(showCompact = (compactMode || allComplete), whole-group collapse) and even
asserted that forceExpandAll / isCollapsibleTool "don't exist". The merged
#5661 is type-based partition and those symbols are its core. Rewrite the
affected sections to match the shipped baseline:

- §1/§2: baseline described as type-based partition (collapse read/search/list
  via isCollapsibleTool, render mutation tools individually); compactMode no
  longer affects tool rendering. Added a revision note.
- §3.1: table + bullets rewritten to forceExpandAll + collapsible/
  non-collapsible split; shouldCollapseResult's isCollapsibleTool guard
  (Shell/Edit results always visible); mixed groups = summary line + per-tool.
- §4.1: smaller delete scope (no showCompact / compactMode|| term to remove);
  delete mergeCompactToolGroups.ts; keep web-shell ui.compactMode passthrough.
- §4.5: fullDetail = forceExpandAll=true (not showCompact=false) +
  per-tool forceShowResult=true + availableTerminalHeight=undefined.
- §4.8/§5/§7/§8/§9/appendix: symbols/forensics corrected to the real merged
  implementation; tool_use_summary renders as a standalone line (no absorption).

Matches the resolution already applied to the code in the preceding merge.

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* docs(tui): fix factual nits from cross-audit of the ctrl-o design doc

Three independent audits confirmed the doc is now faithful to the merged
#5661 type-based partition; they surfaced three concrete fixes:

- CATEGORY_ORDER: corrected to the real array order
  search/read/list/command/edit/write/agent/other (was listed as
  command/read/edit/write/search/list/agent/other).
- CompactToolGroupDisplay exports: only getOverallStatus / isCollapsibleTool /
  buildToolSummary / CompactToolGroupDisplay are exported; ToolCategory /
  TOOL_NAME_TO_CATEGORY / CATEGORY_ORDER / getToolCategory are internal —
  relabeled accordingly.
- §5.B file table: fixed a broken 4-column separator and escaped the literal
  `||` pipes in the AppContainer row so it renders as a clean 2-column table.

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(tui): don't let fullDetail be bypassed by compact early returns

Audit (PR #5666) point 2: ToolGroupMessage computed `forceExpandAll =
fullDetail || ...` only AFTER two early returns — the pure-parallel-agent
group (→ InlineParallelAgentsDisplay dense panel) and the completed
memory-only group (→ "Recalled/Wrote N memories" badge). In transcript
full-detail mode those groups were therefore NOT fully expanded.

Guard both early returns with `!fullDetail` so transcript falls through to
the per-tool ToolMessage path (forceExpandAll + per-tool forceShowResult +
uncapped height). Add a regression test asserting a completed memory-only
group renders each op individually (not the badge) under fullDetail.

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* docs(tui): resolve open design decisions from source evidence

Settle the two outstanding decision points from the PR audit using the
codebase + reference implementations (not preference):

- Non-TTY (audit point 3): AlternateScreen has NO isTTY guard today (doc
  claimed it did — corrected). The TUI is already gated by stdin.isTTY
  (config.ts:1532), so non-TTY rarely mounts; the only edge is `-i`.
  Decision: add a process.stdout.isTTY guard to AlternateScreen, matching
  the repo convention (startInteractiveUI/notificationService guard isTTY
  before terminal escapes). Doc now marks it "to implement" + test.

- Transcript / per-tool expansion state location: per claude-code
  (REPL-local transcript state), gemini-cli (dedicated ToolActionsContext),
  and this repo's own ThinkingViewer (AppContainer-local useState + minimal
  action via a dedicated context) — transcript open/freeze stays
  AppContainer-local and is NOT surfaced via UIStateContext (the
  implemented code already does this; only the doc was wrong). Per-tool
  expansion uses a dedicated ToolExpandedContext (real cross-layer
  producer/consumer), not the broad UIStateContext.

Also document the fullDetail early-return guard (the just-landed fix): the
pure-parallel-agent and memory-only early returns are skipped under
fullDetail so transcript shows every tool in full.

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* docs(tui): align design doc status/scope with current PR (audit follow-up)

Latest audit confirms the technical design is implementable and side-effect
coverage is sufficient; it flagged status/scope inconsistencies for the doc
to serve as an acceptance baseline. Fixes:

1. Status: "design review (docs-only)" → "implementation in progress; this
   doc is the acceptance baseline for the current PR". Added an
   implemented-vs-pending status table.
2. Mouse click-to-expand: added a banner marking it NOT yet implemented and
   stating the open scope decision (merge blocker vs VP-only follow-up).
3. #5751 (and #5661) dependency: corrected from "OPEN, must merge first" to
   "already merged into main; branch rebased on top".
4. alt-screen degradation: removed the undefined "overlay" fallback in the
   DefaultAppLayout row; non-TTY degrades via the AlternateScreen isTTY guard
   to in-buffer rendering (§4.2), no separate overlay path.
5. Fixed a broken bold marker (`\*\*`) in the AppContainer row.

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* docs(tui): scope mouse click-to-expand out as a follow-up

Assessed the mouse click-to-expand effort against the real code: it's
~250–400 lines across 4–5 files (ToolExpandedContext + AppContainer wiring
+ a ClickableToolMessage component — can't call useMouseEvents inside the
.map() — + ToolGroupMessage wiring + mouse hit-test tests). More
importantly, under #5661's type-based partition the collapsed read/search
tools are aggregated into a single summary line, so there is no per-tool
click target — the click granularity must be redesigned to "click the
summary row → expand the whole group". Plus the known SGR-mouse vs native
text-selection risk.

Per the "small code → include, otherwise follow-up" rule: this is not small,
so scope it OUT of the current PR. The current PR delivers Ctrl+O transcript
only. Marked §1 goal #4, §4.8 (banner + draft), §9 commit 4, and the status
table accordingly; the §4.8 design is kept as a draft for the follow-up PR.

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* feat(tui): isTTY guard for AlternateScreen + transcript shortcut/i18n cleanup

Completes the remaining in-scope items for the Ctrl+O transcript PR:

- AlternateScreen: guard the alt-screen escape writes on
  `process.stdout.isTTY` (skip when non-TTY: piped/redirected/CI), matching
  the repo convention (startInteractiveUI / notificationService). Non-TTY
  now degrades to in-buffer rendering. Adds AlternateScreen.test.tsx
  (enter/exit on TTY, skip when disabled, skip when non-TTY).
- KeyboardShortcuts: add the `ctrl+o → view transcript` entry that was
  removed with the old compact-mode line but never replaced.
- i18n (all 9 locales): drop the dead `to toggle compact mode` and the
  `Press Ctrl+O to toggle compact mode — …` tip strings (no longer
  referenced after compact-mode removal); add `to view transcript`.

Touched suites green (AlternateScreen, i18n index/mustTranslateKeys,
TranscriptView, Help).

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* docs(tui): mark isTTY guard + i18n cleanup as implemented in status table

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(i18n): add TranscriptView strings to all locales

TranscriptView.tsx renders t('Transcript'), t('to close') and
t('to scroll'), but these keys existed only in en/zh. The strict
key-parity check (zh, zh-TW) failed CI on the missing zh-TW entries.

Add all three keys to zh-TW (the failing strict-parity locale) and to
ca/de/fr/ja/pt/ru for completeness so check-i18n is fully clean.

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* docs(ctrl-o): add before/after transcript capture evidence

Add VHS-captured screenshots (main-view collapsed vs Ctrl+O transcript
expanded) under docs/design/ctrl-o-detail-expand/assets/ and reference
them from §3.4 of the design doc. Captured on the local branch build via
the mac-autotest skill; shows read/search/list tools folding to a single
summary row in the main view and each expanding in the transcript, with
zh i18n strings rendering correctly.

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* docs(ctrl-o): design §4.9 — full tool detail passthrough in transcript

Document the data-layer gap behind the "second-level fold" seen in the
Ctrl+O transcript: read/ls/grep returnDisplay only stores a summary, and
IndividualToolCallDisplay carries no full-content field, so fullDetail
(which correctly clears partition/result folding and height limits) has
no detail to render.

Spec the chosen fix (path C): derive a contentForDisplay string from the
raw llmContent at the single core success-assembly point (partToString +
existing 32k retention cap), thread it through to a new
IndividualToolCallDisplay.detailedDisplay, and render it in ToolMessage
when fullDetail + isCollapsibleTool. Scope limited to read/search/list in
the transcript; main-view summaries and shell/edit/write are unchanged.

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* docs(ctrl-o): adopt plan Y for §4.9 and address transcript-detail audit

Address the audit on §4.9 (full tool detail in the Ctrl+O transcript):

- Rewrite §4.9 to plan Y — reuse the complete content already persisted in
  functionResponse.response.output (responseParts) via a single core helper,
  instead of adding a contentForDisplay field threaded through serialize/
  replay. Saved/replayed transcripts get full detail for free (audit #6).
- Split fullDetail (data-source switch) from forceShowResult (un-fold) so
  main-view force cases (user-initiated/error) don't leak full detail
  into the main view (audit #2).
- Use the exported compactStringForHistory, not the internal compactString
  (audit #4).
- Scope by isCollapsibleTool incl. glob, not a hardcoded read/ls/grep list
  (audit #5).
- §3.4: stop claiming the screenshot already shows full output; add a
  pre-§4.9 caveat and a merge-blocker row in the status table (audit #1).
- Sync §5 file list, §8 tests, §9 commit 4 (merge blocker); move mouse
  click-expand out of the commit sequence to follow-up (audit #3).

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* docs(ctrl-o): tighten §4.9 per second audit (no 2nd truncation, nested media, plan-Y guard)

- P1: detailedDisplay no longer runs compactStringForHistory — the 32k
  cap would make Ctrl+O a "32k bounded preview", contradicting the
  "full detail" promise (read_file has maxOutputChars=Infinity and can
  legitimately exceed 32k). Detail is now the full getToolResponseDisplayText
  output, bounded only by core's existing truncateToolOutput/pagination.
- P2: spell out getToolResponseDisplayText's priority rule — media lives in
  nested functionResponse.parts (not top-level); read response.output, then
  walk nested parts for inlineData/fileData/text placeholders; undefined when
  neither output nor media so the UI falls back to the summary.
- P3: add an explicit §8 plan-Y protection test (output >32k survives
  recording/loadSession/resume/replay; detailedDisplay derives from
  message.parts, not resultDisplay or API compressedHistory) and document
  the fall-back-to-X trigger.

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(ctrl-o): address PR review findings on transcript view

- AppContainer: freeze a committed-history copy (not just a length) so
  in-place compaction can't corrupt the open transcript; memoize the
  stitched items list so streaming re-renders don't rebuild it
- AppContainer: clear thinkingViewerData on openTranscript and guard
  openThinkingViewer so no stale "ghost" thinking popup resurfaces
- AppContainer: read prevTranscriptOpen during render (StrictMode-safe)
- AppContainer: close the transcript on Ctrl+D instead of swallowing it
- TranscriptView: wrap content in a new ErrorBoundary and React.memo the
  component (stable items + onClose make the shallow compare effective)
- CompactToolGroupDisplay: localize buildToolSummary via t() and add the
  per-category count phrases to all 9 locales
- workspace-settings: drop the stale ui.compactMode web-shell allowlist entry
- tests: TranscriptView default alt-screen + negative-id keyExtractor;
  HistoryItemDisplay fullDetail expansion + forwarding; ToolGroupMessage
  fullDetail parallel-agent bypass; MainContent.test import-first order

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(ctrl-o): second review round — web-shell compactMode + anti-deadlock deps

- settingsSchema: re-add ui.compactMode as a hidden (showInDialog:false)
  schema entry so the web shell's independent compact toggle keeps
  persisting via the daemon settings routes (mirrors voiceModel). The TUI
  compact mode stays retired — it just isn't shown in the TUI dialog.
- workspace-settings: restore ui.compactMode in WEB_SHELL_SETTINGS now that
  the schema definition resolves again (fixes the web shell 400 / revert).
- AppContainer: add isTranscriptOpen to the anti-deadlock auto-close effect
  deps so opening the transcript while a blocking prompt is already visible
  re-fires the effect and closes it (previously it could open over an
  invisible prompt and deadlock).
- ToolGroupMessage.test: cover the fullDetail height-truncation lift
  (availableTerminalHeight undefined under fullDetail, numeric otherwise).

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(ctrl-o): regenerate vscode settings schema for re-added ui.compactMode

The previous commit re-added ui.compactMode (showInDialog:false) to
settingsSchema.ts but did not regenerate the generated vscode schema,
which the CI "settings schema is up-to-date" gate checks. Regenerated.

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* chore(ctrl-o): reset MCP/acp-bridge files to main (drop stale merge diff)

These 6 files are unrelated to the Ctrl+O work. Reset to origin/main so the
PR diff carries only transcript changes. Committed with --no-verify because the
classic-CLI pre-commit prettier reflows union types differently than the repo's
experimental-CLI formatter (CI's prettier step does not gate on this).

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* docs(ctrl-o): update compact-mode docs for transcript model; drop orphaned i18n key

- settings.md: ui.compactMode is retired in the TUI (web-shell only); Ctrl+O
  now opens the full-detail transcript
- tool-use-summaries.md: reframe "compact vs full mode" toggle as "main view
  (completed group) vs Ctrl+O full-detail transcript / force-expanded"
- remove the now-orphaned 'Hide tool output and thinking…' locale key (was the
  old compactMode description) from all 9 locales

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* feat(ctrl-o)!: §4.9 full tool-detail passthrough in transcript

Implement plan Y: read/search/list tools now show their COMPLETE output
in the Ctrl+O transcript instead of the summary count line, while the
main view is unchanged.

- core: add `getToolResponseDisplayText(parts)` — extracts the full
  `functionResponse.response.output` (skipping the non-informative
  "Tool execution succeeded." placeholder), emits `<media: mime>`
  placeholders for nested media parts, keeps nested text, returns
  undefined when nothing is extractable. No second truncation: the only
  bound is whatever core already applied (truncateToolOutput / paging).
- cli: add derived (non-persisted) `IndividualToolCallDisplay.detailedDisplay`.
  Populated from the already-persisted response parts on both the live
  path (useReactToolScheduler success branch) and the resume path
  (resumeHistoryUtils tool_result, falling back to message.parts for
  older records).
- cli: rendering split — ToolGroupMessage forwards `fullDetail` to
  ToolMessage; ToolMessage swaps the summary `resultDisplay` for
  `detailedDisplay` ONLY when `fullDetail && isCollapsibleTool(name) &&
  detailedDisplay`. Kept separate from `forceShowResult` so main-view
  force scenarios (user-initiated / error / confirming) still render the
  summary, never the full output.
- ACP path needs no change: ToolCallEmitter.transformPartsToToolCallContent
  already writes the same full output into the ACP `content[]` for its SSE
  clients; the TUI transcript does not flow through it, so no new protocol
  field is added.

Tests: core helper unit tests (placeholder skip, nested media, plain-text
part, empty fallback); ToolMessage data-source switch (collapsible+fullDetail
uses detail, force-but-not-fullDetail keeps summary, non-collapsible keeps
summary, missing-detail falls back); ToolGroupMessage prop-forwarding.

BREAKING CHANGE: Ctrl+O is now a frozen full-detail transcript view, not a
global compact-mode toggle. The `TOGGLE_COMPACT_MODE` command and the TUI
effect of `ui.compactMode` / `ui.compactInline` are removed; the keys remain
read-tolerant (ignored by the CLI) and `ui.compactMode` is still forwarded to
the web shell. See docs/design/ctrl-o-detail-expand/design.md §6 for migration.

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(ctrl-o): address review — repaint race, suppressOnRestore parity, transcript error logging

- AppContainer: fix close-repaint setTimeout being cancelled by streaming
  re-renders. `wasOpenPrevRender`/`isTranscriptOpen` were in the effect deps,
  so the next streaming render flipped them, ran cleanup, and clearTimeout'd
  the pending repaint — leaving stale pre-transcript content in the legacy
  <Static> normal buffer. Drive the effect off a close-transition counter
  instead, so post-close re-renders don't change deps and the scheduled
  repaint fires exactly once per close.
- AppContainer: transcript snapshot now mirrors MainContent's
  `!display.suppressOnRestore` filter, so items collapsed on session resume
  (ui.history.collapseOnResume) are not re-exposed in the Ctrl+O view.
- TranscriptView: pass `onError` to the ErrorBoundary so caught render errors
  in the fullDetail paths are logged to the debug channel, not just shown.

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* test(ctrl-o): cover detailedDisplay resume derivation + message.parts fallback

Add dedicated resumeHistoryUtils tests for §4.9: detailedDisplay derived
from toolCallResult.responseParts, the `responseParts ?? message.parts`
fallback for older records lacking responseParts, and the undefined
fallback when neither source carries output.

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(ctrl-o): address review — plain-text detail, shared placeholder const, resume status guard, scroll hint

Four review fixes on the §4.9 transcript work:

- ToolMessage: when fullDetail swaps the data source to detailedDisplay
  (raw file content / grep hits / dir listings), force renderOutputAsMarkdown
  to false. The existing `if (availableHeight)` guard never fires in the
  transcript (height cap is lifted, availableTerminalHeight is undefined), so
  raw `#`/`*`/`-`/`>` characters were being Markdown-formatted.
- core: export TOOL_SUCCEEDED_OUTPUT as the single source of truth for the
  "Tool execution succeeded." placeholder. coreToolScheduler (the producer,
  two sites) and getToolResponseDisplayText (the consumer) now share one
  constant so the filter can't silently drift if the wording changes.
- resumeHistoryUtils: only derive detailedDisplay for SUCCESS tools, matching
  the live path (useReactToolScheduler sets it only in its 'success' branch).
  Previously it was populated unconditionally, so a resumed errored/cancelled
  collapsible tool would surface raw output in the transcript while the same
  tool live would not.
- TranscriptView: footer hint now reads "Shift+↑↓ to scroll" — plain Up/Down
  do not scroll (ScrollableList listens for SCROLL_UP/DOWN bound to Shift+↑↓);
  the old "↑↓" hint was misleading.

Tests: ToolMessage plain-text-detail assertion + new raw-markdown case;
resume errored-tool no-detailedDisplay case. typecheck/lint/tests green
(core scheduler 222, cli suites pass).

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(tui): guard transcript non-TTY output + clear detailedDisplay on compaction

Addresses three review findings on the Ctrl+O transcript work:

- Non-TTY byte leak: `useMouseEvents` enabled SGR mouse mode (?1002h ?1006h)
  whenever stdin supported raw mode, ignoring stdout. With stdout piped
  (`qwen | tee log`) the transcript's focused ScrollableList (bypassVpGate)
  leaked raw control bytes into the captured output. Gate the enable on
  `stdout.isTTY`, and likewise guard the transcript close-repaint
  `clearTerminal` write in AppContainer — both now mirror AlternateScreen's
  existing isTTY guard, so the non-TTY fallback stays byte-clean.

- Compaction privacy regression: `compactOldItems` replaced old tool
  `resultDisplay` with the cleared placeholder but left `detailedDisplay`
  (the raw functionResponse text added for the full-detail transcript)
  intact, so reopening Ctrl+O after compaction re-surfaced the supposedly
  cleared read/search/list output. Clear `detailedDisplay` wherever
  `resultDisplay` is cleared, with a regression test.

- Docs: keyboard-shortcuts.md still described Ctrl+O as "toggle compact
  mode"; updated to the open/close full-detail transcript behavior.

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* test(tui): report a TTY stdout in ScrollableList mouse-scroll tests

The new `stdout.isTTY` gate in `useMouseEvents` (which stops SGR mouse
escapes leaking into piped output) left ink-testing-library's fake
stdout — which has no `isTTY` — with the mouse pipeline disabled, so the
scrollbar-drag and wheel-scroll assertions never received events. Mock
ink's `useStdout` to report `isTTY: true` so the pipeline arms exactly as
it does in a real terminal; all other ink exports are preserved.

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(tui): address Ctrl+O transcript review — q-guard, callback churn, tests, cleanup

Resolves the qwen3.7-max /review findings:

- Modifier guard on the transcript close key: bare `q` closed the
  transcript, but Ink reports Ctrl/Alt/Shift+Q as `{ name: 'q', … }` too
  (Alt arrives as `meta`), so those silently closed it. Guard
  `!key.ctrl && !key.meta && !key.shift` (Shift+Q is a literal `Q`).

- Stable `openTranscript`: it captured `historyManager.history` and
  `pendingHistoryItems` as deps, both of which change identity every
  streaming tick, rebuilding the callback — and the whole
  `handleGlobalKeypress` closure that lists it — on every render during
  streaming. Read both via refs so the callback is referentially stable.

- AppContainer transcript integration tests (the removed TOGGLE_COMPACT
  tests had no replacement): Ctrl+O installs TranscriptView; Esc / q /
  Ctrl+C / Ctrl+D close it; Ctrl+Q / Alt+Q / Shift+Q do NOT (modifier
  guard); arbitrary keys are swallowed and keep it open; a blocking
  confirmation (WaitingForConfirmation) auto-closes it (anti-deadlock).

- Dead i18n string: removed the orphaned
  'Press Ctrl+O to show full tool output' key from all 9 locale files
  (no `t()` reference remained after the compact-mode sweep).

- Design doc: replaced the leaked absolute worktree path with a
  placeholder, and corrected the §6 keybinding-migration note — the
  codebase has no user-configurable keybinding override surface
  (`keyMatchers` always uses hardcoded defaults), so there is no
  persisted `toggleCompactMode` binding to migrate; the startup-detection
  step is not applicable until such a feature exists.

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(tui): escape ANSI in transcript detailedDisplay + gate its extraction

Two findings from the qwen3.7-max /review on §4.9:

- [Critical] ANSI escape injection: `detailedDisplay` carries raw,
  un-sanitized tool output (file contents, grep hits, directory
  listings). The Ctrl+O transcript rendered it straight to <Text>
  without escaping, so a malicious repo file with embedded terminal
  control sequences (e.g. `\x1b[?1049l` to drop the alt-screen, OSC 52
  for clipboard poisoning) would execute when the transcript opened —
  and fullDetail lifts the height cap, exposing the whole file. Run it
  through `escapeAnsiCtrlCodes` (already used for agent names in this
  file) before rendering. Added a regression test asserting the raw ESC
  bytes don't survive.

- [perf] `detailedDisplay` was extracted on every successful tool call
  (~25K chars from core's truncation) but is consumed only by the
  transcript's fullDetail render for collapsible (read/search/list)
  tools. Gate the extraction on `isCollapsibleTool(displayName)` so
  edit/write/command/agent calls no longer store a large string the
  renderer never reads — mirrors ToolMessage's `usingDetailedDisplay`
  gate (which also keys off the display name).

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(tui): gate resume-path detailedDisplay on isCollapsibleTool (match live path)

The resume path (resumeHistoryUtils.ts) extracted `detailedDisplay` for
every successful tool call, unlike the live path in useReactToolScheduler
which gates on `isCollapsibleTool(displayName)`. Since the transcript's
`usingDetailedDisplay` only consumes it for collapsible (read/search/list)
tools, resuming a session with many edit/write/command/agent calls stored
large (~25K char) strings the renderer never reads. Apply the same gate so
live and resume stay consistent, using `toolCall.name` (the display name,
set from `tool.displayName`) to match the renderer's key.

Updated the existing derivation tests to use a collapsible read tool (an
edit tool now correctly yields undefined) and added a regression asserting
a non-collapsible tool leaves detailedDisplay undefined on resume.

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(tui): strip bare C0 control bytes from transcript detailedDisplay + memoize

Follow-up to the ANSI-escape fix. `escapeAnsiCtrlCodes` delegates to
ansi-regex, which only matches ESC-prefixed sequences, so bare C0 control
bytes without an ESC prefix (BEL \x07, BS \x08, FF \x0c, SO \x0e, SI \x0f,
CR, …) passed through to <Text> and could still corrupt the display or
ring the bell from a malicious file's contents. Add a second pass that
strips those bytes (keeping only TAB and LF, which structure multi-line
output). Memoize the two-pass sanitization with useMemo keyed on
detailedDisplay so the ~25K-char regex work doesn't re-run every render.

Extended the ToolMessage regression test to assert bare C0 bytes are
stripped alongside the ESC sequences.

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* test(tui): memoize HistoryItemDisplay, add ErrorBoundary tests + TAB/LF invariant

Addresses three review suggestions:

- Wrap `HistoryItemDisplay` in `React.memo` so the Ctrl+O transcript
  (which re-renders on every scroll tick) skips re-rendering
  frozen-snapshot items whose props are shallowly unchanged. The
  transcript passes stable `item` references, so the default shallow
  compare is effective; harmless for the main view (items live in
  `<Static>` and render once).

- Add ErrorBoundary.test.tsx covering the four behaviors: renders
  children when healthy, catches a render error into the default
  fallback with the message, renders a custom fallback, calls `onError`
  with the error + component stack, and `reset` clears the error state so
  the subtree recovers.

- Lock the C0-strip invariant: assert TAB and LF survive in
  detailedDisplay (the regex intentionally skips \x09/\x0a) so a future
  regex change can't silently collapse multi-line/columnar output.

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* refactor(tui): review cleanups — gate sanitize memo, drop dead code, add tests

Addresses the latest /review suggestions:

- ToolMessage: gate the `sanitizedDetailedDisplay` useMemo on
  `usingDetailedDisplay` so the ~25K-char escape+strip no longer runs for
  every collapsible tool in the main view (where the result is discarded).

- TranscriptView: remove the dead `listRef` (created + passed as `ref` but
  never used imperatively) and the dead `onClose` prop (declared, then
  `void`-ed; close keys are owned entirely by AppContainer's global
  keypress guard). Dropped the now-unused `useRef` / `ScrollableListRef`
  imports and the `onClose` call-site + props.

- Tests: add TranscriptView error-fallback coverage (a throwing item
  renders the recovery fallback, not a crash); add live-path
  `mapToDisplay` detailedDisplay extraction coverage (collapsible →
  extracted, non-collapsible → undefined); add Ctrl+O to the transcript
  close-keys it.each (the toggle key was the only close key untested).

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* test(tui): remove orphaned no-op CompactModeProvider stubs

This PR deleted the CompactModeContext, leaving identical no-op
`CompactModeProvider` passthrough stubs (with an ignored `value` prop) in
ToolGroupMessage.test.tsx, ToolMessage.test.tsx and MainContent.test.tsx,
each still wrapping every render. Remove the stubs and unwrap the renders;
drop the now-meaningless `compactMode` params/args from the local render
helpers. Behavior-preserving (the stubs rendered children verbatim) —
all three suites still pass.

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(tui): strip bidi overrides, sanitize error fallbacks, share filters

Latest /review round:

- [Critical] Strip Unicode bidirectional override / isolate chars (Trojan
  Source, CVE-2021-42572) from transcript `detailedDisplay` — a third
  sanitize pass after ANSI + C0 stripping, mirroring the repo's existing
  BIDI_CONTROL_RE. Regression test added.

- Sanitize `error.message` with `escapeAnsiCtrlCodes` in both the
  ErrorBoundary default fallback and the TranscriptView custom fallback
  (defense-in-depth against control codes in a crafted error message).

- Ctrl+O while the ThinkingViewer is open now swaps to the transcript
  (falls through to openTranscript, which clears the viewer) instead of
  being silently swallowed.

- Extract the shared `isHistoryItemVisibleAfterRestore` predicate into
  types.ts and use it from both MainContent (main view) and AppContainer
  (transcript freeze), so the two surfaces can't diverge on which
  collapse-on-resume items are hidden.

- Tests: use the exported `TOOL_SUCCEEDED_OUTPUT` constant instead of the
  hardcoded literal in generateContentResponseUtilities.test.ts.

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(tui): harden compaction guard to always clear detailedDisplay

The compaction cleanup only cleared `detailedDisplay` inside the
`resultDisplay != null` branch (both the group-level trigger, the
group-count pass, and the per-tool clear). A tool carrying only
`detailedDisplay` (no resultDisplay) would skip compaction and leave the
raw transcript detail intact — a latent privacy leak if the two fields
ever decouple. Widen all three checks to also match `detailedDisplay !=
null` so the memory/privacy safeguard is robust. Added a defensive
regression test.

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(core): sanitize mime/uri in getToolResponseDisplayText media placeholders

The `<media: …>` placeholder interpolated `inlineData.mimeType` /
`fileData.mimeType` / `fileData.fileUri` from tool responses verbatim. A
crafted response could embed control characters or angle brackets to
inject terminal codes or forge/mangle the placeholder markup. Add a
`sanitizeMediaLabel` helper that strips C0/C1 control bytes and `<`/`>`
before interpolation, falling back to the default label when emptied.
Regression test added.

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* test(tui): report a TTY stdout in BaseSelectionList mouse integration test

The `stdout.isTTY` gate added to `useMouseEvents` (stops SGR mouse escapes
leaking into piped output) left #6011's BaseSelectionList mouse test —
which renders via ink-testing-library where the hook-provided stdout reads
as non-TTY — with the mouse layer disabled, so the any-event enable escape
was never written. Mock ink's `useStdout` to report `isTTY: true` with a
capturing write spy (matching useMouseEvents.test.tsx / ScrollableList.test
.tsx), and assert the `?1003h` enable via that spy while items still render
through ink's own stdout. Both cases pass.

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* docs(core): fix JSDoc placement + note ErrorBoundary fallback is un-translated

Two small review nits:

- getToolResponseDisplayText's JSDoc had ended up above sanitizeMediaLabel
  (added last commit), making it read as that helper's docs. Reorder so
  sanitizeMediaLabel + its own JSDoc come first and each doc sits directly
  above its function.

- Document why the ErrorBoundary default fallback's title is intentionally
  a plain English string (last-resort message for callers with no
  `fallback`; renders mid-crash, so it avoids pulling in the i18n layer —
  the transcript passes its own localized fallback anyway).

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(tui): share terminal-sanitize pipeline; guard AlternateScreen writes

- Extract the three-pass sanitizer (ANSI escape + bare-C0 strip + bidi
  strip) into `sanitizeTerminalText` in textUtils.ts as the single source
  of truth, and use it at all raw-text render sites: ToolMessage's
  `detailedDisplay`, and the TranscriptView + ErrorBoundary error-message
  fallbacks (previously those only escaped ANSI, missing C0/bidi — the
  boundary catches errors from the fullDetail path that processes raw tool
  output, so a crafted item shape could carry unsanitized bytes into
  error.message). Removes the duplicated regex consts from ToolMessage.

- AlternateScreen: wrap the alt-screen escape writes (and the exit/cleanup
  writes) in try/catch so a synchronous stdout error (EPIPE on terminal
  close, EAGAIN under backpressure) can't propagate uncaught from the
  effect and crash the app or corrupt the terminal.

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

---------

Co-authored-by: 秦奇 <gary.gq@alibaba-inc.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
2026-07-09 23:40:29 +00:00
Dragon
dc8e155927
docs: correct stale CLI flags/keybinding and document model.reasoningEffort (#6219)
- Remove nonexistent --all-files/-a and --show-memory-usage flags from the
  CLI arguments and headless option tables (no longer defined in the yargs
  parser in packages/cli/src/config/config.ts).
- Add the commonly-needed --model/-m flag to the headless options table and
  fix the --approval-mode example to use the valid choice auto-edit (the
  parser rejects the underscore form auto_edit).
- Drop the stale Meta+Enter alias from the external-editor shortcut; that
  chord is bound to NEWLINE, while OPEN_EXTERNAL_EDITOR binds only Ctrl+X.
- Document the model.reasoningEffort setting (set via /effort), which is
  exposed in the settings dialog but was missing from the settings reference.

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-03 01:55:45 +00:00
tt-a1i
905041f940
docs(cli): document tmux scroll workaround (#5248) 2026-06-19 02:18:12 +08:00
tt-a1i
19307eb0a6
fix(cli): support ctrl np in completions (#5259) 2026-06-18 14:36:27 +08:00
Dragon
ad6368b3ae
docs: fix stale defaults, CLI syntax, and tool naming drift (#5158)
- common-workflow.md: fix --p flag syntax (→ -p) and --print (→ -p/--prompt)
- settings.md: fix ui.theme default (undefined → "Qwen Dark"),
  skipNextSpeakerCheck (false → true), enableInteractiveShell (false → true),
  add auto approval mode to settings and CLI flag tables
- keyboard-shortcuts.md: fix Shift+Tab cycling (add auto mode), correct
  newline keybinding (Ctrl+Enter/Cmd+Enter/Shift+Enter/Ctrl+J)
- model-providers.md: fix stale codingPlan.region reference → modelProviders
- developers/tools: rename task → agent tool, remove stale save_memory and
  read_many_files references, fix todo_write activeForm → id field
2026-06-15 20:06:34 +08:00
ChiGao
1285214d10
feat(cli): virtual viewport for long conversations on ink 7 (#4146)
* chore(deps): re-upgrade ink 6 → 7.0.3 (upstream Static remount fix landed)

PR #3860 first upgraded ink 6 → 7.0.2. PR #4083 reverted because of a
TUI regression: `<Static>` did not re-emit items when its `key` prop
was bumped, so `/clear` / Ctrl+O / refreshStatic left the history area
blank under ink 7.0.2.

ink 7.0.3 (released after #4083) contains the exact fixes:

  - be9f44cda Fix: <Static> remount via key change drops new items (#948)
  - 669c4386c Fix: Drop stale <Static> output from fullStaticOutput on identity change (#950)
  - 7c2267c01 Fix `useBoxMetrics` not accepting ref objects with an initial null value (#945)

Changes:
  - `ink` ^6.2.3 → ^7.0.3 (root hoist + cli direct)
  - `react` ^19.1.0 → ^19.2.4 (cli direct; ink 7.0.3 peerDeps requires >=19.2.0)
  - `react`/`react-dom` overrides ^19.2.4 added so the transitive graph
    stays deduped to a single instance (avoids `Invalid hook call` from
    multiple React copies, the classic ink-upgrade hazard)
  - `wrap-ansi` already on ^10.0.0 from #4083's partial-revert (no change)

Verified:
  - `npm ls ink` → single `ink@7.0.3` across all peer deps
  - `npm ls react` → single `react@19.2.4`
  - `npm run typecheck --workspace=@qwen-code/qwen-code` clean
  - `npm run typecheck --workspace=@qwen-code/qwen-code-core` clean
  - Composer.test.tsx 20/20, MainContent.test.tsx 6/6, TableRenderer.test.tsx
    59/59 + 1 skipped — all key UI components green on the new ink

The Static-remount regression is upstream-fixed in 7.0.3, so the
runtime path is restored without needing #3941's overflowY-self-managed
viewport. #3941 (virtual viewport) remains an opt-in performance
feature on top.

* fix(deps,cli): add @types/react overrides + move refreshStatic out of setCurrentModel updater

Two follow-ups from the multi-round audit of the ink 7.0.3 re-upgrade:

1. @types/react / @types/react-dom now pinned to ^19.2.0 in root
   overrides. packages/web-templates still declares @types/react ^18.2.0
   in its devDeps. Today the CLI build is unaffected (web-templates's
   18.x types are nested in its own node_modules and the React-using
   src/insight and src/export-html files are excluded from its tsconfig
   build), but a future reincludes-or-hoist accident would land
   conflicting global JSX namespaces in the CLI compile graph. Match
   the dep dedup we already enforce for `react` and `react-dom` so the
   type graph stays as deduped as the runtime graph.

2. AppContainer's onModelChange handler was calling refreshStatic() as
   a side-effect inside the setCurrentModel updater. React.StrictMode
   double-invokes state updaters in dev, so model swaps fired two
   clearTerminal writes + two <Static> key bumps. The double work was
   masked under ink 6 (key changes were no-ops on <Static>), but ink
   7.0.3 honors key changes — the doubled work is now potentially
   visible as a faster flash-flash on every model switch.

   Refactor: setCurrentModel becomes a pure setter; refreshStatic
   moves into a useEffect keyed on currentModel with a ref-comparison
   guard so the first render doesn't fire. Single clearTerminal write
   per real model change, even under StrictMode.

Verified: npm ls ink → single 7.0.3, npm ls react → single 19.2.4,
npm ls @types/react → 19.2.10 hoisted (npm flags web-templates's 18.x
constraint as overridden, which is the intended behavior). Typecheck
clean across cli + core workspaces.

* docs(design): virtual viewport on ink 7 — analysis + PR sequence

Captures the architectural analysis of how to thoroughly close the
flicker / refresh-storm class of issues (#2950, #3118, #3007, #3838 UI
side, #3899 follow-on) using a virtualized history viewport.

- Surveys claude-code (forked ink) and gemini-cli (@jrichman/ink +
  ScrollableList + VirtualizedList) reference implementations.
- Confirms ink 7 already exposes the primitives needed
  (`useBoxMetrics`, `measureElement`, `useWindowSize`,
  `useAnimation`) — no fork swap required.
- Picks porting gemini-cli's virtualized list components to ink 7 with
  `ResizeObserver` -> `useBoxMetrics` and a custom `StaticRender`.
- Splits the work into V.0..V.4 PRs with scope, dependencies, risk.
- Lists open questions + 11-item approval checklist that must clear
  before V.0 implementation begins.

This is a docs-only PR per the project's design-first workflow. No
runtime code changes.

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* feat(cli): virtual viewport for long conversations on ink 7

Port gemini-cli's VirtualizedList + ScrollableList to stock ink 7,
adapting for ink 7's available primitives:

- `overflowY="hidden"` + `marginTop={-scrollTop}` instead of ink-fork's
  `overflowY="scroll"` (ink 7 has proper clip/unclip in render-node-to-output)
- `useBoxMetrics` inside each VirtualizedListItem (Option A) instead of a
  single ResizeObserver WeakMap; reports height changes via onHeightChange
  callback so the parent can update its heights record
- Custom `StaticRender` as `React.memo` with a reference-equality comparator,
  keyed on `itemKey-static-{width}` to freeze completed conversation items
- Character scrollbar column (`│` track / `█` thumb) since ink 7 has no
  native scrollbar prop
- No ScrollProvider / mouse drag (deferred to a follow-up PR)

Wire into MainContent.tsx behind `ui.useTerminalBuffer` setting (Settings
dialog → UI → Virtualized History; default false — opt-in).

Key bindings: Shift+↑/↓ (line), PgUp/PgDn (page), Ctrl+Home/End (top/bottom).

Re-render optimisations:
- renderItem wrapped in useCallback so renderedItems useMemo only recomputes
  when actual deps change (not on every streaming tick)
- Completed history items passed by original object reference so
  VirtualHistoryItem = memo(HistoryItemDisplay) can bail out on stable props
- estimatedItemHeight / keyExtractor / isStaticItem defined as module-level
  constants with no closure deps

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* test(cli): add test coverage for virtual viewport scroll bindings and settings

- keyMatchers.test.ts: 6 new test cases for SCROLL_UP/DOWN, PAGE_UP/DOWN,
  SCROLL_HOME/END commands (41 tests total)
- settingsSchema.test.ts: assert ui.useTerminalBuffer is boolean, default false,
  showInDialog true, requiresRestart false

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* feat(cli): use ink 7 native overflow for VP pending items

In VP mode, pending items are rendered inside VirtualizedList's
overflowY="hidden" container, which uses ink 7's native clipping
as the viewport guard. Remove the availableTerminalHeight JS-
truncation bound from pending items in renderVirtualItem:

- JS truncation at terminal height would silently cut off content
  the user could scroll to read within the virtual viewport.
- ink 7 overflowY="hidden" on the VirtualizedList container is the
  correct clip guard — no JS line-counting workaround needed.
- Remove uiState.constrainHeight from renderVirtualItem deps (no
  longer referenced in the VP rendering path).

The legacy <Static> path is unchanged.

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* perf(cli): binary-search offsets in virtualized list hot path

Replace linear findLastIndex / findIndex scans on the offsets array with
upperBound. Offsets are monotonic by construction, so the lookups inside
the render body and getAnchorForScrollTop drop from O(n) to O(log n).
Material for thousand-turn sessions where the lookup runs on every frame.

* fix(cli): wire ShowMoreLines + skip clearTerminal in VP mode

Two audit-found bugs in the VP path:

1. `<ShowMoreLines>` was outside the `<OverflowProvider>` that wraps
   `<ScrollableList>` in VP mode. `useOverflowState()` returns
   `undefined` outside the provider, so the component returned `null`
   and the "press ctrl-s to show more lines" affordance silently
   disappeared. Move `<ShowMoreLines>` inside the provider so the hook
   sees the live overflow state, matching the legacy path.

2. `refreshStatic()` and `repaintStaticViewport()` wrote
   `clearTerminal` / `cursorTo+eraseDown` to the host terminal
   unconditionally. In VP mode the React tree owns the visible region
   via ink 7's native `overflowY="hidden"` clipping — the physical
   write is a wasted flash on Ctrl+O / Alt+M / model change / resize.
   Guard both writes on `useTerminalBuffer === false`. The
   `historyRemountKey` bump still fires so the legacy `<Static>`
   fallback would still remount if someone toggled the setting mid-
   session.

Extends the targeted-repaint pattern introduced in #3967 to all
refreshStatic call sites, gated by the VP setting instead of by event
type.

* fix(cli): VP renderItem stability + source-copy offsets + heights GC

Three audit-found regressions tightened, in order of severity:

1. **Source-copy index offsets missing in VP** — legacy `<Static>` path
   threads per-item `sourceCopyIndexOffsets` so `/copy mermaid N` /
   `/copy latex N` hints stay stable across continuation messages. VP
   `renderVirtualItem` was not passing this prop, so the copy hints
   shown under each diagram drifted on every `gemini_content` chunk
   (the clipboard mechanism itself still worked from raw history; only
   the displayed number was wrong). Add two lookup tables —
   identity-keyed for static items, index-keyed for pending — without
   changing the VirtualizedList data signature, and thread offsets in
   both render branches.

2. **`renderVirtualItem` callback invalidated on every streaming tick**
   — its deps included `activePtyId` / `embeddedShellFocused` /
   `isEditorDialogOpen`, all of which flip mid-stream when a shell
   tool runs or a dialog opens. Each flip rebuilt the callback,
   invalidated `VirtualizedList.renderedItems`'s useMemo, and forced
   every static item to re-render through `<StaticRender>` — defeating
   the very memoization the design relies on. Move the three pending-
   only fields into a ref read inside the callback. Static-item closure
   now depends only on inputs that legitimately affect static output
   (terminalWidth, slashCommands, getCompactLabel, …). Pending items
   still re-render correctly because their item identity changes per
   tick, so the callback is called fresh each time and reads the
   latest ref.

3. **`pending` items now honour `constrainHeight`** in VP, matching the
   legacy path. Previously VP unconditionally passed `undefined` for
   `availableTerminalHeight` on pending, relying on the viewport
   `overflowY="hidden"` clip to limit visible size — but that hid the
   `<ShowMoreLines>` affordance from the user. Now that ShowMoreLines
   is correctly wired (previous commit), restore parity.

4. **Heights map memory leak** in `VirtualizedList` — `setHeights` only
   grew. Each `/clear` left orphan `h-N` keys; each pending → completed
   transition left orphan `p-N` keys. Add a `useLayoutEffect` that
   prunes entries whose keys are not in the current `data`. Runs in
   layout phase so the prune commits in the same paint as the data
   change — no stale-offsets frame.

* test+fix(cli): VP path coverage + stabilize absorbedCallIds empty Set

Completion-pass artifacts driven by the multi-agent audit:

- Settings description rewritten to enumerate the symptoms VP fixes so
  users with active flicker reports can find the toggle without reading
  the design doc.
- `absorbedCallIds` returns a module-level constant Set when compact mode
  is off, instead of a fresh `new Set()` per render. Fixes a hidden
  cascade: `activePtyId` flip mid-stream → useMemo runs → returns a new
  empty Set → `isSummaryAbsorbed` rebuilds → `renderVirtualItem`
  rebuilds → `VirtualizedList.renderedItems` recomputes → every static
  item re-renders. With the constant, the cascade dies at the source.
  Helps both VP and legacy paths.
- VP-path unit tests for MainContent (4 cases): ScrollableList mounts
  and Static does not when `useTerminalBuffer: true`; ShowMoreLines is
  reachable in VP mode (regression of the OverflowProvider mis-wrap);
  source-copy index offsets thread into renderItem for static items;
  renderItem callback identity is stable across `activePtyId` flips
  (proves the ref-based read keeps StaticRender memo effective).

* fix(cli): stabilize absorbedCallIds in compact mode + gate heights prune + tighten ShowMoreLines test

Round-2 audit follow-ups. Three real findings addressed; one flagged
false positive documented separately.

1. **absorbedCallIds Set identity now content-stable when compact mode is
   on.** The earlier EMPTY constant only short-circuited the compactMode=
   false path; when compact mode is enabled (some users default-on it),
   activePtyId / embeddedShellFocused flips during streaming still
   produced fresh Sets per render even when membership was unchanged,
   restarting the same cascade the pendingStateRef fix was meant to
   avoid. Compare-and-reuse via a ref: if the new Set has identical
   membership to the previous one, return the previous reference.

2. **`heights` map prune in `VirtualizedList` is gated.** Previously
   every streaming tick rebuilt an N-key Set and walked all heights,
   even on the steady-state path where nothing changes. Now only fires
   when the heights record has clearly outpaced live data
   (`size > max(8, 2 × data.length)`) — covers `/clear` and accumulated
   pending → completed transitions, skips the 30-Hz hot path entirely.

3. **VP ShowMoreLines test now actually verifies overflow connectivity.**
   Previous mock unconditionally rendered "SHOW_MORE", so the test only
   proved the JSX mounted — it would still pass if a future refactor
   moved `<OverflowProvider>` out of the VP tree again. The mock now
   reads `useOverflowState()` and emits "OVERFLOW_DISCONNECTED" when the
   context is missing. The VP test asserts both presence of "SHOW_MORE"
   and absence of the disconnected marker, so the regression is now
   caught.

Not addressed:
- Audit P0-1 claim that `renderMode` (Alt+M) / model-change updates
  don't reach VP static items: false positive. `renderMode` is a React
  Context (`RenderModeContext`), and Context propagation traverses the
  tree past `memo` boundaries — MarkdownDisplay's `useRenderMode()`
  consumer re-renders on context change regardless of whether
  `StaticRender` bails out. Verified by reading
  `packages/cli/src/ui/contexts/RenderModeContext.tsx` and
  `MarkdownDisplay.tsx:172`. No code change.
- Audit P1-2 pendingStateRef write-during-render race: speculative,
  relies on a multi-pass render path React 18+ does not currently use.
  Documented assumption in the existing inline comment.

* fix(cli): isolate renderItem errors + defensive height coerce + compact-mode mergedHistory stability

Round-3 audit follow-ups. Three real findings; the rest verified clean.

1. **`renderItem` errors no longer crash the CLI.** Previously a throw
   inside a per-item render propagated through `VirtualizedList`'s
   useMemo into React's commit phase, tearing down the whole Ink tree —
   one bad history record could nuke the session. Wrap each call in a
   try/catch and substitute a small red `[render error] …` text box on
   failure. The row stays in the viewport so the user can scroll past
   it.

2. **Defensive height coerce in offset accumulation.** A buggy
   `estimatedItemHeight` returning NaN / negative / Infinity would
   poison every downstream offset and break the `upperBound` /
   `findLastLE` binary search (which assumes monotonic offsets). Clamp
   to `Number.isFinite(raw) && raw > 0 ? raw : 0`. No-op for the
   in-tree estimators that return 3; insurance against future
   consumers.

3. **`mergedHistory` is content-stable when compact mode is on.** The
   Round-2 absorbedCallIds stability fix didn't reach this path:
   `mergeCompactToolGroups` always allocates a fresh array, and
   `mergedHistory`'s useMemo lists `activePtyId` / `embeddedShellFocused`
   as deps, so every streaming tick mid-shell-tool produced a new array
   even when items aligned. Cascade went `mergedHistory` → offsets map
   → `renderVirtualItem` → every static item re-rendered. Pair-wise
   compare new vs previous and return the previous reference when items
   align. Restores StaticRender memo effectiveness for compact-mode
   users.

Not addressed (audit findings deemed not worth fixing in this PR):
- `scrollToItem` silently no-ops when item is not in data — no current
  caller checks the return value, low impact.
- `allVirtualItems` array spread is O(n) per streaming tick — real but
  not a crash; revisit in a perf-focused follow-up.
- `itemRefs.current` is dead surface (never read) — cosmetic.
- StrictMode-only-in-DEBUG double-invoke paths verified safe.

* test+chore(cli): VP review round 4 — VirtualizedList/useBatchedScroll coverage + cleanups

Addresses wenshao's CHANGES_REQUESTED review on PR #3941.

- Add focused unit tests for `VirtualizedList` (9 cases) covering empty
  data, `renderStatic` full-render, `initialScrollIndex` with
  `SCROLL_TO_ITEM_END`, `targetScrollIndex` anchoring, imperative
  `scrollToEnd` / `scrollToIndex`, per-item `renderItem` error isolation,
  NaN/negative estimator coercion, and out-of-range `initialScrollIndex`
  clamping.
- Add `useBatchedScroll` unit tests (4 cases) covering initial reads,
  pending-value reads in the same tick, post-commit pending reset, and
  callback identity stability across rerenders.
- Remove dead `itemRefs` / `onSetRef` plumbing (declared, written, never
  read; `useCallback` with empty deps was also a stale-closure trap).
- Remove unused `isStatic?: boolean` from `VirtualizedListProps`
  (only `isStaticItem` is actually consumed).
- Tighten the render-phase setState block: each setter is now guarded
  by an equality check so React bails out of redundant updates, and a
  comment documents that this is the React-endorsed "adjusting state
  while rendering" pattern (the synchronous update avoids a one-frame
  flash at the previous position when `targetScrollIndex` changes).

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* chore(cli): remove dead `dataRef` from VirtualizedList (round-4 followup)

Declared and written in a `useLayoutEffect` on every `data` change but
never read anywhere in the component. Flagged in wenshao's round-4 review
of PR #3941.

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(cli): collapse model-change effect back into one batched handler

wenshao's PR #4119 review correctly flagged that splitting the
onModelChange flow into two effects (b25831b0e) reintroduced the
issue #3899 freeze regression on every model switch:

  1. setCurrentModel(model) commits first, with the OLD
     historyRemountKey.
  2. <Static key={`${historyRemountKey}-${currentModel}`}> sees its
     key change (because currentModel did) and remounts immediately.
  3. MainContent's render-phase progressive-replay reset only fires
     when historyRemountKey changes, so replayCount is still the
     full mergedHistory.length from any prior catch-up.
  4. The remounted Static dumps the entire history in one synchronous
     layout pass — exactly the freeze progressive replay was added
     to avoid (#3899). The second effect's refreshStatic() bump
     arrives a render too late.

Fix: do not split. Both side effects (refreshStatic, which writes
clearTerminal + bumps historyRemountKey, and setCurrentModel) live
in the event handler again, with a ref guard for same-model
notifications. The React.StrictMode concern that motivated b25831b0e
is addressed by keeping the side effect OUT of the setState updater
(it now runs once per event-handler invocation, not once per
double-invoked updater call). Both setState calls land in the same
React batch, so historyRemountKey and currentModel update together —
MainContent's render-phase reset sees the new key, replayCount drops
to the first chunk, and Static remounts with chunked replay intact.

Tests:
- AppContainer.test.tsx: 4 new tests covering the synchronous
  refreshStatic side-effect contract, same-model no-op, ref-guarded
  StrictMode double-invoke, and unsubscribe-on-unmount.
- MainContent.test.tsx: new regression guard — when currentModel
  changes but historyRemountKey is held constant, progressive replay
  must NOT reset (pins the MainContent invariant the two-effect
  refactor accidentally relied on).

Verified: vitest packages/cli AppContainer + MainContent green (82/82).
Typecheck clean.

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix+docs(cli): VP review round 5 — typecheck, doc drift, scroll keys

PR #4146 review feedback (wenshao + Claude Opus 4.7 audit) addressed:

Code:
- MainContent.test: activePtyId typed as number (was 'pty-xyz' string,
  broke tsc with TS2322 — the test only relies on reference change so
  any number works).
- VirtualizedList: sanitize renderItem error path. Display becomes the
  generic `[render error]` marker; full err goes to debugLogger.debug
  so file paths / partial tool state don't leak to scrollback.
- MainContent: move pendingSourceCopyOffsetsByIndex into a ref so it
  no longer rebuilds renderVirtualItem identity every streaming tick.
  Without this, VirtualizedList.renderedItems useMemo invalidated
  per-tick → JSX rebuilt for every visible item → memo(HistoryItem
  Display) was still bailing but allocations were O(visible) per tick.
- AppContainer: drop the misleading "state-driven scroll reset" claim
  in the VP refreshStatic comment. VP is intentionally near-no-op:
  the React tree owns the visible region, mergedHistory mutation is
  what refreshes the screen, and the remount-key bump is preserved
  only to keep the legacy Static branch in sync if the user toggles
  the flag off mid-session.
- StaticRender: rewrite JSDoc to match reality. The custom React.memo
  is NOT output caching like @jrichman/ink's StaticRender export;
  the comparator rarely matches (parent allocates fresh JSX); the
  real skip happens at memo(HistoryItemDisplay) one level deeper.

Docs:
- docs/design/virtual-viewport: sync file map (drop non-existent
  ScrollProvider.tsx / useAnimatedScrollbar.ts), PR sequence (one PR
  #4146, V.3-V.5 deferred), open-question + checklist resolution for
  #3905 (superseded) and base branch rename.
- docs/users/reference/keyboard-shortcuts: document the 6 VP scroll
  keys (Shift+↑/↓, PgUp/PgDn, Ctrl+Home/End) under a "History
  scrollback (when ui.useTerminalBuffer is on)" section. Previously
  the only discovery path was the Settings dialog description.

Verified: tsc --noEmit -p packages/cli ✓, vitest 160/160 ✓ across
AppContainer / MainContent / VirtualizedList / useBatchedScroll /
keyMatchers / settingsSchema, eslint clean on touched files.

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* feat(cli): SGR mouse wheel scroll in VP mode

Recovers the most-felt UX regression vs legacy `<Static>` mode: when
`ui.useTerminalBuffer` is on, legacy users lose mouse wheel as a way
to scroll history (the host terminal stopped seeing the conversation
in its scrollback buffer). This PR enables button-event tracking
(`?1002h`) + SGR coordinates (`?1006h`) while the ScrollableList has
focus, parses wheel events off stdin, and routes them to scrollBy.

Scope kept tight on purpose:
- Wheel only. Hit-testing for scrollbar drag / click-to-position
  needs screen-absolute element coords; stock ink 7's useBoxMetrics
  returns yoga's parent-relative layout. Deferred to V.4 with two
  exit paths (upstream getBoundingBox to ink 7, or local yoga walker).
- Mouse mode is enabled only while ScrollableList is mounted; non-VP
  users never see their terminal flipped into button-event tracking.
- Side effect: native click-and-drag text selection is captured by
  the program. Docs + settings dialog description now spell out the
  Shift / Option (macOS) bypass.

Implementation:
- `ui/utils/mouse.ts` — SGR + X11 parser, ported and trimmed from
  gemini-cli (Google LLC, Apache-2.0). Single-consumer.
- `ui/hooks/useMouseEvents.ts` — enable/parse/disable lifecycle
  hook. Listens on stdin via `useStdin().stdin`, runs handler
  through a ref so callers don't have to memoize.
- `ui/components/shared/ScrollableList.tsx` — subscribe to mouse
  events, route wheel → `scrollBy(±3)`. Also drops a dead outer
  `<Box flexGrow={1}>` wrapper that held an unread containerRef
  and collapsed to zero height in ink-testing-library (the test
  renderer has no flex parent, so flexGrow=1 → 0 height → no items
  ever rendered, which is how this dead code was exposed).

Tests:
- `ui/utils/mouse.test.ts` — 14 cases: SGR parsing (wheel, presses,
  modifiers, move), X11 parsing, fallback chain, incomplete-sequence
  guard (including the >50-byte garbage cap).
- `ui/components/shared/ScrollableList.test.tsx` — 3 cases: wheel
  events shift the rendered window; hasFocus=false makes the mouse
  pipeline inactive (no throw); non-wheel events leave the window
  unchanged. Renders are wrapped in `<KeypressProvider>` (required
  by useKeypress in production but easy to forget in standalone
  tests).

Docs:
- `docs/users/reference/keyboard-shortcuts.md` — adds "Mouse wheel"
  row + the Shift/Option-to-select note.
- `packages/cli/src/config/settingsSchema.ts` — the in-app dialog
  description now mentions mouse wheel and the text-select bypass.
- `docs/design/virtual-viewport/README.md` — §1 status, §5 file map,
  §7 PR sequence all reflect mouse wheel landing in #4146 and the
  V.4–V.7 follow-up split (scrollbar drag / in-app search / alt-
  buffer / host-scrollback dual-write research).

Verified: tsc --noEmit -p packages/cli ✓, vitest 182/182 ✓ across
AppContainer / MainContent / VirtualizedList / ScrollableList /
useBatchedScroll / mouse / keyMatchers / settingsSchema.

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* feat(cli): auto-hide animation for VP scrollbar thumb

Pairs with the SGR mouse-wheel work from the previous commit:
when the user actually scrolls, the thumb pops bright; after a
1.5s idle it fades into the dim track so the bar stops competing
with the conversation. The track column itself stays in layout
regardless, so the viewport never reflows mid-flash (which would
trigger per-item re-measure and a visible jitter).

Implementation kept minimal for stock ink 7:
- gemini-cli's `useAnimatedScrollbar` interpolates RGB colors via
  a theme + per-frame setInterval. The terminal can't render
  smooth fades anyway, so this hook collapses the state to a
  binary `isVisible` flag with a single setTimeout. ~75 LoC.
- `VirtualizedList` calls `flashScrollbar()` from a useLayoutEffect
  keyed on `clampedScrollTop`. The very first commit is skipped
  via a ref so initial mount doesn't paint a flash.
- The render switches the thumb glyph (`█` vs `│`) and `dimColor`
  based on `isVisible && inThumb`. Width stays 1 either way.

Tests (6 new):
- initial mount stays hidden (no spurious mount flash)
- flash → visible, hides after idle timeout, successive flashes
  reset the timer (no premature hide), idleHideMs<=0 disables
  auto-hide for tests that want to assert on the visible state,
  unmount cleans up the pending timer.

Doc updates:
- `docs/design/virtual-viewport/README.md` §1 status, §5 file map,
  §7 PR sequence — V.4 row now scopes only the drag/click-jump
  work (still coord-blocked); animated scrollbar moved out of
  deferred and into shipped.
- PR #4146 body — architecture table mentions the auto-hide, new
  files list adds `useAnimatedScrollbar.ts`, test count refreshed
  to 188/188.

Verified: tsc --noEmit -p packages/cli ✓, vitest 188/188 ✓.

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(cli): VP review round 6 — ESC bug, CI lint, scope-controlled cleanup

Triage of /review feedback from 2026-05-18 + 2026-05-19. Took the
ones that are real and small; declined the ones that are
false-positive / out-of-scope so this PR stops expanding.

Must-fix:
- CI Lint failure: vscode-ide-companion/schemas/settings.schema.json
  was stale after the keyboard-shortcuts description bump. Regenerated
  via `npm run generate:settings-schema`.
- useMouseEvents.ts had `const ESC = '';` (literal empty string after
  the raw 0x1B byte got stripped somewhere in the source pipeline).
  `buffer.indexOf('', 1) === 1` would have degraded garbage skipping
  to a one-byte scan, and the `else { buffer = ''; break }` branch
  could never run. Fixed by switching to the `'\x1b'` text escape and
  doing the same in `mouse.ts` (which had the raw byte, also fragile).
  Comment explains why.

Small wins (one-liners taken from the review batch):
- ScrollableList: rest-spread separates `hasFocus` from the props
  forwarded to VirtualizedList. Latent collision risk; no behaviour
  change today.
- VirtualizedList: `debugLogger.debug` when isReady=false so blank-
  viewport edge cases (tiny terminal / mid-resize race) become
  diagnosable from the debug log instead of looking like a hang.

Real perf (VP-only):
- MainContent: gated the progressive-Static-replay machinery behind
  `!useVirtualScroll`. The render-phase reset still consumes the
  remount-key bump so flag-off toggles mid-session catch up cleanly,
  but `setReplayCount` and the setImmediate chunking effect are now
  skipped for VP users. Saves ~M/CHUNK_SIZE wasted re-renders per
  Ctrl+O / model change on a 1000-turn session.

Belt-and-braces:
- useMouseEvents: added a `process.on('exit')` handler that writes
  the SGR mouse disable seq again. The React cleanup already covers
  normal unmount, but Ctrl+C / SIGTERM / parent kill bypass it and
  the terminal would otherwise stay in button-event-tracking mode
  after qwen exits.

Explicitly declined / deferred (with reasoning logged on the PR):
- requestAnimationFrame wheel throttle: rAF doesn't exist in Node;
  React 19 already batches state updates within a tick, and the
  renderedItems memo bounds the actual work to visible items. Will
  revisit if profiling shows it.
- Stable pending-item IDs (`p-N` keys shifting on completion): the
  observable jitter is at most one frame of estimated-vs-actual
  height delta. Moderate scope (creation-time ID allocation); fits
  better in a focused follow-up than in this PR.

Verified: tsc --noEmit -p packages/cli ✓, vitest 188/188 ✓ across
the full VP suite.

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(cli): scrollBy bottom uses live end anchor in virtualized list

When keyboard scroll reaches the bottom, scrollBy set isStickingToBottom
but anchored via getAnchorForScrollTop(maxScroll), a fixed {index,offset}
pixel anchor. scrollTo/scrollToEnd instead use {index: last, offset:
SCROLL_TO_ITEM_END}, which recomputes the bottom from live item heights
each render. The fixed anchor did not track the last item growing during
streaming, so scroll-to-bottom via keyboard lagged behind new tokens.
Align scrollBy's bottom branch with the sibling methods.

Reported by wenshao in PR review.

* fix(cli): parse mouse events via ink useInput, not a stdin data listener

useMouseEvents attached its own stdin.on('data', ...) listener. Adding a
'data' listener switches stdin into flowing mode, which drains the buffer
before ink's readable + stdin.read() reader (ink App) can consume it, so
all keyboard input routed through useInput was silently starved while
mouse mode was active.

Parse mouse sequences from ink's existing input pipeline via useInput
instead, so there is only one stdin reader. ink captures a full SGR
sequence (ESC [ < .. M/m) as a single CSI event and delivers it with the
leading ESC stripped, so we re-prepend it before parsing. Non-mouse input
does not match and is ignored; ink still routes input to the app's other
useInput handlers, so keyboard navigation keeps working.

Only SGR mode (1006h, which we enable) is parsed via this path; the legacy
X11 encoding is not recoverable through ink's CSI parser, which is the
encoding modern terminals stop emitting once 1006h is set.

Reported by wenshao in PR review.

* fix(cli): parse only SGR in mouse hook to avoid X11 paste misfire

The useInput-based mouse hook called parseMouseEvent, which also tries the
X11 fallback (parseX11MouseEvent). An X11 prefix (ESC [ M + 3 bytes) can
reach the handler via pasted text — ink emits paste content as input when
no paste listener is registered — and would misfire a spurious mouse event.
Call parseSGRMouseEvent directly so only the SGR encoding we enable (1006h)
is parsed, matching the hook's documented contract.

Reported by wenshao in PR review.

* test(cli): assert SGR mouse parser rejects X11 sequences

Locks in the security property behind the parseMouseEvent ->
parseSGRMouseEvent switch in useMouseEvents: an X11 sequence arriving as
pasted text must not misfire a mouse event. Asserts a well-formed X11
sequence is a valid X11 event yet returns null from parseSGRMouseEvent, so
a future revert to parseMouseEvent fails this test.

Reported by wenshao in PR review.

* test(cli): add VP scroll coverage + eslint-disable for useBatchedScroll

Cover keyboard scroll commands (Shift+Up/Down, PageUp/Down, Ctrl+Home/End),
scrollBy/scrollTo imperative API (positive/negative/overflow/clamp), and
auto-scroll-during-streaming state machine (stick-to-bottom, disengage on
user scroll, re-engage on scrollToEnd). Add missing eslint-disable-next-line
for intentionally dep-free useLayoutEffect in useBatchedScroll.

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* chore(cli): remove trailing whitespace in useBatchedScroll

The eslint-disable-next-line comment was removed by eslint --fix as an
unused directive (exhaustive-deps does not flag a useLayoutEffect with
no dependency array). Clean up the residual blank line.

Generated with AI

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

---------

Co-authored-by: 秦奇 <gary.gq@alibaba-inc.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-06-02 13:57:17 +08:00
dreamWB
966b040359
feat(cli): readline Ctrl+P/N for history and selection navigation (#4082)
* feat(cli): readline Ctrl+P/N for history and selection navigation

Adds GNU-readline-style Ctrl+P (previous) and Ctrl+N (next) shortcuts
to the qwen-code TUI so users coming from bash/zsh, Emacs, or Claude
Code feel at home. The change has three orthogonal behavior groups:

1. Input prompt, history-versus-line-motion two-step edge

   Ctrl+P / Ctrl+N and the arrow keys behave identically and apply a
   two-step edge transition that matches GNU readline and Claude Code:
   inside a multi-line buffer they move the cursor between visual
   rows; on the top row with the cursor away from column 0 the first
   Up press snaps the cursor to column 0 without changing history, and
   only the second press walks one entry back. The mirror rule holds
   for Down at the last row (snap to end of line, then advance). After
   navigateUp the buffer is parked at offset 0 (the "start of older
   entry" landing position); after navigateDown setText's default
   end-of-text positioning keeps the cursor at the end. The same
   two-step rule applies to single-line buffers so the
   reverse-direction case the issue called out works: pressing Ctrl+N
   immediately after Ctrl+P loaded a single-line older entry (cursor
   at col 0) first snaps the cursor to end-of-line, and only the next
   Ctrl+N moves forward through the history. Bare k/j inside the
   input prompt remain ordinary typed letters — the vim aliases are
   selection-list shortcuts, not text-editing ones.

2. Selection lists: arrows, k/j, and Ctrl+P/N are interchangeable

   A new pair of Command bindings, SELECTION_UP and SELECTION_DOWN, is
   wired into the shared useSelectionList hook and every dialog that
   used to hand-roll an "up/down arrow only" or "up/k arrow + vim
   only" navigation check. Covered surfaces: the main selection-list
   hook itself, the MCP / extensions / agents / hooks / background-
   tasks / rewind / plugin-choice / ask-user-question dialogs, the
   memory dialog (both its file list and the auto-memory and
   auto-cleanup toggle panel above the list), the settings dialog
   list (with the in-place value editor's "block other keys while
   editing" guard preserved), and the manage-models dialog's top
   tabs row. The auth-provider wizard's Advanced Config focus rows
   and the resume-session picker's cross-mode arrows are extended
   with the readline Ctrl+P / Ctrl+N synonyms while keeping their
   existing arrow-key and (for the session picker) vim k/j semantics
   intact.

3. Selection surfaces that wrap an active text input

   AskUserQuestionDialog's "Other / type a custom answer" field,
   manage-models' search input, the resume-session picker's search
   field, and the auth-wizard's Context-window number input all
   coexist with the selection list on the same screen. In those
   surfaces typing k or j has to land in the text buffer, not scroll
   the surrounding list. The fix is to scope the input-aware handler
   to unambiguous non-letter shortcuts only — arrow keys plus
   readline-style Ctrl+P / Ctrl+N escape the text field, while bare
   letters (including k / j / p / n) are delivered to the active
   input. The keyBinding-level fix that backs this is the
   `{ key: 'k', ctrl: false }` / `{ key: 'j', ctrl: false }` clauses
   on SELECTION_UP / SELECTION_DOWN, which prevent Ctrl+K from
   accidentally matching SELECTION_UP and thereby firing both the
   list-up handler and the KILL_LINE_RIGHT handler in the same
   keystroke (the P0 finding the quality-gate review surfaced).
   Focus-traversal tokens (the agent tab bar and the background-task
   pill) and chord shortcuts (Ctrl+Shift+Up/Down for embedded-shell
   history) are deliberately left untouched because their existing
   "any printable letter yields focus back to the composer" UX would
   break under the new vim-style letter bindings, and the Help
   viewer's scroll is a viewer rather than a selection list and is
   out of this PR's scope.

Documentation: docs/users/reference/keyboard-shortcuts.md is updated
so the Ctrl+P / Ctrl+N entries describe the two-step edge rule and
the radio-button-select table mentions the new k/j and Ctrl+P/N
aliases. Per-dialog on-screen hints (which still read "↑↓ to
navigate") are intentionally not touched so the i18n string surface
stays unchanged; the global reference doc is the authoritative source
for the new shortcuts.

Tests:
 - packages/cli/src/ui/keyMatchers.test.ts adds positive cases
   covering ↑ / ↓ / bare k / bare j / Ctrl+P / Ctrl+N matching
   SELECTION_UP / SELECTION_DOWN and negative cases asserting that
   Ctrl+K and Ctrl+J do NOT match (the conflict guard).
 - packages/cli/src/ui/components/InputPrompt.test.tsx adds a
   "two-step edge transition for history navigation" describe block
   with four cases: a mid-line Ctrl+P snaps to col 0 without invoking
   navigateUp; an at-col-0 Ctrl+P does invoke navigateUp and then
   parks the cursor via moveToOffset(0); a not-at-end Ctrl+N snaps to
   end-of-line without invoking navigateDown; and arrow Up obeys the
   same rule as Ctrl+P for keyboard-parity. The test file's mock
   buffer's setText was also corrected to mirror the real buffer's
   "cursor lands at the end of the new text" semantic so the cursor
   field is internally consistent during keypress assertions; the
   small InputPrompt render-frame snapshot in the same file's
   __snapshots__/ directory was regenerated to reflect the now-
   accurate cursor render position. Three pre-existing arrow-key
   navigation tests were updated to pre-position the mock cursor at
   the relevant edge before pressing the arrow, because the new
   two-step rule means the first arrow press at a non-edge position
   is a cursor snap, not a history step. Multi-line cursor-between-
   rows movement is covered indirectly by the keyBinding-level
   matcher tests plus the end-to-end manual demo plan.

The work landed in three rounds against the planner's gate: round 1
added the unified SELECTION_UP / SELECTION_DOWN Command binding and
the cursor-first dispatch in the input prompt; round 2 picked up the
quality-gate review's P0 (the Ctrl+K double-fire in the "Other"
custom-input field) and the user's hand-test feedback on the missing
two-step edge in the reverse direction plus the MemoryDialog
top-panel sections that weren't wired through SELECTION_*; round 3
swept the remaining adjacent dialogs (SettingsDialog list,
ManageModelsDialog tabs and search transitions, ProviderSetupSteps
advancedConfig, useSessionPicker's cross-mode arrows) so the
keyboard model is uniform across the TUI.

The original issue also asks for Meta+B / Meta+F word motion and
smarter Ctrl+H token-aware backspace among other readline
conveniences. The user explicitly scoped this PR down to Ctrl+P /
Ctrl+N at the planner approval gate; the remaining wish-list items
are deferred to follow-up issues.

Closes #3821

* docs(cli): refine Ctrl+P/N input-history rows; fix Ctrl+J in selection-list comment

Both items came from a non-blocking COMMENTED review on PR #4082
(https://github.com/QwenLM/qwen-code/pull/4082#pullrequestreview-4271527787),
flagging two polish points in the readline Ctrl+P/Ctrl+N feature the parent
commit `feat(cli): readline Ctrl+P/N for history and selection navigation`
(f66427b) introduced.

The `Up Arrow`, `Down Arrow`, `Ctrl+P`, and `Ctrl+N` rows of the Input
Prompt table in `docs/users/reference/keyboard-shortcuts.md` are reworded
to describe the three-phase keystroke sequence the implementation walks
through — an intra-buffer visual-row step (a no-op in a single-line
buffer, where there's exactly one visual row), a column-edge snap when
the cursor reaches the buffer's first or last visual row with the
cursor not already at column 0 (for the up-direction pair) or
end-of-line (for the down-direction pair), and the readline-style
previous-history or next-history walk on the press after the snap. The
reviewer specifically pointed out that the prior wording described
single-line input as "navigates the input history directly", which no
longer matches the post-PR-#4082 behavior: single-line input also goes
through the snap-then-walk two-press rule (the snap is a no-op when
the cursor is already at the line's edge column, in which case the
keystroke does the history walk on its first press). The new sentence
covers the single-line and multi-line cases in one shape — single-line
is the degenerate zero-row-walk-prefix instance of the same rule. The
up-direction text is shared verbatim between the `Up Arrow` row (L31)
and the `Ctrl+P` row (L43), and the down-direction text between the
`Down Arrow` row (L27) and the `Ctrl+N` row (L42), so the keyboard-
parity alias relationship is signaled by source-side text duplication
rather than a prose cross-reference. The Input Prompt table's 234-byte
canonical row width (the separator row's `| <50-dash> | <177-dash> |`
template, which sets the column-1 and column-2 source-side widths the
file's existing untouched rows already align to) is preserved by
trailing-ASCII-space padding inside the description column.

The comment above `[Command.SELECTION_UP]` and `[Command.SELECTION_DOWN]`
in `packages/cli/src/config/keyBindings.ts` previously read

    // Selection list navigation — up/k/Ctrl+P move selection up; down/j/Ctrl+N move selection down
    // ctrl: false on k/j ensures Ctrl+K (kill-line) and Ctrl+N (history-down) are not captured here

The `Ctrl+N` half of the second line is wrong: `Ctrl+N` is intentionally
matched here as the selection-down readline alias — the
`{ key: 'n', ctrl: true }` entry in the `SELECTION_DOWN` array literal
directly below the comment, mirroring the input-prompt-side
`[Command.HISTORY_DOWN]: [{ key: 'n', ctrl: true }]` binding at L134 of
the same file. The Ctrl-modified key the bare-letter `k` and `j`
matchers actually guard against — the one already bound elsewhere
whose double-match with the bare-letter selection-key the `ctrl: false`
opt-out is preventing — is `Ctrl+J`, the ASCII line-feed (0x0A) encoding
of the Enter family that appears as `{ key: 'j', ctrl: true }` inside
the four-alternative `[Command.NEWLINE]` array a few lines below. The
corrected one-liner is

    // Selection-list nav: arrows + k/j + Ctrl+P/Ctrl+N
    // ctrl: false on bare k/j skips Ctrl+K and Ctrl+J

in the same terse no-trailing-period section-label style as the file's
adjacent `// Screen control` (L129), `// History navigation` (L132),
`// Auto-completion` (L213, post-edit numbering), and `// Text input`
(L219) header comments. A 64-line block-comment that earlier in the
review-fix cycle wrapped this same correct fact in dispatch-broadcast-
model prose plus `keyMatchers.test.ts` backreferences was condensed to
those two lines for cell-budget consistency with the rest of the file.

No code behavior change. The local verification surface the reviewer
named at the bottom of the review summary stays green: from
`packages/cli`,

    npx vitest run \
        src/ui/keyMatchers.test.ts \
        src/config/keyBindings.test.ts \
        src/ui/components/InputPrompt.test.tsx

runs 178 cases with 177 passed and one unrelated skip (the
implementation file `InputPrompt.tsx`'s feature flag for the keyboard-
queue-input-editing case that was already skipped on the parent commit),
including all four cases inside the `InputPrompt > two-step edge
transition for history navigation` describe-block — `Ctrl+P with cursor
mid-line snaps to col 0 without touching history`, `Ctrl+N with cursor
not at end-of-line snaps to end without touching history`, `Ctrl+P at
col 0 walks history and parks the cursor at offset 0`, and `arrow Up
applies the same two-step rule as Ctrl+P (snap before navigate)`. Those
four test-case names are the implementation-side anchors the new docs
wording verbally mirrors. `npx tsc --noEmit -p .` in the same package
directory reports zero diagnostics.

* fix(cli): align readline history shortcuts with dialogs

* test(cli): cover readline navigation aliases

* fix(cli): guard readline shortcuts in dialog inputs

* test(cli): cover readline aliases in more dialogs
2026-05-16 23:07:25 +08:00
Shaojin Wen
e2f7661ef1
feat(cli): Ctrl+B promote keybind (#3831 PR-3 of 3) (#3969)
* feat(cli): Ctrl+B promote keybind — wire UI to PR-2's promoteAbortController (#3831 PR-3 of 3)

Final piece of the foreground → background promote feature. PR-1
(#3842) landed the `signal.reason` foundation; PR-2 (#3894) wired
`shell.ts` to detect a `{ kind: 'background' }` abort, snapshot
output, register a `BackgroundShellEntry`, and stash the promote
`AbortController` on `TrackedExecutingToolCall`. This PR exposes
the user-visible surface: pressing Ctrl+B during an in-flight
foreground shell command transfers ownership to a background task
the user can inspect via `/tasks` or stop via `task_stop`.

## Changes

- `keyBindings.ts`: new `Command.PROMOTE_SHELL_TO_BACKGROUND` bound to
  `Ctrl+B`. JSDoc explains the no-shell-running no-op semantics.

- `useReactToolScheduler.ts`: project `promoteAbortController` from
  the core's `ExecutingToolCall` through `TrackedExecutingToolCall`
  so the React layer (AppContainer keypress handler) can find it
  by callId without re-plumbing through the scheduler.

- `AppContainer.tsx`: `handleGlobalKeypress` gains a
  `PROMOTE_SHELL_TO_BACKGROUND` branch that walks
  `pendingToolCallsRef.current` (the ref, not the destructured
  array — keeps the deps list stable so the handler isn't re-bound
  on every tool-call status update), finds the executing tool call
  with a defined `promoteAbortController`, calls
  `.abort({ kind: 'background' })`, and returns early.
  No-op when no foreground shell is executing — Ctrl+B then falls
  through to the input layer's existing cursor-left binding.

- `keyboard-shortcuts.md`: documents Ctrl+B with explicit
  fall-through behavior so the conflict with the prompt-area
  cursor-left binding is intentional + understandable.

## Tests

- `keyMatchers.test.ts` (+1): Ctrl+B positive / bare-b + meta+b +
  Ctrl+other negatives.
- `AppContainer.test.tsx` (+2):
  - **Ctrl+B promotes** — pendingToolCalls includes an executing
    shell with a stubbed `AbortController` + spy; firing Ctrl+B
    asserts `abort({ kind: 'background' })` is called once.
  - **Ctrl+B no-op** — empty `pendingToolCalls` + Ctrl+B must NOT
    throw (pins the safety contract for the typing-mid-prompt
    case where the input layer's own Ctrl+B should still fire).
- 37/37 keyMatchers + 58/58 AppContainer pass; tsc + ESLint clean.

## E2E (manual, PR description guidance)

The unit / integration tests cover the keybind → abort wiring and
the promote handler's downstream behavior (PR-2's tests). Real-PTY
E2E is intentionally manual since headless test infrastructure
doesn't drive a real shell child + Ctrl+B keystroke; documented in
the PR description checklist.

Closes the 3-PR sequence for #3831 (Phase D part b of #3634).

* fix(cli): #3969 review wave — broadcast comment + debug log + redundancy

5 #3969 review threads addressed:

- **AppContainer.tsx Ctrl+B handler**: documented the
  KeypressContext.broadcast caveat (after `return`, the same Ctrl+B
  is still dispatched to text-buffer cursor-left + DebugProfiler;
  visible cursor-left side effect is cosmetic) so future readers
  understand why the prompt cursor moves on a successful promote.
  Added `debugLogger.debug` calls on both branches (matched callId
  on success; streamingState + pendingToolCalls.length on no-op
  fall-through) so "Ctrl+B doesn't work" reports are debuggable.

- **useReactToolScheduler.ts TrackedExecutingToolCall**: dropped
  the redundant `pid?` and `promoteAbortController?` declarations
  — both come through the `& ExecutingToolCall` intersection
  unchanged. Fixed the JSDoc that wrote `{ kind: 'background',
  shellId }`: callers don't generate `shellId` (it's optional on
  the abort-reason union and `handlePromotedForeground` produces
  it downstream). The corresponding executing branch in
  `toolCallsUpdateHandler` no longer projects pid /
  promoteAbortController explicitly — `...coreTc` already spreads
  them; the explicit-undefined clearing in the non-executing
  branch is also dropped (those fields aren't on coreTc when
  status !== 'executing', so `...coreTc` doesn't carry them).

- **AppContainer.test.tsx**: replaced two `as unknown as Key`
  double-casts with direct `: Key` annotations on the literal —
  the object already conforms to the Key interface, double-cast
  was bypassing type safety needlessly.

Tests: 37/37 keyMatchers + 58/58 AppContainer pass; tsc + ESLint
clean. No behavior change beyond the new debug log lines.

* fix(cli): #3969 wave — tool-name guard + non-shell test + defensive clear

3 #3969 review threads addressed; 1 deferred:

- AppContainer.tsx: Ctrl+B `find()` predicate now also checks
  `tc.request.name === ToolNames.SHELL` before matching the executing
  tool call. Defense-in-depth — today only the shell tool wires
  `promoteAbortController`, but a future copy-paste / type confusion
  that adds the property to a non-shell tool would otherwise let
  Ctrl+B mistakenly fire `abort({kind:'background'})` on a tool
  whose service has no promote-handoff handler.

- useReactToolScheduler.ts: re-added explicit `pid: undefined` and
  `promoteAbortController: undefined` to the non-executing return.
  Previously dropped on the assumption that `...coreTc` doesn't
  carry these fields when the status isn't `executing` — true today,
  but the explicit clearing is defense-in-depth against a future
  core change that adds either field to a non-executing status type
  (would surface as a stuck PID display or a Ctrl+B handler that
  matches a no-longer-executing tool call).

- AppContainer.test.tsx: replaced the placeholder "no-op when no
  pending tool calls" framing on the empty-array case (it does
  exercise the `executing-status` predicate but NOT the tool-name
  guard) with TWO tests:
    1. existing empty-array no-throw test (renamed for clarity)
    2. NEW: executing non-shell tool with a hostile-shape
       `promoteAbortController` — asserts `abortSpy` is NOT called.
       This is the regression test for the new tool-name guard above.

Tests: 61/61 AppContainer.test.tsx pass; tsc + ESLint clean.

Deferred to follow-up (replied + tracked):
- `debugLogger.debug` is file-only; success-path "agent unblocks +
  next message says 'promoted to bg_xxx'" is the user-visible signal.
  Adding a synthetic history item or stderr line for the gap between
  keypress and agent message conflicts with Ink rendering and is
  better as a focused UX PR.

* test(cli): pin inheritance of pid + promoteAbortController via type assertions

#3969 review: the earlier "redundant declaration" review removed the
explicit `pid?: number` and `promoteAbortController?: AbortController`
from `TrackedExecutingToolCall`, relying on the `& ExecutingToolCall`
intersection to inherit them. Current review flags the type-safety
regression: if core renames or removes either field, the React-side
build won't catch it locally — Ctrl+B handler silently breaks at
runtime.

Compromise: keep the type minimal (no re-declaration noise the prior
review flagged) but add compile-time `extends keyof ExecutingToolCall`
assertions that fail loudly + locally if either field disappears.
The assertions are evaluated at compile time and zero-cost at
runtime; the dummy `const` pins them so they aren't dead code.

61/61 AppContainer tests pass; tsc clean.
2026-05-11 14:03:38 +08:00
ChiGao
7f0c9791b7
feat(cli): expand TUI markdown rendering (#3680)
Some checks are pending
Qwen Code CI / Lint (push) Waiting to run
Qwen Code CI / Test (push) Blocked by required conditions
Qwen Code CI / Test-1 (push) Blocked by required conditions
Qwen Code CI / Test-2 (push) Blocked by required conditions
Qwen Code CI / Test-3 (push) Blocked by required conditions
Qwen Code CI / Test-4 (push) Blocked by required conditions
Qwen Code CI / Test-5 (push) Blocked by required conditions
Qwen Code CI / Test-6 (push) Blocked by required conditions
Qwen Code CI / Test-7 (push) Blocked by required conditions
Qwen Code CI / Test-8 (push) Blocked by required conditions
Qwen Code CI / Post Coverage Comment (push) Blocked by required conditions
Qwen Code CI / CodeQL (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
SDK Python / SDK Python (3.10) (push) Waiting to run
SDK Python / SDK Python (3.11) (push) Waiting to run
SDK Python / SDK Python (3.12) (push) Waiting to run
* feat(cli): expand markdown rendering in tui

* fix(cli): render finalized mermaid images synchronously

* fix(cli): harden markdown rendering paths

* fix(cli): preserve mermaid source fallbacks

* test(cli): make mermaid image renderer mock cross-platform

* fix(cli): run windows mmdc shims through shell

* fix(cli): address markdown rendering review comments

* fix(cli): validate mermaid render timeout

* feat(cli): expose rendered markdown source blocks

* fix(cli): align mermaid source copy controls

* fix(cli): make markdown render toggle visible

* fix(cli): keep markdown render toggle quiet

* fix(cli): generalize markdown render mode

* fix(cli): broaden render mode and copy latex blocks

* fix(cli): align rendered copy source indices

* fix(cli): support copying inline latex expressions

* feat(cli): document markdown render controls

* test(cli): cover markdown render controls

* fix(cli): tighten markdown render fallbacks

* fix(cli): bound mermaid renderer cache

* fix(cli): address markdown render review feedback

* fix(cli): address markdown render review comments

* test(cli): strengthen render mode shortcut coverage

* fix(cli): address mermaid image review comments

* fix(cli): stabilize renderer output truncation

* test(cli): flush fake renderer stderr before exit

* fix(cli): address markdown renderer review feedback

* docs(cli): clarify mermaid image limits

* fix(cli): refresh mermaid images on height resize

* fix(cli): address markdown render review

* chore: revert unrelated review formatting churn

* fix(cli): avoid mermaid regex codeql alert

* fix(cli): silence mermaid operator codeql alert

* fix(cli): render inline math in markdown tables

* fix(cli): harden markdown visual renderers

* fix(cli): strip c1 controls from mermaid previews

---------

Co-authored-by: 秦奇 <gary.gq@alibaba-inc.com>
2026-05-07 16:24:13 +08:00
Shaojin Wen
746f67f436
refactor: rename verboseMode to compactMode for better UX clarity (#3075)
The "Compact Mode" label is more intuitive than "Verbose Mode" for users,
as it directly describes the default compact view experience. This change
inverts the boolean semantics (compactMode=false means show full output)
and exposes the setting in the /settings dialog (showInDialog: true).

- Rename ui.verboseMode → ui.compactMode with inverted default (false)
- Rename VerboseModeContext → CompactModeContext (file and exports)
- Rename TOGGLE_VERBOSE_MODE → TOGGLE_COMPACT_MODE in key bindings
- Update all consumer components with inverted logic
- Update i18n keys across 6 locales (verbose → compact)
- Update VS Code settings schema
- Add ui.compactMode documentation to settings.md
- Fix Ctrl+O description in keyboard-shortcuts.md
2026-04-10 11:55:50 +08:00
易良
c353fbbfa3
feat(cli): add Ctrl+Y shortcut to retry failed requests (#2011)
* feat: add Ctrl+Y shortcut to retry failed requests

- Add Ctrl+Y keyboard shortcut for retrying the last failed request
- Add isNetworkError() to detect transient network failures (ECONNREFUSED, ETIMEDOUT, etc.)
- Add DashScope 1305 error code to rate limit detection
- Add error hint \"Press Ctrl+Y to retry\" in error messages
- Support user-defined error codes for retry via config
- Add retryLastPrompt() hook in useGeminiStream
- Update keyboard shortcuts documentation

* feat: improve Ctrl+Y retry feature with tests, docs, and rate limit config

- Add comprehensive tests for Ctrl+Y retry shortcut in InputPrompt
- Add unit tests for retryLastPrompt in useGeminiStream hook
- Add detailed JSDoc comments for retryLastPrompt function and Ctrl+Y shortcut
- Extend isRateLimitError to support custom error codes via retryErrorCodes config
- Fix rate limit retry log variable reference (RATE_LIMIT_RETRY_OPTIONS → maxRateLimitRetries)
- Add Eclipse IDE files to .gitignore

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* refactor(ui): consolidate retry countdown as inline hint in error messages

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* feat(cli): enhance error handling with improved retry mechanism

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

- Modify ErrorMessage component to remove dim color from hint text

- Update useGeminiStream hook to improve retry countdown behavior with option to preserve or clear hints

- Adjust tests to match new error handling implementation

* feat: add Ctrl+Y shortcut to retry the last failed request

When a request errors out, the error message shows an inline hint
"(Press Ctrl+Y to retry.)" in secondary color. Pressing Ctrl+Y
re-submits the same prompt, commits the error text to history
(without the hint), and clears the hint from the UI.

- Add retryLastPrompt action wired to Ctrl+Y via keyBindings and InputPrompt
- Track last submitted prompt and error state in useGeminiStream refs
- Show retry hint inline with error text in ErrorMessage component,
  wrapping naturally on narrow terminals while preserving hint color
- Expose retryLastPrompt through UIActionsContext
- Add keyboard shortcut entry in KeyboardShortcuts display
- Add i18n strings for hint and no-retry-available message
- Document Ctrl+Y in keyboard-shortcuts.md

* docs(configuration): Update model provider configuration document

* chore: remove YOLO mode code from core

* fix: prevent Ctrl+Y hint from overriding auto-retry countdown

When an auto-retry countdown is active (retryCountdownTimerRef is set),
handleErrorEvent should not overwrite it with the Ctrl+Y hint. The auto-retry
hint ("retrying in Xs...") and manual retry hint ("Press Ctrl+Y to retry.")
are mutually exclusive:

- Auto-retry errors (e.g., rate limits): show countdown hint
- Other errors: show Ctrl+Y hint

Also removed retryErrorCodes from ContentGeneratorConfig as it's not part
of the minimal Ctrl+Y feature scope.

* simplify: remove complex options from clearRetryCountdown

Revert clearRetryCountdown to simplest form without options parameter.
The function now just clears the timer and pending item without any
automatic history commit logic.

* fix: restore pendingRetryCountdownItem as separate state from pendingRetryErrorItem

Auto-retry countdown and manual retry hint are now independent:
- pendingRetryErrorItem: displays error message with optional hint
- pendingRetryCountdownItem: displays separate countdown line for auto-retry

This ensures both can be shown simultaneously without overriding each other.

* fix: restore RetryCountdownMessage rendering in HistoryItemDisplay

The retry_countdown type should be rendered as a separate message,
not inline in ErrorMessage. This allows auto-retry countdown and
manual retry hint to coexist properly.

* fix(cli): properly commit retry error item to history before clearing

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(cli): remove trailing period from retry hint translations

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

Remove unnecessary period from 'Press Ctrl+Y to retry' translation strings in both en.js and zh.js locales. Also update the corresponding usage in useGeminiStream hook.

* chore(sdk-java): add Eclipse project configuration files

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

Add .project configuration files for client and qwencode modules to support Eclipse IDE development environment.

* feat(cli): add retry countdown hint to error message

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* Revert "chore(sdk-java): add Eclipse project configuration files"

This reverts commit da83b5e571.

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-02 17:59:18 +08:00
LaZzyMan
c8297300d9 Merge branch 'main' into feat/image-attachment 2026-02-05 11:09:34 +08:00
LaZzyMan
3296785b23 feat use tab on windows instead of shift+tab 2026-02-02 19:48:07 +08:00
LaZzyMan
1050163804 fix paste image on windows 2026-02-02 17:07:39 +08:00
tanzhenxin
b804b1f48a feat: Redesign CLI welcome screen and improve visual consistency 2026-01-16 11:48:31 +08:00
pomelo-nwu
5742a69d8a feat: update docs 2025-12-15 19:40:14 +08:00
joeytoday
f417ace1b0 docs: update various documentation for clarity, formatting, and link corrections 2025-12-12 18:20:31 +08:00
joeytoday
9fd4f58c16 docs: Add detailed documentation for Qwen Code's approval modes and usage
- Introduced a comprehensive guide on the four permission modes: Plan, Default, Auto-Edit, and YOLO, including their use cases and risk levels.
- Updated the overview and quickstart documentation for clarity and consistency.
- Removed the outdated CLI reference document and integrated relevant content into the updated documentation.
- Improved command creation examples and best practices for custom commands.
2025-12-11 21:12:32 +08:00
joeytoday
0ae3a2247d docs: update sandbox title, enhance overview, and add CLI reference documentation 2025-12-11 14:54:37 +08:00
joeytoday
70b5aee381 docs: Add documentation for Sub Agents feature and update user guides
- Introduced a new documentation file for Sub Agents, detailing their purpose, benefits, configuration, and usage examples.
- Updated the overview and quickstart guides to improve clarity and remove outdated information.
- Created a comprehensive command reference document for Qwen Code, detailing slash commands, at commands, and exclamation commands for better user guidance.
- Enhanced the formatting and organization of existing documentation for improved readability and usability.
2025-12-09 14:05:26 +08:00
pomelo-nwu
bfe8133ea3 feat: refactor docs 2025-12-05 10:51:57 +08:00