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: 2 additions & 2 deletions src/vs/sessions/LAYOUT.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ Within a session view, chats default to a single **chat group** rendered as a ta
- **Persistence.** When a created session holds more than one group, its partition (each group's ordered chat resources + active chat, the grid tree, sizes, and active group) is persisted to **workspace storage** keyed by `session.sessionId` (a single `sessions.chatGroupsLayout` map). New-session drafts always use one fresh group and clear stale state for their ID. The layout is captured on mutations (split / move / active-group change / reconcile) and re-captured on session switch-away and dispose (to snapshot the latest sash sizes). On reopen, `_tryRestoreLayout` deserializes the grid (each leaf's `index` maps a node back to groups) instead of building a single group. Because a session's chat catalog loads asynchronously after reload, restore keeps a saved `resource → group` assignment alive and routes each chat (including late-loading ones) back to its saved group via the reconcile autorun. Restoration completes when all saved chats are present, the catalog changes from its initial snapshot, or `session.loading` reports that initialization has settled. Missing chats are then treated as deleted and empty groups collapse. Restore is fully observable-driven (no timeouts). A single-group session stores nothing and clears any prior entry.
- `ChatGroupView` ([browser/parts/chatGroupView.ts](src/vs/sessions/browser/parts/chatGroupView.ts)) is a single grid leaf hosting a `ChatCompositeBar` (its group's tab strip) above a kind-switched chat view (see the table below). Each group independently renders its own active chat, so multiple chats can be visible side-by-side. When the group's active chat is **read-only** (non-interactive — e.g. a subagent transcript or an archived session), a `SessionReadOnlyBanner` ([browser/parts/sessionReadOnlyBanner.ts](src/vs/sessions/browser/parts/sessionReadOnlyBanner.ts)) is shown flush below the tab strip in place of the composer, with an inline **Restore** action for archived sessions. The banner is aligned with the tab strip: with a lone group it is capped to the centered content band and centered (via a `.single-group` CSS rule in [chatGroupsView.css](src/vs/sessions/browser/parts/media/chatGroupsView.css), mirroring the tab strip's rule); with more than one group it spans the full leaf width. Its `toJSON` carries the group's serialization `index` so the grid deserializer can map restored nodes back to groups.
- `ChatCompositeBar` ([browser/parts/chatCompositeBar.ts](src/vs/sessions/browser/parts/chatCompositeBar.ts)) is a tab-strip renderer driven by an `IChatCompositeBarDelegate` supplied by the owning group. Its tabs are draggable and render this group's `visibleChatTabs` in the group's order; a read-only chat's tab shows a **lock** icon. The tab strip is shown (via the group's `tabsVisible` observable) when more than one group exists, or — for a lone group — when `IActiveSession.shouldShowChatTabs` is set (the session has more than one visible chat tab). At the end of the strip a trailing **New Chat** button (gated on `ISessionCapabilities.supportsMultipleChats`, disabled for archived sessions) is pinned; New Chat routes back through the delegate so the new chat opens into the clicked group. The **Conversations** menu is not on the tab strip — it lives in the session header meta row (see §4.1). Each non-main tab renders its close button from the contributed per-tab `Menus.SessionChatTab` (context = `{ session, chat }`), whose `sessions.chatCompositeBar.closeChat` command hides the chat session-wide (reopenable from Conversations); the tab context menu offers **Rename** / **Delete Chat** gated on `getChatCapabilities`. A tab is also a drag source for a `#chat` reference (via `fillChatReferenceDragData`, resolving the chat's backend resource through `ISessionsProvidersService`) so it can be dropped into an agent-host chat input. Because the Agents workbench is always modern, the tab DOM consumes reusable editor-tab hooks from [workbench/contrib/styleOverrides/browser/media/tabs.css](src/vs/workbench/contrib/styleOverrides/browser/media/tabs.css), while [chatCompositeBar.css](src/vs/sessions/browser/parts/media/chatCompositeBar.css) retains only chat-specific layout and adornments. `SessionView` exposes the shared hook's focused/unfocused group state so side-by-side sessions follow the same color branches as editor groups, and `applySessionBarThemeColors` ([browser/parts/sessionBarStyles.ts](src/vs/sessions/browser/parts/sessionBarStyles.ts)) supplies the shared visual tokens.
- Drag-and-drop is handled by `ChatGroupDropTarget` ([browser/parts/chatGroupDropTarget.ts](src/vs/sessions/browser/parts/chatGroupDropTarget.ts)), which displays a 5-zone overlay (left / right / top / bottom / center) on the hovered group. Dropping a chat onto a group's **center** moves it into that group; dropping it onto an **edge** splits it into a new group in that direction. The dragged chat's `{ sessionId, resource }` is carried on the drag event's **`dataTransfer`** (mime `SessionsDataTransfers.CHAT`, via `fillSessionChatDragData`/`isSessionChatDrag`/`getSessionChatDragData` in [browser/dnd.ts](src/vs/sessions/browser/dnd.ts)); drops from a different session are ignored. **Pitfall:** the group-move payload must **not** use the shared `LocalSelectionTransfer` singleton, because a chat-tab drag also offers a chat-*reference* payload (`DraggedChatReferenceIdentifier`, dropped into a chat input) that uses that same singleton — and `LocalSelectionTransfer` is a single global slot, so whichever payload is set last wins. Reference-carrying tabs (agent-host chats) would otherwise clobber the group-move identifier, so the drop target's `dragenter` saw no chat drag and never showed the split zones. The `dataTransfer` keeps the two payloads independent: its `types` are readable during `dragover` (to gate the overlay) and its value on `drop`.
- Drag-and-drop is handled by `ChatGroupDropTarget` ([browser/parts/chatGroupDropTarget.ts](src/vs/sessions/browser/parts/chatGroupDropTarget.ts)), which displays a 5-zone overlay (left / right / top / bottom / center) on the hovered group. Dropping a chat onto a group's **center** moves it into that group; dropping it onto an **edge** splits it into a new group in that direction. Subagent pills in the transcript use the same payload and drop zones; because subagents are hidden from the tab strip until opened, the drop first surfaces the subagent and then places it in the selected group or split. Alt+Enter on a focused subagent pill provides the keyboard-equivalent open-to-side action. The dragged chat's `{ sessionId, resource }` is carried on the drag event's **`dataTransfer`** (mime `SessionsDataTransfers.CHAT`, via `fillSessionChatDragData`/`isSessionChatDrag`/`getSessionChatDragData` in [browser/dnd.ts](src/vs/sessions/browser/dnd.ts)); drops from a different session are ignored. **Pitfall:** the group-move payload must **not** use the shared `LocalSelectionTransfer` singleton, because a chat-tab drag also offers a chat-*reference* payload (`DraggedChatReferenceIdentifier`, dropped into a chat input) that uses that same singleton — and `LocalSelectionTransfer` is a single global slot, so whichever payload is set last wins. Reference-carrying tabs (agent-host chats) would otherwise clobber the group-move identifier, so the drop target's `dragenter` saw no chat drag and never showed the split zones. The `dataTransfer` keeps the two payloads independent: its `types` are readable during `dragover` (to gate the overlay) and its value on `drop`.
- Keyboard users can focus the previous/next chat group, split the active chat right/down, and move it to the previous/next group through the corresponding Sessions commands. In multi-group layouts, each group and tab list announces its one-based position and total count.
- A newly opened, unassigned chat whose `origin.parentChat` is visible uses an existing group adjacent to that parent when one is available, regardless of whether it was opened from the transcript, Chats menu, or another surface. Existing and manually moved assignments remain authoritative. Without an adjacent group it opens normally; explicit open-to-side creates a new group.
- **Width.** With a lone group the session reads like the classic centered chat: the header, the group's tab strip, and the inner chat content (message/input cards) all align to the centered 950px band. Once the session holds **more than one group**, everything spans full width — `ChatGroupsView` drops the `.single-group` class so the centered cap on the chat content and tab strips is removed (`max-width: none` in [chatGroupsView.css](src/vs/sessions/browser/parts/media/chatGroupsView.css)), and `SessionView` adds a `.grid-layout` class and lays the header band out at full width too (driven by an autorun on `ChatGroupsView.groupCount`).
Expand Down Expand Up @@ -319,7 +319,7 @@ The entire third-pane redesign is gated behind the experimental setting `session
- Applying the Existing Session visibility profile restores both Editor and Details visibility.
- During reload there is a window after the workbench reaches `Restored` but before `restoreVisibleSessions()` supplies an active session. The New/Existing Session strategies (via `SinglePaneDetailPanelCoordinator`) return `Preserve` in that state; treating the missing session as `Hidden` would close persisted Aux, whose layout invariant reveals Editor, and paint Editor-only until the session profile arrives.
- Widening a detail-only editor node does not automatically reveal editor content. The editor area remains hidden until the user explicitly opens an editor workflow or toggles the editor area. This preserves the user's detail-only choice across sash drags and grid relayouts.
- When the outer editor sash makes a visible editor and its docked details too narrow to coexist, single-pane automatically hides details and leaves editor content visible. If the user widens the node past the detail width plus the editor minimum and a 100px hysteresis margin, it restores the details. This responsive detail behavior is exclusive to the single-pane layout.
- When the outer editor sash makes a visible editor and its docked details too narrow to coexist, single-pane automatically hides details and leaves editor content visible. It captures the editor width after that hide and restores details only when the node can fit both the captured editor width and the detail width, so restoring details does not shrink the editor. This responsive detail behavior is exclusive to the single-pane layout.
- Revealing the side pane from *closed* (`setEditorHidden(false)`, e.g. the session-header Changes button opening the Changes editor) passes `Sizing.Distribute` to `SerializableGrid.setViewVisible`. The grid already knows the revealed view's location, so it distributes that containing split and Sessions and the side pane receive equal space without either part computing pixels, percentages, or a split reference. The side pane sash's double-click reset uses the same native grid distribution because the visible editor part has no fixed `preferredWidth`. In docked mode this runs on every reveal that has no saved user width to restore; a genuinely user-chosen width still takes precedence.
- Side-pane sizes are **workbench-level, not per session**: the editor grid node width is owned by the workbench grid and persisted globally (`workbench.sessions.partSizes`), so switching between sessions keeps the same side-pane width the user last set — the layout controller does not track or restore a per-session width. The workbench persists the docked side-pane geometry across reloads via `_savePartSizes` on `onWillSaveState`, restored by `createDesktopGridDescriptor`. Because the docked detail (auxiliary bar) lives **inside** the editor grid node, the persisted editor value is the pure editor-content width: `_persistedEditorWidth` subtracts the docked detail width **only when the detail is visible**, mirroring the descriptor, which adds it back only when the detail is visible. Subtracting it unconditionally (the earlier bug) shrank an **Editor-only** session's side pane by the detail width on every reload, compounding toward zero.
- `_dockedEditorSizeBeforeHide` is captured on hide **only for "Hide Editor"** (detail/auxiliary bar still visible, so the editor node stays visible at a real user-chosen width). When the **whole** side pane closes, the editor grid node collapses to `0px`; that is not captured as a user width, so reopening falls through to the last persisted width or the equal Sessions/side-pane split.
Expand Down
2 changes: 1 addition & 1 deletion src/vs/sessions/SINGLE_PANE_SCENARIOS.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ width) captures a width to restore later.
| **`+` Add Tab** | End of the tab strip | Opens the Add Tab menu (Browser `⇧⌘K B`; Search `⌘K S` for workspace-backed sessions; 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 and is unavailable for Quick Chats. **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. |
| **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 until the pane can restore details without shrinking the expanded editor restores it. |
| **Changes pill** | Session header meta row | Opens the managed Changes multi-diff editor and explicitly reveals the editor area when the side pane was closed or in detail-only mode. The managed Changes tab still remains excluded from automatic reveal-on-open, so merely activating its tab does not reveal the editor. |

**Editor action visibility.** Maximize/Restore, Toggle Details, and Open in Modal are hidden while the **editor area is closed** (`MainEditorAreaVisibleContext`). Hide Editor and Show Editor are the mutually-exclusive pair that controls that very state: both render in the tab strip's editor-title layout cluster (`MenuId.EditorTitleLayout`), immediately after Maximize/Restore, gated only on `MainEditorAreaVisibleContext` being true/false respectively — unlike Toggle Details, they always show and are always enabled regardless of whether the active tab has a docked detail panel or the detail panel is currently visible (no `HasDockedDetailsContext` gate and no `AuxiliaryBarVisibleContext` precondition), consistent with Maximize/Restore's own always-shown behavior in that same cluster. Hide Editor unconditionally reveals the auxiliary bar as part of its `run()`, so it always has somewhere to fall back to even if the detail panel was hidden beforehand — the New/Existing Session strategy's detail-panel mapping (via the shared `SinglePaneDetailPanelCoordinator`) decides what that panel actually shows (the active tab's own detail, or the Changes/Files fallback for a Browser tab with none of its own; see §5). Show Editor reveals the editor via the same explicit-reveal API (`revealEditorPartExplicitly()`) used by the session-header Changes pill, then focuses the editor group. Toggle Details remains alone in its own trailing editor-header cluster and keeps its **has a docked detail panel** (`HasDockedDetailsContext`) gating — a managed Changes/Files tab or a text file editor — since toggling a nonexistent detail panel is never meaningful.
Expand Down
12 changes: 6 additions & 6 deletions src/vs/sessions/browser/dnd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { DraggedChatReferenceIdentifier, fillInChatReferenceDragData, LocalSelec
export const SessionsDataTransfers = {
/** Mime type used to identify a session being dragged within the application. */
SESSION: 'application/vnd.code.session',
/** Mime type used to identify a chat being dragged between groups within a session. */
/** Mime type used to identify a chat being dragged into or between groups within a session. */
CHAT: 'application/vnd.code.session.chat',
};

Expand All @@ -31,13 +31,13 @@ export class DraggedSessionIdentifier {
}

/**
* The group-move payload carried on a chat-tab drag via the
* The group-placement payload carried on a chat drag via the
* {@link SessionsDataTransfers.CHAT} `dataTransfer` mime. Used to move/split a
* chat between chat groups within a session.
* visible chat between groups or open a hidden chat in a group within a session.
*
* This is deliberately carried on the drag event's `dataTransfer` (not on the
* shared {@link LocalSelectionTransfer} singleton) because a chat-tab drag also
* offers a chat *reference* payload, and that reference uses the singleton. The
* shared {@link LocalSelectionTransfer} singleton) because a chat-tab drag can
* also offer a chat *reference* payload, and that reference uses the singleton. The
* singleton holds only one payload at a time, so relying on it here would let
* the reference payload clobber the group-move payload (and vice versa). The
* `dataTransfer` mime keeps the two independent: its `types` are readable during
Expand All @@ -49,7 +49,7 @@ export interface IDraggedSessionChat {
}

/**
* Attaches the {@link IDraggedSessionChat} group-move payload to a chat-tab drag.
* Attaches the {@link IDraggedSessionChat} group-placement payload to a chat drag.
*/
export function fillSessionChatDragData(e: DragEvent, sessionId: string, resource: URI): void {
const data: IDraggedSessionChat = { sessionId, resource: resource.toString() };
Expand Down
Loading
Loading