fix(diff-view): restore scroll position and fix tab handling on save/deny#589
Conversation
Manual Test Plan: Diff View Scroll Position + Tab/Focus HandlingCovers the fixes in Run this matrix manually in the Extension Development Host. Bugs this plan must catchThese are the concrete defects found and fixed. Each has dedicated coverage below.
Prerequisites
What "correct" looks like
Matrix 1 -- Diff opens on the first change (scroll-to-first-diff)For each combination below, trigger the edit and observe ONLY the diff-open scroll Edit positions to test: line 0 / top, midpoint of the file, end of the file. For the "Already open" rows, first scroll the file to the middle before prompting, so
Pay special attention to 1.2-1.7, 1.11, 1.12 -- these are the regressions where an 1.S -- "Stuck scroll" sequential check (bug 4)
Matrix 2 -- Save (accept) behaviorSet up a known pre-edit scroll position before each run by manually scrolling the file, so
Matrix 3 -- Deny (reject) behavior
Matrix 4 -- Auto-approve late-timer race (bug 5)Enable auto-approve writes for these rows so save runs immediately after the diff
Watch for ~100ms-late movement: the buggy behavior was a brief correct restore followed Matrix 5 -- Preview-tab eviction (bug 6)A preview tab is the italicized, single reusable tab VS Code shows when you open a file
Matrix 6 -- Focus preservation on keep-open (bug 7)
Regression smoke checks
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughDiffViewProvider extended to track pin/scroll/touch state, snapshot unrelated preview tabs, install activation/scroll/interact listeners on open, reliably reveal the first changed line with EOF clamping and guarded deferred reveal, apply user-configurable auto-close settings on save/revert, and restore or close tabs accordingly. Settings UI and schema updated to expose three auto-close options across all locales. ChangesDiff View Scroll Position, Tab State, and Auto-Close Settings
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Poem
Suggested reviewers
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Videos from Manual Testing: Target file open - scrolled to top - diff scrolls to edit at top, middle, bottom: Target file open - scrolled to middle - diff scrolls to edit and restores viewport after accept: Target file not open - diff scrolls to edit - tab is cleaned up after accept or reject: Stress test multiple sequential writes & scrolling with apply_diff - no race conditions or errors: Preview tab open with file 1, diff targets unopened file 2, after accept, preview tab is restored: Permanent tab open with file 1, diff targets unopened file 2, user changes tab then accepts - no jump back: Pinned file targeted with diff - stays pinned correctly: |
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/integrations/editor/DiffViewProvider.ts`:
- Around line 615-633: captureUnrelatedPreviewTabs currently returns only { uri,
scrollLine } so restorePreviewTabs reopens previews in the active group; include
the originating group/viewColumn in the snapshot (e.g., add tab.group.viewColumn
or tab.group.id to the captured object inside captureUnrelatedPreviewTabs) and
update restorePreviewTabs to call vscode.window.showTextDocument(snapshot.uri, {
preview: true, preserveFocus: true, viewColumn: snapshot.viewColumn }) (or open
in the original group via its id) and honor the saved scrollLine after opening;
update types/usages of the snapshot array and any related helper functions to
handle the new viewColumn field.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 82610a67-0e7a-4e94-8022-c666f20722a9
📒 Files selected for processing (3)
.changeset/fix-diff-scroll-position.mdsrc/integrations/editor/DiffViewProvider.tssrc/integrations/editor/__tests__/DiffViewProvider.spec.ts
|
Follow-up commit summary A second commit ( Fix: restore preview tabs to their original editor group The initial implementation of Changes:
|
|
Update -- commit Two additional behaviors have been implemented on top of the original scroll/tab fix: Keep target tab open when user interacts with the diff pane Target file scrolls to most-recently-viewed position
Tests: 59 tests pass (was 44). New coverage: |
There was a problem hiding this comment.
🧹 Nitpick comments (2)
.changeset/fix-diff-scroll-position.md (1)
5-5: 💤 Low valueMinor wording improvement on preview-tab preservation claim.
Line 5 reads "A first file that was open in a preview tab is restored" — "first file" is grammatically awkward. Consider: "A file that was open in a preview tab is restored" or "A preview tab that was open for a file is restored".
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In @.changeset/fix-diff-scroll-position.md at line 5, Replace the awkward phrase "A first file that was open in a preview tab is restored" with clearer wording such as "A file that was open in a preview tab is restored" (or alternatively "A preview tab that was open for a file is restored") so the sentence reads naturally and unambiguously.src/integrations/editor/__tests__/DiffViewProvider.spec.ts (1)
1536-1536: 💤 Low valueConsider adding a guard for clearer test failures.
The non-null assertion on
scrollCallback!will produce a confusing runtime error if the mock setup changes. Adding a defensive check would make failures more diagnosable.Suggested improvement
- return { scrollCallback: scrollCallback!, activeDiffEditor: (diffViewProvider as any).activeDiffEditor, fsPath } + if (!scrollCallback) { + throw new Error("onDidChangeTextEditorVisibleRanges mock was not invoked during open()") + } + return { scrollCallback, activeDiffEditor: (diffViewProvider as any).activeDiffEditor, fsPath }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/integrations/editor/__tests__/DiffViewProvider.spec.ts` at line 1536, Replace the non-null assertion on scrollCallback in the test return with a defensive check: before returning the object from the test helper, assert that scrollCallback is defined (e.g. if (!scrollCallback) throw new Error('scrollCallback not initialized - ensure mock setup sets it') or use expect(scrollCallback).toBeDefined()), then return { scrollCallback, activeDiffEditor: (diffViewProvider as any).activeDiffEditor, fsPath }; this makes failures clearer and avoids a cryptic runtime error from the `!` operator.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In @.changeset/fix-diff-scroll-position.md:
- Line 5: Replace the awkward phrase "A first file that was open in a preview
tab is restored" with clearer wording such as "A file that was open in a preview
tab is restored" (or alternatively "A preview tab that was open for a file is
restored") so the sentence reads naturally and unambiguously.
In `@src/integrations/editor/__tests__/DiffViewProvider.spec.ts`:
- Line 1536: Replace the non-null assertion on scrollCallback in the test return
with a defensive check: before returning the object from the test helper, assert
that scrollCallback is defined (e.g. if (!scrollCallback) throw new
Error('scrollCallback not initialized - ensure mock setup sets it') or use
expect(scrollCallback).toBeDefined()), then return { scrollCallback,
activeDiffEditor: (diffViewProvider as any).activeDiffEditor, fsPath }; this
makes failures clearer and avoids a cryptic runtime error from the `!` operator.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 59610f4e-2eb5-4a1a-9dd5-8c49b849a4e4
📒 Files selected for processing (3)
.changeset/fix-diff-scroll-position.mdsrc/integrations/editor/DiffViewProvider.tssrc/integrations/editor/__tests__/DiffViewProvider.spec.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- src/integrations/editor/DiffViewProvider.ts
edelauna
left a comment
There was a problem hiding this comment.
thanks for this, had some comments related to the implementation.
- Add autoCloseZooOpenedFiles, autoCloseZooOpenedFilesAfterUserEdited, and autoCloseZooOpenedNewFiles to global settings schema - Render checkboxes for all three in UISettings component - Wire through SettingsView cachedState - Add i18n keys under ui namespace in all 18 locales - Add UISettings.spec.tsx tests for the new checkboxes - Add changeset
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@webview-ui/src/i18n/locales/de/settings.json`:
- Around line 988-998: Replace the ASCII transliterated German text with proper
German characters containing umlauts and special characters in the three
settings: autoCloseZooOpenedFiles, autoCloseZooOpenedFilesAfterUserEdited, and
autoCloseZooOpenedNewFiles. Specifically, replace `geoffnete` with `geöffnete`,
`schliessen` with `schließen`, `Anderung` with `Änderung`, `fuer` with `für`,
`voriibergehend` with `vorübergehend`, and `wahrend` with `während` throughout
all labels and descriptions to restore proper German readability and consistency
with the rest of the locale file.
In `@webview-ui/src/i18n/locales/fr/settings.json`:
- Around line 988-998: The French translations in the three newly added settings
entries (autoCloseZooOpenedFiles, autoCloseZooOpenedFilesAfterUserEdited, and
autoCloseZooOpenedNewFiles) are missing accents on multiple French words, making
them inconsistent with proper French spelling conventions. Restore the missing
diacritical marks: "activee" should be "activée", "apres" should be "après",
"deja" should be "déjà", "meme" should be "même", "crees" should be "créés",
"plutot" should be "plutôt", "modifies" should be "modifiés", "egalement" should
be "également", and "fermes" should be "fermés" in both the label and
description fields of these three settings entries.
In `@webview-ui/src/i18n/locales/it/settings.json`:
- Around line 992-994: In the description field of the
autoCloseZooOpenedFilesAfterUserEdited key in the Italian settings locale file,
correct two Italian grammatical errors: replace `e disabilitata` with `è
disabilitata` (adding the accent to the verb) and replace `gia` with `già`
(adding the accent to the adverb). These corrections ensure proper Italian
spelling and grammar in the user-facing description text.
In `@webview-ui/src/i18n/locales/ru/settings.json`:
- Around line 989-998: The Russian translation strings in the settings.json file
for keys including autoCloseZooOpenedFilesAfterUserEdited and
autoCloseZooOpenedNewFiles are written in Latin transliteration instead of
proper Cyrillic script, which creates poor user experience for Russian speakers.
Replace all transliterated label and description values with their proper
Cyrillic equivalents throughout this section to ensure the UI displays correct
Russian text.
In `@webview-ui/src/i18n/locales/vi/settings.json`:
- Around line 989-998: The Vietnamese translation strings for the new settings
entries (autoCloseZooOpenedFiles, autoCloseZooOpenedFilesAfterUserEdited, and
autoCloseZooOpenedNewFiles) are using unaccented transliteration instead of
proper Vietnamese orthography with diacritical marks. Replace all occurrences of
unaccented text like "Tu dong", "cac", "bat", "da", "mo", "se", "dong", "chap
nhan", "tu choi", "thay doi", "khong", "bao gio", "bi", "ngay ca", "sau khi",
"nguoi dung", "tuong tac", "nhan", "chuot", "go phim", "vao", "trong", "phien",
"chinh sua", "tat", "moi", "tao", "cua", and similar words with their properly
accented Vietnamese equivalents to ensure consistency with the rest of the
Vietnamese locale file.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 4a722397-2897-4b24-b877-27c9bd5e12f1
📒 Files selected for processing (27)
.changeset/add-editor-auto-close-settings.mdpackages/types/src/global-settings.tspackages/types/src/vscode-extension-host.tssrc/integrations/editor/DiffViewProvider.tssrc/integrations/editor/__tests__/DiffViewProvider.spec.tswebview-ui/src/components/settings/SettingsView.tsxwebview-ui/src/components/settings/UISettings.tsxwebview-ui/src/components/settings/__tests__/SettingsView.change-detection.spec.tsxwebview-ui/src/components/settings/__tests__/UISettings.spec.tsxwebview-ui/src/i18n/locales/ca/settings.jsonwebview-ui/src/i18n/locales/de/settings.jsonwebview-ui/src/i18n/locales/en/settings.jsonwebview-ui/src/i18n/locales/es/settings.jsonwebview-ui/src/i18n/locales/fr/settings.jsonwebview-ui/src/i18n/locales/hi/settings.jsonwebview-ui/src/i18n/locales/id/settings.jsonwebview-ui/src/i18n/locales/it/settings.jsonwebview-ui/src/i18n/locales/ja/settings.jsonwebview-ui/src/i18n/locales/ko/settings.jsonwebview-ui/src/i18n/locales/nl/settings.jsonwebview-ui/src/i18n/locales/pl/settings.jsonwebview-ui/src/i18n/locales/pt-BR/settings.jsonwebview-ui/src/i18n/locales/ru/settings.jsonwebview-ui/src/i18n/locales/tr/settings.jsonwebview-ui/src/i18n/locales/vi/settings.jsonwebview-ui/src/i18n/locales/zh-CN/settings.jsonwebview-ui/src/i18n/locales/zh-TW/settings.json
✅ Files skipped from review due to trivial changes (10)
- webview-ui/src/i18n/locales/hi/settings.json
- webview-ui/src/i18n/locales/pl/settings.json
- webview-ui/src/i18n/locales/zh-CN/settings.json
- webview-ui/src/i18n/locales/en/settings.json
- .changeset/add-editor-auto-close-settings.md
- webview-ui/src/i18n/locales/pt-BR/settings.json
- webview-ui/src/i18n/locales/es/settings.json
- webview-ui/src/i18n/locales/id/settings.json
- webview-ui/src/i18n/locales/tr/settings.json
- webview-ui/src/i18n/locales/ca/settings.json
🚧 Files skipped from review as they are similar to previous changes (1)
- src/integrations/editor/DiffViewProvider.ts
| --- | ||
| "zoo-code": patch | ||
| --- | ||
|
|
There was a problem hiding this comment.
we need to investigate why so many agents create these changeset files lol, I saw it in other PRs too. I think it might be some instruction in the zoo repository
There was a problem hiding this comment.
figured it out, looks like they look at changesets in the repo and assume we need to generate one. We need to add instructions about it in agents.md, or just allow changesets in general
|
Taking a look, @awschmeder can you see why CI is failing? |
CI fixes (DiffViewProvider.spec.ts): - check-types: cast partial task mock to satisfy Task param (TS2345) - platform-unit-test: convert Range/Position/Selection arrow mocks to regular functions so they are constructable under vitest v4 (v4 invokes mockImplementation as a constructor for `new`) Review feedback: - reset(): dispose listeners + cancelDeferredScroll before closeAllDiffViews - add regression test: revertChanges ignores userTouchedDiffEditor (deny) - replace scrollCallback non-null assertion with a defensive guard - reword changeset preview-tab sentence - restore proper diacritics/Cyrillic in de/fr/it/ru/vi auto-close settings
|
Resolved @edelaunas comments in my commit, merging this, really improves the UX, but files almost flash before your eyes lol. We should promote the experimental feature of not opening files further |
Related GitHub Issue
Closes: #586
Description
Fixes the diff-view scroll and tab-handling problems described in #586.
The core logic changes are confined to
DiffViewProvider.ts;no tool files needed to change because the default diff path already calls
open()/update()/scrollToFirstDiff()/saveChanges()in the right order.The "how", broken down by the symptoms in the issue:
Diff opens at the wrong scroll position.
scrollEditorToLine(0)inopen(). It fired after thediff rendered and overrode whatever position should have been shown, leaving
off-screen edits out of view.
scrollToFirstDiff()is refactored into three private helpers:findFirstDiffLine()walksdiff.diffLines()to compute the first-change line andclamps it to a valid index so pure end-of-file removals (where the computed index
lands past
document.lineCount - 1) are still brought into view.resolveLiveEditor()re-resolves the modified-side editor fromvscode.window.visibleTextEditorsat scroll time, replacing any stale/detachedactiveDiffEditorreference whosevisibleRangesno longer reflect the on-screendiff widget. A
revealRangeon a stale reference is a silent no-op; this was theroot cause of "scrolls to top" when the file was already open.
revealDiffLine()anchors the editor selection on the target line before callingrevealRange. The selection nudge is necessary when the target is already insidethe current viewport -- a bare
revealRangeon a visible line is a no-op.vscode.diffcommand re-lays-out the diff widget after a short delay; the deferred reveal ensures
the scroll sticks after that layout pass. It is gated on
activeDiffEditor === editorso a stale timer from a previous edit never acts on a later edit's diff widget.
Viewport jumped to the top after Save/Deny.
open()now capturespreEditScrollLine(the first visible line) BEFORE thetab-closing loop -- after that loop the editor is no longer in
visibleTextEditorsand the capture would always be
undefined.showEditedFileWithoutDisruptingFocus(),which calls
revealRange(..., AtTop)to restore the pre-edit scroll position.cancelDeferredScroll()is called at the start of bothsaveChanges()andrevertChanges()before any programmatic editor activation. With auto-approve writes,saveChanges()runs in the tight window right afterscrollToFirstDiff()'s deferredtimer is set; the cancellation prevents the late reveal from firing after the viewport
is restored and yanking the user back to the diff target.
Tabs left open for files that were not previously open.
saveChanges()previously re-showed the file unconditionally;closeAllDiffViews()only closes diff-scheme tabs, so the plain file tab leaked. The re-show is now gated.
closeFileTab()to close the transiently opened plain file tab when the filewas not open before the edit AND the user did not interact with it.
userTouchedDocumentsignal: while the diff is open for anot-previously-open file, an
onDidChangeActiveTextEditorlistener marks the file as"touched" if the user activates its own (non-diff) editor tab. Touched files are kept
open; untouched ones are closed. The listener is disposed before any programmatic
showTextDocumentso our own re-show never counts as a touch, and is cleared inreset().Additional fixes discovered during implementation:
Focus disruption on keep-open. After Save/Deny, showing the edited file previously
stole the foreground even when the user had navigated to a different file.
showEditedFileWithoutDisruptingFocus()captures the user's active editor before there-show, uses
preserveFocus: true, and re-activates the user's prior editor afterwardwhen they were viewing something else.
Pin state lost. Closing the target file's tab to open the diff dropped VS Code's pin
state.
open()now recordsdocumentWasPinned; after the diff closes the pin isrestored via
workbench.action.pinEditor.Preview-tab eviction. Opening the diff reuses the editor group's single preview slot
and permanently evicts unrelated preview tabs.
open()now snapshots all unrelatedpreview tabs (with their scroll positions) before the diff opens;
restorePreviewTabs()re-opens any that were evicted after the diff closes. Files that no longer exist on disk
are skipped gracefully.
Diff pane interaction keeps target tab open.
If the user clicks or edits inside the diff pane itself, the target file's tab is kept
open after Save/Deny -- even if the file was not previously open. This extends the
existing
userTouchedDocumentconcept to cover diff-pane interaction viaonDidChangeTextEditorSelection(filtered tokind = Mouse | Keyboardto excludeprogrammatic selection changes such as those from
scrollToFirstDiff()). The listenercompares by
documentidentity rather than editor object identity to handle staleeditor references that VS Code creates when the diff widget refreshes. If the user only
scrolls in the diff pane, the existing close behavior is preserved.
Scroll position mirrors the most-recently-viewed pane.
When the target file is re-revealed after a diff closes, it scrolls to whichever
position the user last viewed:
Both the diff and target file editors are tracked via
onDidChangeTextEditorVisibleRanges;lastScrolledSourcerecords which fired most recently so the correct position wins.User-configurable auto-close settings (UI tab in Settings).
The auto-close decisions from item 3 above are now user-adjustable via three new settings
exposed in Settings > UI:
autoCloseZooOpenedFiles, default: on) -- closetransient file tabs Zoo opened once the diff is accepted/denied.
autoCloseZooOpenedFilesAfterUserEdited,default: off) -- override the "keep if touched" guard; only effective when the base
toggle is also on.
autoCloseZooOpenedNewFiles, default: off) -- alsoclose the tab when Zoo created the file (not just edited an existing one).
All three are wired from the global settings schema through
SettingsViewcached stateto
UISettingscheckboxes, and read inDiffViewProvider.keepOrCloseEditedFile().i18n keys added under the
uinamespace in all 18 locale files.Reviewers should pay attention to: the ordering of
disposeActiveEditorListener()->cancelDeferredScroll()->closeAllDiffViews()->keepOrCloseEditedFile()->restorePreviewTabs()in bothsaveChanges()andrevertChanges(), the clamp logic infindFirstDiffLine()and the second clamp inrevealDiffLine(), and the stale-editorguard in
resolveLiveEditor().Incidental change (discovered during this work): the diff tab label constant
DIFF_VIEW_LABEL_CHANGESstill read "Original <-> Roo's Changes". Since this is auser-visible label in the rebranded fork, it was corrected to "Original <-> Zoo's
Changes". It is a one-line string change referenced only via the constant (tests use the
constant, not the literal), so it is behavior-neutral for tab matching. Called out here for
transparency since it is outside the strict scope of the scroll fix.
Test Procedure
Automated:
DiffViewProvider: 67 tests pass, covering:
scrollToFirstDiff(): first-change line for addition-only, deletion-only, mixed, andend-of-file removal (clamped); selection anchor before reveal (in-viewport fix); deferred
re-reveal fires at 100 ms; live editor resolved at reveal time (stale reference fix);
stale timer from a previous edit does not act on a new diff editor; no-op when no editor.
preEditScrollLine: captured fromvisibleRanges[0].start.lineinopen();undefinedwhen no
visibleRanges;saveChanges()andrevertChanges()callrevealRange(AtTop)when set; no
revealRangewhenundefined; deferred scroll cancelled so it cannot fightthe scroll restore (auto-approve race).
userTouchedDocument:saveChanges()andrevertChanges()close the tab when untouched;keep it open when touched; activation listener fires correctly; listener ignored for diff
tab activations.
userTouchedDiffEditor:saveChanges()keeps the tab open when the user clicked/editedin the diff pane; closes it when the user only scrolled; listener registered in
open();programmatic selection changes (kind=Command or undefined) do not set the flag; stale
editor reference resolved by document identity; unrelated editor events ignored.
showEditedFileWithoutDisruptingFocus(): re-activates user's prior editor when theynavigated away; does not re-activate when already on the edited file; re-pins and focuses
correctly; does not pin when not originally pinned.
diff target; restores evicted tabs in preview mode with scroll; skips still-open tabs;
skips files that no longer exist.
diffScrollLinewhenlastScrolledSourceis'diff';uses
targetFileScrollLinewhenlastScrolledSourceis'targetFile'; falls back topreEditScrollLinewhen source is undefined; norevealRangewhen all sources areundefined; target-file scroll overrides diff scroll regardless of values.
diffScrollListenerwiring: recordsdiffScrollLineand setslastScrolledSourceto'diff'on diff editor scroll; recordstargetFileScrollLineand sets source to'targetFile'on target file scroll; ignores unrelated editors;lastScrolledSourcereflects the most recent scroll between the two editors.
kept when
autoCloseZooOpenedFilesis false; closed when true (default); touched tab keptby default; closed when
autoCloseZooOpenedFilesAfterUserEditedis true (and base is on);new-file tab closed when
autoCloseZooOpenedNewFilesis true; all unset settings preserveexisting behavior.
UISettings: auto-close checkbox tests -- renders all three checkboxes; each reflects its
prop value (true/false); each calls
setCachedStateFieldwith the correct key on toggle.Manual (Extension Development Host, F5): unit tests cannot confirm the composite diff
widget visibly scrolls or that focus/tabs behave correctly across real VS Code sessions.
Full manual matrix is documented in a comment attached to this PR.
Summary: for a 100+ line file, verify the diff opens on the first changed line
and the viewport is restored / the tab is closed appropriately after Save and after Deny,
for both already-open and not-open files, across pure-add, pure-delete, and mixed edits,
including diffs at the top, midpoint, and end of the file. Confirm no focus steal, no
preview-tab loss, and no stuck-scroll across sequential edits. Also verify: clicking or
editing inside the diff pane keeps the target tab open; scrolling only in the diff pane
still closes the tab; after Save the target file scrolls to the last-viewed position in
the diff or target file editor, whichever was scrolled most recently. Verify the three
auto-close checkboxes in Settings > UI toggle the behavior as documented.
Pre-Submission Checklist
Screenshots / Videos
Attached in a follow-up comments.
Documentation Updates
Additional Notes
The fix targets the DEFAULT (diff-visible) path only. When the
EXPERIMENT_IDS.PREVENT_FOCUS_DISRUPTIONexperiment is enabled, tools callsaveDirectly()instead, which intentionally avoids any diff view, scroll-to-change, orviewport restoration -- none of the code in this PR runs on that path.
Commentary / Opinion
Automatically closing an un-touched tab after applying an edit or creating a new file could be somewhat confusing to novice coders.
On the other hand, not closing un-touched files after edits creates significant tab pollution in long sessions, leading to dozens of tabs left open.
The unclosed-tab pollution problem also burdens the context window in an uncontrolled manner and drives up inference cost, as Zoo's default includes 20 open tabs (probably set too high, by the way... 1 is a better default).
On balance, I think the best solution is to automatically close files that the user did not either explicitly open or have some affirmative interaction with by viewing / focusing on or editing. The three new settings in Settings > UI give users full control to opt out of any part of this behavior.
Summary by CodeRabbit