Skip to content

feat(cli): per-turn /diff with interactive dialog#4277

Open
BZ-D wants to merge 9 commits into
mainfrom
feat/diff-per-turn
Open

feat(cli): per-turn /diff with interactive dialog#4277
BZ-D wants to merge 9 commits into
mainfrom
feat/diff-per-turn

Conversation

@BZ-D
Copy link
Copy Markdown
Collaborator

@BZ-D BZ-D commented May 18, 2026

Summary

Closes #4272.

Reworks /diff so users can drill into the changes from each conversation turn — not just the aggregate working-tree-vs-HEAD summary that the command used to print.

In interactive mode /diff now opens a dialog with:

  • A source switcher: Current (git diff HEAD) plus one entry per past user turn (T1 · T2 · …, most recent first)
  • ↑/↓ to pick a file, Enter for hunk-level detail, ←/→ to switch source, Esc to back out / close
  • Paginated file list (8 visible), color-coded +N -M, and tags for new / deleted / untracked / binary

Non-interactive and ACP paths keep the existing plain-text summary unchanged so pipes, logs, and remote transports are unaffected.

How it works

Per-turn data comes from the existing FileHistoryService snapshots (one per user query, see client.ts:1278). New method FileHistoryService.getTurnDiff(promptId) walks every file tracked by the target turn's snapshot and compares it against the next snapshot's backup — or the live worktree for the most recent turn — using diff.structuredPatch. Files the snapshotter failed to capture are skipped instead of rendered against a stale predecessor.

This means turns that survived chat compression still show their diff, since snapshots live independently of the API history (capped at 100, same as /rewind).

Caveat (already true for /rewind): only files touched via edit / write_file are snapshotted, so changes from run_shell_command won't appear in per-turn entries. The Current source still covers them via git diff.

Files changed

Core (2)

  • packages/core/src/services/fileHistoryService.ts — new TurnDiff / TurnFileDiff types and getTurnDiff() (~190 lines)
  • packages/core/src/services/fileHistoryService.test.ts — 5 new unit tests

CLI (8 changed + 3 new)

  • packages/cli/src/ui/components/DiffDialog.tsx (new) — the dialog
  • packages/cli/src/ui/hooks/useTurnDiffs.ts (new) — bridges HistoryItemUser.promptId to getTurnDiff
  • packages/cli/src/ui/hooks/useDiffData.ts (new) — Current source via existing fetchGitDiff / fetchGitDiffHunks
  • packages/cli/src/ui/commands/diffCommand.ts — interactive path now returns { type: 'dialog', dialog: 'diff' }
  • packages/cli/src/ui/commands/types.ts — dialog union gains 'diff'
  • packages/cli/src/ui/hooks/slashCommandProcessor.ts — dispatcher + SlashCommandProcessorActions add openDiffDialog
  • packages/cli/src/ui/AppContainer.tsxisDiffDialogOpen state, open/close handlers, included in dialogsVisible
  • packages/cli/src/ui/contexts/{UIStateContext,UIActionsContext}.tsx — surface the new state/actions
  • packages/cli/src/ui/components/DialogManager.tsx — mount <DiffDialog>
  • packages/cli/src/ui/commands/diffCommand.test.ts — re-aligned interactive tests with the new contract

Test plan

  • tsc --noEmit clean in both packages/core and packages/cli
  • eslint clean on changed files
  • vitest runfileHistoryService.test.ts 35/35, diffCommand.test.ts 25/25, slashCommandProcessor.test.ts 46/46, AppContainer.test.tsx + RewindSelector.test.tsx 78/78
  • Manual smoke test in TUI: open /diff, page through turns, view hunks, Esc out cleanly
  • Manual smoke test non-interactive: qwen --no-interactive /diff still prints the plain-text summary

Design notes

  • Source order is most-recent-first to match how users mentally reach for /diff ("what just changed").
  • IDE mode is not blocked here — unlike /rewind, this dialog only reads from physical snapshots and git diff, so the IDE-injected user Content that breaks rewind's API-history mapping doesn't affect it.
  • First turn: when there is no predecessor snapshot, the dialog's "before" is the very first snapshot's recorded backup (which captured the pre-edit state via trackEdit), so T1's diff is still meaningful.
  • Empty turns: filtered out — no point showing "T7" if nothing changed.

`/diff` now opens an interactive dialog in TUI mode with:
- Current (working tree vs HEAD) plus one entry per past user turn
- ←/→ to switch source, ↑/↓ to select a file, Enter for hunks, Esc to close
- File list paginates at 8 entries, with new/deleted/untracked/binary tags

Per-turn diffs are computed by FileHistoryService.getTurnDiff(promptId),
which compares the snapshot at the start of that turn against the next
snapshot (or the live worktree for the most recent turn). Files the
snapshotter failed to capture are skipped rather than rendered against a
stale predecessor.

Non-interactive and ACP modes keep the existing plain-text summary so
pipes, logs, and remote transports are unchanged.
@github-actions
Copy link
Copy Markdown
Contributor

📋 Review Summary

This PR introduces a per-turn diff dialog for the interactive CLI, allowing users to inspect file changes from each conversation turn rather than only viewing the aggregate working-tree-vs-HEAD summary. The implementation is well-structured, with clean separation between core services and UI components, and includes comprehensive test coverage. The non-interactive and ACP paths remain unchanged, preserving backward compatibility for pipe consumers and remote transports.

🔍 General Feedback

  • Architecture: Clean separation of concerns — core FileHistoryService handles snapshot management and diff computation, while React hooks (useTurnDiffs, useDiffData) bridge data to the DiffDialog component.
  • Backward compatibility: Non-interactive mode continues to return plain-text summaries; only interactive mode opens the dialog. This is the correct design decision.
  • Test coverage: The getTurnDiff tests cover key scenarios (disabled service, missing snapshots, turn-to-turn diffs, latest-turn vs worktree, new files, unchanged files).
  • Type safety: New types (TurnDiff, TurnFileDiff, UnifiedFile, Source) are well-defined and used consistently across the codebase.
  • Performance considerations: Pagination (8 visible files), lazy loading of turn diffs, and cancellation tokens in useEffect cleanup are all correctly implemented.

🎯 Specific Feedback

🟡 High

  • File: packages/core/src/services/fileHistoryService.ts:677 — The computeTurnFileDiff method reuses backup content comparison logic that could be extracted into a helper. The method is ~140 lines and handles multiple responsibilities (backup resolution, content loading, diff computation). Consider splitting into smaller units: resolveBackupContent, compareFileContents, computeHunkStats.

  • File: packages/cli/src/ui/components/DiffDialog.tsx:558 — The DiffDialog component is ~560 lines with nested helper components (SourceSwitcher, FileList, FileRow, FileDetail). While this pattern is common in Ink apps, extracting hunksToUnifiedDiff and emptyMessage to a separate utility module would improve testability and reduce component complexity.

🟢 Medium

  • File: packages/cli/src/ui/hooks/useTurnDiffs.ts:74 — The previewOfUserItem function uses a magic constant 60 for PREVIEW_MAX. This should be named (e.g., TURN_PREVIEW_MAX_CHARS) and documented with the reasoning behind the limit (terminal width constraints vs. readability).

  • File: packages/cli/src/ui/commands/diffCommand.ts:38 — The interactive mode guard returns { type: 'dialog', dialog: 'diff' } early, but the cwd validation that follows (lines 44-52) only applies to non-interactive mode. This is correct behavior, but adding a comment explaining why the dialog path doesn't need cwd validation (the dialog's hooks handle missing cwd via empty state) would help future maintainers. The test file already documents this rationale, but the implementation should mirror it.

  • File: packages/cli/src/ui/components/DiffDialog.tsx:133 — The useEffect that keeps sourceIndex in bounds (lines 127-132) has a dependency on sources which changes when turns load. This could cause an unnecessary re-render loop if sourceIndex is already valid. Consider checking sourceIndex >= sources.length before calling setSourceIndex.

🔵 Low

  • File: packages/core/src/services/fileHistoryService.ts:601 — The comment explaining the turn diff semantics ("before" = snapshot's backups, "after" = next snapshot or live worktree) is excellent. Consider adding a similar high-level comment at the top of getTurnDiff explaining what happens when chat compression removes turns but snapshots survive (the PR body mentions this, but the code comment would help future debuggers).

  • File: packages/cli/src/ui/hooks/useDiffData.ts:28 — The comment about swallowing failures and surfacing them as empty result is correct, but consider adding a debugLogger call (matching the pattern in fileHistoryService.ts) so developers can diagnose why the dialog shows empty state in unexpected cases.

  • File: packages/cli/src/ui/components/DiffDialog.tsx:211 — The key key={${s.kind}:${i}} in SourceSwitcher uses the array index, which is acceptable here since sources are stable once loaded. However, using entry.diff.promptId for turn sources would be more robust if the source list ever becomes dynamic.

  • File: packages/cli/src/ui/commands/diffCommand.test.ts:313 — The test comment "Dialog ownership: the data fetch happens inside the dialog's hooks, not in the command" is a critical design contract. Consider adding a matching comment in the diffCommand.ts implementation to reinforce this separation of concerns.

✅ Highlights

  • Excellent test design: The getTurnDiff tests mirror the exact sequence client.ts follows (makeSnapshottrackEdit → mutate → makeSnapshot), ensuring the service API is tested as it's used in production.
  • Thoughtful UX: Source order (most recent first) matches user mental model for "what just changed," and the source switcher uses ←/→ navigation consistent with other dialogs.
  • Graceful degradation: When file checkpointing is disabled, the dialog shows an informative empty state rather than crashing or showing confusing errors.
  • Deduplication awareness: The cleanupOrphanedBackups method correctly handles content-deduplicated backups across snapshots, preventing premature deletion of shared backup files.
  • ANSI safety: The sanitizeFilenameForDisplay function escapes both ANSI sequences and standalone control bytes, protecting against hostile filenames in non-interactive mode.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 18, 2026

Code Coverage Summary

Package Lines Statements Functions Branches
CLI 76.9% 76.9% 79.5% 79.91%
Core 79.45% 79.45% 82.09% 82.86%
CLI Package - Full Text Report
-------------------|---------|----------|---------|---------|-------------------
File               | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
-------------------|---------|----------|---------|---------|-------------------
All files          |    76.9 |    79.91 |    79.5 |    76.9 |                   
 src               |    75.9 |    69.11 |   80.55 |    75.9 |                   
  gemini.tsx       |   68.53 |     66.4 |   76.47 |   68.53 | ...29,946-949,957 
  ...ractiveCli.ts |   80.23 |     68.3 |   78.57 |   80.23 | ...1054,1092,1195 
  ...liCommands.ts |   74.51 |    73.17 |     100 |   74.51 | ...41-265,290,391 
  ...ActiveAuth.ts |     100 |     87.5 |     100 |     100 | 66-80             
 ...cp-integration |   61.97 |    65.24 |   78.12 |   61.97 |                   
  acpAgent.ts      |   63.32 |    65.35 |   83.05 |   63.32 | ...2112,2126-2134 
  authMethods.ts   |   12.19 |      100 |       0 |   12.19 | 11-31,34-38,41-50 
  errorCodes.ts    |       0 |        0 |       0 |       0 | 1-22              
  ...DirContext.ts |     100 |      100 |     100 |     100 |                   
 ...ration/service |   68.65 |    83.33 |   66.66 |   68.65 |                   
  filesystem.ts    |   68.65 |    83.33 |   66.66 |   68.65 | ...32,77-94,97-98 
 ...ration/session |   77.07 |    72.32 |   86.25 |   77.07 |                   
  ...ryReplayer.ts |   67.34 |     75.6 |   81.81 |   67.34 | ...54-269,282-283 
  Session.ts       |   76.45 |    71.11 |   88.46 |   76.45 | ...2566,2572-2575 
  ...entTracker.ts |   90.85 |    84.84 |      90 |   90.85 | ...35,199,251-260 
  index.ts         |       0 |        0 |       0 |       0 | 1-40              
  ...ssionUtils.ts |   84.21 |    77.77 |     100 |   84.21 | ...37-153,209-211 
  types.ts         |       0 |        0 |       0 |       0 | 1                 
 ...ssion/emitters |   96.01 |    90.75 |    92.3 |   96.01 |                   
  BaseEmitter.ts   |   76.92 |    66.66 |      80 |   76.92 | 23-24,39-40,55-56 
  ...ageEmitter.ts |     100 |    89.47 |     100 |     100 | 109,111           
  PlanEmitter.ts   |     100 |      100 |     100 |     100 |                   
  ...allEmitter.ts |   98.06 |     92.3 |     100 |   98.06 | 227-228,327,335   
  index.ts         |       0 |        0 |       0 |       0 | 1-10              
 ...ession/rewrite |   90.36 |    87.83 |   94.11 |   90.36 |                   
  LlmRewriter.ts   |      81 |       84 |     100 |      81 | ...,88-89,155-159 
  ...Middleware.ts |   95.83 |    85.71 |     100 |   95.83 | 119,127-129       
  TurnBuffer.ts    |     100 |      100 |     100 |     100 |                   
  config.ts        |     100 |      100 |     100 |     100 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  types.ts         |       0 |        0 |       0 |       0 | 1                 
 src/auth          |    97.7 |    94.81 |   95.45 |    97.7 |                   
  allProviders.ts  |     100 |      100 |     100 |     100 |                   
  ...iderConfig.ts |    97.6 |    95.04 |     100 |    97.6 | ...61,411,433-434 
  types.ts         |       0 |        0 |       0 |       0 | 1                 
 src/auth/install  |   98.57 |    88.88 |     100 |   98.57 |                   
  ...nstallPlan.ts |   98.57 |    88.88 |     100 |   98.57 | 80,93             
 ...viders/alibaba |   96.96 |    66.66 |   66.66 |   96.96 |                   
  ...baStandard.ts |     100 |      100 |     100 |     100 |                   
  codingPlan.ts    |   93.67 |    66.66 |   66.66 |   93.67 | 83,87-89,94       
  tokenPlan.ts     |     100 |      100 |     100 |     100 |                   
 ...oviders/custom |     100 |      100 |     100 |     100 |                   
  ...omProvider.ts |     100 |      100 |     100 |     100 |                   
 ...roviders/oauth |    91.5 |    77.03 |   97.05 |    91.5 |                   
  openrouter.ts    |   84.37 |    33.33 |     100 |   84.37 | 43-48             
  ...outerOAuth.ts |    91.9 |    79.06 |   96.87 |    91.9 | ...53-655,699-701 
 ...ers/thirdParty |     100 |      100 |     100 |     100 |                   
  deepseek.ts      |     100 |      100 |     100 |     100 |                   
  idealab.ts       |     100 |      100 |     100 |     100 |                   
  minimax.ts       |     100 |      100 |     100 |     100 |                   
  modelscope.ts    |     100 |      100 |     100 |     100 |                   
  zai.ts           |     100 |      100 |     100 |     100 |                   
 src/commands      |   47.93 |    85.71 |   43.47 |   47.93 |                   
  auth.ts          |     100 |    83.33 |     100 |     100 | 11,14             
  channel.ts       |   56.66 |      100 |       0 |   56.66 | 15-19,27-34       
  extensions.tsx   |   96.55 |      100 |      50 |   96.55 | 37                
  hooks.tsx        |   66.66 |      100 |       0 |   66.66 | 20-24             
  mcp.ts           |   94.73 |      100 |      50 |   94.73 | 28                
  review.ts        |   51.85 |      100 |       0 |   51.85 | 24-35,38          
  serve.ts         |    7.74 |      100 |       0 |    7.74 | ...51-147,149-230 
 ...mmands/channel |   39.25 |    79.45 |      50 |   39.25 |                   
  ...l-registry.ts |    8.57 |      100 |       0 |    8.57 | 6-21,24-42        
  config-utils.ts  |      92 |      100 |   66.66 |      92 | 21-26             
  configure.ts     |    14.7 |      100 |       0 |    14.7 | 18-21,23-84       
  pairing.ts       |   26.31 |      100 |       0 |   26.31 | ...30,40-50,52-65 
  pidfile.ts       |   96.34 |    86.95 |     100 |   96.34 | 49,59,91          
  start.ts         |   30.98 |       52 |   69.23 |   30.98 | ...72-475,484-486 
  status.ts        |   17.85 |      100 |       0 |   17.85 | 15-26,32-76       
  stop.ts          |      20 |      100 |       0 |      20 | 14-48             
 ...nds/extensions |    84.5 |    88.95 |   81.81 |    84.5 |                   
  consent.ts       |   71.65 |    89.28 |   42.85 |   71.65 | ...85-141,156-162 
  disable.ts       |     100 |      100 |     100 |     100 |                   
  enable.ts        |     100 |      100 |     100 |     100 |                   
  install.ts       |    75.6 |    66.66 |   66.66 |    75.6 | ...39-142,145-153 
  link.ts          |     100 |      100 |     100 |     100 |                   
  list.ts          |     100 |      100 |     100 |     100 |                   
  new.ts           |     100 |      100 |     100 |     100 |                   
  settings.ts      |   99.15 |      100 |   83.33 |   99.15 | 151               
  uninstall.ts     |    37.5 |      100 |   33.33 |    37.5 | 23-45,57-64,67-70 
  update.ts        |   96.32 |      100 |     100 |   96.32 | 101-105           
  utils.ts         |   60.24 |    28.57 |     100 |   60.24 | ...81,83-87,89-93 
 ...les/mcp-server |       0 |        0 |       0 |       0 |                   
  example.ts       |       0 |        0 |       0 |       0 | 1-60              
 src/commands/mcp  |   92.29 |    86.08 |   88.88 |   92.29 |                   
  add.ts           |     100 |    98.03 |     100 |     100 | 293               
  list.ts          |   91.22 |    80.76 |      80 |   91.22 | ...19-121,146-147 
  reconnect.ts     |   76.72 |    71.42 |   85.71 |   76.72 | 35-48,153-175     
  remove.ts        |     100 |       80 |     100 |     100 | 21-25             
 ...ommands/review |   11.57 |      100 |       0 |   11.57 |                   
  cleanup.ts       |   17.94 |      100 |       0 |   17.94 | ...01-106,108-109 
  deterministic.ts |   13.75 |      100 |       0 |   13.75 | ...22-738,740-741 
  fetch-pr.ts      |   11.36 |      100 |       0 |   11.36 | ...80-201,203-204 
  load-rules.ts    |   11.32 |      100 |       0 |   11.32 | ...41-153,155-156 
  pr-context.ts    |    6.22 |      100 |       0 |    6.22 | ...97-312,314-315 
  presubmit.ts     |    9.35 |      100 |       0 |    9.35 | ...62-287,289-290 
 ...nds/review/lib |      30 |      100 |       0 |      30 |                   
  gh.ts            |   22.58 |      100 |       0 |   22.58 | ...49,53-54,62-69 
  git.ts           |   22.72 |      100 |       0 |   22.72 | 15-18,29-39,43-44 
  paths.ts         |   52.94 |      100 |       0 |   52.94 | ...26,37-38,42-43 
 src/config        |    92.8 |    85.18 |   88.09 |    92.8 |                   
  auth.ts          |   86.98 |    80.32 |     100 |   86.98 | ...26-227,243-244 
  config.ts        |   88.31 |    84.87 |      80 |   88.31 | ...1841,1843-1851 
  keyBindings.ts   |   96.55 |       50 |     100 |   96.55 | 193-196           
  ...idersScope.ts |      92 |       90 |     100 |      92 | 11-12             
  sandboxConfig.ts |   61.64 |    71.87 |   66.66 |   61.64 | ...54-68,73,77-89 
  settings.ts      |   85.76 |    87.25 |   89.18 |   85.76 | ...1148,1153-1156 
  ...ingsSchema.ts |     100 |      100 |     100 |     100 |                   
  ...tedFolders.ts |   96.22 |       94 |     100 |   96.22 | ...88-190,205-206 
 ...nfig/migration |   94.89 |    78.94 |   83.33 |   94.89 |                   
  index.ts         |   94.87 |    88.88 |     100 |   94.87 | 91-92             
  scheduler.ts     |   96.55 |    77.77 |     100 |   96.55 | 19-20             
  types.ts         |       0 |        0 |       0 |       0 | 1                 
 ...ation/versions |   94.74 |       96 |     100 |   94.74 |                   
  ...-v2-shared.ts |     100 |      100 |     100 |     100 |                   
  v1-to-v2.ts      |   81.75 |    90.19 |     100 |   81.75 | ...28-229,231-247 
  v2-to-v3.ts      |     100 |      100 |     100 |     100 |                   
  v3-to-v4.ts      |     100 |      100 |     100 |     100 |                   
 src/core          |     100 |      100 |     100 |     100 |                   
  auth.ts          |     100 |      100 |     100 |     100 |                   
  initializer.ts   |     100 |      100 |     100 |     100 |                   
  theme.ts         |     100 |      100 |     100 |     100 |                   
 src/dualOutput    |   63.09 |    64.51 |   55.55 |   63.09 |                   
  ...tputBridge.ts |   62.94 |    65.51 |   56.25 |   62.94 | ...22-323,331-334 
  ...utContext.tsx |     100 |      100 |     100 |     100 |                   
  index.ts         |       0 |        0 |       0 |       0 | 1-8               
 src/export        |       0 |        0 |       0 |       0 |                   
  index.ts         |       0 |        0 |       0 |       0 | 1-7               
 src/generated     |     100 |      100 |     100 |     100 |                   
  git-commit.ts    |     100 |      100 |     100 |     100 |                   
 src/i18n          |   81.47 |    75.94 |   65.71 |   81.47 |                   
  index.ts         |   63.68 |    69.56 |   53.84 |   63.68 | ...70-271,281-286 
  languages.ts     |   96.92 |    86.66 |     100 |   96.92 | 134-135,167,184   
  ...nslateKeys.ts |     100 |      100 |     100 |     100 |                   
  ...lationDict.ts |   93.33 |    66.66 |     100 |   93.33 | 15                
 src/i18n/locales  |     100 |      100 |     100 |     100 |                   
  ca.js            |     100 |      100 |     100 |     100 |                   
  de.js            |     100 |      100 |     100 |     100 |                   
  en.js            |     100 |      100 |     100 |     100 |                   
  fr.js            |     100 |      100 |     100 |     100 |                   
  ja.js            |     100 |      100 |     100 |     100 |                   
  pt.js            |     100 |      100 |     100 |     100 |                   
  ru.js            |     100 |      100 |     100 |     100 |                   
  zh-TW.js         |     100 |      100 |     100 |     100 |                   
  zh.js            |     100 |      100 |     100 |     100 |                   
 ...nonInteractive |   72.57 |    71.12 |   74.07 |   72.57 |                   
  session.ts       |   76.64 |     69.4 |   85.71 |   76.64 | ...23-824,833-843 
  types.ts         |    42.5 |      100 |   33.33 |    42.5 | ...80-581,584-585 
 ...active/control |   77.04 |    88.23 |      80 |   77.04 |                   
  ...rolContext.ts |    7.14 |        0 |       0 |    7.14 | 49-84             
  ...Dispatcher.ts |   91.66 |    91.83 |   88.88 |   91.66 | ...54-372,388,391 
  ...rolService.ts |       8 |        0 |       0 |       8 | 46-179            
 ...ol/controllers |    7.04 |       80 |   13.33 |    7.04 |                   
  ...Controller.ts |   19.32 |      100 |      60 |   19.32 | 81-118,127-210    
  ...Controller.ts |       0 |        0 |       0 |       0 | 1-56              
  ...Controller.ts |    3.96 |      100 |   11.11 |    3.96 | ...61-379,389-494 
  ...Controller.ts |   14.06 |      100 |       0 |   14.06 | ...82-117,130-133 
  ...Controller.ts |    5.21 |      100 |       0 |    5.21 | ...21-433,442-471 
 .../control/types |       0 |        0 |       0 |       0 |                   
  serviceAPIs.ts   |       0 |        0 |       0 |       0 | 1                 
 ...Interactive/io |   97.98 |     93.7 |   95.18 |   97.98 |                   
  ...putAdapter.ts |   97.89 |    92.82 |   98.07 |   97.89 | ...1303,1398-1399 
  ...putAdapter.ts |      96 |     90.9 |   85.71 |      96 | 51-52             
  ...nputReader.ts |     100 |    94.73 |     100 |     100 | 67                
  ...putAdapter.ts |   98.28 |      100 |      90 |   98.28 | 81-82,122-123     
  index.ts         |     100 |      100 |     100 |     100 |                   
 src/patches       |       0 |        0 |       0 |       0 |                   
  is-in-ci.ts      |       0 |        0 |       0 |       0 | 1-17              
 src/remoteInput   |   86.98 |       75 |   85.71 |   86.98 |                   
  ...utContext.tsx |     100 |      100 |     100 |     100 |                   
  ...putWatcher.ts |   88.12 |    76.08 |   91.66 |   88.12 | ...21-222,233-236 
  index.ts         |       0 |        0 |       0 |       0 | 1-8               
 src/serve         |    79.3 |     78.8 |   92.85 |    79.3 |                   
  auth.ts          |   88.49 |    88.63 |     100 |   88.49 | ...49-150,153-155 
  capabilities.ts  |     100 |     90.9 |     100 |     100 | 264               
  ...usProvider.ts |   67.01 |    51.42 |     100 |   67.01 | ...40-245,278-286 
  debugMode.ts     |     100 |      100 |     100 |     100 |                   
  demo.ts          |     100 |      100 |     100 |     100 |                   
  envSnapshot.ts   |    92.3 |       84 |     100 |    92.3 | 108-111,170-177   
  eventBus.ts      |     100 |      100 |     100 |     100 |                   
  httpAcpBridge.ts |   79.62 |    78.84 |   96.38 |   79.62 | ...4246,4277-4318 
  ...oryChannel.ts |     100 |      100 |     100 |     100 |                   
  index.ts         |       0 |        0 |       0 |       0 | 1-106             
  loopbackBinds.ts |     100 |      100 |     100 |     100 |                   
  runQwenServe.ts  |   73.98 |    87.83 |   55.55 |   73.98 | ...94-710,735-737 
  server.ts        |   86.18 |    82.94 |   90.62 |   86.18 | ...2478,2543-2552 
  status.ts        |     100 |      100 |     100 |     100 |                   
  types.ts         |     100 |      100 |     100 |     100 |                   
  ...paceAgents.ts |   64.87 |    70.45 |    90.9 |   64.87 | ...1306,1316-1326 
  ...paceMemory.ts |   87.13 |    78.46 |     100 |   87.13 | ...54-361,421-428 
 src/serve/auth    |   86.54 |    78.75 |   93.75 |   86.54 |                   
  deviceFlow.ts    |   96.33 |    79.51 |    97.5 |   96.33 | ...1526,1630,1700 
  ...owProvider.ts |   45.23 |    74.07 |      75 |   45.23 | ...90-359,375,379 
 src/serve/fs      |   84.85 |    79.75 |     100 |   84.85 |                   
  audit.ts         |     100 |    96.15 |     100 |     100 | 201               
  errors.ts        |     100 |      100 |     100 |     100 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  paths.ts         |   77.82 |    77.08 |     100 |   77.82 | ...64,493-497,510 
  policy.ts        |   90.32 |    89.18 |     100 |   90.32 | 142-150           
  ...FileSystem.ts |   83.55 |    76.22 |     100 |   83.55 | ...1859,1886-1887 
 src/serve/routes  |   89.41 |       70 |     100 |   89.41 |                   
  ...ceFileRead.ts |   94.41 |    76.92 |     100 |   94.41 | ...28-329,390-392 
  ...eFileWrite.ts |    82.1 |    60.52 |     100 |    82.1 | ...42-244,247-249 
 src/services      |   91.67 |    91.21 |   97.56 |   91.67 |                   
  ...mandLoader.ts |     100 |    93.75 |     100 |     100 | 93                
  ...killLoader.ts |     100 |    96.15 |     100 |     100 | 47                
  ...andService.ts |    98.7 |      100 |     100 |    98.7 | 107               
  ...mandLoader.ts |   86.83 |    83.87 |     100 |   86.83 | ...30-335,340-345 
  ...omptLoader.ts |   75.84 |    80.64 |   83.33 |   75.84 | ...10-211,277-278 
  ...mandLoader.ts |     100 |      100 |     100 |     100 |                   
  ...nd-factory.ts |   91.42 |    91.66 |     100 |   91.42 | 128,137-144       
  ...ation-tool.ts |     100 |    95.45 |     100 |     100 | 125               
  ...ndMetadata.ts |   98.21 |    96.66 |     100 |   98.21 | 83,87             
  commandUtils.ts  |      96 |     90.9 |     100 |      96 | 48                
  ...and-parser.ts |   90.69 |    85.71 |     100 |   90.69 | 63-66             
  ...ionService.ts |     100 |      100 |     100 |     100 |                   
  types.ts         |     100 |      100 |     100 |     100 |                   
 ...ght/generators |    85.9 |    85.61 |   90.47 |    85.9 |                   
  DataProcessor.ts |   85.63 |     85.6 |   92.85 |   85.63 | ...1122,1126-1133 
  ...tGenerator.ts |   98.21 |    85.71 |     100 |   98.21 | 46                
  ...teRenderer.ts |   45.45 |      100 |       0 |   45.45 | 13-51             
 .../insight/types |       0 |       50 |      50 |       0 |                   
  ...sightTypes.ts |       0 |        0 |       0 |       0 |                   
  ...sightTypes.ts |       0 |        0 |       0 |       0 | 1                 
 ...mpt-processors |   97.27 |    94.04 |     100 |   97.27 |                   
  ...tProcessor.ts |     100 |      100 |     100 |     100 |                   
  ...eProcessor.ts |   94.52 |    84.21 |     100 |   94.52 | 46-47,93-94       
  ...tionParser.ts |     100 |      100 |     100 |     100 |                   
  ...lProcessor.ts |   97.41 |    95.65 |     100 |   97.41 | 95-98             
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/services/tips |   97.35 |    83.07 |     100 |   97.35 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  tipHistory.ts    |   92.45 |       70 |     100 |   92.45 | ...22,144,151,160 
  tipRegistry.ts   |     100 |    95.23 |     100 |     100 | 33                
  tipScheduler.ts  |     100 |    91.66 |     100 |     100 | 55                
 src/test-utils    |   93.75 |    83.33 |      80 |   93.75 |                   
  ...omMatchers.ts |   69.69 |       50 |      50 |   69.69 | 32-35,37-39,45-47 
  ...andContext.ts |     100 |      100 |     100 |     100 |                   
  render.tsx       |     100 |      100 |     100 |     100 |                   
 src/ui            |    65.2 |    73.08 |   60.34 |    65.2 |                   
  App.tsx          |     100 |      100 |     100 |     100 |                   
  AppContainer.tsx |   63.28 |    64.68 |      50 |   63.28 | ...3159,3163-3167 
  ...tionNudge.tsx |    9.58 |      100 |       0 |    9.58 | 24-94             
  ...ackDialog.tsx |   29.23 |      100 |       0 |   29.23 | 25-75             
  ...tionNudge.tsx |    7.69 |      100 |       0 |    7.69 | 25-103            
  colors.ts        |      60 |      100 |   35.29 |      60 | ...52,54-55,60-61 
  constants.ts     |     100 |      100 |     100 |     100 |                   
  keyMatchers.ts   |   95.91 |    97.05 |     100 |   95.91 | 25-26             
  ...tic-colors.ts |     100 |      100 |     100 |     100 |                   
  ...inePresets.ts |   98.17 |    88.88 |     100 |   98.17 | ...12,239,387-389 
  textConstants.ts |     100 |      100 |     100 |     100 |                   
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/ui/auth       |   55.06 |    51.13 |   35.48 |   55.06 |                   
  AuthDialog.tsx   |   64.26 |    44.44 |   16.66 |   64.26 | ...59,366-388,392 
  ...nProgress.tsx |       0 |        0 |       0 |       0 | 1-64              
  ...etupSteps.tsx |    39.5 |       32 |   38.46 |    39.5 | ...69,472,478,481 
  useAuth.ts       |   76.63 |    68.29 |     100 |   76.63 | ...48,493-499,560 
  ...rSetupFlow.ts |   44.61 |    33.33 |      50 |   44.61 | ...57-378,395-438 
 src/ui/commands   |   73.38 |    81.28 |   81.48 |   73.38 |                   
  aboutCommand.ts  |     100 |      100 |     100 |     100 |                   
  agentsCommand.ts |   83.78 |      100 |      60 |   83.78 | 30-32,42-44       
  ...odeCommand.ts |     100 |      100 |     100 |     100 |                   
  arenaCommand.ts  |   62.81 |    58.73 |   65.21 |   62.81 | ...91-596,681-689 
  authCommand.ts   |     100 |      100 |     100 |     100 |                   
  branchCommand.ts |     100 |      100 |     100 |     100 |                   
  btwCommand.ts    |   95.59 |    71.42 |     100 |   95.59 | 72,154-159        
  bugCommand.ts    |   81.13 |    71.42 |     100 |   81.13 | 60-69             
  clearCommand.ts  |      92 |    76.47 |     100 |      92 | 43-44,72-73,91-92 
  ...essCommand.ts |    64.7 |       50 |      75 |    64.7 | ...48-149,163-166 
  ...extCommand.ts |   34.78 |    22.22 |   45.45 |   34.78 | ...86-521,532-533 
  copyCommand.ts   |   98.28 |    94.89 |     100 |   98.28 | ...80,280,321,327 
  deleteCommand.ts |     100 |      100 |     100 |     100 |                   
  diffCommand.ts   |     100 |     87.5 |     100 |     100 | ...61,224-225,238 
  ...ryCommand.tsx |   68.09 |    77.77 |   77.77 |   68.09 | ...56-261,315-323 
  docsCommand.ts   |     100 |    88.88 |     100 |     100 | 25                
  doctorCommand.ts |   95.06 |    88.28 |     100 |   95.06 | ...92-293,320-321 
  dreamCommand.ts  |      75 |    66.66 |   66.66 |      75 | 22-27,44-47       
  editorCommand.ts |     100 |      100 |     100 |     100 |                   
  exportCommand.ts |   98.25 |    91.02 |     100 |   98.25 | ...81,198-199,364 
  ...onsCommand.ts |   48.66 |     90.9 |   63.63 |   48.66 | ...05-109,159-211 
  forgetCommand.ts |   26.82 |      100 |      50 |   26.82 | 18-51             
  goalCommand.ts   |   91.25 |    83.33 |      90 |   91.25 | ...83-186,198-201 
  helpCommand.ts   |     100 |      100 |     100 |     100 |                   
  hooksCommand.ts  |    20.4 |       40 |      40 |    20.4 | ...48-180,204-205 
  ideCommand.ts    |   60.75 |    64.28 |   41.17 |   60.75 | ...05-306,310-324 
  initCommand.ts   |   84.33 |    72.72 |     100 |   84.33 | 68,82-87,89-94    
  ...ghtCommand.ts |   74.56 |    68.42 |     100 |   74.56 | ...31-245,250-273 
  ...ageCommand.ts |   92.17 |    82.69 |     100 |   92.17 | ...43,164,173-183 
  lspCommand.ts    |     100 |    86.95 |     100 |     100 | 31,101-102        
  ...elsCommand.ts |     100 |      100 |     100 |     100 |                   
  mcpCommand.ts    |     100 |      100 |     100 |     100 |                   
  memoryCommand.ts |     100 |      100 |     100 |     100 |                   
  modelCommand.ts  |   75.09 |    78.18 |      75 |   75.09 | ...20-225,262-267 
  ...onsCommand.ts |     100 |      100 |     100 |     100 |                   
  planCommand.ts   |   78.82 |    76.92 |     100 |   78.82 | 30-35,51-56,68-73 
  quitCommand.ts   |     100 |      100 |     100 |     100 |                   
  recapCommand.ts  |   21.81 |      100 |      50 |   21.81 | 24-73             
  ...berCommand.ts |   32.43 |      100 |      50 |   32.43 | 23-57             
  renameCommand.ts |   85.71 |    86.04 |     100 |   85.71 | ...02-209,216-221 
  ...oreCommand.ts |    92.3 |    87.87 |     100 |    92.3 | ...,83-88,129-130 
  resumeCommand.ts |     100 |      100 |     100 |     100 |                   
  rewindCommand.ts |      80 |      100 |      50 |      80 | 19-21             
  ...ngsCommand.ts |     100 |      100 |     100 |     100 |                   
  ...hubCommand.ts |   81.43 |    65.21 |      80 |   81.43 | ...70-173,176-179 
  skillsCommand.ts |   15.04 |      100 |      25 |   15.04 | ...90-106,109-136 
  statsCommand.ts  |   88.19 |    84.21 |     100 |   88.19 | ...,58-61,143-146 
  ...ineCommand.ts |     100 |      100 |     100 |     100 |                   
  ...aryCommand.ts |    6.46 |      100 |      50 |    6.46 | 31-329            
  tasksCommand.ts  |   77.22 |    72.13 |     100 |   77.22 | ...46-150,172-177 
  ...tupCommand.ts |     100 |      100 |     100 |     100 |                   
  themeCommand.ts  |     100 |      100 |     100 |     100 |                   
  toolsCommand.ts  |     100 |      100 |     100 |     100 |                   
  trustCommand.ts  |     100 |      100 |     100 |     100 |                   
  types.ts         |     100 |      100 |     100 |     100 |                   
  vimCommand.ts    |   54.54 |      100 |      50 |   54.54 | 19-29             
 src/ui/components |   62.92 |    74.64 |   65.25 |   62.92 |                   
  AboutBox.tsx     |     100 |      100 |     100 |     100 |                   
  AnsiOutput.tsx   |   65.57 |      100 |      50 |   65.57 | 69-90             
  ApiKeyInput.tsx  |       0 |        0 |       0 |       0 | 1-97              
  AppHeader.tsx    |   89.39 |       75 |     100 |   89.39 | 35,37-42,44       
  ...odeDialog.tsx |     9.7 |      100 |       0 |     9.7 | 35-47,50-182      
  AsciiArt.ts      |     100 |      100 |     100 |     100 |                   
  ...Indicator.tsx |   14.63 |      100 |       0 |   14.63 | 18-56             
  ...TextInput.tsx |   77.01 |       76 |     100 |   77.01 | ...20,234-236,263 
  Composer.tsx     |    81.6 |     64.7 |     100 |    81.6 | ...90,108,160,173 
  ...entPrompt.tsx |     100 |      100 |     100 |     100 |                   
  ...ryDisplay.tsx |   75.89 |    62.06 |     100 |   75.89 | ...,88,93-108,113 
  ...geDisplay.tsx |   68.42 |    57.14 |     100 |   68.42 | 16-17,31-32,42-50 
  ...ification.tsx |   28.57 |      100 |       0 |   28.57 | 16-36             
  ...gProfiler.tsx |       0 |        0 |       0 |       0 | 1-36              
  ...ogManager.tsx |   11.99 |      100 |       0 |   11.99 | 66-517            
  DiffDialog.tsx   |     2.5 |      100 |       0 |     2.5 | 68-721            
  ...ngsDialog.tsx |    8.44 |      100 |       0 |    8.44 | 37-195            
  ExitWarning.tsx  |     100 |      100 |     100 |     100 |                   
  ...hProgress.tsx |    87.8 |    33.33 |     100 |    87.8 | 28-31,56          
  ...ustDialog.tsx |     100 |      100 |     100 |     100 |                   
  Footer.tsx       |   76.59 |    48.64 |     100 |   76.59 | ...35-136,175-180 
  ...ngSpinner.tsx |   68.42 |       80 |      50 |   68.42 | 35-52,73,80-81    
  GoalPill.tsx     |   76.19 |    81.81 |     100 |   76.19 | 24-30,46-50       
  Header.tsx       |   98.62 |    94.28 |     100 |   98.62 | 162,164           
  Help.tsx         |   98.32 |    89.88 |     100 |   98.32 | ...24,381,447-448 
  ...emDisplay.tsx |    61.7 |       36 |     100 |    61.7 | ...42,345,348-354 
  ...ngeDialog.tsx |     100 |      100 |     100 |     100 |                   
  InputPrompt.tsx  |   83.01 |    79.78 |   83.33 |   83.01 | ...1399,1531,1581 
  ...Shortcuts.tsx |   20.87 |      100 |       0 |   20.87 | ...6,49-51,67-125 
  ...Indicator.tsx |     100 |    91.42 |     100 |     100 | 65,74             
  ...firmation.tsx |   91.42 |      100 |      50 |   91.42 | 26-31             
  MainContent.tsx  |   81.75 |       75 |     100 |   81.75 | ...70-274,282-286 
  ...elsDialog.tsx |   71.05 |    69.11 |   72.72 |   71.05 | ...77,590,601-603 
  MemoryDialog.tsx |    55.1 |    54.54 |   57.14 |    55.1 | ...56,368,381-383 
  ...geDisplay.tsx |       0 |        0 |       0 |       0 | 1-41              
  ModelDialog.tsx  |   80.12 |    63.55 |     100 |   80.12 | ...39-555,612-616 
  ...tsDisplay.tsx |     100 |    97.22 |     100 |     100 | 270               
  ...fications.tsx |   18.18 |      100 |       0 |   18.18 | 15-58             
  ...onsDialog.tsx |    2.13 |      100 |       0 |    2.13 | 62-133,148-1004   
  ...ryDisplay.tsx |     100 |      100 |     100 |     100 |                   
  ...icePrompt.tsx |   92.64 |    85.71 |     100 |   92.64 | 102-106,134-139   
  PrepareLabel.tsx |   91.66 |    77.27 |     100 |   91.66 | 73-75,77-79,110   
  ...atePrompt.tsx |    8.57 |      100 |       0 |    8.57 | 24-55,58-134      
  ...geDisplay.tsx |     100 |      100 |     100 |     100 |                   
  ...ngDisplay.tsx |   21.42 |      100 |       0 |   21.42 | 13-39             
  ...hProgress.tsx |   85.25 |    88.46 |     100 |   85.25 | 121-147           
  ...dSelector.tsx |   41.26 |    61.53 |   71.42 |   41.26 | ...74-472,476-520 
  ...ionPicker.tsx |   83.66 |    72.13 |     100 |   83.66 | ...96,402,444-466 
  ...onPreview.tsx |   92.42 |    84.37 |     100 |   92.42 | ...,70-71,143-145 
  ...ryDisplay.tsx |     100 |      100 |     100 |     100 |                   
  ...putPrompt.tsx |   72.56 |       80 |      40 |   72.56 | ...06-109,114-117 
  ...ngsDialog.tsx |   66.27 |    71.16 |      75 |   66.27 | ...12-820,826-827 
  ...ionDialog.tsx |    87.8 |      100 |   33.33 |    87.8 | 36-39,44-51       
  ...putPrompt.tsx |    15.9 |      100 |       0 |    15.9 | 20-63             
  ...Indicator.tsx |   57.14 |      100 |       0 |   57.14 | 12-15             
  ...MoreLines.tsx |      28 |      100 |       0 |      28 | 18-40             
  ...ionPicker.tsx |   17.59 |      100 |       0 |   17.59 | 55-172            
  StatsDisplay.tsx |     100 |      100 |     100 |     100 |                   
  ...ineDialog.tsx |   93.69 |    83.92 |     100 |   93.69 | ...11,273,293-295 
  ...yTodoList.tsx |   94.17 |       80 |     100 |   94.17 | 56-57,131-134     
  ...nsDisplay.tsx |   87.25 |       64 |     100 |   87.25 | ...45-147,154-156 
  ThemeDialog.tsx  |   89.95 |    46.15 |      75 |   89.95 | ...71-173,243-245 
  Tips.tsx         |   93.54 |       75 |     100 |   93.54 | 39-40             
  TodoDisplay.tsx  |     100 |      100 |     100 |     100 |                   
  ...tsDisplay.tsx |     100 |     87.5 |     100 |     100 | 31-32             
  TrustDialog.tsx  |     100 |    81.81 |     100 |     100 | 71-86             
  ...ification.tsx |   36.36 |      100 |       0 |   36.36 | 15-22             
  ...ackDialog.tsx |    7.84 |      100 |       0 |    7.84 | 24-134            
  ...xitDialog.tsx |   80.36 |    43.47 |      60 |   80.36 | ...24-238,248-251 
 ...nts/agent-view |   38.33 |    70.83 |   36.36 |   38.33 |                   
  ...atContent.tsx |    8.79 |      100 |       0 |    8.79 | 53-265,271-273    
  ...tChatView.tsx |   21.05 |      100 |       0 |   21.05 | 21-39             
  ...tComposer.tsx |    9.95 |      100 |       0 |    9.95 | 57-308            
  AgentFooter.tsx  |   17.07 |      100 |       0 |   17.07 | 28-66             
  AgentHeader.tsx  |   15.38 |      100 |       0 |   15.38 | 27-64             
  AgentTabBar.tsx  |    87.8 |    27.27 |     100 |    87.8 | ...,85,98-106,124 
  ...oryAdapter.ts |     100 |    91.83 |     100 |     100 | 103,109-110,138   
  index.ts         |       0 |        0 |       0 |       0 | 1-12              
 ...mponents/arena |   45.72 |    70.53 |   60.86 |   45.72 |                   
  ArenaCards.tsx   |   73.06 |    71.79 |   85.71 |   73.06 | ...83-185,321-326 
  ...ectDialog.tsx |   83.48 |    69.86 |   88.88 |   83.48 | ...88-392,409-410 
  ...artDialog.tsx |   10.15 |      100 |       0 |   10.15 | 27-161            
  ...tusDialog.tsx |    5.63 |      100 |       0 |    5.63 | 33-75,80-288      
  ...topDialog.tsx |    6.17 |      100 |       0 |    6.17 | 33-213            
 ...ackground-view |   75.63 |    84.49 |   85.29 |   75.63 |                   
  ...sksDialog.tsx |   70.92 |    80.48 |   76.19 |   70.92 | ...1118,1194-1196 
  ...TasksPill.tsx |   63.75 |    86.95 |     100 |   63.75 | 44,86-106,114-122 
  ...gentPanel.tsx |   99.53 |    93.18 |     100 |   99.53 | 123               
 ...nts/extensions |   45.28 |    33.33 |      60 |   45.28 |                   
  ...gerDialog.tsx |   44.31 |    34.14 |      75 |   44.31 | ...71-480,483-488 
  index.ts         |       0 |        0 |       0 |       0 | 1-9               
  types.ts         |     100 |      100 |     100 |     100 |                   
 ...tensions/steps |   54.88 |    94.23 |   66.66 |   54.88 |                   
  ...ctionStep.tsx |   95.12 |    92.85 |   85.71 |   95.12 | 84-86,89          
  ...etailStep.tsx |    6.18 |      100 |       0 |    6.18 | 17-128            
  ...nListStep.tsx |   88.43 |    94.73 |      80 |   88.43 | 52-53,59-72,106   
  ...electStep.tsx |   13.46 |      100 |       0 |   13.46 | 20-70             
  ...nfirmStep.tsx |   19.56 |      100 |       0 |   19.56 | 23-65             
  index.ts         |     100 |      100 |     100 |     100 |                   
 ...mponents/hooks |   68.67 |    69.07 |   69.56 |   68.67 |                   
  ...etailStep.tsx |   74.68 |    66.66 |   66.66 |   74.68 | ...71-184,188-201 
  ...etailStep.tsx |    87.4 |    73.68 |     100 |    87.4 | 41-42,99-113,119  
  ...abledStep.tsx |     100 |      100 |     100 |     100 |                   
  ...sListStep.tsx |     100 |      100 |     100 |     100 |                   
  ...entDialog.tsx |   34.51 |    47.05 |   42.85 |   34.51 | ...78,482-495,499 
  constants.ts     |     100 |      100 |     100 |     100 |                   
  index.ts         |       0 |        0 |       0 |       0 | 1-13              
  types.ts         |     100 |      100 |     100 |     100 |                   
 ...components/mcp |   20.98 |    86.36 |   83.33 |   20.98 |                   
  ...ealthPill.tsx |   68.42 |    85.71 |     100 |   68.42 | 40-46             
  ...entDialog.tsx |    3.64 |      100 |       0 |    3.64 | 41-717            
  constants.ts     |     100 |      100 |     100 |     100 |                   
  index.ts         |       0 |        0 |       0 |       0 | 1-30              
  types.ts         |     100 |      100 |     100 |     100 |                   
  utils.ts         |   95.83 |    88.88 |     100 |   95.83 | 16,20,109-110     
 ...ents/mcp/steps |   26.74 |    54.54 |   42.85 |   26.74 |                   
  ...icateStep.tsx |    5.88 |      100 |       0 |    5.88 | 40-55,58-296      
  ...electStep.tsx |   10.95 |      100 |       0 |   10.95 | 16-88             
  ...etailStep.tsx |    5.26 |      100 |       0 |    5.26 | 31-247            
  ...rListStep.tsx |   75.18 |    59.37 |     100 |   75.18 | ...53-158,169-173 
  ...etailStep.tsx |   10.41 |      100 |       0 |   10.41 | ...1,67-79,82-139 
  ToolListStep.tsx |   69.02 |       50 |     100 |   69.02 | ...22,125,134-143 
 ...nents/messages |   82.44 |    79.55 |    72.6 |   82.44 |                   
  ...ionDialog.tsx |   80.84 |     77.6 |    62.5 |   80.84 | ...98,516,534-536 
  BtwMessage.tsx   |     100 |      100 |     100 |     100 |                   
  ...upDisplay.tsx |   97.67 |    83.72 |     100 |   97.67 | 119,142,150       
  ...onMessage.tsx |   91.93 |    82.35 |     100 |   91.93 | 57-59,61,63       
  ...nMessages.tsx |   79.06 |      100 |      70 |   79.06 | ...51-264,268-280 
  DiffRenderer.tsx |   93.19 |    86.17 |     100 |   93.19 | ...09,237-238,304 
  ...tsDisplay.tsx |   97.82 |    77.27 |     100 |   97.82 | 87,89             
  ...usMessage.tsx |   76.31 |     42.1 |   66.66 |   76.31 | ...99,101,124,155 
  ...ssMessage.tsx |    12.5 |      100 |       0 |    12.5 | 18-59             
  ...edMessage.tsx |   16.66 |      100 |       0 |   16.66 | 22-38             
  ...sMessages.tsx |   55.67 |       40 |   28.57 |   55.67 | ...20-125,133-145 
  ...ryMessage.tsx |   14.28 |      100 |       0 |   14.28 | 23-62             
  ...onMessage.tsx |   81.02 |    69.23 |   33.33 |   81.02 | ...24-426,433-435 
  ...upMessage.tsx |      84 |    93.61 |     100 |      84 | ...56-383,405-420 
  ToolMessage.tsx  |   88.84 |    75.71 |    92.3 |   88.84 | ...44-749,776-778 
 ...ponents/shared |   85.36 |    78.48 |   95.77 |   85.36 |                   
  ...ctionList.tsx |   99.03 |    95.65 |     100 |   99.03 | 85                
  ...tonSelect.tsx |     100 |      100 |     100 |     100 |                   
  EnumSelector.tsx |     100 |    96.42 |     100 |     100 | 58                
  MaxSizedBox.tsx  |   83.01 |    86.25 |   88.88 |   83.01 | ...12-513,618-619 
  MultiSelect.tsx  |   84.31 |    74.19 |     100 |   84.31 | ...37,193-195,205 
  ...tonSelect.tsx |     100 |      100 |     100 |     100 |                   
  ...eSelector.tsx |     100 |       60 |     100 |     100 | 40-45             
  TextInput.tsx    |   77.01 |    48.78 |      80 |   77.01 | ...08-212,224-230 
  ...apsedTime.tsx |     100 |      100 |     100 |     100 |                   
  ...Indicator.tsx |     100 |      100 |     100 |     100 |                   
  text-buffer.ts   |   83.68 |    78.55 |   97.61 |   83.68 | ...2270-2272,2368 
  ...er-actions.ts |   86.71 |    67.79 |     100 |   86.71 | ...07-608,809-811 
 ...ents/subagents |   30.87 |        0 |       0 |   30.87 |                   
  constants.ts     |     100 |      100 |     100 |     100 |                   
  index.ts         |       0 |        0 |       0 |       0 | 1-11              
  reducers.tsx     |    12.1 |      100 |       0 |    12.1 | 33-190            
  types.ts         |     100 |      100 |     100 |     100 |                   
  utils.ts         |   10.95 |      100 |       0 |   10.95 | ...1,56-57,60-102 
 ...bagents/create |    9.13 |      100 |       0 |    9.13 |                   
  ...ionWizard.tsx |    7.28 |      100 |       0 |    7.28 | 34-299            
  ...rSelector.tsx |   14.75 |      100 |       0 |   14.75 | 26-85             
  ...onSummary.tsx |    4.26 |      100 |       0 |    4.26 | 27-331            
  ...tionInput.tsx |    8.63 |      100 |       0 |    8.63 | 23-177            
  ...dSelector.tsx |   33.33 |      100 |       0 |   33.33 | 20-21,26-27,36-63 
  ...nSelector.tsx |    37.5 |      100 |       0 |    37.5 | 20-21,26-27,36-58 
  ...EntryStep.tsx |   12.76 |      100 |       0 |   12.76 | 34-78             
  ToolSelector.tsx |    4.16 |      100 |       0 |    4.16 | 31-253            
 ...bagents/manage |   21.51 |    59.52 |   27.27 |   21.51 |                   
  ...ctionStep.tsx |   10.25 |      100 |       0 |   10.25 | 21-103            
  ...eleteStep.tsx |   20.93 |      100 |       0 |   20.93 | 23-62             
  ...tEditStep.tsx |   25.53 |      100 |       0 |   25.53 | ...2,37-38,51-124 
  ...ctionStep.tsx |   35.42 |    59.52 |     100 |   35.42 | ...20-432,437-439 
  ...iewerStep.tsx |   13.72 |      100 |       0 |   13.72 | 18-73             
  ...gerDialog.tsx |    6.74 |      100 |       0 |    6.74 | 35-341            
 ...mponents/views |   42.16 |    69.23 |   21.42 |   42.16 |                   
  ContextUsage.tsx |     4.7 |      100 |       0 |     4.7 | ...52-167,170-456 
  DoctorReport.tsx |     9.8 |      100 |       0 |     9.8 | 25-54,57-131      
  ...sionsList.tsx |   87.69 |    73.68 |     100 |   87.69 | 65-72             
  McpStatus.tsx    |   89.53 |    60.52 |     100 |   89.53 | ...72,175-177,262 
  SkillsList.tsx   |   27.27 |      100 |       0 |   27.27 | 18-35             
  ToolsList.tsx    |     100 |      100 |     100 |     100 |                   
 src/ui/contexts   |   77.34 |    78.06 |   80.35 |   77.34 |                   
  ...ewContext.tsx |    64.7 |    85.71 |      50 |    64.7 | ...22-225,231-241 
  AppContext.tsx   |      80 |       50 |     100 |      80 | 19-20             
  ...ewContext.tsx |   95.18 |    67.56 |      50 |   95.18 | ...94-195,222-226 
  ...deContext.tsx |     100 |      100 |     100 |     100 |                   
  ...igContext.tsx |   81.81 |       50 |     100 |   81.81 | 15-16             
  ...ssContext.tsx |   82.31 |    82.84 |     100 |   82.31 | ...1153,1159-1161 
  ...owContext.tsx |   89.28 |       80 |   66.66 |   89.28 | 34,47-48,60-62    
  ...deContext.tsx |     100 |      100 |      50 |     100 |                   
  ...onContext.tsx |   43.28 |     62.5 |    62.5 |   43.28 | ...56-259,263-266 
  ...gsContext.tsx |   83.33 |       50 |     100 |   83.33 | 17-18             
  ...usContext.tsx |     100 |      100 |     100 |     100 |                   
  ...ngContext.tsx |   71.42 |       50 |     100 |   71.42 | 17-20             
  ...utContext.tsx |   85.71 |      100 |   66.66 |   85.71 | 13-14             
  ...nsContext.tsx |   88.23 |       50 |     100 |   88.23 | 120-121           
  ...teContext.tsx |   86.66 |       50 |     100 |   86.66 | 195-196           
  ...deContext.tsx |   76.08 |    72.72 |     100 |   76.08 | 47-48,52-59,77-78 
 src/ui/daemon     |   90.76 |    73.73 |   95.45 |   90.76 |                   
  ...TuiAdapter.ts |   90.76 |    73.73 |   95.45 |   90.76 | ...53,771-772,858 
 src/ui/editors    |   93.33 |    85.71 |   66.66 |   93.33 |                   
  ...ngsManager.ts |   93.33 |    85.71 |   66.66 |   93.33 | 49,63-64          
 src/ui/hooks      |   81.58 |    82.44 |   85.58 |   81.58 |                   
  ...dProcessor.ts |   83.12 |    82.56 |     100 |   83.12 | ...88-389,408-435 
  keyToAnsi.ts     |    3.92 |      100 |       0 |    3.92 | 19-77             
  ...dProcessor.ts |    94.8 |    70.58 |     100 |    94.8 | ...76-277,282-283 
  ...dProcessor.ts |   75.59 |    62.58 |   61.53 |   75.59 | ...88,912,931-935 
  ...amingState.ts |   12.22 |      100 |       0 |   12.22 | 54-157            
  ...agerDialog.ts |   88.23 |      100 |     100 |   88.23 | 20,24             
  ...ationFrame.ts |      32 |       60 |     100 |      32 | 42-44,51-90       
  ...odeCommand.ts |   58.82 |      100 |     100 |   58.82 | 28,33-48          
  ...enaCommand.ts |      85 |      100 |     100 |      85 | 23-24,29          
  ...aInProcess.ts |   19.81 |    66.66 |      25 |   19.81 | 57-175            
  ...Completion.ts |   92.77 |    89.09 |     100 |   92.77 | ...86-187,220-223 
  ...ifications.ts |   92.07 |    96.29 |     100 |   92.07 | 116-124           
  ...tIndicator.ts |     100 |    93.75 |     100 |     100 | 63                
  ...waySummary.ts |   96.22 |    69.69 |     100 |   96.22 | 125-127,169       
  ...ndTaskView.ts |   94.21 |    76.08 |     100 |   94.21 | 122-126,213,219   
  ...ketedPaste.ts |    23.8 |      100 |       0 |    23.8 | 19-37             
  ...nchCommand.ts |   94.36 |    74.35 |     100 |   94.36 | ...60,168-169,209 
  ...ompletion.tsx |   95.95 |    82.75 |     100 |   95.95 | ...22-223,225-226 
  ...dMigration.ts |   90.62 |       75 |     100 |   90.62 | 38-40             
  useCompletion.ts |    92.4 |     87.5 |     100 |    92.4 | 68-69,93-94,98-99 
  ...nitMessage.ts |     100 |      100 |     100 |     100 |                   
  ...extualTips.ts |   76.92 |       50 |     100 |   76.92 | 55,68,71-75,88-96 
  ...eteCommand.ts |   78.53 |    88.57 |     100 |   78.53 | ...96-104,112-113 
  ...ialogClose.ts |   13.33 |      100 |     100 |   13.33 | 91-182            
  useDiffData.ts   |   11.62 |      100 |       0 |   11.62 | 44-87             
  ...oublePress.ts |   53.12 |       75 |     100 |   53.12 | 33-35,41-54       
  ...orSettings.ts |     100 |      100 |     100 |     100 |                   
  ...Completion.ts |   99.12 |     97.7 |     100 |   99.12 | 182-183           
  ...ionUpdates.ts |   93.45 |     92.3 |     100 |   93.45 | ...83-287,300-306 
  ...agerDialog.ts |   88.88 |      100 |     100 |   88.88 | 21,25             
  ...backDialog.ts |   54.47 |       50 |   33.33 |   54.47 | ...69-171,193-194 
  useFocus.ts      |     100 |      100 |     100 |     100 |                   
  ...olderTrust.ts |     100 |      100 |     100 |     100 |                   
  ...ggestions.tsx |   89.15 |     62.5 |      50 |   89.15 | ...22-124,149-150 
  ...miniStream.ts |    77.7 |    74.93 |   91.66 |    77.7 | ...2497,2510-2518 
  ...BranchName.ts |    90.9 |     92.3 |     100 |    90.9 | 19-20,55-58       
  ...oryManager.ts |   93.15 |    93.75 |     100 |   93.15 | 44,107-110        
  ...ooksDialog.ts |    87.5 |      100 |     100 |    87.5 | 19,23             
  ...stListener.ts |     100 |      100 |     100 |     100 |                   
  ...nAuthError.ts |   76.19 |       50 |     100 |   76.19 | 39-40,43-45       
  ...putHistory.ts |   92.59 |    85.71 |     100 |   92.59 | 63-64,72,94-96    
  ...storyStore.ts |     100 |    94.11 |     100 |     100 | 69                
  useKeypress.ts   |     100 |      100 |     100 |     100 |                   
  ...rdProtocol.ts |   36.36 |      100 |       0 |   36.36 | 24-31             
  ...unchEditor.ts |    9.67 |      100 |       0 |    9.67 | 11-32,39-90       
  ...gIndicator.ts |     100 |      100 |     100 |     100 |                   
  useLogger.ts     |   21.05 |      100 |       0 |   21.05 | 15-37             
  useMCPHealth.ts  |   63.15 |       75 |      50 |   63.15 | 42-52,64-67       
  ...elsCommand.ts |     100 |      100 |     100 |     100 |                   
  useMcpDialog.ts  |    87.5 |      100 |     100 |    87.5 | 19,23             
  ...moryDialog.ts |    87.5 |      100 |     100 |    87.5 | 19,23             
  ...oryMonitor.ts |     100 |      100 |     100 |     100 |                   
  ...ssageQueue.ts |     100 |      100 |     100 |     100 |                   
  ...delCommand.ts |     100 |       75 |     100 |     100 | 22                
  ...raseCycler.ts |   84.74 |    76.47 |     100 |   84.74 | ...49,52-53,69-71 
  ...derUpdates.ts |   86.38 |    77.19 |     100 |   86.38 | ...22,281-293,341 
  useQwenAuth.ts   |     100 |      100 |     100 |     100 |                   
  ...lScheduler.ts |    84.7 |    93.33 |     100 |    84.7 | ...71-276,372-382 
  ...oryCommand.ts |       0 |        0 |       0 |       0 | 1-7               
  ...umeCommand.ts |   97.08 |    83.33 |     100 |   97.08 | 103-104,133       
  ...ompletion.tsx |   90.59 |    83.33 |     100 |   90.59 | ...01,104,137-140 
  ...ectionList.ts |   96.98 |    95.69 |     100 |   96.98 | ...83-184,238-241 
  ...sionPicker.ts |   92.87 |    90.35 |     100 |   92.87 | ...99-501,503-505 
  ...earchInput.ts |     100 |      100 |     100 |     100 |                   
  ...ngsCommand.ts |   18.75 |      100 |       0 |   18.75 | 10-25             
  ...ellHistory.ts |   91.74 |    79.41 |     100 |   91.74 | ...74,122-123,133 
  ...oryCommand.ts |       0 |        0 |       0 |       0 | 1-73              
  ...Completion.ts |   82.67 |    85.41 |   94.73 |   82.67 | ...68-670,678-714 
  ...tateAndRef.ts |     100 |      100 |     100 |     100 |                   
  useStatusLine.ts |   96.09 |    90.37 |     100 |   96.09 | ...62-365,450-457 
  ...eateDialog.ts |   88.23 |      100 |     100 |   88.23 | 14,18             
  ...tification.ts |     100 |    85.71 |     100 |     100 | 47                
  ...alProgress.ts |   53.06 |       50 |   66.66 |   53.06 | ...53,61-68,79-85 
  ...rminalSize.ts |   76.19 |      100 |      50 |   76.19 | 21-25             
  ...emeCommand.ts |   67.01 |    29.41 |     100 |   67.01 | ...10-111,115-116 
  useTimer.ts      |   88.09 |    85.71 |     100 |   88.09 | 44-45,51-53       
  ...lMigration.ts |       0 |        0 |       0 |       0 |                   
  ...rustModify.ts |     100 |      100 |     100 |     100 |                   
  useTurnDiffs.ts  |   10.97 |      100 |       0 |   10.97 | 55-143,147-157    
  ...elcomeBack.ts |   87.36 |     90.9 |     100 |   87.36 | ...,94-96,114-115 
  ...reeSession.ts |   93.75 |       75 |     100 |   93.75 | 44-45,87          
  vim.ts           |   83.77 |    80.31 |     100 |   83.77 | ...55,759-767,776 
 src/ui/layouts    |   89.72 |     87.5 |     100 |   89.72 |                   
  ...AppLayout.tsx |   89.88 |     87.5 |     100 |   89.88 | 51-53,93-98       
  ...AppLayout.tsx |   89.47 |     87.5 |     100 |   89.47 | 58-63             
 ...i/manageModels |   93.61 |       48 |     100 |   93.61 |                   
  manageModels.ts  |   93.61 |       48 |     100 |   93.61 | ...63-166,179,209 
 src/ui/models     |   80.24 |    79.16 |   71.42 |   80.24 |                   
  ...ableModels.ts |   80.24 |    79.16 |   71.42 |   80.24 | ...,61-71,123-125 
 ...noninteractive |     100 |      100 |   14.28 |     100 |                   
  ...eractiveUi.ts |     100 |      100 |   14.28 |     100 |                   
 src/ui/state      |   94.91 |    81.81 |     100 |   94.91 |                   
  extensions.ts    |   94.91 |    81.81 |     100 |   94.91 | 68-69,88          
 src/ui/themes     |   98.53 |    70.58 |     100 |   98.53 |                   
  ansi-light.ts    |     100 |      100 |     100 |     100 |                   
  ansi.ts          |     100 |      100 |     100 |     100 |                   
  atom-one-dark.ts |     100 |      100 |     100 |     100 |                   
  ayu-light.ts     |     100 |      100 |     100 |     100 |                   
  ayu.ts           |     100 |      100 |     100 |     100 |                   
  color-utils.ts   |     100 |      100 |     100 |     100 |                   
  default-light.ts |     100 |      100 |     100 |     100 |                   
  default.ts       |     100 |      100 |     100 |     100 |                   
  ...inal-theme.ts |   88.59 |    85.96 |     100 |   88.59 | ...57-261,266-270 
  dracula.ts       |     100 |      100 |     100 |     100 |                   
  github-dark.ts   |     100 |      100 |     100 |     100 |                   
  github-light.ts  |     100 |      100 |     100 |     100 |                   
  googlecode.ts    |     100 |      100 |     100 |     100 |                   
  no-color.ts      |     100 |      100 |     100 |     100 |                   
  qwen-dark.ts     |     100 |      100 |     100 |     100 |                   
  qwen-light.ts    |     100 |      100 |     100 |     100 |                   
  ...tic-tokens.ts |     100 |      100 |     100 |     100 |                   
  ...-of-purple.ts |     100 |      100 |     100 |     100 |                   
  theme-manager.ts |   87.98 |    82.89 |     100 |   87.98 | ...48-357,362-363 
  theme.ts         |     100 |    38.02 |     100 |     100 | ...34-449,457-461 
  xcode.ts         |     100 |      100 |     100 |     100 |                   
 src/ui/utils      |   83.98 |    82.97 |   92.61 |   83.98 |                   
  ...Colorizer.tsx |   79.53 |    83.78 |     100 |   79.53 | ...51-152,249-275 
  ...nRenderer.tsx |   68.83 |    70.14 |      50 |   68.83 | ...52-254,274-293 
  ...wnDisplay.tsx |   86.01 |    87.41 |     100 |   86.01 | ...87,704,729-754 
  ...idDiagram.tsx |   87.79 |    95.34 |     100 |   87.79 | 156-179           
  ...eRenderer.tsx |   92.08 |    80.45 |      95 |   92.08 | ...76-679,723-728 
  ...dWorkUtils.ts |     100 |      100 |     100 |     100 |                   
  ...boardUtils.ts |   59.61 |    58.82 |     100 |   59.61 | ...,86-88,107-149 
  commandUtils.ts  |    95.9 |    88.42 |     100 |    95.9 | ...62,164-165,289 
  computeStats.ts  |     100 |      100 |     100 |     100 |                   
  customBanner.ts  |   90.68 |    91.22 |     100 |   90.68 | ...13,324-327,334 
  displayUtils.ts  |   88.37 |    72.22 |     100 |   88.37 | 23,25,29,31,33    
  formatters.ts    |   95.23 |    98.27 |     100 |   95.23 | 117-120           
  gradientUtils.ts |     100 |      100 |     100 |     100 |                   
  highlight.ts     |     100 |      100 |     100 |     100 |                   
  ...oryMapping.ts |     100 |    94.28 |     100 |     100 | 35,57             
  historyUtils.ts  |   94.11 |       94 |     100 |   94.11 | 94-97             
  isNarrowWidth.ts |     100 |      100 |     100 |     100 |                   
  ...olDetector.ts |    8.23 |      100 |       0 |    8.23 | ...31-132,135-136 
  latexRenderer.ts |   94.95 |     73.8 |     100 |   94.95 | ...76-178,184-187 
  layoutUtils.ts   |     100 |      100 |     100 |     100 |                   
  ...ightLoader.ts |     100 |    89.47 |     100 |     100 | 81,110            
  ...nUtilities.ts |   69.84 |    85.71 |     100 |   69.84 | 75-91,100-101     
  ...ToolGroups.ts |   98.66 |    96.77 |     100 |   98.66 | 48-49             
  ...geRenderer.ts |   86.23 |    69.06 |   95.12 |   86.23 | ...1284,1324-1330 
  ...alRenderer.ts |   86.69 |     71.9 |     100 |   86.69 | ...1476,1513-1519 
  ...lsBySource.ts |     100 |    95.23 |     100 |     100 | 84                
  osc8.ts          |   94.71 |    87.41 |     100 |   94.71 | ...43,428,432-433 
  ...mConstants.ts |     100 |      100 |     100 |     100 |                   
  restoreGoal.ts   |   98.98 |    97.05 |     100 |   98.98 | 98                
  ...storyUtils.ts |   61.89 |    69.87 |      90 |   61.89 | ...76,424,429-451 
  ...ickerUtils.ts |     100 |      100 |     100 |     100 |                   
  ...izedOutput.ts |   94.94 |      100 |   88.88 |   94.94 | 112-117           
  ...wOptimizer.ts |     100 |    96.77 |     100 |     100 | 69                
  terminalSetup.ts |    4.37 |      100 |       0 |    4.37 | 44-393            
  textUtils.ts     |   97.61 |    94.84 |   92.85 |   97.61 | ...50-251,386-387 
  todoSnapshot.ts  |   89.11 |    93.33 |     100 |   89.11 | ...,66-78,180-181 
  updateCheck.ts   |     100 |    80.95 |     100 |     100 | 30-42             
 ...i/utils/export |   56.77 |     40.8 |   79.41 |   56.77 |                   
  collect.ts       |   55.92 |    50.58 |   86.36 |   55.92 | ...25-640,642-647 
  index.ts         |     100 |      100 |     100 |     100 |                   
  normalize.ts     |   57.47 |    20.51 |      80 |   57.47 | ...09-310,324-359 
  types.ts         |       0 |        0 |       0 |       0 | 1                 
  utils.ts         |      40 |      100 |       0 |      40 | 11-13             
 ...ort/formatters |    3.38 |      100 |       0 |    3.38 |                   
  html.ts          |    9.61 |      100 |       0 |    9.61 | ...28,34-76,82-84 
  json.ts          |      50 |      100 |       0 |      50 | 14-15             
  jsonl.ts         |     3.5 |      100 |       0 |     3.5 | 14-76             
  markdown.ts      |    0.94 |      100 |       0 |    0.94 | 13-295            
 src/utils         |   76.06 |    89.51 |   93.82 |   76.06 |                   
  acpModelUtils.ts |     100 |      100 |     100 |     100 |                   
  apiPreconnect.ts |   96.72 |    97.14 |     100 |   96.72 | 165-168           
  checks.ts        |   33.33 |      100 |       0 |   33.33 | 23-28             
  cleanup.ts       |   84.12 |    93.33 |      80 |   84.12 | 75,106-115        
  commands.ts      |     100 |      100 |     100 |     100 |                   
  commentJson.ts   |   87.17 |     90.9 |     100 |   87.17 | 64-73             
  ...Calculator.ts |     100 |      100 |     100 |     100 |                   
  deepMerge.ts     |     100 |       90 |     100 |     100 | 41-43,49          
  ...ScopeUtils.ts |   97.56 |    88.88 |     100 |   97.56 | 67                
  doctorChecks.ts  |   71.06 |       75 |     100 |   71.06 | ...95-301,325-341 
  ...putCapture.ts |   90.65 |    86.17 |     100 |   90.65 | ...72,370,372-373 
  ...arResolver.ts |   94.28 |       88 |     100 |   94.28 | 28-29,125-126     
  errors.ts        |   98.67 |    96.36 |     100 |   98.67 | 67-68             
  events.ts        |     100 |      100 |     100 |     100 |                   
  gitUtils.ts      |   91.91 |    84.61 |     100 |   91.91 | 78-81,124-127     
  ...AutoUpdate.ts |   90.76 |    93.33 |   88.88 |   90.76 | 103-114           
  ...lationInfo.ts |     100 |      100 |     100 |     100 |                   
  languageUtils.ts |   97.89 |    96.42 |     100 |   97.89 | 132-133           
  math.ts          |       0 |        0 |       0 |       0 | 1-15              
  ...iagnostics.ts |   94.57 |    83.01 |   88.88 |   94.57 | ...05,311,315-317 
  ...onfigUtils.ts |     100 |      100 |     100 |     100 |                   
  ...iveHelpers.ts |   96.79 |    93.28 |     100 |   96.79 | ...76-477,575,588 
  osc.ts           |    97.5 |      100 |   88.88 |    97.5 | 195-196           
  package.ts       |   88.88 |       80 |     100 |   88.88 | 33-34             
  processUtils.ts  |     100 |      100 |     100 |     100 |                   
  readStdin.ts     |   79.62 |       90 |      80 |   79.62 | 33-40,52-54       
  relaunch.ts      |   98.07 |    76.92 |     100 |   98.07 | 70                
  resolvePath.ts   |   66.66 |       25 |     100 |   66.66 | 12-13,16,18-19    
  sandbox.ts       |       0 |        0 |       0 |       0 | 1-1047            
  settingsUtils.ts |   82.89 |    90.67 |   89.47 |   82.89 | ...52-663,670-678 
  spawnWrapper.ts  |     100 |      100 |     100 |     100 |                   
  ...upProfiler.ts |   98.46 |    94.52 |     100 |   98.46 | 130-131,305       
  ...upWarnings.ts |     100 |      100 |     100 |     100 |                   
  stdioHelpers.ts  |     100 |       60 |     100 |     100 | 23,32             
  systemInfo.ts    |   95.12 |    89.06 |     100 |   95.12 | ...43-244,249-253 
  ...InfoFields.ts |   87.61 |       65 |     100 |   87.61 | ...22-123,144-145 
  ...iffPreview.ts |   94.11 |    83.33 |     100 |   94.11 | 13                
  ...entEmitter.ts |     100 |      100 |     100 |     100 |                   
  ...upWarnings.ts |   91.17 |    82.35 |     100 |   91.17 | 67-68,73-74,77-78 
  version.ts       |     100 |       50 |     100 |     100 | 11                
  windowTitle.ts   |     100 |      100 |     100 |     100 |                   
  ...WithBackup.ts |   63.15 |    81.25 |     100 |   63.15 | 93,118-157        
-------------------|---------|----------|---------|---------|-------------------
Core Package - Full Text Report
-------------------|---------|----------|---------|---------|-------------------
File               | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
-------------------|---------|----------|---------|---------|-------------------
All files          |   79.45 |    82.86 |   82.09 |   79.45 |                   
 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        |   87.58 |    79.07 |   91.76 |   87.58 |                   
  ...transcript.ts |   92.25 |    85.71 |     100 |   92.25 | ...87,306-307,438 
  ...ent-resume.ts |    82.5 |     71.5 |   77.41 |    82.5 | ...1035-1039,1042 
  ...ound-tasks.ts |    95.4 |    86.48 |     100 |    95.4 | ...55-756,827-828 
  index.ts         |     100 |      100 |     100 |     100 |                   
 src/agents/arena  |   76.54 |    66.87 |   78.72 |   76.54 |                   
  ...gentClient.ts |   79.47 |    88.88 |   81.81 |   79.47 | ...68-183,189-204 
  ArenaManager.ts  |   75.37 |    63.37 |   78.26 |   75.37 | ...1860,1866-1867 
  arena-events.ts  |   64.44 |      100 |      50 |   64.44 | ...71-175,178-183 
  diff-summary.ts  |    87.5 |    72.34 |     100 |    87.5 | ...32-133,137-138 
  index.ts         |     100 |      100 |     100 |     100 |                   
  types.ts         |     100 |      100 |     100 |     100 |                   
 ...gents/backends |   76.29 |    86.15 |   73.04 |   76.29 |                   
  ITermBackend.ts  |   97.97 |    93.93 |     100 |   97.97 | ...78-180,255,307 
  ...essBackend.ts |   91.25 |    90.62 |   86.66 |   91.25 | ...94,249-269,328 
  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 |   81.14 |     76.7 |   71.42 |   81.14 |                   
  agent-context.ts |     100 |      100 |     100 |     100 |                   
  agent-core.ts    |   76.49 |    72.35 |   60.86 |   76.49 | ...1608,1635-1682 
  agent-events.ts  |     100 |      100 |     100 |     100 |                   
  ...t-headless.ts |   81.19 |    71.73 |   60.86 |   81.19 | ...98-399,402-403 
  ...nteractive.ts |   79.71 |    79.62 |      75 |   79.71 | ...54,456,458,461 
  ...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/agents/tasks  |     100 |      100 |     100 |     100 |                   
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/config        |   78.32 |    81.27 |   65.39 |   78.32 |                   
  config.ts        |   76.12 |    79.96 |   60.63 |   76.12 | ...3659,3670-3682 
  constants.ts     |     100 |      100 |     100 |     100 |                   
  models.ts        |     100 |      100 |     100 |     100 |                   
  storage.ts       |   95.01 |     90.9 |   90.47 |   95.01 | ...71-372,375-376 
 ...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/core          |   87.17 |    83.07 |      90 |   87.17 |                   
  baseLlmClient.ts |   92.35 |    80.85 |   86.66 |   92.35 | ...34,342-356,495 
  client.ts        |   87.62 |    81.53 |   86.11 |   87.62 | ...1926,1965-1968 
  ...tGenerator.ts |    72.1 |    61.11 |     100 |    72.1 | ...63,365,372-375 
  ...lScheduler.ts |   83.06 |    81.67 |   93.47 |   83.06 | ...2447,2499-2503 
  geminiChat.ts    |   89.32 |     84.8 |   91.48 |   89.32 | ...1454,1521-1522 
  geminiRequest.ts |     100 |      100 |     100 |     100 |                   
  ...htProtocol.ts |    9.09 |      100 |       0 |    9.09 | 34-42,45-49,52-87 
  logger.ts        |   87.33 |    87.02 |     100 |   87.33 | ...61-565,611-625 
  ...tyDefaults.ts |     100 |      100 |     100 |     100 |                   
  ...olExecutor.ts |   92.59 |       75 |      50 |   92.59 | 41-42             
  ...on-helpers.ts |   85.71 |    70.58 |     100 |   85.71 | ...90-191,205-214 
  ...issionFlow.ts |   98.59 |    94.73 |     100 |   98.59 | 93                
  prompts.ts       |   89.16 |    86.41 |   76.92 |   89.16 | ...-965,1168-1169 
  tokenLimits.ts   |     100 |    89.47 |     100 |     100 | 51-52             
  ...okTriggers.ts |   99.31 |    90.41 |     100 |   99.31 | 124,135           
  turn.ts          |   96.44 |    88.88 |     100 |   96.44 | ...08,421-422,470 
 ...ntentGenerator |   94.92 |    82.59 |   93.87 |   94.92 |                   
  ...tGenerator.ts |   96.48 |    84.28 |   92.59 |   96.48 | ...01,919-923,963 
  converter.ts     |   94.51 |    80.72 |     100 |   94.51 | ...06-607,617,823 
  index.ts         |       0 |        0 |       0 |       0 | 1-21              
  usage.ts         |     100 |      100 |     100 |     100 |                   
 ...ntentGenerator |   91.53 |    71.64 |   93.33 |   91.53 |                   
  ...tGenerator.ts |      90 |    70.96 |   92.85 |      90 | ...80-286,304-305 
  index.ts         |     100 |       80 |     100 |     100 | 50                
 ...ntentGenerator |   93.32 |    80.28 |   90.32 |   93.32 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...tGenerator.ts |    93.3 |    80.28 |   90.32 |    93.3 | ...99,909-910,938 
 ...ntentGenerator |   81.66 |    84.08 |    90.9 |   81.66 |                   
  constants.ts     |     100 |      100 |     100 |     100 |                   
  converter.ts     |   76.88 |    82.25 |    87.5 |   76.88 | ...1589,1610-1616 
  errorHandler.ts  |     100 |      100 |     100 |     100 |                   
  index.ts         |   52.38 |    44.44 |      50 |   52.38 | ...77,81-85,89-93 
  ...tGenerator.ts |    66.4 |    70.58 |   88.88 |    66.4 | ...51-157,168-169 
  pipeline.ts      |   93.67 |     84.9 |     100 |   93.67 | ...80-481,489,554 
  ...ureContext.ts |     100 |      100 |     100 |     100 |                   
  ...ingOptions.ts |       0 |        0 |       0 |       0 | 1                 
  ...CallParser.ts |   90.66 |    88.57 |     100 |   90.66 | ...15-319,349-350 
  ...kingParser.ts |     100 |    96.87 |     100 |     100 | 42                
  types.ts         |       0 |        0 |       0 |       0 | 1                 
 ...rator/provider |   96.69 |    89.17 |   95.45 |   96.69 |                   
  dashscope.ts     |   97.29 |    89.77 |   93.33 |   97.29 | ...81-282,358-359 
  deepseek.ts      |   95.55 |    90.56 |     100 |   95.55 | ...31-132,145-146 
  default.ts       |   94.62 |    86.36 |   85.71 |   94.62 | 86-87,157-159     
  index.ts         |     100 |      100 |     100 |     100 |                   
  minimax.ts       |     100 |      100 |     100 |     100 |                   
  mistral.ts       |   96.07 |    73.33 |     100 |   96.07 | 32-33             
  modelscope.ts    |     100 |      100 |     100 |     100 |                   
  openrouter.ts    |     100 |      100 |     100 |     100 |                   
  types.ts         |       0 |        0 |       0 |       0 |                   
 src/extension     |   60.56 |    79.46 |    78.4 |   60.56 |                   
  ...-converter.ts |   62.35 |    47.82 |      90 |   62.35 | ...90-791,800-832 
  ...ionManager.ts |   47.04 |    82.06 |    65.9 |   47.04 | ...1398,1408-1427 
  ...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       |     100 |      100 |     100 |     100 |                   
  ...ableSchema.ts |     100 |      100 |     100 |     100 |                   
  variables.ts     |   88.75 |    83.33 |     100 |   88.75 | ...28-231,234-237 
 src/followup      |   46.91 |     92.3 |   71.87 |   46.91 |                   
  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 | 93                
  ...nGenerator.ts |    38.4 |    95.12 |   33.33 |    38.4 | ...16-318,353-383 
 src/generated     |       0 |        0 |       0 |       0 |                   
  git-commit.ts    |       0 |        0 |       0 |       0 | 1-10              
 src/goals         |   89.57 |    83.45 |   94.44 |   89.57 |                   
  ...eGoalStore.ts |    85.1 |    95.45 |   84.61 |    85.1 | ...63-166,174-182 
  goalHook.ts      |   97.26 |    91.48 |     100 |   97.26 | 100-105           
  goalJudge.ts     |   84.33 |    74.28 |     100 |   84.33 | ...57-358,366-368 
  index.ts         |     100 |      100 |     100 |     100 |                   
 src/hooks         |   83.48 |    84.87 |   86.83 |   83.48 |                   
  ...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.4 |    90.78 |     100 |    96.4 | ...91,293-294,367 
  ...entHandler.ts |   94.56 |    83.78 |   93.33 |   94.56 | ...38,795-796,806 
  hookPlanner.ts   |   84.13 |    76.59 |      90 |   84.13 | ...38,144,162-173 
  hookRegistry.ts  |   90.17 |    83.33 |     100 |   90.17 | ...33,352,356,360 
  hookRunner.ts    |   58.56 |    71.26 |   66.66 |   58.56 | ...48-749,758-759 
  hookSystem.ts    |   84.57 |      100 |   65.85 |   84.57 | ...21-622,628-629 
  ...HookRunner.ts |   75.51 |     61.9 |      80 |   75.51 | ...05-406,424-425 
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...HookRunner.ts |   93.63 |    89.47 |      90 |   93.63 | ...45-353,427-428 
  ...SkillHooks.ts |   78.75 |       75 |   66.66 |   78.75 | 62-66,137-152     
  ...oksManager.ts |   96.66 |    91.66 |     100 |   96.66 | ...90,209-210,223 
  ssrfGuard.ts     |   77.22 |    85.36 |     100 |   77.22 | ...57,261-267,273 
  stopHookCap.ts   |     100 |      100 |     100 |     100 |                   
  trustedHooks.ts  |       0 |        0 |       0 |       0 | 1-124             
  types.ts         |   91.18 |    92.04 |   85.71 |   91.18 | ...40-441,501-505 
  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           |   41.24 |    52.14 |   51.42 |   41.24 |                   
  ...nfigLoader.ts |   70.27 |    35.89 |   94.73 |   70.27 | ...20-422,426-432 
  ...ionFactory.ts |   42.69 |    79.16 |      50 |   42.69 | ...62-413,419-436 
  ...Normalizer.ts |   23.09 |    13.72 |   30.43 |   23.09 | ...04-905,909-924 
  ...verManager.ts |   25.31 |    62.06 |   41.66 |   25.31 | ...85-704,710-740 
  ...eLspClient.ts |   32.77 |       80 |   17.64 |   32.77 | ...84-288,294-295 
  ...LspService.ts |   48.49 |    67.16 |   65.71 |   48.49 | ...1352,1369-1379 
  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.52 |    86.66 |   86.36 |   79.52 |                   
  ...en-storage.ts |     100 |      100 |     100 |     100 |                   
  ...en-storage.ts |   82.87 |    82.35 |   92.85 |   82.87 | ...63-173,181-182 
  ...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        |      68 |    76.57 |   66.66 |      68 |                   
  const.ts         |     100 |      100 |     100 |     100 |                   
  dream.ts         |   65.65 |    73.33 |      50 |   65.65 | 50,107-148        
  ...entPlanner.ts |   57.84 |    72.72 |   33.33 |   57.84 | ...35,140-147,152 
  entries.ts       |   63.77 |    79.16 |      50 |   63.77 | ...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        |    45.8 |    61.53 |   44.44 |    45.8 | ...04,211,214-346 
  indexer.ts       |   83.87 |    45.45 |     100 |   83.87 | ...50,56-57,69-70 
  manager.ts       |   75.31 |    81.04 |    75.6 |   75.31 | ...1278,1291-1293 
  memoryAge.ts     |   90.47 |    77.77 |     100 |   90.47 | 50-51             
  paths.ts         |   55.47 |    89.47 |   85.71 |   55.47 | ...,89-90,106-114 
  prompt.ts        |   93.36 |    71.42 |     100 |   93.36 | ...58,161,228-229 
  recall.ts        |   77.54 |    69.38 |   88.88 |   77.54 | ...53-258,282-293 
  ...ceSelector.ts |   91.86 |    77.27 |     100 |   91.86 | ...15,117-118,126 
  scan.ts          |   87.91 |    68.42 |     100 |   87.91 | ...47-48,58,82-87 
  ...entPlanner.ts |    11.5 |      100 |       0 |    11.5 | ...57-192,210-298 
  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 |                   
  ...ontextFile.ts |   79.38 |    81.03 |   81.81 |   79.38 | ...58-272,286-291 
 src/mocks         |       0 |        0 |       0 |       0 |                   
  msw.ts           |       0 |        0 |       0 |       0 | 1-9               
 src/models        |   89.35 |    86.14 |    87.5 |   89.35 |                   
  constants.ts     |     100 |      100 |     100 |     100 |                   
  ...tor-config.ts |   90.24 |    91.42 |     100 |   90.24 | 142,148,151-160   
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...nfigErrors.ts |   74.22 |    47.82 |   84.61 |   74.22 | ...,67-74,106-117 
  ...igResolver.ts |   98.66 |    92.85 |     100 |   98.66 | 162,324,330       
  modelRegistry.ts |     100 |    98.59 |     100 |     100 | 222               
  modelsConfig.ts  |   84.57 |    82.14 |   81.57 |   84.57 | ...1223,1252-1253 
  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   |   71.18 |    88.76 |   48.57 |   71.18 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...on-manager.ts |   81.42 |    86.66 |      80 |   81.42 | ...29-830,837-846 
  rule-parser.ts   |   95.99 |    93.22 |     100 |   95.99 | ...-864,1013-1015 
  ...-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          |   83.87 |    77.23 |   95.83 |   83.87 |                   
  ...tGenerator.ts |   98.64 |    98.18 |     100 |   98.64 | 105-106           
  qwenOAuth2.ts    |   80.85 |    70.27 |   90.32 |   80.85 | ...1169-1185,1215 
  ...kenManager.ts |   83.76 |    76.22 |     100 |   83.76 | ...62-767,788-793 
 src/services      |   85.21 |    83.13 |   91.25 |   85.21 |                   
  ...ionTrailer.ts |     100 |      100 |     100 |     100 |                   
  ...llRegistry.ts |   98.44 |    91.83 |     100 |   98.44 | 268-269           
  ...ionService.ts |    95.6 |    96.36 |     100 |    95.6 | ...32,400,402-406 
  ...ingService.ts |   83.91 |       83 |   83.33 |   83.91 | ...1267,1284-1285 
  ...ttribution.ts |   91.73 |    87.71 |      90 |   91.73 | ...80-685,826-827 
  ...utSlimming.ts |     100 |    96.77 |     100 |     100 | 133,182           
  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 
  ...oryService.ts |   85.57 |    76.32 |    90.9 |   85.57 | ...1140,1181-1184 
  fileReadCache.ts |     100 |      100 |     100 |     100 |                   
  ...temService.ts |   91.27 |    82.69 |    90.9 |   91.27 | ...94,196,294-301 
  ...ratedFiles.ts |      96 |    88.23 |     100 |      96 | 119-120,146-147   
  gitInit.ts       |     100 |      100 |     100 |     100 |                   
  gitService.ts    |   68.75 |     92.3 |   55.55 |   68.75 | ...12-122,125-129 
  ...reeService.ts |   73.83 |    69.31 |    97.5 |   73.83 | ...1460,1488-1489 
  ...ionService.ts |   98.13 |     97.8 |   95.45 |   98.13 | ...32-333,380-381 
  ...orRegistry.ts |   96.54 |    91.73 |     100 |   96.54 | ...70-471,622-623 
  sessionRecap.ts  |   12.04 |      100 |       0 |   12.04 | 49-160            
  ...ionService.ts |   90.23 |     78.8 |   96.77 |   90.23 | ...1294,1298-1299 
  sessionTitle.ts  |   93.87 |    69.81 |     100 |   93.87 | ...33-236,267-268 
  ...ionService.ts |   81.07 |    77.92 |   89.28 |   81.07 | ...1923,1929-1934 
  ...UseSummary.ts |   94.73 |    87.71 |     100 |   94.73 | ...73-175,225-226 
  ...reeCleanup.ts |   14.56 |      100 |   33.33 |   14.56 | 58-185            
  ...ionService.ts |   84.21 |    79.41 |     100 |   84.21 | ...22-223,239-240 
 ...icrocompaction |   98.05 |     91.8 |     100 |   98.05 |                   
  microcompact.ts  |   98.05 |     91.8 |     100 |   98.05 | ...19,289,293,391 
 src/skills        |    87.5 |    83.86 |   94.23 |    87.5 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...activation.ts |     100 |     93.1 |     100 |     100 | 93,112            
  skill-load.ts    |   92.94 |    81.63 |     100 |   92.94 | ...06,226,238-240 
  skill-manager.ts |   83.31 |    79.66 |   90.32 |   83.31 | ...1120,1127-1131 
  skill-paths.ts   |   86.74 |    77.77 |     100 |   86.74 | ...00-101,106-107 
  symlinkScope.ts  |     100 |      100 |     100 |     100 |                   
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/subagents     |   83.13 |    80.24 |   95.23 |   83.13 |                   
  ...tin-agents.ts |     100 |      100 |     100 |     100 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...-selection.ts |     100 |      100 |     100 |     100 |                   
  ...nt-manager.ts |   77.21 |    72.09 |   92.85 |   77.21 | ...1180,1202-1203 
  types.ts         |     100 |      100 |     100 |     100 |                   
  validation.ts    |   92.46 |    95.18 |     100 |   92.46 | 51-56,69-74,78-83 
 src/telemetry     |   74.72 |    87.26 |   78.85 |   74.72 |                   
  config.ts        |     100 |      100 |     100 |     100 |                   
  constants.ts     |     100 |      100 |     100 |     100 |                   
  ...attributes.ts |   98.13 |       88 |     100 |   98.13 | 185-187           
  ...-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             
  ...-processor.ts |   93.93 |    90.21 |   94.11 |   93.93 | ...75-280,299-300 
  ...t.circular.ts |       0 |        0 |       0 |       0 | 1-128             
  loggers.ts       |    51.9 |       64 |   57.77 |    51.9 | ...1214,1231-1251 
  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           |   90.45 |    83.56 |   76.92 |   90.45 | ...17-318,338-342 
  ...on-context.ts |     100 |      100 |     100 |     100 |                   
  ...on-tracing.ts |   92.24 |    88.77 |     100 |   92.24 | ...21-424,522-525 
  ...etry-utils.ts |     100 |      100 |     100 |     100 |                   
  ...l-decision.ts |     100 |      100 |     100 |     100 |                   
  ...e-id-utils.ts |     100 |      100 |     100 |     100 |                   
  tracer.ts        |   98.61 |    89.36 |     100 |   98.61 | 53,108            
  types.ts         |   79.17 |    94.49 |   83.33 |   79.17 | ...1149,1152-1181 
  uiTelemetry.ts   |   92.97 |    96.96 |   81.25 |   92.97 | ...93-194,200-207 
 ...ry/qwen-logger |   68.24 |    79.56 |   64.91 |   68.24 |                   
  event-types.ts   |       0 |        0 |       0 |       0 |                   
  qwen-logger.ts   |   68.24 |    79.34 |   64.28 |   68.24 | ...1055,1093-1094 
 src/test-utils    |   93.16 |    95.91 |   76.47 |   93.16 |                   
  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.19 |    97.14 |   72.41 |   91.19 | ...38,202-203,216 
  ...aceContext.ts |     100 |      100 |     100 |     100 |                   
 src/tools         |    78.6 |    81.66 |    86.8 |    78.6 |                   
  ...erQuestion.ts |   88.93 |    76.74 |    90.9 |   88.93 | ...39-340,347-348 
  cron-create.ts   |   97.75 |    88.88 |   83.33 |   97.75 | 30-31             
  cron-delete.ts   |   96.82 |      100 |   83.33 |   96.82 | 26-27             
  cron-list.ts     |   96.66 |      100 |   83.33 |   96.66 | 25-26             
  diffOptions.ts   |     100 |      100 |     100 |     100 |                   
  edit.ts          |   80.52 |    85.98 |   73.33 |   80.52 | ...15-716,803-853 
  ...r-worktree.ts |   82.95 |    67.56 |    87.5 |   82.95 | ...82-185,276-277 
  exit-worktree.ts |   84.23 |    85.96 |   91.66 |   84.23 | ...92-293,298-312 
  exitPlanMode.ts  |   85.09 |    85.71 |     100 |   85.09 | ...60-163,177-189 
  glob.ts          |   90.63 |    88.33 |   84.61 |   90.63 | ...28,171,302,305 
  grep.ts          |   79.19 |    85.71 |   78.94 |   79.19 | ...20,560,569-576 
  ls.ts            |   96.74 |    90.27 |     100 |   96.74 | 176-181,212,216   
  lsp.ts           |   72.77 |    60.09 |   90.32 |   72.77 | ...1211,1213-1214 
  ...nt-manager.ts |   84.36 |    82.74 |   84.21 |   84.36 | ...2099-2103,2142 
  mcp-client.ts    |   33.18 |    77.65 |   66.66 |   33.18 | ...1490,1494-1497 
  mcp-tool.ts      |   90.98 |    88.88 |   96.42 |   90.98 | ...95-596,646-647 
  memory-config.ts |       0 |        0 |       0 |       0 | 1-47              
  ...iable-tool.ts |     100 |    84.61 |     100 |     100 | 102,109           
  monitor.ts       |   92.36 |    83.94 |      92 |   92.36 | ...29,558-561,574 
  ...nforcement.ts |   82.44 |       90 |     100 |   82.44 | 174-185,234-247   
  read-file.ts     |   95.09 |    88.75 |      90 |   95.09 | ...99,293-296,299 
  ripGrep.ts       |   94.59 |    85.71 |   93.33 |   94.59 | ...60,463,541-542 
  ...-transport.ts |    6.34 |        0 |       0 |    6.34 | 47-145            
  send-message.ts  |   89.32 |    91.66 |   83.33 |   89.32 | 44-45,68-76       
  shell.ts         |   72.96 |     79.6 |    91.3 |   72.96 | ...4216,4265-4271 
  skill-utils.ts   |     100 |      100 |     100 |     100 |                   
  skill.ts         |   88.11 |    91.17 |   84.61 |   88.11 | ...95,399,422-444 
  ...eticOutput.ts |   95.12 |      100 |      80 |   95.12 | 87-88             
  task-stop.ts     |   93.14 |    96.15 |   85.71 |   93.14 | 39-40,54-64       
  todoWrite.ts     |   89.17 |    82.05 |   92.85 |   89.17 | ...41-546,568-569 
  tool-error.ts    |     100 |      100 |     100 |     100 |                   
  tool-names.ts    |     100 |      100 |     100 |     100 |                   
  tool-registry.ts |   74.85 |    76.85 |   80.95 |   74.85 | ...30-831,839-840 
  tool-search.ts   |   95.19 |    86.48 |    92.3 |   95.19 | ...47-153,208-213 
  tools.ts         |   91.98 |    90.19 |   88.88 |   91.98 | ...50-451,467-473 
  web-fetch.ts     |   88.59 |    79.48 |    92.3 |   88.59 | ...12-313,315-316 
  write-file.ts    |   82.23 |    81.17 |   83.33 |   82.23 | ...65-668,680-715 
 src/tools/agent   |   75.01 |    82.55 |   74.62 |   75.01 |                   
  agent.ts         |   75.29 |    82.86 |    75.4 |   75.29 | ...2203,2265-2272 
  fork-subagent.ts |   69.62 |    71.42 |   66.66 |   69.62 | ...04-105,140-151 
 src/utils         |   88.98 |    87.56 |   93.69 |   88.98 |                   
  LruCache.ts      |       0 |        0 |       0 |       0 | 1-41              
  ...ssageQueue.ts |     100 |      100 |     100 |     100 |                   
  ...cFileWrite.ts |   77.96 |    80.48 |     100 |   77.96 | ...35,156,173-176 
  bareMode.ts      |   27.27 |      100 |       0 |   27.27 | 9-15,18-19        
  browser.ts       |    7.69 |      100 |       0 |    7.69 | 17-56             
  bundlePaths.ts   |     100 |      100 |     100 |     100 |                   
  ...igResolver.ts |     100 |      100 |     100 |     100 |                   
  ...engthError.ts |   89.11 |    86.66 |     100 |   89.11 | ...28-129,132-133 
  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   |    95.9 |    93.84 |   94.73 |    95.9 | 106-107,214-218   
  editHelper.ts    |   93.63 |    83.52 |     100 |   93.63 | ...28-429,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.45 |     100 |     100 | 83                
  errorParsing.ts  |    97.7 |    97.05 |     100 |    97.7 | 72-73             
  ...rReporting.ts |   88.46 |       90 |     100 |   88.46 | 69-74             
  errors.ts        |   70.92 |       80 |   53.33 |   70.92 | ...03-219,223-229 
  fetch.ts         |   70.18 |    71.42 |   71.42 |   70.18 | ...42,148,161,186 
  fileUtils.ts     |   91.46 |    86.19 |   95.23 |   91.46 | ...1188,1192-1198 
  forkedAgent.ts   |    78.5 |    70.73 |   85.71 |    78.5 | ...30-436,441-447 
  formatters.ts    |   81.81 |       75 |     100 |   81.81 | 15-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             
  gitDiff.ts       |   92.36 |    79.53 |     100 |   92.36 | ...55-856,928-929 
  ...noreParser.ts |    92.3 |    89.36 |     100 |    92.3 | ...15-116,186-187 
  gitUtils.ts      |   56.66 |    85.71 |      75 |   56.66 | ...2,72-73,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   |    74.1 |    90.76 |   58.33 |    74.1 | ...23-326,336-342 
  ...-detection.ts |     100 |      100 |     100 |     100 |                   
  ...iagnostics.ts |   96.87 |    91.83 |     100 |   96.87 | 214-219,272       
  ...yDiscovery.ts |    83.9 |    79.36 |     100 |    83.9 | ...16,319,411-414 
  ...tProcessor.ts |   93.63 |       90 |     100 |   93.63 | ...96-302,384-385 
  ...Inspectors.ts |   61.53 |      100 |      50 |   61.53 | 18-23             
  modelId.ts       |   98.55 |    96.87 |     100 |   98.55 | 103               
  ...kerChecker.ts |   88.75 |    85.71 |     100 |   88.75 | 69-70,87-93       
  notebook.ts      |   94.35 |    84.78 |     100 |   94.35 | ...10,122,174-176 
  openaiLogger.ts  |   88.05 |    84.09 |     100 |   88.05 | ...44-146,169-174 
  partUtils.ts     |     100 |    98.61 |     100 |     100 | 206               
  pathReader.ts    |     100 |      100 |     100 |     100 |                   
  paths.ts         |   93.21 |    91.86 |     100 |   93.21 | ...89-390,392-394 
  pdf.ts           |   93.68 |    87.05 |     100 |   93.68 | ...96-297,321-325 
  projectPath.ts   |     100 |      100 |     100 |     100 |                   
  ...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     |   92.55 |    85.92 |     100 |   92.55 | ...70-272,309-310 
  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.79 |    84.37 |   66.66 |   46.79 | ...45-246,258-335 
  ...sDiscovery.ts |   97.42 |    92.85 |     100 |   97.42 | ...04,182-183,202 
  ...tchOptions.ts |   81.72 |    85.04 |   95.23 |   81.72 | ...11,536,565-574 
  runtimeStatus.ts |    97.5 |    88.57 |     100 |    97.5 | 167-168           
  safeJsonParse.ts |   74.07 |    83.33 |     100 |   74.07 | 40-46             
  ...nStringify.ts |     100 |      100 |     100 |     100 |                   
  ...aConverter.ts |   90.78 |    88.23 |     100 |   90.78 | ...41-42,93,95-96 
  ...aValidator.ts |   94.57 |    80.26 |     100 |   94.57 | ...04,213-216,270 
  ...r-launcher.ts |   76.92 |     91.3 |   66.66 |   76.92 | ...34,136,157-195 
  ...orageUtils.ts |   96.89 |    85.84 |     100 |   96.89 | ...51,367,447,466 
  shell-utils.ts   |   82.93 |    89.89 |     100 |   82.93 | ...1522,1529-1533 
  ...lAstParser.ts |   95.58 |    85.79 |     100 |   95.58 | ...1059-1061,1071 
  ...nlyChecker.ts |   95.75 |    92.39 |     100 |   95.75 | ...00-301,313-314 
  sideQuery.ts     |   98.73 |    94.59 |     100 |   98.73 | 111               
  ...pEventSink.ts |     100 |       80 |     100 |     100 | 61                
  ...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 |    89.28 |   93.33 |   93.71 | ...24-225,249-251 
  xml.ts           |     100 |      100 |     100 |     100 |                   
  yaml-parser.ts   |      92 |    84.61 |     100 |      92 | 49-53,65-69       
 ...ils/filesearch |   86.21 |    81.61 |   96.42 |   86.21 |                   
  crawlCache.ts    |     100 |      100 |     100 |     100 |                   
  crawler.ts       |   82.84 |    77.49 |   94.82 |   82.84 | ...1451,1485-1486 
  fileSearch.ts    |   93.58 |    87.32 |     100 |   93.58 | ...46-247,249-250 
  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.

Two small audit-driven cleanups, no behavior change in normal sessions:

- Match findSnapshot's last-occurrence-wins semantics so /rewind and
  /diff agree if a promptId is ever reused (defensive — promptIds are
  unique per submission in practice).
- Drop the redundant `?? undefined` in the fast-path skip; `?.` already
  short-circuits to undefined, so the extra coalesce was noise.
@BZ-D BZ-D requested review from doudouOUC and wenshao May 18, 2026 09:16
Long absolute paths (~> 90 chars) previously overflowed the dialog and
wrapped, shattering the file-list and detail-view alignment. Reserve a
fixed budget for the tag/stats columns, head-truncate the path with a
leading ellipsis so the basename — the part users actually read — is
always visible.

Also drop the dead MAX_FILES_FOR_DETAILS guard from currentToFiles:
fetchGitDiff already bounds perFileStats at MAX_FILES (=50), and returns
an empty map when the diff exceeds MAX_FILES_FOR_DETAILS upstream, so
the 500-entry counter could never fire.
Comment thread packages/core/src/services/fileHistoryService.ts Outdated
Comment thread packages/cli/src/ui/components/DiffDialog.tsx
Comment thread packages/core/src/services/fileHistoryService.ts
Comment thread packages/cli/src/ui/components/DiffDialog.tsx Outdated
}

let cancelled = false;
setLoading(true);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] Opening /diff eagerly computes getTurnDiff() for every real user turn. With up to 100 retained snapshots, this can fan out into many backup reads and structured diff computations before the user selects any historical turn.

Please load per-turn diffs lazily for the selected turn, or load only lightweight metadata initially and cache/limit concurrent getTurnDiff() calls.

— gpt-5.5 via Qwen Code /review

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.

Acknowledged — deferring to a follow-up PR. Rationale:

  1. The critical memory risk that motivated lazy loading is now mitigated at the source by the per-file MAX_DIFF_SIZE_BYTES cap (see #3257766252 reply). Per-turn fan-out is bounded by MAX_SNAPSHOTS = 100 and the makeSnapshot fast-path skip (unchanged backup pointers → no I/O) keeps the typical-session cost low.
  2. Going lazy means restructuring the source-switcher tab labels (which currently rely on per-turn presence to filter empty turns) into a two-phase "list candidates → fetch on focus" flow. That's a meaningful UX redesign and worth landing in its own PR.

Will open a follow-up issue tagged perf after this merges. Marking as deferred rather than resolved so it stays on your radar.

setLoading(true);
Promise.all([
fetchGitDiff(cwd).catch(() => null),
fetchGitDiffHunks(cwd).catch(() => new Map<string, Hunk[]>()),
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] The hook fetches full current diff hunks as soon as the dialog mounts, even while the user is only viewing the file list or may switch to a per-turn source. fetchGitDiffHunks() runs git diff HEAD and buffers/parses the full output before parser caps apply, so opening /diff can pay a large hunk-generation cost unnecessarily.

Please fetch current hunks lazily when entering detail mode for the selected current file, or add a scoped hunk API for one file and call it on demand.

— gpt-5.5 via Qwen Code /review

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.

Acknowledged — deferring to the same follow-up as #3257766265.

For the immediate memory concern, fetchGitDiffHunks is bounded by MAX_FILES (50) and MAX_DIFF_SIZE_BYTES (1 MB per file) inside parseGitDiff, so the worst-case eager fetch is ~50 MB stdout buffer (capped by runGit's maxBuffer). Adding a single-file scoped hunk API would be cleaner but is a non-trivial new surface on the core utility — better to bundle it with the lazy-load refactor.

Comment thread packages/cli/src/ui/AppContainer.tsx
Comment thread packages/core/src/services/fileHistoryService.ts Outdated
Comment thread packages/core/src/services/fileHistoryService.ts Outdated
…p, sanitization, Ctrl+C routing

Five review-driven fixes; details inline on the PR.

Core (getTurnDiff):
- Treat unreadable backup files as "unavailable" (return null for the
  row) instead of coercing to '' and fabricating phantom hunks. Same
  guard for both before and after endpoints.
- Cap structuredPatch input at MAX_DIFF_SIZE_BYTES so a single multi-MB
  file in history can no longer balloon TUI memory when /diff opens.
  Oversized rows still surface in the file list with best-effort line
  stats and a new `oversized` flag.

CLI (DiffDialog):
- Distinguish over-large dirty trees (filesCount > 0 but empty
  perFileStats) from a clean tree; the empty state now reports the
  capped file count and totals instead of claiming "Working tree is
  clean."
- Render the `oversized` flag with an explicit "(oversized — diff
  omitted)" tag in the file list and a corresponding detail-view note.

Sanitization (#4):
- Move sanitizeFilenameForDisplay from diffCommand.ts into the shared
  textUtils module, apply it to every path rendered in DiffDialog
  (file rows, detail header, empty messages, DiffRenderer filename
  prop, generated unified-diff envelope), and keep raw paths for map
  lookups via a separate UnifiedFile.displayPath field.

Ctrl+C routing (#7):
- Register isDiffDialogOpen / closeDiffDialog with useDialogClose so
  Ctrl+C dismisses the dialog through the centralized handleExit path,
  matching how the background-tasks dialog is wired. Drop the dialog's
  internal Ctrl+C handler to avoid double-fire that would close the
  dialog AND escalate to the exit prompt.

Tests: 2 new core regression tests (unreadable backup, oversized cap)
plus the existing 35 still pass. CLI tests for diff/slashCommand/
AppContainer paths unchanged at 148/148.
Comment thread packages/cli/src/ui/components/DiffDialog.tsx Outdated
Comment thread packages/core/src/services/fileHistoryService.test.ts
Comment thread packages/cli/src/ui/hooks/useDiffData.ts
Comment thread packages/cli/src/ui/hooks/useTurnDiffs.ts Outdated
Comment thread packages/cli/src/ui/components/DiffDialog.tsx Outdated
Comment thread packages/cli/src/ui/components/DiffDialog.tsx Outdated
Copy link
Copy Markdown
Collaborator

@doudouOUC doudouOUC left a comment

Choose a reason for hiding this comment

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

Review 总结

整体设计思路清晰:核心逻辑下沉到 FileHistoryService.getTurnDiff(),UI 通过两个独立 hook 消费数据,职责划分合理。diffCommand 的交互模式从「执行 git + 渲染」改为「打开对话框」的重构方向也是正确的。

但在实现细节上存在 2 个 P0 缺陷会导致功能不完整(新建/删除文件在 per-turn diff 中被静默丢弃),需要在合并前修复。

必须修复(P0/P1)

# 文件 问题
1 fileHistoryService.ts Fast-path 当 beforeafter 都不含某文件时误判为「无变化」,新建文件被丢弃
2 fileHistoryService.ts afterBackup 缺失时 fallback 用 beforeContent 代表 after 状态,文件删除事件被忽略
3 DiffDialog.tsx isNewFile === isUntracked 语义错误,混淆 staged new file 与 untracked file
4 fileHistoryService.test.ts 缺少新建文件、删除文件、failed backup 的测试用例

建议修复(P2/P3)

# 文件 问题
5 useDiffData.ts "Current" tab 只在 mount 时 fetch 一次,数据陈旧;若有意为之请加注释说明
6 useTurnDiffs.ts 竞态保护不完整,已取消的 effect 仍有在途 I/O
7 DiffDialog.tsx hunksToUnifiedDiff 对空 hunks 返回仅含 header 的非空字符串,绕过调用方的空字符串保护
8 DiffDialog.tsx useEffect 做 state 修正会多触发一帧渲染

Copy link
Copy Markdown
Collaborator

@wenshao wenshao left a comment

Choose a reason for hiding this comment

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

⚠️ CI failing: Test (windows-latest, Node 22.x) — review based on static analysis only.

Dead code from removed interactive diff path

The old interactive /diff path was removed from diffCommand.ts, but HistoryItemDiffStats type, MessageType.DIFF_STATS enum value, DiffStatsDisplay component + test, and diff_stats case handlers in HistoryItemDisplay.tsx and historyUtils.ts remain as dead code — nothing in the codebase creates diff_stats items anymore. These should be cleaned up to avoid maintenance burden and confusion.

Three new files (~715 lines) have zero tests

DiffDialog.tsx (558 lines), useTurnDiffs.ts (97 lines), and useDiffData.ts (60 lines) are brand new with no corresponding test files. The dialog contains 10+ keyboard navigation branches, 4 empty-state branches, tab switching, view mode toggling, and data transformation pipelines — all untested.

— DeepSeek/deepseek-v4-pro via Qwen Code /review

Comment thread packages/cli/src/ui/components/DiffDialog.tsx Outdated
Comment thread packages/cli/src/ui/components/DiffDialog.tsx
Comment thread packages/cli/src/ui/components/DiffDialog.tsx
Comment thread packages/core/src/services/fileHistoryService.ts
Comment thread packages/cli/src/ui/components/DiffDialog.tsx
promptPreview: previewOfUserItem(item),
diff,
} satisfies TurnDiffEntry;
} catch {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] useTurnDiffs eagerly loads ALL turn diffs at once via Promise.all(userTurns.map(...)) on dialog open. For sessions with 50+ turns, this blocks the UI with "Loading diff..." until every getTurnDiff completes. Consider incremental loading — at least render results as they arrive instead of waiting for all.

Additionally, the useEffect dependency array includes history, causing a full refetch on any history array reference change (e.g., during streaming).

— DeepSeek/deepseek-v4-pro via Qwen Code /review

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.

Deferring to the same lazy-load follow-up as #3257766265. For the immediate concerns:

  • history ref stability: while the dialog is open the composer is muted, so HistoryManager's useState reference is stable — the effect doesn't actually refire spuriously in normal flow.
  • 'Loading diff...' blocking the entire list: in b0428c5 the per-file cap (MAX_TURN_DIFF_FILES = 500) plus binary/oversized short-circuits bring typical cold-start under a second. Incremental rendering would still be a UX win for very-long sessions and will land with the lazy refactor.

);
}

function FileDetail({
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] turnFileToUnified hardcodes truncated: false. Unlike perFileToUnified which checks MAX_LINES_PER_FILE (400), turn diffs have no line-count guard. A turn modifying a 10,000-line generated file will render the full diff with no truncation indicator.

Add MAX_LINES_PER_FILE check and expose a truncated flag on TurnFileDiff.

— DeepSeek/deepseek-v4-pro via Qwen Code /review

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.

Partially addressed. For oversized files the new oversized: true flag (landed in 9a63bb6) skips hunk generation entirely — so a 10,000-line turn-diff would either be oversized (>1 MB) or render fully. Adding a separate line-count truncated flag below the byte cap would be a clearer UX signal; I'll bundle it with the lazy-load follow-up rather than carry yet another flag through the pipeline in this PR. Tracking in the deferred-perf thread.

Comment thread packages/core/src/services/fileHistoryService.ts
Comment thread packages/cli/src/ui/components/DiffDialog.tsx
Comment thread packages/cli/src/ui/hooks/useDiffData.ts
Copy link
Copy Markdown
Collaborator

@wenshao wenshao left a comment

Choose a reason for hiding this comment

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

Test submission — single comment to identify line resolution issue.

Comment thread packages/cli/src/ui/components/DiffDialog.tsx
Copy link
Copy Markdown
Collaborator

@wenshao wenshao left a comment

Choose a reason for hiding this comment

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

Batch 2 test.

Comment thread packages/cli/src/ui/components/DiffDialog.tsx
Comment thread packages/cli/src/ui/components/DiffDialog.tsx Outdated
Comment thread packages/core/src/services/fileHistoryService.ts
Copy link
Copy Markdown
Collaborator

@wenshao wenshao left a comment

Choose a reason for hiding this comment

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

Batch 3 test.

Comment thread packages/cli/src/ui/components/DiffDialog.tsx
Comment thread packages/cli/src/ui/components/DiffDialog.tsx Outdated
Comment thread packages/core/src/services/fileHistoryService.ts Outdated
Copy link
Copy Markdown
Collaborator

@wenshao wenshao left a comment

Choose a reason for hiding this comment

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

Batch 4 test.

Comment thread packages/cli/src/ui/hooks/useTurnDiffs.ts Outdated
Comment thread packages/cli/src/ui/hooks/useTurnDiffs.ts
Comment thread packages/cli/src/ui/components/DialogManager.tsx
Copy link
Copy Markdown
Collaborator

@wenshao wenshao left a comment

Choose a reason for hiding this comment

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

⚠️ CI failing: Test (windows-latest, Node 22.x)

Critical issues to fix before merge:

  1. [typecheck] TurnDiff and TurnFileDiff are exported from fileHistoryService.ts but not re-exported from the core package's index.ts. Consumers get TS2305.
  2. [review] DiffDialog.tsx, useTurnDiffs.ts, and useDiffData.ts have zero test coverage — every other dialog and hook has tests.
  3. [review] fileHistoryService.test.ts is missing boundary cases: isDeleted flag, afterBackup.failed skip, same-backup fast-path optimization.
  4. [review] Dead code: HistoryItemDiffStats type, DiffStatsDisplay component, DIFF_STATS union member — the interactive path returns { type: 'dialog' } instead.
  5. [Suggestion] diffCommand.ts non-interactive error path exposes raw error.message in user-visible output. Replace with a generic label.

Detection: tsc (3 actionable type errors), eslint clean, 7 review agents (Correctness/Security/CodeQuality/Performance/TestCoverage/Oncall/Maintainer). Core tests 35/35, CLI diffCommand 25/25.

Comment thread packages/cli/src/ui/components/DiffDialog.tsx
Comment thread packages/cli/src/ui/components/DiffDialog.tsx
Comment thread packages/cli/src/ui/components/DiffDialog.tsx
Comment thread packages/cli/src/ui/components/DiffDialog.tsx Outdated
Comment thread packages/cli/src/ui/components/DiffDialog.tsx Outdated
Comment thread packages/core/src/services/fileHistoryService.ts
Comment thread packages/core/src/services/fileHistoryService.ts Outdated
Comment thread packages/cli/src/ui/hooks/useTurnDiffs.ts Outdated
Comment thread packages/cli/src/ui/hooks/useTurnDiffs.ts
);
}

if (uiState.isDiffDialogOpen) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] No Error Boundary wraps DiffDialog. A render exception crashes the entire Ink TUI to shell with no visible stack trace.

— DeepSeek/deepseek-v4-pro via Qwen Code /review

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.

Deferring — no ErrorBoundary exists anywhere in packages/cli/src today (grep -r componentDidCatch returns zero hits). Adding one for just this dialog would introduce a class-component pattern that doesn't exist in the otherwise pure-functional codebase, and the right answer is a uniform boundary at the DialogManager level — both out of scope for this PR.

The other correctness gates landed in f9667f2 (per-file try/catch in computeTurnFileDiff, swallow-on-fail in useTurnDiffs) substantially reduce the surface that would actually crash the dialog at runtime. I'll open a follow-up issue tagged scope/components for the global boundary.

Comment thread packages/core/src/services/fileHistoryService.ts Outdated
Comment thread packages/core/src/services/fileHistoryService.ts Outdated
Comment thread packages/cli/src/ui/components/DiffDialog.tsx Outdated
…, semantics

Addresses 14 of 19 outstanding review comments. Per-thread detail will
be replied on the PR.

Correctness (P0):
- Restrict getTurnDiff candidates to keys(target.trackedFileBackups).
  Files first tracked in turn N+1 no longer get phantom-attributed to
  turn N. Drop the now-redundant union with state.trackedFiles for the
  latest-turn case (makeSnapshot guarantees state.trackedFiles ⊆
  keys(latest.trackedFileBackups)).
- Add `beforeBackup !== undefined` guard to the fast-path skip so a
  future broadening of the candidate set can't silently collapse a
  newly created file as "unchanged".
- Add binary detection via NUL-byte sniff (`looksBinary`, mirrors git's
  heuristic). New `TurnFileDiff.isBinary` flag short-circuits hunk
  generation; the dialog renders the existing italic "binary" marker
  instead of feeding raw bytes to DiffRenderer.
- Cap per-turn concurrent file reads at MAX_TURN_DIFF_FILES=500 so a
  500-file turn won't issue 1000+ simultaneous open()s and hit the
  process fd ceiling.

UX / stability:
- Stabilize the dialog's keypress handler with `useCallback(()=>..,[])`
  reading state via refs, eliminating subscribe/unsubscribe churn on
  every render.
- Disentangle `isNewFile` (snapshot-derived, "added in this turn")
  from `isUntracked` (git "never tracked") in perFileToUnified so
  untracked files no longer get mislabeled as "(new)" — they could
  not be recovered by /rewind, and the wrong tag implied otherwise.
- Reorder FileRow tag priority around the disentangled flags; remove
  duplicate "(binary)" tag (the stats column already shows it italic).
- Drop the early-exit `useEffect` clamps for sourceIndex / fileIndex
  in favor of inline `Math.min` derivations; effect-based clamping
  caused an extra render frame that could look like a flicker in Ink.
- Inner `cancelled` checks in useTurnDiffs reduce wasted disk I/O when
  the dialog is closed mid-load.
- Guard hunksToUnifiedDiff against empty hunk arrays (would otherwise
  hand DiffRenderer a header-only string).
- Surface "…and N more (showing first M)" indicator for the Current
  source when fetchGitDiff capped perFileStats at MAX_FILES.
- useDiffData JSDoc clarifies the snapshot-at-open semantics; catch
  branches now console.debug the underlying error instead of swallowing
  silently.

Tests:
- 3 new core regression tests: deleted-during-turn detection, binary
  detection, and the cross-turn attribution boundary. fileHistoryService
  tests now at 40/40.

Pending review comments (deferred): the lazy-load suggestions remain
intentionally deferred per the earlier reply chain; the MAX_DIFF_SIZE
cap landed in the prior round mitigated the underlying memory risk.
Comment thread packages/core/src/services/fileHistoryService.ts Outdated
// the same constant the git path uses (MAX_FILES_FOR_DETAILS = 500
// files total), with two reads each → 1000 open()s worst case, still
// comfortably below the typical 4096 fd ceiling.
const cappedPaths = candidatePaths.slice(0, MAX_TURN_DIFF_FILES);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] candidatePaths.slice(0, MAX_TURN_DIFF_FILES) 静默截断且顺序不确定

  1. 截断到 500 个文件无 debugLogger.warn,无 UI 提示(hiddenFileCount 模式仅用于 "Current" 源)。
  2. Object.keys() 迭代顺序在 JS 引擎间不保证一致,导致截断结果不可复现。
Suggested change
const cappedPaths = candidatePaths.slice(0, MAX_TURN_DIFF_FILES);
const sorted = candidatePaths.slice().sort((a, b) => a.localeCompare(b));
if (sorted.length > MAX_TURN_DIFF_FILES) {
debugLogger.warn(
`getTurnDiff truncating ${sorted.length - MAX_TURN_DIFF_FILES} files ` +
`for prompt ${promptId} (cap: ${MAX_TURN_DIFF_FILES})`,
);
}
const cappedPaths = sorted.slice(0, MAX_TURN_DIFF_FILES);

— DeepSeek/deepseek-v4-pro via Qwen Code /review

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.

Fixed in 5cf48b0.

candidatePaths is now sorted via localeCompare before the
MAX_TURN_DIFF_FILES slice — even though Object.keys() is spec-defined
as insertion order, sorting makes the kept-vs-dropped split reproducible
regardless of how the snapshot was populated (live session vs. resumed
from disk). When the cap kicks in we now emit a
debugLogger.warn('FileHistory: getTurnDiff truncating <N> files for prompt <id> (cap: 500)') so the dropped count is at least traceable in
--debug runs.

Surface-level UI signal stays deferred to the same lazy-load follow-up
as #3257997114 — hiddenFileCount would still be the natural home, but
plumbing it across the per-turn source pipeline (which only ever sees
the post-cap file list) is out of scope for this PR.

hunks: [],
isNewFile: !beforeExists && afterExists,
isDeleted: beforeExists && !afterExists,
linesAdded: 0,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] 二进制检测返回 linesAdded: 0, linesRemoved: 0 导致统计不一致

二进制提前返回硬编码零值统计。仅修改二进制文件的回合在对话框头部显示 "+0 -0",与 "Current" git 源的真实统计不一致。

Suggested change
linesAdded: 0,
// Compute best-effort line counts even for binary files
const beforeLines = beforeExists ? countLines(beforeContent) : 0;
const afterLines = afterExists ? countLines(afterContent) : 0;
return {
filePath,
hunks: [],
isNewFile: !beforeExists && afterExists,
isDeleted: beforeExists && !afterExists,
linesAdded: Math.max(0, afterLines - beforeLines),
linesRemoved: Math.max(0, beforeLines - afterLines),
oversized: false,
isBinary: true,
};

— DeepSeek/deepseek-v4-pro via Qwen Code /review

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.

Not adopting — the proposed change would actually introduce the
inconsistency. The "Current" git source also reports +0 / -0 for
binary files (see parseGitNumstat in packages/core/src/utils/gitDiff.ts:367-368):

const isBinary = addStr === '-' || remStr === '-';
const fileAdded = isBinary ? 0 : parseInt(addStr, 10) || 0;
const fileRemoved = isBinary ? 0 : parseInt(remStr, 10) || 0;

git diff --numstat itself emits -\t-\tpath for binary files, and we
already collapse that to 0/0 in stats while flagging isBinary: true.
That matches the well-established convention you see in git diff --stat
("Bin <N> -> <M> bytes" rather than line counts) — counting lines of a
binary blob is meaningless and would produce mismatched semantics
between the two sources.

Per-turn +0 / -0 for binary files is therefore consistent with what
Current shows for the same file, which I think is the more important
invariant. Leaving as-is.

// A non-null backup name that fails to read means we cannot produce a
// trustworthy "before" content — fabricating an empty string would
// present every line as a fresh addition. Skip the row instead.
if (beforeRead === 'unreadable') return null;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] 不可读端点静默丢弃,零诊断信号

readEndpointContent 返回 'unreadable'(备份文件损坏/权限变更/竞态删除)时,computeTurnFileDiff 返回 null 且无 debugLogger.warn。该文件已有的 debugLogger 导入,添加日志成本为零。

Suggested change
if (beforeRead === 'unreadable') return null;
if (beforeRead === 'unreadable') {
debugLogger.warn(
`Skipping turn diff for ${trackingPath}: before backup is unreadable`,
);
return null;
}

afterRead === 'unreadable'(L798)同理。

— DeepSeek/deepseek-v4-pro via Qwen Code /review

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.

Fixed in 5cf48b0. Both unreadable branches now emit a debugLogger.warn
before dropping the row:

FileHistory: skipping turn diff for <path>: before backup unreadable
FileHistory: skipping turn diff for <path>: after <worktree|backup> unreadable

We still drop the row rather than fabricate an empty-string endpoint
(that's the bug the prior round fixed), but now the drop leaves a
trace in --debug so backup corruption, permission flips, and
EISDIR races are diagnosable instead of invisible.

Added warn: vi.fn() to the mocked createDebugLogger in
fileHistoryService.test.ts so the new call paths don't crash the
existing regressions.

Promise.all([
fetchGitDiff(cwd).catch((err) => {
// eslint-disable-next-line no-console
console.debug('[DiffDialog] fetchGitDiff failed:', err);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] console.debug 应使用 createDebugLogger 匹配项目约定

项目有 177 处 createDebugLogger 调用;此处用裸 console.debug 并压制 ESLint 规则。createDebugLogger 提供一致的过滤和日志行为。

Suggested change
console.debug('[DiffDialog] fetchGitDiff failed:', err);
const log = createDebugLogger('useDiffData');
// ...
fetchGitDiff(cwd).catch((err) => {
log.debug('fetchGitDiff failed:', err);
return null;
}),

— DeepSeek/deepseek-v4-pro via Qwen Code /review

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.

Fixed in 5cf48b0. useDiffData now imports createDebugLogger from
@qwen-code/qwen-code-core and uses a DiffDialog-tagged logger
instead of the bare console.debug + eslint-disable pair:

const debugLogger = createDebugLogger('DiffDialog');
// …
fetchGitDiff(cwd).catch((err) => {
  debugLogger.debug(`fetchGitDiff failed: ${err}`);
  return null;
}),

Aligns with the other 177 usages in the codebase and inherits the
project's debug-namespace filtering.

}
if (name === 'return') {
const sel = selectedFileRef.current;
if (sel && !sel.isBinary && !sel.oversized) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] 对二进制/超尺寸文件按 Enter 无视觉反馈

if (sel && !sel.isBinary && !sel.oversized) 在条件不满足时静默消费按键。虽然文件行标记了原因,但按键本身无反馈可使用户怀疑对话框冻结。

建议在守卫中添加提示:例如在对话框底部短暂显示 "Binary files cannot be previewed" 或 "File diff omitted (oversized)"。

— DeepSeek/deepseek-v4-pro via Qwen Code /review

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.

Fixed in 5cf48b0. The dialog now surfaces a transient warning-colored
hint in the footer when Enter lands on a non-enterable row, instead of
silently consuming the keypress:

  • Binary file → Binary file — no diff to view.
  • Oversized file → Oversized file — diff omitted. Use \git diff` to inspect.`
  • No hunks (e.g. untracked / capped) → No diff content available for this file.

The hint replaces the regular nav line (←/→ source · ↑/↓ file · …) and
clears the moment the user presses any navigation key, so it doesn't
linger past their response. Color comes from theme.status.warning to
match the existing nudge surfaces.

Implementation: a single keyHint state, surfaced via setKeyHintRef
so the stable useCallback([], ...) keypress handler can write it
without forcing the callback to re-create.

… binary tail scan

Six review-driven correctness fixes; details inline on the PR.

Core:
- `readEndpointContent` now distinguishes ENOENT (genuine deletion) from
  other read failures (EACCES/EISDIR/EBUSY/decoding) on the live
  worktree branch. Previously every failure collapsed to `exists:false`
  and produced a phantom delete hunk for files whose perms changed
  mid-session.
- `computeTurnFileDiff` is wrapped in per-file try/catch so a single
  `structuredPatch` crash or transient read error can no longer poison
  the whole turn's `Promise.all` and silently erase every row.
- `looksBinary` now scans both the head AND the tail of the string. The
  head-only scan could be defeated by an 8KB+ text prefix in front of a
  binary payload; the oversized cap (1 MB) bounds the work either way.
- `getTurnDiff` calls the existing `findSnapshotIndex` helper instead of
  inlining a duplicate reverse-scan loop, so a future change to
  `findSnapshot`'s tie-break rules can't silently desync /rewind and /diff.

UI:
- Add `hasHunks` to `UnifiedFile` and gate Enter on it. Untracked files
  don't appear in `git diff HEAD` output, and capped/oversized turn
  entries have empty hunks — pressing Enter on those previously landed
  the user on a dead-end "No hunks available" screen.
- Drop the misleading `total > MAX_LINES_PER_FILE` heuristic from
  `perFileToUnified`'s `truncated` flag. `s.truncated` (from
  `parseGitNumstat`) is the only authoritative source — the OR was
  conflating "untracked file too big to count" with "tracked file with
  many accurately-counted lines", incorrectly flagging the latter.

Tests:
- 1 new core regression test: live-worktree EISDIR failure must not be
  reported as a deletion. fileHistoryService tests now at 41/41.
@BZ-D BZ-D requested review from doudouOUC and wenshao May 19, 2026 06:57
@BZ-D
Copy link
Copy Markdown
Collaborator Author

BZ-D commented May 19, 2026

Hi @wenshao @doudouOUC — re-requesting review. The latest commit f9667f2cf addresses the remaining round of feedback:

  • #3258405777 ENOENT vs. unreadable on live-worktree reads
  • #3258193880 / #3258193893 per-file isolation in computeTurnFileDiff (single failure no longer drops the whole turn)
  • #3259062403 binary sniff now scans both head and tail
  • #3258193886 getTurnDiff calls shared findSnapshotIndex helper instead of inlining the loop
  • #3258405784 Enter is gated on hasHunks so untracked / capped rows no longer dead-end
  • #3258193875 dropped the misleading total > MAX_LINES_PER_FILE heuristic from the truncated flag
  • Stale-snapshot review items (TS2305, implicit-any) replied inline with verification

Total across the PR: 45 / 50 review threads resolved. Open threads are intentionally deferred (lazy-load perf follow-up + global ErrorBoundary) with replies explaining the rationale.

Tests: core 41/41, CLI 148/148, typecheck + lint clean on both packages.

# Conflicts:
#	packages/cli/src/ui/AppContainer.tsx
#	packages/cli/src/ui/hooks/useDialogClose.ts
return true;
}

if (options.isDiffDialogOpen && options.closeDiffDialog) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] Priority inversion with background-tasks dialog in Ctrl+C handler.

DialogManager.tsx renders DiffDialog before BackgroundTasksDialog (so when both flags are true, the user sees the diff dialog). But this hook checks isBackgroundTasksDialogOpen (line 152) before isDiffDialogOpen (this line). When both are open, Ctrl+C dismisses the background-tasks dialog while the user sees the diff dialog on screen — the user must press Ctrl+C a second time.

Consider swapping the order so the diff dialog check appears before the background-tasks check, matching the rendering priority in DialogManager.tsx.

— qwen-latest-series-invite-beta-v28 via Qwen Code /review

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.

Fixed in 5cf48b0. useDialogClose now checks isDiffDialogOpen before
isBackgroundTasksDialogOpen, matching DialogManager's render order
(DiffDialog block at L490, BackgroundTasksDialog at L507). With both
flags true, Ctrl+C now dismisses the diff dialog — the thing the user
actually sees — instead of silently closing the background-tasks
dialog hidden behind it. Single-keypress behavior restored.

Added a short comment in the hook noting the invariant so the next
person who reorders these doesn't reintroduce the inversion.


function previewOfUserItem(item: HistoryItem): string {
if (item.type !== 'user' || !item.text) return '';
const oneLine = item.text.replace(/\s+/g, ' ').trim();
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] previewOfUserItem reads raw item.text without ANSI escape sanitization — bypasses the defense applied by the chat history display path.

The primary chat rendering applies escapeAnsiCtrlCodes to history items (HistoryItemDisplay.tsx:111). This function reads the same HistoryItem.text but skips that sanitization. When the prompt preview is rendered in the DiffDialog header, any ANSI/OSC escape sequences in the user's prompt text (e.g., from pasted terminal output with color codes, or crafted prompts containing OSC 8 hyperlinks) reach the terminal unsanitized.

Suggested change
const oneLine = item.text.replace(/\s+/g, ' ').trim();
function previewOfUserItem(item: HistoryItem): string {
if (item.type !== 'user' || !item.text) return '';
// Sanitize ANSI/OSC escapes — chat history uses escapeAnsiCtrlCodes
// for the same text; the diff dialog header bypasses that path.
const safe = item.text.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '').replace(/\x1b\].*?[\x07\x1b\\]/g, '');
const oneLine = safe.replace(/\s+/g, ' ').trim();
if (oneLine.length <= PREVIEW_MAX) return oneLine;
return `${oneLine.slice(0, PREVIEW_MAX - 1)}…`;
}

— qwen-latest-series-invite-beta-v28 via Qwen Code /review

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.

Fixed in 5cf48b0. previewOfUserItem now routes item.text through the
shared escapeAnsiCtrlCodes from packages/cli/src/ui/utils/textUtils.ts
before whitespace-collapsing and length-truncating:

const safe = escapeAnsiCtrlCodes(item.text);
const oneLine = safe.replace(/\s+/g, ' ').trim();

Reusing the shared helper instead of inlining a regex keeps the
sanitization rules consistent with the chat surface
(HistoryItemDisplay.tsx:111) — both surfaces are now defended by
the same code path, so future updates to the escape-list (extending
OSC handling, adding C1 control bytes, etc.) propagate automatically.

A pasted ANSI/OSC payload now reaches the source-tab label as
JSON-escaped text (\x1b[...m) instead of as live terminal escapes.

before: FileHistorySnapshot,
after: FileHistorySnapshot | undefined,
): Promise<TurnFileDiff | null> {
const filePath = this.maybeExpandFilePath(trackingPath);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] TurnFileDiff.filePath uses maybeExpandFilePath(trackingPath), producing absolute paths — inconsistent with the "Current" tab which uses repo-relative paths from fetchGitDiff.

In the DiffDialog, the "Current" source renders paths like packages/cli/src/ui/components/DiffDialog.tsx, while per-turn sources render /Users/wenshao/git/.../packages/cli/src/ui/components/DiffDialog.tsx. On narrow terminals, absolute paths are more aggressively truncated by truncatePathStart, losing project context (e.g., the packages/cli scope).

Consider returning trackingPath as filePath instead — it is already repo-relative and matches the convention used by the Current source.

— qwen-latest-series-invite-beta-v28 via Qwen Code /review

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.

Fixed in 5cf48b0. TurnFileDiff.filePath is now trackingPath directly
(which maybeShortenFilePath already made repo-relative for in-cwd
files), matching the convention fetchGitDiff uses for the "Current"
source. The absolute form lives only in a local absoluteFilePath
variable inside computeTurnFileDiffUnsafe and is used solely for the
live-worktree readEndpointContent call.

Verified that the existing regressions still pass after I updated the
test expectations from f.filePath === file (absolute) to
f.filePath === basename(file) (relative).

Side effect: the patch headers we pass into structuredPatch are now
also repo-relative, but those headers are internal to the diff
package's hunk struct and we discard them in the renderer, so no
user-facing change there.

Narrow-terminal context loss (/Users/.../packages/cli/... chopped to
...e/DiffDialog.tsx) is gone — both sources now render the same
packages/cli/... prefix.

@BZ-D
Copy link
Copy Markdown
Collaborator Author

BZ-D commented May 19, 2026

image

@BZ-D
Copy link
Copy Markdown
Collaborator Author

BZ-D commented May 19, 2026

image

@BZ-D
Copy link
Copy Markdown
Collaborator Author

BZ-D commented May 19, 2026

image

@BZ-D
Copy link
Copy Markdown
Collaborator Author

BZ-D commented May 19, 2026

image

- Deterministic candidate cap in getTurnDiff: sort trackedFileBackups
  keys before slicing at MAX_TURN_DIFF_FILES; emit debugLogger.warn
  when truncating so the dropped count is traceable.
- Log unreadable before/after endpoints in computeTurnFileDiffUnsafe
  instead of dropping rows silently — backup corruption, permission
  flips and EISDIR now leave a trace.
- Return trackingPath as TurnFileDiff.filePath (already repo-relative
  via maybeShortenFilePath) so per-turn rows match the Current source
  on narrow terminals. The internal absolute path is kept only for
  live-worktree I/O.
- useDiffData: replace bare console.debug with createDebugLogger
  ('DiffDialog') to match project convention.
- DiffDialog: show a transient warning-coloured hint in the footer
  when Enter lands on a binary / oversized / no-hunks row (cleared on
  the next navigation key) so the keypress isn't silently consumed.
- useDialogClose: swap diff-dialog and background-tasks branches to
  match DialogManager render order — Ctrl+C now dismisses whichever
  dialog the user actually sees when both flags are open.
- useTurnDiffs: sanitize previewOfUserItem via escapeAnsiCtrlCodes so
  prompt previews on the source tabs can't reach the terminal raw
  (matching the chat-history defense).
- Tests: expect repo-relative filePath in getTurnDiff regression
  cases; add `warn` to the mocked debugLogger.

Refs PR #4277 review comments 3259062434, 3259062465, 3264541365,
3259062480, 3259062498, 3264541346, 3264541351.
@BZ-D BZ-D requested a review from wenshao May 19, 2026 08:33
Promise.all(
userTurns.map(async (item, idx) => {
// Early-exit so a quick close → reopen doesn't keep paying for
// disk reads from the previous effect. The outer cancellation
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] Cross-turn I/O fan-out: Promise.all(userTurns.map(...)) fires getTurnDiff for all user turns simultaneously. Each getTurnDiff can open up to MAX_TURN_DIFF_FILES × 2 = 1000 file descriptors. With 50 turns, this is 50,000 concurrent readFile calls — exceeding ulimit -n on most systems (256 on macOS) and causing EMFILE errors that silently cascade through unrelated file operations.

The per-call cap (500 files) is safe for a single turn, but the N-way multiplication across turns is unbounded. The cancelled guard only suppresses setState — all I/O completes regardless.

Consider batching turns in groups of 3-5, or adding a concurrency semaphore:

const CONCURRENCY = 4;
for (let i = 0; i < userTurns.length; i += CONCURRENCY) {
  if (cancelled) break;
  const batch = userTurns.slice(i, i + CONCURRENCY);
  // ... process batch
}

— qwen-latest-series-invite-beta-v28 via Qwen Code /review

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.

Fixed in 1628101. useTurnDiffs now batches calls at TURN_CONCURRENCY = 4
instead of one unbounded Promise.all over every user turn:

for (let i = 0; i < userTurns.length; i += TURN_CONCURRENCY) {
  if (cancelled) return out;
  const slice = userTurns.slice(i, i + TURN_CONCURRENCY);
  const batch = await Promise.all(slice.map((item, j) => loadOne(item, i + j)));
  // …
}

Worst-case fd usage drops from O(turns × MAX_TURN_DIFF_FILES × 2) to
O(TURN_CONCURRENCY × MAX_TURN_DIFF_FILES × 2) ≈ 4000 — well under the
default macOS 256-and-friends ulimit problem you flagged, and within
common 4096 ceilings. cancelled is now also checked between batches,
so close→reopen really does stop the disk reads rather than just
suppressing setState.

Picked 4 because turns batch through quickly enough that loading
doesn't feel slower in practice (each getTurnDiff is fast-path-heavy
when most files share backups across turns), while still cutting the
fan-out by an order of magnitude on realistic 50-turn sessions.

): Promise<EndpointReadOk | 'unreadable'> {
if (worktreePath !== undefined) {
try {
const text = await readFile(worktreePath, 'utf-8');
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] readFile(worktreePath, 'utf-8') reads the entire file into memory before the MAX_DIFF_SIZE_BYTES check at line ~906. The oversized cap only prevents hunk expansion, not the initial allocation. A user who asks the model to write_file a 2 GB blob then opens /diff would allocate 2 GB in the Node.js heap before the cap fires. With multiple such files, the process is OOM-killed.

Consider stat()-ing the path first and returning an oversized sentinel without reading:

const stats = await stat(worktreePath).catch(() => null);
if (stats && stats.size > MAX_DIFF_SIZE_BYTES) {
  return { oversized: true };
}

— qwen-latest-series-invite-beta-v28 via Qwen Code /review

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.

Fixed in 1628101. readEndpointContent now stats both the live
worktree and the backup path before readFile, and returns a new
{ kind: 'oversized', exists } sentinel when stat.size > MAX_DIFF_SIZE_BYTES:

if (size > MAX_DIFF_SIZE_BYTES) {
  return { kind: 'oversized', exists: true };
}

computeTurnFileDiffUnsafe short-circuits on either endpoint being
oversized and returns the standard oversized: true TurnFileDiff
shape without ever calling readFile — so a 2 GB write_file blob
no longer lands in the Node heap just for the downstream
Buffer.byteLength check to reject it. The post-read
Buffer.byteLength > MAX_DIFF_SIZE_BYTES cap stays as defense-in-depth
for the rare case where stat.size < cap but utf-8 decoding inflates
the string past it (invalid bytes → U+FFFD).

I also extended the cap to backup files for the same reason — a
backup of a giant file is just as fatal to allocate as the live one,
and the previous code would have OOM'd on backup reads too.

Test update: the oversized regression now expects
linesAdded === 0 and linesRemoved === 0 because we no longer hold
the bytes needed to count newlines. The row's purpose is to signal
the omission, not to estimate; users get the badge and the hint
"Use `git diff` to inspect" to follow up.

@@ -390,3 +390,50 @@ export function sanitizeSensitiveText(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] sanitizeFilenameForDisplay is a security-relevant pure function that strips ANSI sequences and control characters from git-supplied filenames to prevent terminal injection. It has no unit tests. A regression in the control-character regex or the escape mapping could allow raw terminal escape sequences to reach the TUI via crafted filenames with no test to catch it.

Suggested test cases: (1) C0 controls (\n, \r, \x00) produce \uXXXX escapes, (2) multi-byte ANSI sequences are stripped, (3) clean filenames pass through unchanged.

— qwen-latest-series-invite-beta-v28 via Qwen Code /review

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.

Fixed in 1628101. Added a focused describe('sanitizeFilenameForDisplay')
block to packages/cli/src/ui/utils/textUtils.test.ts covering:

  1. Clean filenames pass through unchanged — repo paths, non-ASCII
    (文件.txt), empty string.
  2. C0 control bytes that slip past escapeAnsiCtrlCodes
    \n, \r, \t, \b, \f, NUL, BEL — produce \\n /
    \\r / \\t / \\b / \\f / \\u0000 / \\u0007 escapes.
  3. DEL (0x7F) and C1 controls (0x80–0x9F) — produce \\u00XX escapes.
  4. Multi-byte ANSI CSI sequences\x1b[31mred\x1b[0m survives
    only as red, no ESC bytes remain.
  5. Mixed crafted path (evil\x1b[2K\npath\x00.txt) — exercises
    both regex passes together; output preserves visible substrings and
    contains no raw C0 / DEL bytes (verified by char-code scan).

Total: 29 tests pass, including the 5 new ones.

let cancelled = false;
setLoading(true);

const userTurns = history.filter(isRealUserTurn) as HistoryItem[];
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] Double unsafe as cast bypasses TypeScript type narrowing:

const userTurns = history.filter(isRealUserTurn) as HistoryItem[];
// ...
const promptId = (item as HistoryItemUser).promptId;

If isRealUserTurn were changed to allow non-user items, or if HistoryItemUser lost promptId, TypeScript would not catch the regression.

Make isRealUserTurn a type predicate to eliminate both casts:

export function isRealUserTurn(item: HistoryItem): item is HistoryItemUser {

— qwen-latest-series-invite-beta-v28 via Qwen Code /review

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.

Fixed in 1628101. isRealUserTurn is now a TypeScript predicate:

export function isRealUserTurn(
  item: HistoryItem,
): item is HistoryItem & HistoryItemUser {
  // …
}

The intersection form is needed because HistoryItem = HistoryItemWithoutId & { id: number }
while HistoryItemUser doesn't carry id — narrowing to plain
HistoryItemUser would actually widen the input. The & keeps the
id part and adds the user-specific shape, so the predicate is
strictly narrowing.

Both as HistoryItem[] and (item as HistoryItemUser).promptId are
gone from useTurnDiffs:

const userTurns = history.filter(isRealUserTurn);  // UserTurn[] naturally
// …
const { promptId } = item;  // already narrowed

A regression that loosens either isRealUserTurn or removes
promptId from HistoryItemUser will now fail tsc instead of
silently bypassing the narrowing.

debugLogger.debug(`fetchGitDiffHunks failed: ${err}`);
return new Map<string, Hunk[]>();
}),
]).then(([statsRes, hunksRes]) => {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] The Promise.all([...]).then(...) chain has no .catch(). While inner promises are individually .catch()-guarded, if the .then() callback throws (e.g., React state update error during unmount), the resulting rejection is unhandled. In Node.js ≥ 22, unhandled rejections terminate the process.

Add a trailing .catch():

}).then(([statsRes, hunksRes]) => {
  if (cancelled) return;
  setResult(statsRes);
  setHunks(hunksRes);
  setLoading(false);
}).catch((err) => {
  debugLogger.debug(`diff data pipeline failed: ${err}`);
  if (!cancelled) setLoading(false);
});

The same pattern applies to useTurnDiffs.ts line 85.

— qwen-latest-series-invite-beta-v28 via Qwen Code /review

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.

Fixed in 1628101. Both useDiffData and useTurnDiffs now terminate
their Promise chains with a trailing .catch():

loadAll()
  .then((entries) => {
    if (cancelled) return;
    entries.reverse();
    setTurns(entries);
    setLoading(false);
  })
  .catch((err) => {
    debugLogger.debug(`useTurnDiffs pipeline failed: ${err}`);
    if (!cancelled) setLoading(false);
  });

Same pattern on useDiffData. The inner .catch() blocks already
handle expected I/O failures; the trailing .catch() is defense in
depth against a setState-during-unmount throw or a future refactor
that introduces a synchronous error inside .then. Without it,
Node ≥ 22 would terminate the process via the default
unhandled-rejection handler — exactly the failure mode you flagged.

Both catch branches log via createDebugLogger('DiffDialog') and
unstick loading so the dialog falls through to its existing
empty-state rendering instead of spinning forever.

return true;
}

// Order must match `DialogManager`'s render priority — the diff
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] The comment says "Order must match DialogManager's render priority" but the actual ordering differs: in DialogManager, DiffDialog renders near position 35 (after most dialogs), but here it is checked at position ~12 (after WelcomeBackDialog, before BackgroundTasksDialog). Multiple dialogs (Help, Memory, Arena, RewindSelector) render above DiffDialog in DialogManager but are checked before it here.

The "must match" contract is misleading and not enforced. If a new dialog is added between rewind and diff in DialogManager without a matching update here, Ctrl+C would close the wrong dialog. Consider extracting the close priority order into a shared constant.

— qwen-latest-series-invite-beta-v28 via Qwen Code /review

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.

Fixed (comment scope) in 1628101. You're right — the previous "Order must
match DialogManager's render priority" comment overclaimed; the hook
deliberately doesn't mirror the full DialogManager order (theme /
auth / settings / … each have their own priority elsewhere in this
hook). The actual invariant the swap was protecting is narrower:
DiffDialog must beat BackgroundTasksDialog specifically, because
DialogManager renders the diff dialog over the background-tasks
dialog when both flags are open.

New comment scopes the claim correctly:

// Scoped invariant: the diff-dialog branch MUST sit above the
// background-tasks branch because DialogManager renders the diff
// dialog over BackgroundTasksDialog when both flags are true …
// The rest of this hook's ordering is **not** a mirror of
// DialogManager and isn't intended to be …

Extracting a shared priority constant is appealing but out of scope
for this PR — every dialog touches this hook on top of its own setup,
and lifting them into a central registry is a refactor that warrants
its own change. Filing as a follow-up note in my notes.

// the same constant the git path uses (MAX_FILES_FOR_DETAILS = 500
// files total), with two reads each → 1000 open()s worst case, still
// comfortably below the typical 4096 fd ceiling.
if (candidatePaths.length > MAX_TURN_DIFF_FILES) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] When candidatePaths.length > MAX_TURN_DIFF_FILES, the file list is silently truncated and stats.filesChanged reflects only the capped subset. The TurnDiff type has no field indicating truncation occurred. Consequently, DiffDialog has no data to show a truncation indicator for turn sources — unlike the "Current" source which computes hiddenFileCount from stats.filesCount.

A turn editing 600 files would show "500 files +1200 -800" with no hint that 100+ additional files were dropped.

Consider adding a filesOmitted: number field to TurnDiff.stats and surfacing it in the dialog analogous to hiddenFileCount.

— qwen-latest-series-invite-beta-v28 via Qwen Code /review

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.

Fixed in 1628101. TurnDiff.stats now carries filesOmitted: number,
computed at the slice site in getTurnDiff:

const filesOmitted = Math.max(0, candidatePaths.length - MAX_TURN_DIFF_FILES);
if (filesOmitted > 0) {
  debugLogger.warn(`getTurnDiff truncating ${filesOmitted} files …`);
}
// …
stats: { filesChanged: files.length, linesAdded, linesRemoved, filesOmitted }

DiffDialog's hiddenFileCount now reads from this field for turn
sources, mirroring the existing Current-source branch:

const hiddenFileCount =
  activeSource?.kind === 'current'
    ? Math.max(0, stats.filesCount - files.length)
    : activeSource?.kind === 'turn'
      ? activeSource.entry.diff.stats.filesOmitted
      : 0;

Result: a turn editing 600 files now renders "500 files +X -Y" and
"…and 100 more (showing first 500)" beneath the list, instead of
silently hiding the gap. Same string the Current source already used,
so the UX is symmetric across sources.

return;
}
setLoading(true);
Promise.all([
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] fetchGitDiff(cwd) and fetchGitDiffHunks(cwd) are two independent git diff HEAD subprocess invocations. If the working tree changes between them (concurrent tool edit, file watcher), perFileStats and hunks can disagree — a file may show +5 -3 in the list but have no hunks, or have hunks but zero stats.

This is a pre-existing architecture but newly exposed by the dialog which presents both datasets side-by-side for the first time. Consider either deriving both from a single git diff HEAD invocation, or surfacing a transient note when the datasets disagree.

— qwen-latest-series-invite-beta-v28 via Qwen Code /review

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.

Acknowledged — deferring to a follow-up. The race is real but
pre-existing arch (fetchGitDiff and fetchGitDiffHunks were two
separate git diff HEAD invocations long before this PR added a
caller that surfaces both side-by-side), so fixing it cleanly means
either:

  1. Merging the two into a single git diff HEAD invocation that
    returns numstat + hunks together — invasive refactor of the public
    packages/core/src/utils/gitDiff.ts API and its non-interactive
    consumers.
  2. Diffing two child processes' git outputs against each other and
    surfacing a note when they disagree
    — adds UI state and a
    detection heuristic for a window that is microseconds wide in
    practice (fetchGitDiff finishes shortly before fetchGitDiffHunks
    starts, both inside Promise.all).

For this PR the contained surface is acceptable: the dialog opens on
a frozen snapshot (snapshot-at-open semantics, already documented in
useDiffData's JSDoc), and users running concurrent tool edits while
manually opening /diff is rare enough that we haven't seen it cause
visible disagreement in testing.

Filing as a tracked follow-up under "merge git-diff invocations into
a single subprocess" — the cleanest fix is path 1, but it benefits
from being scoped on its own rather than bundled into this PR.

- readEndpointContent now stats both worktree and backup paths before
  readFile and returns a `{ kind: 'oversized' }` sentinel when the
  file exceeds MAX_DIFF_SIZE_BYTES. computeTurnFileDiffUnsafe handles
  the sentinel without allocating, so a 2 GB write_file blob no longer
  lands in the Node heap just to be rejected downstream.
- useTurnDiffs now batches `getTurnDiff` calls at TURN_CONCURRENCY = 4
  instead of an unbounded Promise.all across every user turn. Prevents
  EMFILE on long sessions (worst case ~4000 fds vs. unbounded N × 1000).
- Add `filesOmitted` to `TurnDiff.stats` and plumb it through the
  dialog's `hiddenFileCount` so per-turn rows now also surface "…and N
  more" when MAX_TURN_DIFF_FILES truncates the candidate list (matches
  the Current source's existing behavior).
- Make isRealUserTurn a type predicate (`item is HistoryItem &
  HistoryItemUser`) so callers in useTurnDiffs drop both `as` casts —
  a future regression that loosens either side will now be caught by
  tsc rather than silently bypassing the narrowing.
- Add trailing `.catch()` to the Promise.all chains in useDiffData and
  useTurnDiffs so a thrown setState during unmount doesn't propagate
  to Node 22+'s default unhandled-rejection terminator. Both branches
  log via createDebugLogger and unstick `loading`.
- Tighten the comment above the diff/background-tasks branch in
  useDialogClose: the invariant is scoped to that pair, not a
  full mirror of DialogManager's render priority.
- Add focused unit tests for sanitizeFilenameForDisplay (C0 controls,
  DEL + C1, multi-byte ANSI CSI, mixed crafted paths, clean
  passthrough) — security-relevant function previously untested.

Refs PR #4277 review comments 3265032536, 3265032548, 3265032551,
3265032556, 3265032560, 3265032569, 3265032574.
@BZ-D BZ-D requested a review from wenshao May 19, 2026 10:19
return { filesChanged, insertions, deletions };
}

interface EndpointReadOk {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] EndpointReadOk lacks an explicit kind discriminant while EndpointReadOversized has kind: 'oversized'. Lines 930-960 rely on 'kind' in checks and 6 explicit as casts (as EndpointReadOk, as EndpointReadOversized) instead of TypeScript narrowing.

Adding kind: 'ok' to EndpointReadOk and converting 'unreadable' to { kind: 'unreadable' } would make this an idiomatic discriminated union, eliminate all manual casts, and let the compiler catch missing branches:

interface EndpointReadOk { kind: 'ok'; content: string; exists: boolean; }
interface EndpointReadOversized { kind: 'oversized'; exists: boolean; }
interface EndpointReadUnreadable { kind: 'unreadable' }
type EndpointRead = EndpointReadOk | EndpointReadUnreadable | EndpointReadOversized;

Then all typeof === 'object' && 'kind' in narrowing and as casts become simple if (read.kind === 'oversized') switches.

— qwen-latest-series-invite-beta-v34 via Qwen Code /review

try {
const st = await stat(worktreePath);
size = st.size;
} catch (e: unknown) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] The pre-read stat() then readFile() are two separate syscalls with no atomicity. If the file grows past MAX_DIFF_SIZE_BYTES between the two calls (e.g., a concurrent write_file tool call appending to the same file), the full content is loaded into the Node heap — defeating the OOM guard.

Using fs.open() + fstat(fd) + readFile({ fd }) eliminates the TOCTOU race by operating on the same inode:

const fh = await open(worktreePath, 'r');
try {
  const st = await fh.stat();
  if (st.size > MAX_DIFF_SIZE_BYTES) return { kind: 'oversized', exists: true };
  const text = await readFile(fh, 'utf-8');
  return { content: text, exists: true };
} finally {
  await fh.close();
}

The window is narrow but the consequence (heap spike up to full file size) is severe — especially during active agent writes.

— qwen-latest-series-invite-beta-v34 via Qwen Code /review

// the same constant the git path uses (MAX_FILES_FOR_DETAILS = 500
// files total), with two reads each → 1000 open()s worst case, still
// comfortably below the typical 4096 fd ceiling.
const filesOmitted = Math.max(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] filesOmitted counts candidates dropped by MAX_TURN_DIFF_FILES before diff computation, while stats.filesChanged (line 829) counts only files with non-null diffs after. The two use different denominators: if a turn touches 600 files (cap 500) and 50 of the 500 processed files have no changes, the dialog shows "450 files + 100 more" but 450 + 100 = 550 ≠ 600.

Consider computing the hidden count as totalCandidates - files.length in the dialog (matching the Current source's stats.filesCount - files.length pattern) so the accounting is consistent across both source kinds.

— qwen-latest-series-invite-beta-v34 via Qwen Code /review

/** Number of candidate files dropped because the turn touched more
* than `MAX_TURN_DIFF_FILES`. Mirrors the Current source's
* `hiddenFileCount` so the dialog can show "…and N more". */
filesOmitted: number;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] Three test coverage gaps in the new OOM/safety code:

  1. filesOmitted is computed at line 797, set on stats at line 832, consumed by DiffDialog.tsx — but no test asserts on it (not even baseline === 0). A regression in the cap math would silently break the dialog's truncation indicator.

  2. The worktree stat guard in readEndpointContent (lines 358-390) is never exercised by the existing oversized test, which uses two backup-based snapshots. To hit this path, a test needs a single snapshot where the live worktree file exceeds MAX_DIFF_SIZE_BYTES.

  3. The mixed-size endpoint case (small before + oversized after) in computeTurnFileDiffUnsafe uses different type-cast logic than the both-oversized case but is untested.

— qwen-latest-series-invite-beta-v34 via Qwen Code /review

* enough that loading never feels slower in practice while bounding the
* worst case to ~4000 concurrent fds (well under typical 4096 ceilings).
*/
const TURN_CONCURRENCY = 4;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] useTurnDiffs contains substantial logic — concurrency batching (TURN_CONCURRENCY = 4), cancellation via cancelled flag, per-turn error swallowing, most-recent-first ordering, empty-diff filtering — but has no test file. A renderHook test covering at minimum: (1) empty turns are filtered, (2) output is most-recent-first, (3) getTurnDiff errors for individual turns don't block others, and (4) all results arrive when loading >4 turns would catch regressions in the batching/cancellation logic.

— qwen-latest-series-invite-beta-v34 via Qwen Code /review

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.

[Feature] /diff: add per-turn diff with interactive selection (parity with claude-code)

3 participants