diff --git a/src/vs/sessions/SINGLE_PANE_SCENARIOS.md b/src/vs/sessions/SINGLE_PANE_SCENARIOS.md index a37e63cec2fe61..0ca8cd05fdea3c 100644 --- a/src/vs/sessions/SINGLE_PANE_SCENARIOS.md +++ b/src/vs/sessions/SINGLE_PANE_SCENARIOS.md @@ -57,7 +57,7 @@ Let **E** = editor content visible, **D** = detail panel visible. The pane suppo | **Editor only** | ✅ | ❌ | Detail toggled off; editor content fills the pane; tab bar across the top. | | **Side pane closed** | ❌ | ❌ | The whole third pane is closed (chat-only). Reached via **Toggle Side Panel** or when the last editor tab closes; never via the detail toggle. **Closing the whole side pane does NOT close editors** — only a *Detail-only* collapse (editor hidden while the detail stays open) closes them; when both parts hide the editors are left intact so they return when the side pane is reopened. | -Only **Existing Sessions** share a persisted Editor/Details visibility profile. A New Session does not apply or capture that profile; on entry it hides Editor once only when the restored editor set contains no input other than Empty Files. Submit seeds the Existing profile. The active editor selects the detail content: every diff editor selects Changes and every file editor selects Files. +Only **Existing Sessions** share a persisted Editor/Details visibility profile. A New Session does not apply or capture that profile; on entry it hides Editor once only when the restored editor set contains no input other than the managed Changes and Empty Files inputs. Submit seeds the Existing profile. The active editor selects the detail content: every diff editor selects Changes and every file editor selects Files. **Size distribution when opening the side pane.** Opening the side pane from *closed* (e.g. clicking **Changes** while the chat is full-width) reveals the editor with `Sizing.Distribute`. The grid uses @@ -137,7 +137,7 @@ layout keeps it; single-pane's own **Show Editor** action is its counterpart to ## 4. Tabs - **Changes** — a custom `SessionChangesEditor` (Branch Changes dropdown + diff stats + embedded - multi-diff). Pinned first, present for every session with a workspace. + multi-diff). Pinned first, present for every session with a workspace, including New Session drafts. - **File** — the empty File tab (`EmptyFileEditorInput`) as a landing tab, plus real file editors the user opens. Opened **pinned, inactive, preserve-focus** so it never steals focus from the chat. - **Browser** — the integrated browser (`BrowserEditorInput`). diff --git a/src/vs/sessions/contrib/changes/browser/changes.contribution.ts b/src/vs/sessions/contrib/changes/browser/changes.contribution.ts index 97094f38f5806d..1d2c25162cdc10 100644 --- a/src/vs/sessions/contrib/changes/browser/changes.contribution.ts +++ b/src/vs/sessions/contrib/changes/browser/changes.contribution.ts @@ -18,7 +18,7 @@ import { CHANGES_VIEW_CONTAINER_ID, CHANGES_VIEW_ID, SESSIONS_CHANGES_OPEN_SINGL import { ChangesViewPane, SinglePaneChangesViewPane, ChangesViewPaneContainer } from './changesView.js'; import { SessionChangesEditor } from './sessionChangesEditor.js'; import { SessionChangesEditorInput, SessionChangesEditorSerializer } from './sessionChangesEditorInput.js'; -import { IsPhoneLayoutContext, SessionHasWorkspaceContext, SessionIsCreatedContext } from '../../../common/contextkeys.js'; +import { IsPhoneLayoutContext, SessionHasWorkspaceContext } from '../../../common/contextkeys.js'; import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; import { ISessionChangesService, SessionChangesService } from './sessionChangesService.js'; import './changesActions.js'; @@ -98,7 +98,7 @@ const changesViewContainer = viewContainersRegistry.registerViewContainer({ const viewsRegistry = Registry.as(ViewContainerExtensions.ViewsRegistry); -export const changesViewWhen = ContextKeyExpr.and(IsPhoneLayoutContext.negate(), SessionHasWorkspaceContext, SessionIsCreatedContext); +export const changesViewWhen = ContextKeyExpr.and(IsPhoneLayoutContext.negate(), SessionHasWorkspaceContext); /** * Registers the Changes view with the layout-appropriate pane class: the single-pane diff --git a/src/vs/sessions/contrib/changes/test/browser/changesViewActions.test.ts b/src/vs/sessions/contrib/changes/test/browser/changesViewActions.test.ts index 140b5785588e92..a08b6217b84ce4 100644 --- a/src/vs/sessions/contrib/changes/test/browser/changesViewActions.test.ts +++ b/src/vs/sessions/contrib/changes/test/browser/changesViewActions.test.ts @@ -39,7 +39,7 @@ suite('Changes View Actions', () => { ({ changesViewWhen } = await import('../../browser/changes.contribution.js')); }); - test('Changes view is hidden until the session is created', () => { + test('Changes view is available for new and created workspace sessions', () => { assert.ok(changesViewWhen); const context = new Context(1, null); context.setValue(IsPhoneLayoutContext.key, false); @@ -52,7 +52,7 @@ suite('Changes View Actions', () => { whileNew, afterCreation: changesViewWhen.evaluate(context), }, { - whileNew: false, + whileNew: true, afterCreation: true, }); }); diff --git a/src/vs/sessions/contrib/layout/browser/singlePane/singlePaneDockedTabsCoordinator.ts b/src/vs/sessions/contrib/layout/browser/singlePane/singlePaneDockedTabsCoordinator.ts index d84f22255b1a70..9e9f433c5ece08 100644 --- a/src/vs/sessions/contrib/layout/browser/singlePane/singlePaneDockedTabsCoordinator.ts +++ b/src/vs/sessions/contrib/layout/browser/singlePane/singlePaneDockedTabsCoordinator.ts @@ -42,8 +42,8 @@ const FILES_TAB_OPTIONS: IEditorOptions = { pinned: true, inactive: true, preser /** * What the active session wants from its managed docked tabs. - * - `changesSessionResource`: set for a created workspace session (the Changes multi-diff tab). `undefined` otherwise. - * - `wantsChangesTab`: `true` for a created workspace session. + * - `changesSessionResource`: set for any workspace session (the Changes multi-diff tab). `undefined` otherwise. + * - `wantsChangesTab`: `true` for any workspace session. * - `wantsFilesTab`: `true` for any workspace, non-quick-chat session (the empty Files placeholder tab). */ export interface IManagedTabsTarget { @@ -147,14 +147,17 @@ export class SinglePaneDockedTabsCoordinator extends Disposable { // [Ambient trigger] Session switch / created transition, kind-agnostic (fires for New, // Existing, and Quick Chat alike — a quick chat's target wants neither tab, so this - // reconciles any stray managed tabs away). The New/Existing-specific "ensure the - // Changes tab" nuances are supplied by those strategies via `queueReconcile`. + // reconciles any stray managed tabs away). + let previousChangesSessionResource: URI | undefined; this._register(autorun(reader => { const target = this._readTarget(reader); + const ensureChanges = !!target.changesSessionResource + && (!previousChangesSessionResource || !isEqual(previousChangesSessionResource, target.changesSessionResource)); + previousChangesSessionResource = target.changesSessionResource; if (!target.wantsChangesTab) { this._filesTabDismissed = false; } - this.queueReconcile(target, { openDefaultsIfEmpty: true }); + this.queueReconcile(target, { openDefaultsIfEmpty: true, ensureChanges }); })); // [Ambient trigger] The user opened the side pane. @@ -302,8 +305,7 @@ export class SinglePaneDockedTabsCoordinator extends Disposable { if (!session || isQuickChat || !workspace) { return { changesSessionResource: undefined, workspace: undefined, wantsChangesTab: false, wantsFilesTab: false }; } - const isCreated = read(session.isCreated); - return { changesSessionResource: isCreated ? session.resource : undefined, workspace, wantsChangesTab: isCreated, wantsFilesTab: true }; + return { changesSessionResource: session.resource, workspace, wantsChangesTab: true, wantsFilesTab: true }; } // --- Reconcile -------------------------------------------------------- diff --git a/src/vs/sessions/contrib/layout/browser/singlePane/singlePaneNewSessionStrategy.ts b/src/vs/sessions/contrib/layout/browser/singlePane/singlePaneNewSessionStrategy.ts index 3a4f3d91ce284a..2b17fdba22115d 100644 --- a/src/vs/sessions/contrib/layout/browser/singlePane/singlePaneNewSessionStrategy.ts +++ b/src/vs/sessions/contrib/layout/browser/singlePane/singlePaneNewSessionStrategy.ts @@ -111,7 +111,7 @@ export class SinglePaneNewSessionStrategy extends SinglePaneLayoutStrategy { if (editors.length === 0) { return; } - if (!editors.every((editor) => editor instanceof EmptyFileEditorInput)) { + if (!editors.every(editor => editor instanceof EmptyFileEditorInput || isChangesEditorInput(editor, this._sessionChangesService))) { return; } @@ -165,10 +165,7 @@ export class SinglePaneNewSessionStrategy extends SinglePaneLayoutStrategy { return; } this._pendingSidePaneOpenHideSessionKey = undefined; - if ( - editors.length !== 1 || - !(editors[0] instanceof EmptyFileEditorInput) - ) { + if (!editors.every(editor => editor instanceof EmptyFileEditorInput || isChangesEditorInput(editor, this._sessionChangesService))) { return; } diff --git a/src/vs/sessions/contrib/layout/test/browser/desktopSessionLayoutController.test.ts b/src/vs/sessions/contrib/layout/test/browser/desktopSessionLayoutController.test.ts index 21262258025760..0f9776b969993f 100644 --- a/src/vs/sessions/contrib/layout/test/browser/desktopSessionLayoutController.test.ts +++ b/src/vs/sessions/contrib/layout/test/browser/desktopSessionLayoutController.test.ts @@ -2762,7 +2762,7 @@ suite('LayoutController (desktop)', () => { }); }); - test('[managed tabs / Scenario 9] shows only Files for a new-session view', async () => { + test('[managed tabs / Scenario 9] shows Changes and Files for a new-session view', async () => { createSinglePaneController({ activateAux: true }); await settle(); @@ -2774,13 +2774,13 @@ suite('LayoutController (desktop)', () => { hasFilesTab: hasFilesTab(), changesTabMissing: harness.contextKeyService.getContextKeyValue(SinglePaneChangesTabMissingContext.key), }, { - hasChangesTab: false, + hasChangesTab: true, hasFilesTab: true, changesTabMissing: false, }); }); - test('[managed tabs / new session] keeps Changes unavailable after a delayed different-folder restore', async () => { + test('[managed tabs / new session] restores Changes after a delayed different-folder restore', async () => { const controller = createSinglePaneController({ activateAux: true }); await settle(); @@ -2794,7 +2794,7 @@ suite('LayoutController (desktop)', () => { harness.activeSessionObs.set(makeSession(URI.parse('session:new'), { status: SessionStatus.Untitled, isCreated: false }), undefined); await settle(); - assert.deepStrictEqual({ hasChangesTab: hasChangesTab(), hasFilesTab: hasFilesTab() }, { hasChangesTab: false, hasFilesTab: true }); + assert.deepStrictEqual({ hasChangesTab: hasChangesTab(), hasFilesTab: hasFilesTab() }, { hasChangesTab: true, hasFilesTab: true }); // A different default folder delays this restore until after the draft reconcile. const filesTab = harness.activeGroupEditors.find(editor => editor instanceof EmptyFileEditorInput); @@ -2806,7 +2806,7 @@ suite('LayoutController (desktop)', () => { }); await settle(); - assert.deepStrictEqual({ hasChangesTab: hasChangesTab(), hasFilesTab: hasFilesTab() }, { hasChangesTab: false, hasFilesTab: true }); + assert.deepStrictEqual({ hasChangesTab: hasChangesTab(), hasFilesTab: hasFilesTab() }, { hasChangesTab: true, hasFilesTab: true }); }); test('[managed tabs / submit] activates Changes only after a submitted session reports changes', async () => { @@ -2816,7 +2816,7 @@ suite('LayoutController (desktop)', () => { const session = makeSession(URI.parse('session:new'), { status: SessionStatus.Untitled, isCreated: false }); harness.activeSessionObs.set(session, undefined); await settle(); - assert.deepStrictEqual({ hasChangesTab: hasChangesTab(), hasFilesTab: hasFilesTab() }, { hasChangesTab: false, hasFilesTab: true }); + assert.deepStrictEqual({ hasChangesTab: hasChangesTab(), hasFilesTab: hasFilesTab() }, { hasChangesTab: true, hasFilesTab: true }); // Submit from the Files tab: visibility and the active tab stay unchanged. harness.activeEditorInput = harness.activeGroupEditors.find(e => e instanceof EmptyFileEditorInput); @@ -2840,11 +2840,11 @@ suite('LayoutController (desktop)', () => { createSinglePaneController({ activateAux: true }); await settle(); - // New-session draft active: only Files is present. + // New-session draft active: Changes and Files are present. const draft = makeSession(URI.parse('session:draft'), { status: SessionStatus.Untitled, isCreated: false }); harness.activeSessionObs.set(draft, undefined); await settle(); - assert.deepStrictEqual({ hasChangesTab: hasChangesTab(), hasFilesTab: hasFilesTab() }, { hasChangesTab: false, hasFilesTab: true }); + assert.deepStrictEqual({ hasChangesTab: hasChangesTab(), hasFilesTab: hasFilesTab() }, { hasChangesTab: true, hasFilesTab: true }); // The provider commits the draft by replacing it with a new created resource. const committedResource = URI.parse('session:committed'); @@ -2872,11 +2872,11 @@ suite('LayoutController (desktop)', () => { createSinglePaneController({ activateAux: true }); await settle(); - // Session A is a new-session draft with only Files. + // Session A is a new-session draft with Changes and Files. const sessionA = makeSession(URI.parse('session:a'), { status: SessionStatus.Untitled, isCreated: false }); harness.activeSessionObs.set(sessionA, undefined); await settle(); - assert.deepStrictEqual({ hasChangesTab: hasChangesTab(), hasFilesTab: hasFilesTab() }, { hasChangesTab: false, hasFilesTab: true }); + assert.deepStrictEqual({ hasChangesTab: hasChangesTab(), hasFilesTab: hasFilesTab() }, { hasChangesTab: true, hasFilesTab: true }); // Pause the very next Changes open so A's submit reconcile stalls mid-open. let releaseChangesOpen!: () => void; @@ -3065,7 +3065,7 @@ suite('LayoutController (desktop)', () => { assert.deepStrictEqual({ hasChangesTab: hasChangesTab(), hasFilesTab: hasFilesTab() }, { hasChangesTab: true, hasFilesTab: false }); }); - test('[managed tabs / new session] re-opens Files when a working-set apply empties the group during the switch', async () => { + test('[managed tabs / new session] re-opens managed tabs when a working-set apply empties the group during the switch', async () => { const controller = createSinglePaneController({ activateAux: true }); await settle(); @@ -3084,11 +3084,11 @@ suite('LayoutController (desktop)', () => { }); await settle(); - // Only Files is restored for the uncreated session. - assert.deepStrictEqual({ hasChangesTab: hasChangesTab(), hasFilesTab: hasFilesTab() }, { hasChangesTab: false, hasFilesTab: true }); + // Changes and Files are restored for the uncreated session. + assert.deepStrictEqual({ hasChangesTab: hasChangesTab(), hasFilesTab: hasFilesTab() }, { hasChangesTab: true, hasFilesTab: true }); }); - test('[managed tabs / new session] re-opens Files on restore-end even if no editor-change fires during the restore', async () => { + test('[managed tabs / new session] re-opens managed tabs on restore-end even if no editor-change fires during the restore', async () => { const controller = createSinglePaneController({ activateAux: true }); await settle(); @@ -3099,14 +3099,14 @@ suite('LayoutController (desktop)', () => { // Switch to a new (uncreated) session; the working-set apply empties the // group during the restore but the transient editor-change is NOT observed // (it races the async close). Only the settled restore-end must re-open the - // Files tab. + // managed tabs. harness.activeSessionObs.set(makeSession(URI.parse('session:new'), { status: SessionStatus.Untitled, isCreated: false }), undefined); controller.runWithRestore(() => { harness.activeGroupEditors.splice(0, harness.activeGroupEditors.length); }); await settle(); - assert.deepStrictEqual({ hasChangesTab: hasChangesTab(), hasFilesTab: hasFilesTab() }, { hasChangesTab: false, hasFilesTab: true }); + assert.deepStrictEqual({ hasChangesTab: hasChangesTab(), hasFilesTab: hasFilesTab() }, { hasChangesTab: true, hasFilesTab: true }); }); test('[managed tabs / Scenario 9] removes the Files tab while a real editor is open and does not re-add it when that file closes', async () => {