Skip to content

fix(cli): harden TUI flicker and streaming output stability#3663

Closed
chiga0 wants to merge 17 commits into
mainfrom
codex/tui-streaming-clear-storm
Closed

fix(cli): harden TUI flicker and streaming output stability#3663
chiga0 wants to merge 17 commits into
mainfrom
codex/tui-streaming-clear-storm

Conversation

@chiga0

@chiga0 chiga0 commented Apr 27, 2026

Copy link
Copy Markdown
Collaborator

Summary

Closes #3279.

This PR consolidates the metric-backed TUI flicker, narrow-output, streaming blank-tail, OpenAI-compatible cumulative-delta, shell-output, and tool/detail stability fixes into one focused PR.

It closes the locally reproduced classes below and ties each claim to source changes plus deterministic metrics:

  • bounds long pending assistant/thought streaming text by visual height before it reaches Ink/Yoga;
  • commits completed streaming Markdown code/table/list blocks to Static before the unfinished tail;
  • hard-bounds the remaining pending Markdown tail with MaxSizedBox, then caps it to a small live viewport;
  • suppresses live pending hidden-line marker rows and Markdown fence delimiter rows such as raw ```mermaid;
  • trims trailing blank rows from the live pending viewport so model blank-tail chunks cannot push useful streaming content into scrollback repeatedly;
  • normalizes OpenAI-compatible cumulative delta.content chunks to suffixes before they reach the Gemini stream pipeline, preventing normal streaming table/Markdown sections from being appended repeatedly;
  • emits opt-in QWEN_STREAM_DEBUG=1 stream-delta metrics (rawDeltaBytes, emittedDeltaBytes, suppressedBytes, prefix overlap, cumulative/exact-repeat counters) to make provider/model/UI attribution easier;
  • preserves final transcript fidelity because committed messages still render through full MarkdownDisplay;
  • reuses the visual-height slicer for tool string output, including single long JSON/base64/minified lines;
  • fixes exactly-fit tool string output so it no longer loses one visible line or renders a false hidden-line banner;
  • bounds subagent default/verbose detail by assigned tool-message height and visual width;
  • changes non-explicit refreshStatic() paths from full-screen clearTerminal to targeted viewport repaint (cursorTo(0, 0) + eraseDown) plus static-history remount;
  • keeps explicit /clear on its own terminal reset path and avoids the historical second clearTerminal write;
  • gates shell live viewport updates on new renderable output so resize-only soft-wrap reflow is not emitted as fresh shell output;
  • throttles core shell tool live text updates while preserving immediate ANSI viewport updates and final output fidelity;
  • uses a tmux-safe fixed-width dots spinner so tmux panes do not receive high-frequency spinner redraws;
  • extends terminal-capture so narrow Markdown evidence scans every live frame's xterm scrollback, not only the final settled screen.

Problem Definition And Metrics

Raw full-screen clear signal:

ESC[2J ESC[3J ESC[H]

Evidence levels:

  • raw ANSI metrics are authoritative for full-screen clear/flicker claims;
  • event metrics are authoritative for shell live-output duplication claims;
  • sentinel amplification metrics are authoritative for blank-tail and cumulative-delta duplicate scrollback claims;
  • stream-delta debug metrics are diagnostic for provider-vs-model attribution;
  • source-level tests are authoritative for exact-fit output, throttle, delta normalization, and terminal-specific fallback behavior;
  • GIFs/videos explain the metric visually, but must not be treated as proof without the matching metric from the same run.
Scenario Branch Expected clearTerminalPairCount clearScreenCodeCount Frames Result
Streaming long assistant text origin/main failure-first reproduction 427 854 93 reproduced
Streaming long assistant text this branch strict pass 0 0 93 passed
Narrow streaming + active resize origin/main failure-first reproduction 498 996 93 reproduced
Narrow streaming + active resize this branch strict pass 0 0 93 passed
Resize 88x26 -> 62x26 -> 100x26 origin/main failure-first reproduction 2 4 50 reproduced
Resize 88x26 -> 62x26 -> 100x26 this branch strict pass 0 0 50 passed

Shell live-output reflow uses a different metric because the bug is an extra live-output event after resize-only soft-wrap changes.

Scenario Branch Expected Data events Resize-only events Result
Shell live reflow 24x8 -> 12x8 -> 18x8 origin/main failure-first reproduction 2 1 reproduced
Shell live reflow 24x8 -> 12x8 -> 18x8 this branch strict pass 1 0 passed

Narrow Markdown/Mermaid scrollback evidence checks every captured live frame, not just the final screen:

Scenario Branch Expected Clear pairs Final hidden markers Final raw fences Max frame hidden markers Max frame raw fences Frames Result
Narrow Markdown + Mermaid resize this branch strict pass 0 0 0 0 0 93 passed

Blank-tail evidence covers the manual issue where useful content is followed by a long empty streaming tail before the done event. The payload contains exactly six sentinel labels (QWEN_A1 through QWEN_F1). A duplicated screen/scrollback shows an amplification ratio above 1.

Scenario Branch Expected Final sentinel occurrences Max frame sentinel occurrences Amplification Post-done viewport min Result
Narrow Markdown pure stall this branch pure stall does not repeat 6 6 1.0 6 passed
Narrow Markdown blank tail before source fix failure-first reproduction 12 12 2.0 6 reproduced
Narrow Markdown blank tail this branch strict pass, no duplication 6 6 1.0 6 passed

Cumulative-delta evidence covers OpenAI-compatible upstreams that send accumulated full text in delta.content instead of incremental suffixes. The payload contains exactly eight table sentinel labels (QWEN_TABLE_01 through QWEN_TABLE_08). A duplicated stream shows an amplification ratio above 1.

Scenario Branch Expected Final sentinel occurrences Max frame sentinel occurrences Amplification Result
Narrow Markdown table incremental this branch normal incremental stream stable 8 8 1.0 passed
Narrow Markdown table cumulative delta before cumulative fix failure-first reproduction 112 112 14.0 reproduced
Narrow Markdown table cumulative delta this branch strict pass, no duplicate append 8 8 1.0 passed

Claude Code Cross-Check

Local Claude Code source confirms this is not solved there by a bigger truncate threshold:

  • src/screens/REPL.tsx exposes only complete streaming lines to the renderer (visibleStreamingText is truncated to the last newline).
  • src/components/Markdown.tsx uses StreamingMarkdown, lexing from a monotonic stable boundary. Unclosed code fences remain Markdown structure, not ordinary raw text rows.
  • src/ink/log-update.ts detects diffs that would touch rows already in scrollback and falls back to a reset instead of patching unreachable rows.
  • src/ink/renderer.ts and src/ink/ink.tsx keep alt-screen frames fixed-height, clamp the cursor inside the viewport, and anchor the cursor before diffs.

Copying Claude's modified Ink fork wholesale would import a large renderer/terminal lifecycle surface. This PR ports the relevant low-risk principles into Qwen's current architecture: bounded live viewport, no synthetic marker/fence rows in live pending scrollback, blank-tail suppression in the live viewport, committed Markdown fidelity through Static, cumulative-delta normalization before UI rendering, and frame-level evidence.

Diagnostics

Set QWEN_STREAM_DEBUG=1 to write stream_delta_metrics records to the normal debug log. The useful fields are:

  • rawDeltaBytes: bytes received from the provider for this chunk;
  • emittedDeltaBytes: bytes forwarded after normalization;
  • suppressedBytes: repeated cumulative-prefix bytes removed;
  • prefixOverlapBytes: bytes shared with previously emitted text;
  • cumulativeDeltaCount and exactRepeatCount: stream-level counters.

How to read them:

  • high suppressedBytes with cumulativeDeltaCount > 0 means provider cumulative delta behavior;
  • repeated final text with low suppressedBytes points more toward model-generated repetition;
  • fixed source/sentinel counts but growing screen counts points toward a renderer/scrollback issue.

UI Evidence

Please upload comparison GIFs and replace these placeholders with GitHub attachment URLs:

  • Streaming before/after GIF: streaming-clear-storm-before-after.gif
  • Narrow streaming resize before/after GIF: narrow-streaming-resize-before-after.gif
  • Resize/static refresh before/after GIF: resize-clear-regression-before-after.gif
  • Shell live reflow before/after GIF: shell-reflow-regression.gif
  • Narrow Markdown blank-tail before/after GIF: narrow-markdown-blank-tail.gif
  • Narrow Markdown cumulative-delta before/after GIF: narrow-markdown-table-cumulative-delta.gif

What the current metric evidence proves:

  • full-screen clear writes are removed from the reproduced streaming/resize paths;
  • shell resize-only reflow no longer emits duplicate live-output events;
  • live Markdown frames no longer emit hidden-line banners or raw Mermaid fence delimiters;
  • a long blank streaming tail no longer duplicates the six sentinel labels in live scrollback;
  • OpenAI-compatible cumulative delta.content chunks no longer duplicate normal Markdown/table output during streaming.

What it does not claim:

  • it does not replace Ink with Claude's fork;
  • it does not prove every terminal emulator has identical paint behavior without manual matrix validation;
  • it does not remove genuine repeated text produced by the model itself;
  • it does not make GIFs the source of truth without the matching metric summary.

Verification

  • cd packages/core && npx vitest run src/core/openaiContentGenerator/converter.test.ts
  • cd packages/cli && npx vitest run src/ui/utils/markdownUtilities.test.ts src/ui/components/messages/ConversationMessages.test.tsx src/ui/components/messages/ToolMessage.test.tsx src/ui/AppContainer.test.tsx
  • npm run build && npm run bundle
  • cd integration-tests/terminal-capture && npm run capture:narrow-markdown-regression
  • cd integration-tests/terminal-capture && npm run capture:narrow-markdown-stall-regression
  • cd integration-tests/terminal-capture && npm run capture:narrow-markdown-blank-tail-regression
  • cd integration-tests/terminal-capture && npm run capture:narrow-markdown-table-regression
  • cd integration-tests/terminal-capture && npm run capture:narrow-markdown-table-cumulative-regression
  • git diff --check

Latest fixed cumulative-delta E2E summary:

{
  "streamPayload": "markdown-table-cumulative",
  "framesCaptured": 103,
  "clearTerminalPairCount": 0,
  "clearScreenCodeCount": 0,
  "hiddenMarkerCount": 0,
  "rawMermaidFenceCount": 0,
  "maxFrameHiddenMarkerCount": 0,
  "maxFrameRawMermaidFenceCount": 0,
  "tableSentinelExpectedCount": 8,
  "tableSentinelOccurrenceCount": 8,
  "maxFrameTableSentinelOccurrenceCount": 8,
  "tableSentinelAmplificationRatio": 1,
  "maxFrameTableSentinelAmplificationRatio": 1,
  "pass": true
}

Latest fixed blank-tail E2E summary:

{
  "streamPayload": "markdown-blank-tail",
  "framesCaptured": 103,
  "clearTerminalPairCount": 0,
  "clearScreenCodeCount": 0,
  "hiddenMarkerCount": 0,
  "rawMermaidFenceCount": 0,
  "maxFrameHiddenMarkerCount": 0,
  "maxFrameRawMermaidFenceCount": 0,
  "stallSentinelExpectedCount": 6,
  "stallSentinelOccurrenceCount": 6,
  "maxFrameStallSentinelOccurrenceCount": 6,
  "stallSentinelAmplificationRatio": 1,
  "maxFrameStallSentinelAmplificationRatio": 1,
  "minPostDoneViewportStallSentinelOccurrenceCount": 6,
  "pass": true
}

@github-actions

github-actions Bot commented Apr 27, 2026

Copy link
Copy Markdown
Contributor

Code Coverage Summary

Package Lines Statements Functions Branches
CLI N/A% N/A% N/A% N/A%
Core 75.14% 75.14% 77.66% 81.48%
CLI Package - Full Text Report
CLI full-text-summary.txt not found at: coverage_artifact/cli/coverage/full-text-summary.txt
Core Package - Full Text Report
-------------------|---------|----------|---------|---------|-------------------
File               | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
-------------------|---------|----------|---------|---------|-------------------
All files          |   75.14 |    81.48 |   77.66 |   75.14 |                   
 src               |     100 |      100 |     100 |     100 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
 src/__mocks__/fs  |       0 |        0 |       0 |       0 |                   
  promises.ts      |       0 |        0 |       0 |       0 | 1-48              
 src/agents        |    85.8 |    83.72 |    92.3 |    85.8 |                   
  ...ound-tasks.ts |   85.33 |    83.72 |    92.3 |   85.33 | ...25-232,245-246 
  index.ts         |     100 |      100 |     100 |     100 |                   
 src/agents/arena  |    76.9 |    66.66 |   78.94 |    76.9 |                   
  ...gentClient.ts |   79.47 |    88.88 |   81.81 |   79.47 | ...68-183,189-204 
  ArenaManager.ts  |   75.84 |     62.9 |   78.57 |   75.84 | ...1889,1895-1896 
  arena-events.ts  |   64.44 |      100 |      50 |   64.44 | ...71-175,178-183 
  diff-summary.ts  |    87.5 |    73.46 |     100 |    87.5 | ...32-133,137-138 
  index.ts         |     100 |      100 |     100 |     100 |                   
  types.ts         |     100 |      100 |     100 |     100 |                   
 ...gents/backends |    76.4 |    86.07 |   72.41 |    76.4 |                   
  ITermBackend.ts  |   97.97 |    93.93 |     100 |   97.97 | ...78-180,255,307 
  ...essBackend.ts |   92.17 |    90.32 |   82.35 |   92.17 | ...24-244,303,403 
  TmuxBackend.ts   |    90.7 |    76.55 |   97.36 |    90.7 | ...87,697,743-747 
  detect.ts        |   31.25 |      100 |       0 |   31.25 | 34-88             
  index.ts         |     100 |      100 |     100 |     100 |                   
  iterm-it2.ts     |     100 |     92.1 |     100 |     100 | 37-38,106         
  tmux-commands.ts |    6.64 |      100 |    3.03 |    6.64 | ...93-363,386-503 
  types.ts         |     100 |      100 |     100 |     100 |                   
 ...agents/runtime |   80.94 |    76.01 |      70 |   80.94 |                   
  agent-core.ts    |      75 |    69.44 |   58.33 |      75 | ...1085,1112-1158 
  agent-events.ts  |   86.48 |      100 |      75 |   86.48 | 225-229           
  ...t-headless.ts |   79.52 |       75 |      55 |   79.52 | ...54-355,358-359 
  ...nteractive.ts |   81.71 |    78.12 |      75 |   81.71 | ...25,527,529,532 
  ...statistics.ts |   98.19 |    82.35 |     100 |   98.19 | 127,151,192,225   
  agent-types.ts   |     100 |      100 |     100 |     100 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
 src/config        |   73.62 |    75.92 |      61 |   73.62 |                   
  config.ts        |      71 |    72.98 |   54.94 |      71 | ...2693,2697-2709 
  constants.ts     |     100 |      100 |     100 |     100 |                   
  models.ts        |     100 |      100 |     100 |     100 |                   
  storage.ts       |   95.72 |    92.85 |   91.66 |   95.72 | ...06-207,241-242 
 ...nfirmation-bus |   98.29 |    97.14 |     100 |   98.29 |                   
  message-bus.ts   |   98.14 |    97.05 |     100 |   98.14 | 42-43             
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/constants     |    4.95 |      100 |       0 |    4.95 |                   
  codingPlan.ts    |    4.95 |      100 |       0 |    4.95 | ...79-291,299-309 
 src/core          |   80.42 |    80.36 |   85.54 |   80.42 |                   
  baseLlmClient.ts |   96.77 |    96.42 |      80 |   96.77 | 123-126           
  client.ts        |   70.75 |    73.59 |   73.07 |   70.75 | ...1115,1119-1135 
  ...tGenerator.ts |    72.1 |    61.11 |     100 |    72.1 | ...45,347,354-357 
  ...lScheduler.ts |   73.63 |    76.97 |   91.17 |   73.63 | ...1888,1945-1949 
  geminiChat.ts    |    89.1 |     84.5 |   85.29 |    89.1 | ...1075,1142-1143 
  geminiRequest.ts |     100 |      100 |     100 |     100 |                   
  ...htProtocol.ts |    9.09 |      100 |       0 |    9.09 | 34-42,45-49,52-87 
  logger.ts        |   82.25 |    81.81 |     100 |   82.25 | ...57-361,407-421 
  ...tyDefaults.ts |     100 |      100 |     100 |     100 |                   
  ...olExecutor.ts |   92.59 |       75 |      50 |   92.59 | 41-42             
  ...on-helpers.ts |   76.53 |    60.71 |     100 |   76.53 | ...81-182,196-205 
  prompts.ts       |    88.8 |    88.05 |      75 |    88.8 | ...-898,1101-1102 
  tokenLimits.ts   |     100 |    89.47 |     100 |     100 | 50-51             
  ...okTriggers.ts |   99.31 |     90.9 |     100 |   99.31 | 124,135           
  turn.ts          |   96.29 |    88.46 |     100 |   96.29 | ...87,400-401,449 
 ...ntentGenerator |   93.72 |    73.43 |    90.9 |   93.72 |                   
  ...tGenerator.ts |   95.99 |    72.17 |   86.66 |   95.99 | ...03-304,438,494 
  converter.ts     |   93.47 |       75 |     100 |   93.47 | ...87-488,498,558 
  index.ts         |       0 |        0 |       0 |       0 | 1-21              
 ...ntentGenerator |   91.53 |    71.21 |   93.33 |   91.53 |                   
  ...tGenerator.ts |      90 |    70.49 |   92.85 |      90 | ...77-283,301-302 
  index.ts         |     100 |       80 |     100 |     100 | 50                
 ...ntentGenerator |   91.08 |    76.14 |   85.71 |   91.08 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...tGenerator.ts |   91.04 |    76.14 |   85.71 |   91.04 | ...23,533-534,562 
 ...ntentGenerator |   76.51 |    83.51 |   89.55 |   76.51 |                   
  constants.ts     |     100 |      100 |     100 |     100 |                   
  converter.ts     |   73.07 |       78 |   86.36 |   73.07 | ...1311,1332-1338 
  errorHandler.ts  |     100 |      100 |     100 |     100 |                   
  index.ts         |       0 |        0 |       0 |       0 | 1-95              
  ...tGenerator.ts |   48.78 |    91.66 |   77.77 |   48.78 | ...10-163,166-167 
  pipeline.ts      |   94.15 |    89.58 |     100 |   94.15 | ...84,454-455,463 
  ...CallParser.ts |   90.66 |     88.4 |     100 |   90.66 | ...15-319,349-350 
  types.ts         |       0 |        0 |       0 |       0 | 1                 
 ...rator/provider |   95.98 |    85.59 |   93.75 |   95.98 |                   
  dashscope.ts     |   97.22 |    87.69 |   93.33 |   97.22 | ...10-211,287-288 
  deepseek.ts      |    91.3 |    73.91 |     100 |    91.3 | 49-50,54-55,68-69 
  default.ts       |   94.62 |    86.36 |   85.71 |   94.62 | 85-86,156-158     
  index.ts         |     100 |      100 |     100 |     100 |                   
  modelscope.ts    |     100 |      100 |     100 |     100 |                   
  openrouter.ts    |     100 |      100 |     100 |     100 |                   
  types.ts         |       0 |        0 |       0 |       0 |                   
 src/extension     |   60.71 |    79.59 |   79.03 |   60.71 |                   
  ...-converter.ts |   62.35 |    47.82 |      90 |   62.35 | ...90-791,800-832 
  ...ionManager.ts |   46.96 |    82.97 |   67.44 |   46.96 | ...1343,1364-1383 
  ...onSettings.ts |   93.46 |    93.05 |     100 |   93.46 | ...17-221,228-232 
  ...-converter.ts |   54.88 |    94.44 |      60 |   54.88 | ...35-146,158-192 
  github.ts        |   44.94 |    88.52 |      60 |   44.94 | ...53-359,398-451 
  index.ts         |     100 |      100 |     100 |     100 |                   
  marketplace.ts   |   97.29 |    93.75 |     100 |   97.29 | ...64,184-185,274 
  npm.ts           |   48.66 |    76.08 |      75 |   48.66 | ...18-420,427-431 
  override.ts      |   94.11 |    88.88 |     100 |   94.11 | 63-64,81-82       
  settings.ts      |   66.26 |      100 |      50 |   66.26 | 81-108,143-149    
  storage.ts       |   94.73 |       90 |     100 |   94.73 | 41-42             
  ...ableSchema.ts |     100 |      100 |     100 |     100 |                   
  variables.ts     |   88.75 |    83.33 |     100 |   88.75 | ...28-231,234-237 
 src/followup      |   46.18 |     92.3 |   71.87 |   46.18 |                   
  followupState.ts |      96 |    89.74 |     100 |      96 | 159-161,218-219   
  index.ts         |     100 |      100 |     100 |     100 |                   
  overlayFs.ts     |   95.06 |       84 |     100 |   95.06 | 78,108,122,133    
  speculation.ts   |   13.22 |      100 |   16.66 |   13.22 | 88-458,518-568    
  ...onToolGate.ts |     100 |    96.29 |     100 |     100 | 92                
  ...nGenerator.ts |   36.67 |    95.12 |   33.33 |   36.67 | ...24-326,361-391 
 src/generated     |       0 |        0 |       0 |       0 |                   
  git-commit.ts    |       0 |        0 |       0 |       0 | 1-10              
 src/hooks         |    80.6 |    84.37 |   84.16 |    80.6 |                   
  ...okRegistry.ts |   86.48 |    77.08 |     100 |   86.48 | ...41-344,362-369 
  ...bortSignal.ts |     100 |      100 |     100 |     100 |                   
  ...terpolator.ts |   96.66 |    93.33 |     100 |   96.66 | 66-67             
  ...HookRunner.ts |   96.68 |    87.23 |     100 |   96.68 | 110-112,231-233   
  ...Aggregator.ts |   96.37 |    90.54 |     100 |   96.37 | ...89,291-292,365 
  ...entHandler.ts |   95.58 |    84.37 |   92.59 |   95.58 | ...29,682-683,693 
  hookPlanner.ts   |   84.13 |    76.59 |      90 |   84.13 | ...38,144,162-173 
  hookRegistry.ts  |   88.83 |    86.36 |     100 |   88.83 | ...21,326,330,334 
  hookRunner.ts    |   53.63 |    72.22 |   61.11 |   53.63 | ...23-724,733-734 
  hookSystem.ts    |   75.47 |      100 |   56.41 |   75.47 | ...75-576,582-583 
  ...HookRunner.ts |   75.51 |     61.9 |      80 |   75.51 | ...05-406,424-425 
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...SkillHooks.ts |   78.75 |       75 |   66.66 |   78.75 | 62-66,137-152     
  ...oksManager.ts |    96.5 |     91.8 |     100 |    96.5 | ...90,209-210,223 
  ssrfGuard.ts     |   77.22 |    85.36 |     100 |   77.22 | ...57,261-267,273 
  trustedHooks.ts  |       0 |        0 |       0 |       0 | 1-124             
  types.ts         |   90.15 |    91.02 |   85.18 |   90.15 | ...91-392,452-456 
  urlValidator.ts  |     100 |      100 |     100 |     100 |                   
 src/ide           |   74.28 |    83.39 |   78.33 |   74.28 |                   
  constants.ts     |     100 |      100 |     100 |     100 |                   
  detect-ide.ts    |     100 |      100 |     100 |     100 |                   
  ide-client.ts    |    64.2 |    81.48 |   66.66 |    64.2 | ...9-970,999-1007 
  ide-installer.ts |   89.06 |    79.31 |     100 |   89.06 | ...36,143-147,160 
  ideContext.ts    |     100 |      100 |     100 |     100 |                   
  process-utils.ts |   84.84 |    71.79 |     100 |   84.84 | ...37,151,193-194 
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/lsp           |   33.39 |    43.56 |   44.91 |   33.39 |                   
  ...nfigLoader.ts |   70.27 |    35.89 |   94.73 |   70.27 | ...20-422,426-432 
  ...ionFactory.ts |    4.29 |        0 |       0 |    4.29 | ...20-371,377-394 
  ...Normalizer.ts |   23.09 |    13.72 |   30.43 |   23.09 | ...04-905,909-924 
  ...verManager.ts |   10.47 |       75 |      25 |   10.47 | ...56-675,681-711 
  ...eLspClient.ts |   17.89 |      100 |       0 |   17.89 | ...37-244,254-258 
  ...LspService.ts |   45.87 |    62.13 |   66.66 |   45.87 | ...1282,1299-1309 
  constants.ts     |     100 |      100 |     100 |     100 |                   
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/mcp           |   78.69 |    75.34 |   75.92 |   78.69 |                   
  constants.ts     |     100 |      100 |     100 |     100 |                   
  ...h-provider.ts |   86.95 |      100 |   33.33 |   86.95 | ...,93,97,101-102 
  ...h-provider.ts |   73.82 |    53.92 |     100 |   73.82 | ...88-895,902-904 
  ...en-storage.ts |   98.62 |    97.72 |     100 |   98.62 | 87-88             
  oauth-utils.ts   |   70.58 |    85.29 |    90.9 |   70.58 | ...70-290,315-344 
  ...n-provider.ts |   89.83 |    95.83 |   45.45 |   89.83 | ...43,147,151-152 
 .../token-storage |   79.48 |    86.66 |   86.36 |   79.48 |                   
  ...en-storage.ts |     100 |      100 |     100 |     100 |                   
  ...en-storage.ts |   82.75 |    82.35 |   92.85 |   82.75 | ...62-172,180-181 
  ...en-storage.ts |     100 |      100 |     100 |     100 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...en-storage.ts |   68.14 |    82.35 |   64.28 |   68.14 | ...81-295,298-314 
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/memory        |   61.95 |    74.52 |    65.3 |   61.95 |                   
  const.ts         |     100 |      100 |     100 |     100 |                   
  dream.ts         |   88.07 |    66.66 |      80 |   88.07 | ...23,131,141-147 
  ...entPlanner.ts |    55.2 |       75 |   28.57 |    55.2 | ...30,135-142,147 
  entries.ts       |   59.84 |       70 |      50 |   59.84 | ...72-180,183-189 
  extract.ts       |    95.2 |    79.16 |     100 |    95.2 | 81-86,125         
  ...entPlanner.ts |   63.08 |    65.71 |   41.17 |   63.08 | ...17,222-223,332 
  ...ionPlanner.ts |       0 |        0 |       0 |       0 | 1                 
  forget.ts        |    8.04 |      100 |       0 |    8.04 | 67-342            
  governance.ts    |       0 |        0 |       0 |       0 | 1-352             
  indexer.ts       |   83.87 |    45.45 |     100 |   83.87 | ...50,56-57,69-70 
  manager.ts       |   74.16 |    76.23 |   70.27 |   74.16 | ...77-878,891-893 
  memoryAge.ts     |   80.95 |     87.5 |      75 |   80.95 | 48-51             
  paths.ts         |   55.47 |    88.88 |   85.71 |   55.47 | ...,88-89,105-113 
  prompt.ts        |   93.36 |    71.42 |     100 |   93.36 | ...58,161,228-229 
  recall.ts        |   82.24 |    78.04 |   88.88 |   82.24 | ...71-188,246-257 
  ...ceSelector.ts |   91.56 |    73.68 |     100 |   91.56 | ...01,103-104,112 
  scan.ts          |   87.91 |    68.42 |     100 |   87.91 | ...47-48,58,82-87 
  status.ts        |   10.52 |      100 |       0 |   10.52 | 41-98             
  store.ts         |   94.44 |    83.33 |     100 |   94.44 | 56-57,92-93       
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/mocks         |       0 |        0 |       0 |       0 |                   
  msw.ts           |       0 |        0 |       0 |       0 | 1-9               
 src/models        |   89.48 |    85.58 |   87.14 |   89.48 |                   
  constants.ts     |     100 |      100 |     100 |     100 |                   
  ...tor-config.ts |   88.67 |     90.9 |     100 |   88.67 | 112,118,121-130   
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...nfigErrors.ts |   74.22 |       44 |   84.61 |   74.22 | ...,67-74,106-117 
  ...igResolver.ts |   98.63 |    92.53 |     100 |   98.63 | 161,323,329       
  modelRegistry.ts |     100 |    98.21 |     100 |     100 | 182               
  modelsConfig.ts  |   85.37 |    83.54 |   81.57 |   85.37 | ...1210,1239-1240 
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/output        |     100 |      100 |     100 |     100 |                   
  ...-formatter.ts |     100 |      100 |     100 |     100 |                   
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/permissions   |    70.5 |    87.96 |    48.2 |    70.5 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...on-manager.ts |   79.18 |    82.65 |   79.16 |   79.18 | ...85-786,793-802 
  rule-parser.ts   |   95.88 |    93.56 |     100 |   95.88 | ...40-841,990-992 
  ...-semantics.ts |   58.28 |    85.27 |    30.2 |   58.28 | ...1604-1614,1643 
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/prompts       |   83.63 |      100 |    87.5 |   83.63 |                   
  mcp-prompts.ts   |   18.18 |      100 |       0 |   18.18 | 11-19             
  ...t-registry.ts |     100 |      100 |     100 |     100 |                   
 src/qwen          |   86.03 |    79.48 |   97.18 |   86.03 |                   
  ...tGenerator.ts |   98.64 |    98.18 |     100 |   98.64 | 105-106           
  qwenOAuth2.ts    |   85.01 |    74.81 |   93.33 |   85.01 | ...,986-1002,1032 
  ...kenManager.ts |   83.79 |    76.22 |     100 |   83.79 | ...63-768,789-794 
 src/services      |   82.51 |    82.17 |   84.32 |   82.51 |                   
  ...ionService.ts |   97.95 |    94.04 |     100 |   97.95 | 255,257-261       
  ...ingService.ts |   72.04 |    78.88 |   73.07 |   72.04 | ...01-902,919-920 
  cronScheduler.ts |   97.56 |    92.98 |     100 |   97.56 | 62-63,77,155      
  ...eryService.ts |   80.43 |    95.45 |      75 |   80.43 | ...19-134,140-141 
  ...temService.ts |   89.76 |     85.1 |   88.88 |   89.76 | ...89,191,266-273 
  gitInit.ts       |     100 |      100 |     100 |     100 |                   
  gitService.ts    |   68.75 |     92.3 |   55.55 |   68.75 | ...12-122,125-129 
  ...reeService.ts |   71.83 |    68.47 |    91.3 |   71.83 | ...89-790,806,822 
  ...ionService.ts |   98.13 |     97.8 |   95.45 |   98.13 | ...32-333,380-381 
  sessionRecap.ts  |   10.71 |      100 |       0 |   10.71 | 48-161            
  ...ionService.ts |   83.91 |    71.72 |      92 |   83.91 | ...-989,1021-1022 
  sessionTitle.ts  |   93.95 |    70.37 |     100 |   93.95 | ...36-239,270-271 
  ...ionService.ts |   84.93 |    82.68 |   83.78 |   84.93 | ...-999,1005-1010 
 ...icrocompaction |   98.62 |    86.44 |     100 |   98.62 |                   
  microcompact.ts  |   98.62 |    86.44 |     100 |   98.62 | 138,142           
 src/skills        |   83.35 |    79.29 |   90.32 |   83.35 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  skill-load.ts    |   91.24 |    78.94 |     100 |   91.24 | ...37,157,169-171 
  skill-manager.ts |   80.66 |    77.85 |   88.46 |   80.66 | ...88-896,903-907 
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/subagents     |   82.44 |    80.76 |   91.11 |   82.44 |                   
  ...tin-agents.ts |     100 |      100 |     100 |     100 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...-selection.ts |     100 |      100 |     100 |     100 |                   
  ...nt-manager.ts |   76.05 |    72.81 |   87.09 |   76.05 | ...1112,1134-1135 
  types.ts         |     100 |      100 |     100 |     100 |                   
  validation.ts    |   92.46 |    95.18 |     100 |   92.46 | 51-56,69-74,78-83 
 src/telemetry     |   68.06 |       82 |   73.68 |   68.06 |                   
  config.ts        |     100 |      100 |     100 |     100 |                   
  constants.ts     |     100 |      100 |     100 |     100 |                   
  ...-exporters.ts |   46.37 |      100 |   44.44 |   46.37 | ...85,88-89,92-93 
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...t.circular.ts |       0 |        0 |       0 |       0 | 1-111             
  ...t.circular.ts |       0 |        0 |       0 |       0 | 1-128             
  loggers.ts       |   52.09 |    61.64 |   57.77 |   52.09 | ...1218,1235-1255 
  metrics.ts       |    74.9 |    82.95 |   74.54 |    74.9 | ...58-978,981-992 
  sanitize.ts      |      80 |    83.33 |     100 |      80 | 35-36,41-42       
  sdk.ts           |   85.13 |    56.25 |     100 |   85.13 | ...78,184-185,191 
  ...etry-utils.ts |     100 |      100 |     100 |     100 |                   
  ...l-decision.ts |     100 |      100 |     100 |     100 |                   
  types.ts         |   79.13 |    85.83 |   83.33 |   79.13 | ...1136,1139-1168 
  uiTelemetry.ts   |   93.04 |    96.55 |   81.25 |   93.04 | ...95-196,202-209 
 ...ry/qwen-logger |   68.05 |    80.21 |   64.91 |   68.05 |                   
  event-types.ts   |       0 |        0 |       0 |       0 |                   
  qwen-logger.ts   |   68.05 |       80 |   64.28 |   68.05 | ...1043,1081-1082 
 src/test-utils    |   93.07 |    95.65 |   73.52 |   93.07 |                   
  config.ts        |     100 |      100 |     100 |     100 |                   
  ...st-helpers.ts |   94.11 |       90 |     100 |   94.11 | 69-70             
  index.ts         |     100 |      100 |     100 |     100 |                   
  mock-tool.ts     |   91.02 |    96.87 |   68.96 |   91.02 | ...32,196-197,210 
  ...aceContext.ts |     100 |      100 |     100 |     100 |                   
 src/tools         |   73.63 |    79.16 |   79.26 |   73.63 |                   
  ...erQuestion.ts |   87.89 |     73.8 |    90.9 |   87.89 | ...44-345,349-350 
  cron-create.ts   |   97.61 |    88.88 |   83.33 |   97.61 | 30-31             
  cron-delete.ts   |   96.55 |      100 |   83.33 |   96.55 | 26-27             
  cron-list.ts     |   96.36 |      100 |   83.33 |   96.36 | 25-26             
  diffOptions.ts   |     100 |      100 |     100 |     100 |                   
  edit.ts          |   80.72 |    85.05 |   73.33 |   80.72 | ...09-510,593-643 
  exitPlanMode.ts  |   84.61 |    85.71 |     100 |   84.61 | ...60-163,177-189 
  glob.ts          |   90.56 |    88.33 |   84.61 |   90.56 | ...24,167,297,300 
  grep.ts          |   71.24 |    87.34 |   72.22 |   71.24 | ...88,528,536-543 
  ls.ts            |   96.74 |    90.27 |     100 |   96.74 | 171-176,207,211   
  lsp.ts           |   72.58 |    60.29 |   90.32 |   72.58 | ...1202,1204-1205 
  ...nt-manager.ts |   47.47 |       60 |   44.44 |   47.47 | ...73-491,494-531 
  mcp-client.ts    |   29.65 |    71.05 |   46.87 |   29.65 | ...1434,1438-1441 
  mcp-tool.ts      |   90.92 |    88.88 |   96.42 |   90.92 | ...89-590,640-641 
  memory-config.ts |       0 |        0 |       0 |       0 | 1-48              
  ...iable-tool.ts |     100 |    84.61 |     100 |     100 | 102,109           
  read-file.ts     |   91.94 |    86.79 |   88.88 |   91.94 | ...,94-95,166-175 
  ripGrep.ts       |   94.42 |    89.33 |   91.66 |   94.42 | ...34,337,415-416 
  ...-transport.ts |    6.34 |        0 |       0 |    6.34 | 47-145            
  shell.ts         |   82.69 |    78.12 |    92.3 |   82.69 | ...84-488,694-695 
  skill-utils.ts   |     100 |      100 |     100 |     100 |                   
  skill.ts         |   86.97 |    87.71 |   83.33 |   86.97 | ...11,315,338-360 
  todoWrite.ts     |   85.42 |    84.09 |   84.61 |   85.42 | ...05-410,432-433 
  tool-error.ts    |     100 |      100 |     100 |     100 |                   
  tool-names.ts    |     100 |      100 |     100 |     100 |                   
  tool-registry.ts |   67.49 |    68.91 |   65.71 |   67.49 | ...59-660,668-669 
  tools.ts         |   84.18 |    89.58 |   82.35 |   84.18 | ...25-426,442-448 
  web-fetch.ts     |   88.44 |    76.92 |    92.3 |   88.44 | ...05-306,308-309 
  write-file.ts    |   83.04 |    77.19 |   83.33 |   83.04 | ...11-414,426-461 
 src/tools/agent   |   78.23 |    82.31 |   84.37 |   78.23 |                   
  agent.ts         |   81.34 |    81.94 |   88.88 |   81.34 | ...1112,1138-1142 
  fork-subagent.ts |   42.02 |      100 |      60 |   42.02 | 54-72,91-128      
 src/utils         |   86.95 |    87.13 |   90.63 |   86.95 |                   
  LruCache.ts      |       0 |        0 |       0 |       0 | 1-41              
  ...ssageQueue.ts |     100 |      100 |     100 |     100 |                   
  ...cFileWrite.ts |   76.08 |    44.44 |     100 |   76.08 | 61-70,72          
  bareMode.ts      |   27.27 |      100 |       0 |   27.27 | 9-15,18-19        
  browser.ts       |    7.69 |      100 |       0 |    7.69 | 17-56             
  ...igResolver.ts |     100 |      100 |     100 |     100 |                   
  cronDisplay.ts   |   42.85 |    23.07 |     100 |   42.85 | 26-31,33-45,47-54 
  cronParser.ts    |   89.74 |    85.71 |     100 |   89.74 | ...,63-64,183-186 
  debugLogger.ts   |   96.12 |    93.75 |   93.75 |   96.12 | 164-168           
  editHelper.ts    |   92.67 |    82.14 |     100 |   92.67 | ...52-454,463-464 
  editor.ts        |   97.61 |    95.71 |     100 |   97.61 | ...70-271,273-274 
  ...arResolver.ts |   94.28 |    88.88 |     100 |   94.28 | 28-29,125-126     
  ...entContext.ts |     100 |       95 |     100 |     100 | 83                
  errorParsing.ts  |   97.05 |       95 |     100 |   97.05 | 39-40             
  ...rReporting.ts |   88.46 |       90 |     100 |   88.46 | 69-74             
  errors.ts        |   70.92 |    80.39 |   53.33 |   70.92 | ...03-219,223-229 
  fetch.ts         |   70.18 |    71.42 |   71.42 |   70.18 | ...42,148,161,186 
  fileUtils.ts     |   89.08 |    85.06 |   94.73 |   89.08 | ...68-875,879-885 
  forkedAgent.ts   |   62.98 |    54.54 |      75 |   62.98 | ...23-432,434-447 
  formatters.ts    |   54.54 |       50 |     100 |   54.54 | 12-16             
  ...eUtilities.ts |   89.21 |    86.66 |     100 |   89.21 | 16-17,49-55,65-66 
  ...rStructure.ts |   94.36 |    94.28 |     100 |   94.36 | ...17-120,330-335 
  getPty.ts        |    12.5 |      100 |       0 |    12.5 | 21-34             
  ...noreParser.ts |    92.3 |    89.36 |     100 |    92.3 | ...15-116,186-187 
  gitUtils.ts      |   36.66 |    76.92 |      50 |   36.66 | ...4,88-89,97-148 
  iconvHelper.ts   |     100 |      100 |     100 |     100 |                   
  ...rePatterns.ts |     100 |      100 |     100 |     100 |                   
  ...ionManager.ts |     100 |     90.9 |     100 |     100 | 26                
  ...lPromptIds.ts |     100 |      100 |     100 |     100 |                   
  jsonl-utils.ts   |   10.07 |      100 |       0 |   10.07 | ...67-200,206-212 
  ...-detection.ts |     100 |      100 |     100 |     100 |                   
  ...yDiscovery.ts |   83.85 |    79.36 |     100 |   83.85 | ...15,318,410-413 
  ...tProcessor.ts |   93.63 |       90 |     100 |   93.63 | ...96-302,384-385 
  ...Inspectors.ts |   61.53 |      100 |      50 |   61.53 | 18-23             
  ...kerChecker.ts |   82.55 |    78.57 |     100 |   82.55 | 68-69,79-84,92-98 
  notebook.ts      |   94.35 |    84.78 |     100 |   94.35 | ...10,122,174-176 
  openaiLogger.ts  |   86.27 |    82.14 |     100 |   86.27 | ...05-107,130-135 
  partUtils.ts     |     100 |      100 |     100 |     100 |                   
  pathReader.ts    |     100 |      100 |     100 |     100 |                   
  paths.ts         |   93.43 |     92.1 |     100 |   93.43 | ...50-351,353-355 
  pdf.ts           |   93.68 |    87.05 |     100 |   93.68 | ...96-297,321-325 
  ...ectSummary.ts |   89.39 |    72.41 |     100 |   89.39 | ...37-142,193-196 
  ...tIdContext.ts |     100 |      100 |     100 |     100 |                   
  proxyUtils.ts    |     100 |      100 |     100 |     100 |                   
  ...rDetection.ts |   58.57 |       76 |     100 |   58.57 | ...4,88-89,95-100 
  ...noreParser.ts |   85.45 |    85.18 |     100 |   85.45 | ...59,65-66,72-73 
  rateLimit.ts     |   91.48 |    94.11 |     100 |   91.48 | 80,93-95          
  readManyFiles.ts |   87.96 |    86.95 |     100 |   87.96 | ...05-207,223-234 
  retry.ts         |   89.81 |    88.05 |     100 |   89.81 | ...29,350,357-358 
  ripgrepUtils.ts  |   46.53 |    83.33 |   66.66 |   46.53 | ...32-233,245-322 
  ...sDiscovery.ts |   97.47 |    93.15 |     100 |   97.47 | ...03,181-182,201 
  ...tchOptions.ts |   63.85 |    64.28 |   83.33 |   63.85 | ...29-130,187-188 
  safeJsonParse.ts |   74.07 |    83.33 |     100 |   74.07 | 40-46             
  ...nStringify.ts |     100 |      100 |     100 |     100 |                   
  ...aConverter.ts |   90.78 |    87.87 |     100 |   90.78 | ...41-42,93,95-96 
  ...aValidator.ts |   93.43 |    77.41 |     100 |   93.43 | ...46,155-158,212 
  ...r-launcher.ts |   76.92 |     91.3 |   66.66 |   76.92 | ...34,136,157-195 
  ...orageUtils.ts |   92.41 |    82.82 |     100 |   92.41 | ...39,423-430,441 
  shell-utils.ts   |    83.6 |    90.63 |     100 |    83.6 | ...1040,1047-1051 
  ...lAstParser.ts |   95.58 |    85.79 |     100 |   95.58 | ...1059-1061,1071 
  ...nlyChecker.ts |   95.75 |    92.47 |     100 |   95.75 | ...00-301,313-314 
  sideQuery.ts     |     100 |    92.85 |     100 |     100 | 43                
  ...tGenerator.ts |     100 |      100 |     100 |     100 |                   
  ...ameContext.ts |     100 |      100 |     100 |     100 |                   
  symlink.ts       |   77.77 |       50 |     100 |   77.77 | 44,54-59          
  ...emEncoding.ts |   96.36 |    91.17 |     100 |   96.36 | 59-60,124-125     
  terminalSafe.ts  |     100 |      100 |     100 |     100 |                   
  ...Serializer.ts |   98.72 |       90 |     100 |   98.72 | 42-43,134,201-203 
  testUtils.ts     |   53.33 |      100 |   33.33 |   53.33 | ...53,59-64,70-72 
  textUtils.ts     |      60 |      100 |   66.66 |      60 | 36-55             
  thoughtUtils.ts  |     100 |    92.85 |     100 |     100 | 71                
  ...-converter.ts |   94.59 |    85.71 |     100 |   94.59 | 35-36             
  tool-utils.ts    |    93.6 |     91.3 |     100 |    93.6 | ...58-159,162-163 
  truncation.ts    |     100 |       92 |     100 |     100 | 52,71             
  windowsPath.ts   |   89.47 |    79.31 |     100 |   89.47 | ...57-58,62,90-91 
  ...aceContext.ts |   93.71 |    88.88 |   93.33 |   93.71 | ...24-225,249-251 
  yaml-parser.ts   |      92 |    84.31 |     100 |      92 | 49-53,65-69       
 ...ils/filesearch |   96.34 |    91.66 |     100 |   96.34 |                   
  crawlCache.ts    |     100 |      100 |     100 |     100 |                   
  crawler.ts       |   96.87 |    94.44 |     100 |   96.87 | 83-84             
  fileSearch.ts    |   93.29 |    86.76 |     100 |   93.29 | ...40-241,243-244 
  ignore.ts        |     100 |      100 |     100 |     100 |                   
  result-cache.ts  |     100 |     92.3 |     100 |     100 | 46                
 ...uest-tokenizer |   56.63 |    74.52 |   74.19 |   56.63 |                   
  ...eTokenizer.ts |   41.86 |    76.47 |   69.23 |   41.86 | ...70-443,453-507 
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...tTokenizer.ts |   68.39 |    69.49 |    90.9 |   68.39 | ...24-325,327-328 
  ...ageFormats.ts |      76 |      100 |   33.33 |      76 | 45-48,55-56       
  textTokenizer.ts |     100 |      100 |     100 |     100 |                   
  types.ts         |       0 |        0 |       0 |       0 | 1                 
-------------------|---------|----------|---------|---------|-------------------

For detailed HTML reports, please see the 'coverage-reports-22.x-ubuntu-latest' artifact from the main CI run.

@chiga0 chiga0 changed the title fix(cli): stop streaming clear storms in main TUI fix(cli): harden TUI flicker and narrow output handling Apr 27, 2026
@chiga0 chiga0 marked this pull request as draft April 27, 2026 13:25

@chiga0 chiga0 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

两条 follow-up 建议(可在本 PR 追加 commit,也可拆成独立小 PR),目标是把 #3279 的窄屏 markdown 重复渲染场景体验进一步收敛。


1. Widen findLastSafeSplitPoint to also recognize closed tables / lists / code blocks

当前 slicePendingTextForHeight 在溢出时把 markdown 渲染降级为扁平 <Text wrap="wrap">,丢失表格框线、syntax highlight、headers 等。这是 #3279 截图里「窄屏写一张大表格 / 长 mermaid 代码块」场景的常见 UX 退化点:流式过程中用户长时间只能看到 ... first N streaming lines hidden ... + 无格式裸文。

根因不在本 PR 引入的 slicer,而在 packages/cli/src/ui/utils/markdownUtilities.tsfindLastSafeSplitPoint 仅以 \n\n 作为安全 split:表格 / 列表 / 完整代码块在没有空行时全段都留在 pending。建议把以下三类纳入安全 split point,让 stable prefix 早一步进入 <Static>

  • 完整代码块结束(match 配对的 fence 之后)
  • 表格段结束(最后一行匹配 tableRowRegex 后下一行不再匹配)
  • 列表段结束(连续 list item 行后第一个非 list 行)

这样窄屏溢出会更少,而且当真的溢出时只剩 tail 是裸文本,已成块的内容仍然以渲染态出现在 Static 里,对 #3279 的体验改善更直接。


2. PENDING_PREVIEW_RESERVED_ROWS = 4 应从已计算的 controlsHeight 推导

packages/cli/src/ui/components/messages/ConversationMessages.tsxPENDING_PREVIEW_RESERVED_ROWS = 4 是个静态常量,意图是为 LoadingIndicator + InputPrompt + Footer + spacer 留出余量。但 footer 在 showShortcuts / KeyboardShortcuts / embeddedShellFocused / agents.size > 0(额外 tab bar)等状态下高度会变化,AppContainer 已经在 controlsHeight + tabBarHeight 里测过实际值(measureElement(fullFooterRef),见 AppContainer.tsxcontrolsHeight / availableTerminalHeight 的计算)。

建议把 availableTerminalHeight 直接作为 slicer 的 maxHeight 传入并把 reservedRows 设为 0(或保留 1-2 行小安全余量处理 marginTop),避免 footer 高度变动时 4 行还不够(仍会触发 Ink 全屏 clear)或多余裁剪一行 hidden-line banner。改动量很小,可以并入本 PR 一起 metric 验证。


另:建议在 PR description 顶部 / commit message 里加 Closes #3279,让 GitHub 自动关闭对应 issue,路径 A(resize-during-stream)和路径 B(pending overflow)当前的修复都已覆盖那张 issue 的截图场景。

@chiga0

chiga0 commented Apr 28, 2026

Copy link
Copy Markdown
Collaborator Author

Handled the latest #3279 follow-up comments in commit 369e1ec.\n\n- Expanded findLastSafeSplitPoint so completed fenced code blocks, Markdown tables, and list segments can move into Static before the unfinished tail; open fenced blocks still split before the block.\n- Reworked pending assistant slicing so it uses the measured availableTerminalHeight directly: exact-fit content is checked with reservedRows: 0, and only true overflow reserves the one hidden-line marker row.\n- Added coverage in markdownUtilities.test.ts and ConversationMessages.test.tsx, and updated docs/design/tui-optimization/streaming-clear-storm.md plus the PR description with Closes #3279.\n\nValidation run locally:\n- cd packages/cli && npx vitest run src/ui/utils/markdownUtilities.test.ts src/ui/components/messages/ConversationMessages.test.tsx src/ui/components/messages/ToolMessage.test.tsx src/ui/AppContainer.test.tsx\n- npx eslint packages/cli/src/ui/utils/markdownUtilities.ts packages/cli/src/ui/utils/markdownUtilities.test.ts packages/cli/src/ui/components/messages/ConversationMessages.tsx packages/cli/src/ui/components/messages/ConversationMessages.test.tsx\n- git diff --check\n- npm run build

@chiga0 chiga0 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Re-reproduction on codex/tui-streaming-clear-storm branch — slicer 仍未拦住 mermaid 代码块场景

附用户在 #3279 上跟进的窄屏复现截图(已 cherry-pick 本 PR 全部 commit):

  • 顶部是助手前缀 ✦ Here is a comprehensive Mermaid flowchart example:
  • 之后 scrollback 中堆了 20+ 条完全一致1 flowchart TD(dim 行号 1 + 高亮 flowchart TD
  • 每条都是同一个代码块的 line 1,证实「每帧 pending 溢出 viewport,最顶行被推进 scrollback,下一帧再次写入又泄漏一行」的经典 log-update 钳制问题仍在

为什么 slicePendingTextForHeight 没拦住

slicer 用的是「源文本markdownWidth 下软换行后的视觉行数」作为渲染高度的代理。这对 plain markdown 准确,但对 MarkdownDisplay 的两类 block 系统性低估:

Block 类型 源文本视觉高度 实际渲染高度 差距来源
Fenced code block N 行 + fence 2 行 N 行 + 围框 / paddingLeft 让有效宽度更窄 → 内部行更容易二次软换行 + colorizeCode 加了 line-number 前缀(侵占 ~2 列) MarkdownDisplay.tsx RenderCodeBlockpaddingLeft={CODE_BLOCK_PREFIX_PADDING} + colorizeCode line-number 前缀
Markdown table M 行 2M+3 行(border 顶/底/分隔 + 每行单独包 <Box> TableRenderer.tsxrenderBorderLine
Empty line 1 行 1 行 持平
List item 1 行 1 行 + paddingLeft={indentation+1} 缩窄宽度 → 易二次换行 MarkdownDisplay.tsx RenderListItem

截图里 mermaid 代码块就踩了 codeblock + 行号宽度收缩两条:源文本可能只有 6-8 行,slicer 觉得「在 availableTerminalHeight - 4 内」,但渲染后被收缩成 12-20 行,超出 viewport,log-update 钳制 → scrollback 累积。


两个互补的修复方向

方向 A(推荐,最稳):streaming 期间不走 markdown 富渲染

PrefixedMarkdownMessage / ContinuationMarkdownMessageisPending === true始终PendingTextPreview,无论 hiddenLinesCount 是否 > 0:

if (isPending) {
  // Always plain text during streaming — markdown rendering only happens
  // once content is promoted into <Static>. Prevents code-block / table
  // rendered height from exceeding the slicer's source-text estimate.
  return <PendingTextPreview ... />;
}
return <MarkdownDisplay ... />;

收益:

  • slicer 的 height 测量和实际渲染对得上(都是 plain <Text wrap="wrap">),不会再有「源 8 行 / 渲染 18 行」错位
  • 流式过程中 React reconcile 成本骤降(不再每 chunk 重新跑 markdown parser + syntax highlighter,正好和 doc §4.1「markdown 应按 block/token 缓存」一致)
  • 与 Claude Code 的 streaming 渲染语义对齐(活动区永远是 plain 缓冲,formatted 渲染只在 commit 后发生)

代价:

  • 流式过程中用户暂时看不到表格框线、代码块语法高亮、headers
  • 一旦 prefix 被 findLastSafeSplitPoint 提升进 <Static>(或流结束),格式化版本完整展示

复现路径同 doc 里的 streaming-clear-storm fixture,metric 期望仍是 clearTerminalPairCount === 0

方向 B(fallback / 配合):slicer 加结构感知的 padding 估算

如果坚持流式期间保留 markdown 渲染,需要让 slicer 「预知」渲染开销:

  • 检测 ``` fence → 每对 fence 多预留 2 + (fence 内行数 × ceil(行内容宽 / (markdownWidth - codeBlockPaddingLeft - lineNumberWidth)))
  • 检测连续 | … | 表格段 → 预留 2N + 3
  • 检测 list item → 按 markdownWidth - leadingWhitespace - prefixLen 二次估算软换行

这条路工程上脆而易漏,且 colorizeCode 的 line-number 宽度依赖 lang/setting,建议只作 A 不可行时的兜底。

方向 C(更长线,doc §4.1「markdown 按 block 缓存」):把已闭合的 code block / table / list 段直接进 findLastSafeSplitPoint

我前一条 comment 提的 split point 拓宽,对当前截图也是有效拦截:mermaid 代码块一旦闭合就立即进 <Static>,不再每帧重渲染,pending 只剩 fence 之外的尾部 plain 文本,叠加方向 A 后窄屏 streaming 才真正「无重复输出」。


建议合 A + 我前一条 comment 的 split point 拓宽,作为 Closes #3279 的最后一公里。复现 fixture 直接用截图里的 prompt(让模型生成 mermaid + 表格 + task list + LaTeX 的综合 markdown 例子)配 40×24 终端。

秦奇 and others added 2 commits April 28, 2026 21:10
#3279)

The slicer measured source-text height, but MarkdownDisplay's code
blocks (line-number prefix + paddingLeft-narrowed wrap), tables, and
list items all render taller than their source. On narrow terminals
that gap let pending content exceed the viewport, so Ink's log-update
under-cleared and leaked the topmost row into scrollback every frame.

Render the still-streaming tail through PendingTextPreview unconditionally
so slicer measurement and rendered height stay aligned; once a stable
prefix is promoted to <Static>, the committed message still gets full
markdown formatting.

Generated with AI

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

chiga0 commented Apr 28, 2026

Copy link
Copy Markdown
Collaborator Author

Followed up on the remaining #3279 screenshot issue in commit 992de1c.\n\nThe prior follow-up made pending Markdown plain, but it still trusted the pre-sliced source-height estimate. The screenshots show the remaining failure mode: on very narrow panes, actual Ink wrapping can still exceed the estimate and leak the top rendered row into scrollback repeatedly.\n\nChange made:\n- Wrap the pre-sliced pending assistant/thought tail in MaxSizedBox as a hard actual-render-height guard.\n- Keep exact-fit behavior: no hidden marker when the pending text truly fits.\n- Keep committed messages rich: once content is promoted to <Static>, it still renders through MarkdownDisplay.\n- Added a narrow blockquote/list-style regression that asserts the actual Ink frame is exactly bounded to availableTerminalHeight.\n\nValidation:\n- cd packages/cli && npx vitest run src/ui/components/messages/ConversationMessages.test.tsx src/ui/utils/markdownUtilities.test.ts src/ui/components/messages/ToolMessage.test.tsx src/ui/AppContainer.test.tsx -> 99 tests passed\n- npx eslint packages/cli/src/ui/components/messages/ConversationMessages.tsx packages/cli/src/ui/components/messages/ConversationMessages.test.tsx packages/cli/src/ui/utils/markdownUtilities.ts packages/cli/src/ui/utils/markdownUtilities.test.ts -> passed\n- git diff --check -> passed\n- npm run build -> passed, existing vscode companion warnings only\n- QWEN_TUI_E2E_OUT=/tmp/qwen-tui-narrow-after-hard-bound npm run capture:narrow-streaming-regression -> pass, clearTerminalPairCount=0, clearScreenCodeCount=0, framesCaptured=93, finalDoneCount=1

@chiga0

chiga0 commented Apr 28, 2026

Copy link
Copy Markdown
Collaborator Author

The latest screenshots exposed one more #3279 failure mode: even with a hard MaxSizedBox, the pending tail was still allowed to use the full measured dynamic budget. When a Static prefix like "Here's another Mermaid..." remains visible above it, the combined visible prefix + pending marker/fence rows can still scroll and leak previous frames into scrollback.\n\nFollow-up commit: 314cf2fa7 fix(cli): cap pending markdown live viewport\n\nChanges:\n- Cap streaming pending Markdown to a small live viewport: max 12 rows.\n- Keep small terminals/exact-fit behavior unchanged when the measured budget is <= 12 rows.\n- Keep final committed Markdown rich/full through MarkdownDisplay; only the still-streaming tail is capped/plain.\n- Added a component regression for tall Mermaid pending content: actual frame <= 12 rows.\n- Added capture:narrow-markdown-regression, which streams a Mermaid block at narrow width and fails if scrollback contains repeated hidden-marker lines or raw ```mermaidfence rows.\n\nValidation:\n-cd packages/cli && npx vitest run src/ui/components/messages/ConversationMessages.test.tsx src/ui/utils/markdownUtilities.test.ts src/ui/components/messages/ToolMessage.test.tsx src/ui/AppContainer.test.tsx` -> 100 tests passed\n- `npx eslint packages/cli/src/ui/components/messages/ConversationMessages.tsx packages/cli/src/ui/components/messages/ConversationMessages.test.tsx packages/cli/src/ui/utils/markdownUtilities.ts packages/cli/src/ui/utils/markdownUtilities.test.ts integration-tests/terminal-capture/streaming-clear-storm.ts` -> passed\n- `git diff --check` -> passed\n- `npm run build` -> passed, existing vscode companion warnings only\n- `cd integration-tests/terminal-capture && QWEN_TUI_E2E_OUT=/tmp/qwen-tui-narrow-markdown-after-cap npm run capture:narrow-markdown-regression` -> pass: `hiddenMarkerCount=0`, `rawMermaidFenceCount=0`, `clearTerminalPairCount=0`, `clearScreenCodeCount=0`, `framesCaptured=93`, `finalDoneCount=1`

@chiga0

chiga0 commented Apr 28, 2026

Copy link
Copy Markdown
Collaborator Author

Follow-up after comparing with the local Claude Code source and the latest narrow Markdown screenshots.

What changed:

  • pending assistant/thought live preview no longer emits synthetic ... first N lines hidden ... rows;
  • pending live preview suppresses raw Markdown fence delimiter rows such as ```mermaid and ``` while preserving code content;
  • committed/final messages still render through full MarkdownDisplay, so transcript fidelity is unchanged;
  • MaxSizedBox now supports keeping overflow tracking while hiding the marker row for live pending output;
  • terminal-capture now scans every captured live frame's xterm scrollback for hidden markers and raw Mermaid fences, not just the final settled screen;
  • the design doc now records the Claude Code comparison and why copying Claude's modified Ink fork wholesale is not the maintainable path for this PR.

Latest fixed-branch validation:

{
  "streamPayload": "markdown",
  "framesCaptured": 93,
  "clearTerminalPairCount": 0,
  "clearScreenCodeCount": 0,
  "finalDoneCount": 1,
  "hiddenMarkerCount": 0,
  "rawMermaidFenceCount": 0,
  "maxFrameHiddenMarkerCount": 0,
  "maxFrameRawMermaidFenceCount": 0,
  "pass": true
}

Verification run:

  • cd packages/cli && npx vitest run src/ui/utils/markdownUtilities.test.ts src/ui/components/messages/ConversationMessages.test.tsx src/ui/components/messages/ToolMessage.test.tsx src/ui/AppContainer.test.tsx
  • cd packages/cli && npx eslint src/ui/components/messages/ConversationMessages.tsx src/ui/components/messages/ConversationMessages.test.tsx src/ui/components/shared/MaxSizedBox.tsx
  • cd integration-tests/terminal-capture && npx eslint streaming-clear-storm.ts
  • git diff --check
  • npm run build && npm run bundle
  • cd integration-tests/terminal-capture && QWEN_TUI_E2E_OUT=/tmp/qwen-tui-narrow-markdown-no-marker npm run capture:narrow-markdown-regression

@chiga0 chiga0 changed the title fix(cli): harden TUI flicker and narrow output handling fix(cli): harden TUI flicker and streaming output stability Apr 29, 2026
@chiga0 chiga0 force-pushed the codex/tui-streaming-clear-storm branch from cbb46bf to 2a9882b Compare April 29, 2026 02:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

总是显示一下错误

1 participant