Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/vs/sessions/LAYOUT_CONTROLLER.md
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,10 @@ editor part before opening the managed Changes editor; this keeps tab activation
non-revealing while the pill reliably shows the multi-diff editor. The `+` Add Tab managed-tab actions
are also explicit tab-add gestures: they pass the active group's end index so a re-added managed
Changes/Files tab lands after the existing tabs rather than at the automatic Changes default position.
While the editor area is hidden, the managed Changes editor and Files placeholder declare
`EditorInputCapabilities.CannotClose`, so standard close actions cannot remove either tab from the
visible detail panel. Revealing the editor area makes both tabs closeable again. Managed-tab
reconciliation uses an explicit forced close when it removes stale inputs or tidies the Files placeholder.
While the detail is visible, every diff editor selects the Changes container and every file editor
selects the Files container, regardless of whether the file is inside the active session workspace.
Rendered Markdown preview and Markdown custom editors also select Files.
Expand Down
10 changes: 5 additions & 5 deletions src/vs/sessions/SINGLE_PANE_SCENARIOS.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ width) captures a width to restore later.
| **Hide Editor** (`right-panel-hide`) | Editor title bar (tab strip), after Maximize/Restore | Closes the editor content and keeps the detail (→ *Detail only*). The docked side pane shrinks to the detail width so the freed editor width goes to the **chat**, not the detail. Always shown and always enabled, regardless of whether a detail panel is currently visible. |
| **Show Editor** (`right-panel-show`) | Editor title bar (tab strip), same slot as Hide Editor | Reveals the (possibly empty) editor content again. Always shown whenever the editor area is closed, regardless of the active tab's detail support. |
| **Collapse All Diffs** | Changes editor header, primary inline | Collapses every file in the Changes multi-diff (`SessionChangesEditor.collapseAllDiffs`). |
| **`+` Add Tab** | End of the tab strip | Opens the Add Tab menu (Browser `⇧⌘K B`, Search `⌘K S`; a **Changes** entry when the Changes editor tab is closed, and a **Files** entry `⌘K B` when the Files tab is closed — both for any workspace session). Re-added managed Changes/Files tabs are inserted at the **end** of the tab strip. Search opens a new Search editor. **Hidden when the editor area is closed.** |
| **`+` Add Tab** | End of the tab strip | Opens the Add Tab menu (Browser `⇧⌘K B`, Search `⌘K S`; a **Changes** entry when the Changes editor tab is absent, and a **Files** entry `⌘K B` when the Files tab is absent — both for any workspace session). Restored managed Changes/Files tabs are inserted at the **end** of the tab strip. Search opens a new Search editor. **Hidden when the editor area is closed.** |
| **Toggle Side Panel** | Command / keybinding | Closes/opens the **whole** side pane (editor + detail together) → chat-only and back. The mechanics live on the workbench layout service (`toggleSidePane`); while the editor area is maximized, the shared `Workbench.toggleSidePane()` remembers maximization, un-maximizes, then performs the collapse so the restored detail is also hidden. Reopening restores the complete side-pane composition before re-maximizing the editor. Hiding a focused side pane moves focus to the sessions list. |
| **Toggle Sessions List** | Title bar / command | Collapses/opens the left sessions list. Collapsing it gives the freed width to the editor/detail side pane (not the chat); reopening restores the previous editor/detail width so the chat gets that space back. No single-pane editor or detail action changes this visibility. |
| **Grid sash** | Between the chat and the third pane | Dragging a detail-only side pane wider keeps the editor content closed. When editor content and details are visible but no longer fit, the detail panel hides; widening past the hysteresis threshold restores it. |
Expand All @@ -92,19 +92,19 @@ width) captures a width to restore later.

**Managed Files tab.** The empty Files placeholder tab (and the Changes tab) is opened when the editor group is **empty** on a view-open trigger (a session switch or a side-pane reveal), and both remain present whenever the layout is **Detail only**. Opening a real workspace file **tidies away** the empty placeholder (a `[Changes][file]` strip) as a **one-shot reaction to that open** — not a standing rule — so the user can still add the Files tab via **`+` Files** while a real file is open (that opens an `EmptyFileEditorInput`, not a real file, so it is not tidied away). The placeholder is **not** re-added when the real file closes while Editor is visible; the defaults return when the group empties and the side pane is reopened or when the layout enters Detail only.

**Layout-driven vs user editor changes.** The default docked tabs are (re)opened into an empty group on a **settled** session-switch restore — the base controller fires `onDidEndSessionLayoutRestore` once the restore epoch (working-set apply + aux restore) completes, and the strategy reconciles off that. This matters for a new session: its **empty** working set closes the previous session's docked tabs, emptying the group *after* the switch; reconciling on the settled restore-end reads the reliably-empty group and re-opens both managed tabs. Reacting to the transient editor-change *during* the async apply would race the empty state. A **user-driven** editor change (opening a file, closing a tab) does not re-open defaults while Editor is visible; in Detail only, however, every reconcile restores both managed inputs because the detail panel depends on them.
**Layout-driven vs user editor changes.** The default docked tabs are (re)opened into an empty group on a **settled** session-switch restore — the base controller fires `onDidEndSessionLayoutRestore` once the restore epoch (working-set apply + aux restore) completes, and the strategy reconciles off that. This matters for a new session: its **empty** working set closes the previous session's docked tabs, emptying the group *after* the switch; reconciling on the settled restore-end reads the reliably-empty group and re-opens both managed tabs. Reacting to the transient editor-change *during* the async apply would race the empty state. A **user-driven** editor change (opening a file, closing a tab) does not re-open defaults while Editor is visible; in Detail only, standard close actions cannot remove the managed inputs and every reconcile restores either input removed by lifecycle work.

**Folder-less composer to workspace draft.** Opening **New Session** first exposes a folder-less composer and then seeds its concrete workspace draft. The first step removes the previous session's Changes tab while the shared Files placeholder can keep the editor group non-empty, so the second step explicitly ensures Changes when `wantsChangesTab` becomes true. When the selected session folder differs from the new-session default folder, the workspace-gated working-set restore can settle later and remove that early Changes tab while retaining Files; the settled restore therefore repeats the one-shot Changes ensure for the uncreated session. Relying only on the empty-group rule or only on the initial eligibility transition leaves Files as the sole tab until another reveal or New Session gesture.

**New-session submit.** Submit preserves the current editor/detail visibility and seeds the Existing Sessions profile from that composition, avoiding any layout jump. The Files tab remains active until the submitted session reports its first file changes; then Changes becomes active without revealing Editor. This pending activation is scoped to the submitted session, so switching away cannot activate Changes in another session.

**Details-only invariant.** Whenever the side pane is **Detail only** (the aux-bar detail panel is visible without the editor area — e.g. the new-session view, or a created session whose editor was hidden), the docked details panel *shows* the managed docked inputs, so Changes and Files are always present. Every reconcile reads the settled, current part visibility and restores either input even when the group is non-empty; closing one while Detail only therefore re-creates it immediately. When Editor is visible, the strict "add only into an empty group" rule remains and a close is respected.
**Details-only invariant.** Whenever the side pane is **Detail only** (the aux-bar detail panel is visible without the editor area — e.g. the new-session view, or a created session whose editor was hidden), the docked details panel *shows* the managed docked inputs, so Changes and Files are always present. Both inputs adopt `EditorInputCapabilities.CannotClose`, and every reconcile reads the settled, current part visibility and restores either input removed by lifecycle work even when the group is non-empty. When Editor is visible, the capability is removed and the strict "add only into an empty group" rule remains.

**Closing managed tabs.** The user can close the managed Changes and Files tabs (they are non-preview, not sticky) while Editor is visible. Those closes are respected without any dismissal bookkeeping: the default tabs are opened **only into an empty editor group** on a view-open trigger (plus the one-shot submit activation above), so closing one tab while another (or a real file) remains leaves the group non-empty and it is not re-created. Detail only is the exception: both inputs are required and immediately restored. Closing the last tab closes the whole side pane; reopening it (empty group) restores the defaults. While a managed tab is closed for a workspace session with Editor visible, the `+` Add Tab menu offers a matching entry to reopen it — **Changes** (gated on `SinglePaneChangesTabMissingContext`) and **Files** (gated on `SinglePaneFilesTabMissingContext`); the re-added tab makes the group non-empty, so it survives.
**Closing managed tabs.** The user can close the managed Changes and Files tabs (they are non-preview, not sticky) while Editor is visible. Those closes are respected without any dismissal bookkeeping: the default tabs are opened **only into an empty editor group** on a view-open trigger (plus the one-shot submit activation above), so closing one tab while another (or a real file) remains leaves the group non-empty and it is not re-created. In Detail only, close commands and tab affordances consistently leave the backing inputs open, while internal lifecycle work can still force-close either input during working-set application, session switches, or stale-tab cleanup. Closing the last tab while Editor is visible closes the whole side pane; reopening it (empty group) restores the defaults. While a managed tab is closed for a workspace session with Editor visible, the `+` Add Tab menu offers a matching entry to reopen it — **Changes** (gated on `SinglePaneChangesTabMissingContext`) and **Files** (gated on `SinglePaneFilesTabMissingContext`); the re-added tab makes the group non-empty, so it survives.

**Per-session detail state.** A created session's detail-panel (aux-bar) visible/hidden choice is captured per session and restored on switch-back and reload (a detail-closed session stays detail-closed when returning to it), even if an external component transiently reveals the aux bar during the working-set restore or a queued detail-container sync from the previous session runs later.

**Reopening after closing all tabs.** Closing all tabs closes the whole side pane; the managed Changes and Files tabs are re-ensured, so reopening the side pane is never empty.
**Reopening after lifecycle cleanup.** If lifecycle work force-closes every tab, the whole side pane can close; the managed Changes and Files tabs are re-ensured when the side pane reopens.

**Side-pane-closed persists across reload.** Closing the whole side pane is remembered across a window reload. On reload the restored managed tab does **not** re-reveal the detail: the detail-panel forced reveal is gated on the editor content being visible, so a fully-closed side pane stays closed until the user reopens it.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/

import { localize } from '../../../../nls.js';
import { mainWindow } from '../../../../base/browser/window.js';
import { Codicon } from '../../../../base/common/codicons.js';
import { ThemeIcon } from '../../../../base/common/themables.js';
import { URI } from '../../../../base/common/uri.js';
Expand All @@ -12,6 +13,7 @@ import { EditorInput } from '../../../../workbench/common/editor/editorInput.js'
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
import { MultiDiffEditorInput } from '../../../../workbench/contrib/multiDiffEditor/browser/multiDiffEditorInput.js';
import { MultiDiffEditorViewModel } from '../../../../editor/browser/widget/multiDiffEditor/multiDiffEditorViewModel.js';
import { IWorkbenchLayoutService, Parts } from '../../../../workbench/services/layout/browser/layoutService.js';
import { DockedEditorInput } from '../../../common/dockedEditorInput.js';

/**
Expand All @@ -29,8 +31,14 @@ export class SessionChangesEditorInput extends DockedEditorInput {
constructor(
readonly multiDiffSource: URI,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService,
) {
super();
this._register(layoutService.onDidChangePartVisibility(event => {
if (event.partId === Parts.EDITOR_PART) {
this._onDidChangeCapabilities.fire();
}
}));
}

override get resource(): URI {
Expand All @@ -46,7 +54,8 @@ export class SessionChangesEditorInput extends DockedEditorInput {
}

override get capabilities(): EditorInputCapabilities {
return super.capabilities | EditorInputCapabilities.Singleton | EditorInputCapabilities.Readonly;
const capabilities = super.capabilities | EditorInputCapabilities.Singleton | EditorInputCapabilities.Readonly;
return this.layoutService.isVisible(Parts.EDITOR_PART, mainWindow) ? capabilities : capabilities | EditorInputCapabilities.CannotClose;
}

override getName(): string {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import assert from 'assert';
import { Emitter } from '../../../../../base/common/event.js';
import { URI } from '../../../../../base/common/uri.js';
import { mock } from '../../../../../base/test/common/mock.js';
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js';
import { EditorInputCapabilities } from '../../../../../workbench/common/editor.js';
import { IPartVisibilityChangeEvent, IWorkbenchLayoutService, Parts } from '../../../../../workbench/services/layout/browser/layoutService.js';
import { SessionChangesEditorInput } from '../../browser/sessionChangesEditorInput.js';

suite('SessionChangesEditorInput', () => {
const disposables = ensureNoDisposablesAreLeakedInTestSuite();

test('updates managed Changes editor capabilities with editor area visibility', () => {
const instantiationService = disposables.add(new TestInstantiationService());
let editorVisible = false;
const onDidChangePartVisibility = disposables.add(new Emitter<IPartVisibilityChangeEvent>());
const layoutService = new class extends mock<IWorkbenchLayoutService>() {
override readonly onDidChangePartVisibility = onDidChangePartVisibility.event;
override isVisible(part: Parts): boolean {
return part === Parts.EDITOR_PART && editorVisible;
}
};
const input = disposables.add(new SessionChangesEditorInput(URI.parse('test-changes:session'), instantiationService, layoutService));
let capabilitiesChanges = 0;
disposables.add(input.onDidChangeCapabilities(() => capabilitiesChanges++));

const hiddenCapabilities = input.capabilities;
editorVisible = true;
onDidChangePartVisibility.fire({ partId: Parts.EDITOR_PART, visible: true });

assert.deepStrictEqual({
hiddenCapabilities,
visibleCapabilities: input.capabilities,
capabilitiesChanges
}, {
hiddenCapabilities: EditorInputCapabilities.ExcludeFromEditorLimit |
EditorInputCapabilities.Singleton |
EditorInputCapabilities.Readonly |
EditorInputCapabilities.CannotClose,
visibleCapabilities: EditorInputCapabilities.ExcludeFromEditorLimit |
EditorInputCapabilities.Singleton |
EditorInputCapabilities.Readonly,
capabilitiesChanges: 1
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export class EmptyFileEditorInput extends DockedEditorInput {
this._register(layoutService.onDidChangePartVisibility(event => {
if (event.partId === Parts.EDITOR_PART) {
this._onDidChangeLabel.fire();
this._onDidChangeCapabilities.fire();
}
}));
}
Expand Down Expand Up @@ -59,7 +60,8 @@ export class EmptyFileEditorInput extends DockedEditorInput {
}

override get capabilities(): EditorInputCapabilities {
return super.capabilities | EditorInputCapabilities.Readonly | EditorInputCapabilities.Singleton | EditorInputCapabilities.ForceReveal;
const capabilities = super.capabilities | EditorInputCapabilities.Readonly | EditorInputCapabilities.Singleton | EditorInputCapabilities.ForceReveal;
return this.layoutService.isVisible(Parts.EDITOR_PART, mainWindow) ? capabilities : capabilities | EditorInputCapabilities.CannotClose;
}

override getName(): string {
Expand Down
8 changes: 8 additions & 0 deletions src/vs/sessions/contrib/editor/browser/media/editorTabs.css
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@
--agent-sessions-editor-tab-padding: 0 var(--vscode-spacing-size100) 0 0;
}

.agent-sessions-workbench .part.editor .tabs-container > .tab.cannot-close {
margin-right: var(--vscode-spacing-size40);
}

.agent-sessions-workbench .part.editor .tabs-container > .tab.cannot-close:not(.dirty):not(.sticky-compact) {
--agent-sessions-editor-tab-padding: 0 var(--vscode-spacing-size40);
}

/* Size tabs to their content and never shrink them, so that when more tabs
* exist than fit the container they overflow and the horizontal scrollbar
* appears (matching the default editor behaviour). The default `sizing-fit`
Expand Down
Loading
Loading