From 86601591bdc2a90216b122cb4e8891d2a19eb6fd Mon Sep 17 00:00:00 2001 From: Peng Lyu Date: Fri, 24 Apr 2026 14:31:45 -0700 Subject: [PATCH 1/4] mobile: drop unneeded !important from phone-layout CSS Remove 22 !important declarations from mobileChatShell.css where the .phone-layout class already wins by specificity. Kept !important only where it fights SplitView inline styles, Part.layoutContents inline sizing, or an equal-specificity desktop rule. Added a top-of-file policy comment documenting the three legitimate reasons to use !important here so future additions don't accrete out of habit. Verified against a 393x852 iPhone emulation with the mock agent host: welcome screen, sidebar overlay, session opening, and chat input layout are unchanged. Also updated the vscode-dev-workbench skill to call out that npm run dev must run in the vscode-dev folder (not vscode) and that the mock agent host is a separate process that must be started in addition to the dev server. --- .github/skills/vscode-dev-workbench/SKILL.md | 20 ++++-- .../browser/parts/mobile/mobileChatShell.css | 66 +++++++++++-------- 2 files changed, 56 insertions(+), 30 deletions(-) diff --git a/.github/skills/vscode-dev-workbench/SKILL.md b/.github/skills/vscode-dev-workbench/SKILL.md index 01589c1716cb1..93716893ca0d9 100644 --- a/.github/skills/vscode-dev-workbench/SKILL.md +++ b/.github/skills/vscode-dev-workbench/SKILL.md @@ -21,12 +21,20 @@ If your paths differ, check `server/` in `vscode-dev` for the source root resolu ## Start the dev server +**Critical:** Run `npm run dev` from the **`vscode-dev`** folder, NOT from `vscode`. The `vscode` repo has no `dev` script and will fail with `npm error Missing script: "dev"`. Terminal tools that simplify/strip leading `cd` into separate commands will silently keep the cwd of a previous terminal — always use an absolute `pushd` or verify with `pwd` before `npm run dev`. + ```bash -cd vscode-dev -npm run dev # runs watch + nodemon; serves https://127.0.0.1:3000 +cd /path/to/vscode-dev # NOT /path/to/vscode +npm run dev # runs watch + nodemon; serves https://127.0.0.1:3000 ``` -On first start you may see one crash like `Cannot find module './indexes'` — it's the watcher racing the first build. nodemon restarts automatically once `out/` finishes compiling. +If you're driving this through an agent/terminal tool, prefer: + +```bash +pushd /absolute/path/to/vscode-dev >/dev/null && pwd && npm run dev +``` + +On first start you may see one crash like `Cannot find module './indexes'` — it's the watcher racing the first build. nodemon restarts automatically once `out/` finishes compiling. The server is ready when `curl -sk -o /dev/null -w "%{http_code}" https://127.0.0.1:3000/` returns `200`. ## URLs @@ -97,15 +105,19 @@ For a true mobile viewport, drive a standalone Playwright script with `devices[' ## Testing the Agents window against a local mock agent host +If the scenario touches the Agents window (`/agents` route), you almost always need the mock agent host running. Without it, the Agents window will sit on the sign-in / tunnel-discovery screen and block any real interaction. Start it **in addition to** the dev server — it's a second terminal, not a replacement. + `vscode-dev` supports a `?mock-agent-host=ws://…` URL parameter that short-circuits tunnel discovery and wires the Agents window to a raw WebSocket. Pair it with the mock agent host binary from `microsoft/vscode`: ```bash -cd vscode +cd /path/to/vscode node out/vs/platform/agentHost/node/agentHostServerMain.js \ --enable-mock-agent --quiet --without-connection-token --port 8765 # Listens on ws://localhost:8765 ``` +Prerequisite: `out/` in the `vscode` repo must be populated by the `VS Code - Build` task (or `npm run watch`). If `out/vs/platform/agentHost/node/agentHostServerMain.js` is missing, start that task first. + `--enable-mock-agent` registers the `ScriptedMockAgent` from `src/vs/platform/agentHost/test/node/mockAgent.ts` with one pre-existing session. Seed additional sessions via the `VSCODE_AGENT_HOST_MOCK_SEED_SESSIONS` env var, using a comma-separated list of session URIs (for example, `VSCODE_AGENT_HOST_MOCK_SEED_SESSIONS=mock://pre-1,mock://pre-2`). Scripted prompts include `hello`, `use-tool`, `error`, `permission`, `write-file`, `run-safe-command`, `slow`, `client-tool`, `subagent`, etc. (see `mockAgent.ts` for the full list). Then open: diff --git a/src/vs/sessions/browser/parts/mobile/mobileChatShell.css b/src/vs/sessions/browser/parts/mobile/mobileChatShell.css index 72eddfcfe05a4..6bc47480f18d5 100644 --- a/src/vs/sessions/browser/parts/mobile/mobileChatShell.css +++ b/src/vs/sessions/browser/parts/mobile/mobileChatShell.css @@ -3,6 +3,21 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +/* + * `!important` policy for this file: + * + * Only use `!important` when fighting one of: + * 1. SplitView.View.layoutContainer — sets inline position/top/left/width/height + * on every `.split-view-view` (src/vs/base/browser/ui/splitview/splitview.ts). + * 2. Part.layoutContents — inlines width/height on `.part > .content` via size(). + * 3. An equal-specificity desktop rule in the main workbench stylesheet. + * + * Rules that only face lower-specificity desktop CSS (e.g. a single `.part.X` + * selector) do NOT need `!important` — the `.phone-layout` class on the + * workbench root already wins. When adding a new rule, omit `!important` + * first and only add it if DevTools shows an actual override. + */ + /* ---- Mobile Top Bar ---- */ .mobile-top-bar { @@ -114,9 +129,9 @@ /* On phone, stack the mobile top bar and grid vertically */ .agent-sessions-workbench.phone-layout { - display: flex !important; - flex-direction: column !important; - overflow: hidden !important; + display: flex; + flex-direction: column; + overflow: hidden; } /* On phone, split-view-views that directly contain a Part fill the full @@ -175,34 +190,33 @@ height: 100% !important; } -/* Remove card appearance from ALL parts on phone */ +/* Remove card appearance from ALL parts on phone. + Specificity wins over the desktop card rule in style.css without !important; + width/height match what the mobile Part.layout() already inlines. */ .agent-sessions-workbench.phone-layout .part.chatbar, .agent-sessions-workbench.phone-layout .part.sidebar, .agent-sessions-workbench.phone-layout .part.auxiliarybar, .agent-sessions-workbench.phone-layout .part.panel { - margin: 0 !important; - border: none !important; - border-radius: 0 !important; - box-shadow: none !important; - --part-border-color: transparent !important; - width: 100% !important; - height: 100% !important; + margin: 0; + border: none; + border-radius: 0; + box-shadow: none; + --part-border-color: transparent; + width: 100%; + height: 100%; } -/* Force content div inside parts to fill the part on phone. - Part.layoutContents() sets inline width/height via size(), which - may use the grid-allocated dimensions rather than the CSS-overridden - 100% dimensions. Override with !important. */ +/* Content div matches the inline size set by Part.layoutContents(). */ .agent-sessions-workbench.phone-layout .part.chatbar > .content, .agent-sessions-workbench.phone-layout .part.sidebar > .content, .agent-sessions-workbench.phone-layout .part.auxiliarybar > .content, .agent-sessions-workbench.phone-layout .part.panel > .content { - width: 100% !important; + width: 100%; } /* Hide the session composite bar (Copilot CLI / Approvals / Branch) on phone */ .agent-sessions-workbench.phone-layout .session-composite-bar { - display: none !important; + display: none; } /* Ensure the grid view element doesn't overflow — flex child must shrink */ @@ -224,8 +238,8 @@ } .agent-sessions-workbench.phone-layout .interactive-session .interactive-input-part { - max-width: none !important; - padding-bottom: calc(10px + env(safe-area-inset-bottom)) !important; + max-width: none !important; /* fights equal-specificity rule in style.css */ + padding-bottom: calc(10px + env(safe-area-inset-bottom)); } /* Chat input minimum font size to prevent iOS auto-zoom */ @@ -247,7 +261,7 @@ } .agent-sessions-workbench.phone-layout .part.sidebar > .composite.title { - display: none !important; + display: none; } .agent-sessions-workbench.phone-layout .part.sidebar > .content { @@ -260,7 +274,7 @@ /* Customization toolbar: hidden on phone (opens editors, not mobile-compatible) */ .agent-sessions-workbench.phone-layout .part.sidebar .ai-customization-toolbar { - display: none !important; + display: none; } /* Make sidebar footer touch-friendly */ @@ -271,7 +285,7 @@ /* Hide the "+ Session" button in the sidebar on phone — replaced by top bar + button */ .agent-sessions-workbench.phone-layout .agent-sessions-new-button-container { - display: none !important; + display: none; } /* Hide sashes on phone */ @@ -291,7 +305,7 @@ /* On phone, push the chat input to the bottom of the chat area */ .agent-sessions-workbench.phone-layout .interactive-session .interactive-input-and-execute-toolbar { - margin-top: auto !important; + margin-top: auto; } /* ---- Phone Layout: Chat Welcome Page ---- */ @@ -351,7 +365,7 @@ } .agent-sessions-workbench.phone-layout .session-workspace-picker-label { - font-size: 18px !important; + font-size: 18px; opacity: 0.6; } @@ -370,12 +384,12 @@ /* Hide the local mode bar (Copilot CLI / Default Approvals / Branch) on phone */ .agent-sessions-workbench.phone-layout .new-chat-bottom-container { - display: none !important; + display: none; } /* Also hide the sessions-chat-widget's DnD overlay on phone */ .agent-sessions-workbench.phone-layout .sessions-chat-dnd-overlay { - display: none !important; + display: none; } /* Chat widget fills full width on phone */ From f5cf1f083f4250fd9268d897c87d84feb261cbf9 Mon Sep 17 00:00:00 2001 From: Peng Lyu Date: Fri, 24 Apr 2026 14:41:51 -0700 Subject: [PATCH 2/4] mobile: full-width sidebar drawer + sidebar-toggle icon Replace the hamburger icon (Codicon.menu) in the mobile titlebar with the agents-app sidebar toggle icon (layoutSidebarLeftOff / layoutSidebarLeft), matching desktop/web. The icon flips based on SideBarVisibleContext, and the aria-label toggles between "Open sessions" and "Close sessions". Make the mobile sidebar cover the full viewport width below the top bar instead of an 85%/360px drawer with scrim. The toggle button in the top bar stays visible and is the dismiss affordance. Removes the no-longer-needed backdrop element and its CSS. --- .../browser/parts/media/sidebarPart.css | 25 +++------------ .../parts/mobile/mobileTitlebarPart.ts | 23 +++++++++++-- src/vs/sessions/browser/workbench.ts | 32 +++++-------------- 3 files changed, 33 insertions(+), 47 deletions(-) diff --git a/src/vs/sessions/browser/parts/media/sidebarPart.css b/src/vs/sessions/browser/parts/media/sidebarPart.css index 43e3e446665cc..5cc1ecf2f89ea 100644 --- a/src/vs/sessions/browser/parts/media/sidebarPart.css +++ b/src/vs/sessions/browser/parts/media/sidebarPart.css @@ -70,15 +70,15 @@ /* ---- Phone Layout: Sidebar Drawer Overlay ---- */ -/* On phone, the sidebar is a drawer that slides over the chat. - It takes 85% width (max 360px) and sits on top of everything. */ +/* On phone, the sidebar is a full-width drawer that slides over the chat. + It covers the full viewport below the mobile top bar; the top bar's + sidebar toggle button remains visible and is used to dismiss it. */ .agent-sessions-workbench.phone-layout .split-view-view:has(> .part.sidebar) { position: absolute !important; top: 0 !important; left: 0 !important; bottom: 0 !important; - width: 85% !important; - max-width: 360px !important; + width: 100% !important; height: 100% !important; z-index: 250; animation: sidebar-slide-in 200ms ease-out; @@ -99,23 +99,6 @@ } } -/* Sidebar backdrop — applied via JS when sidebar is open on phone */ -.mobile-sidebar-backdrop { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(0, 0, 0, 0.5); - z-index: 240; - animation: backdrop-fade-in 200ms ease-out; -} - -@keyframes backdrop-fade-in { - from { opacity: 0; } - to { opacity: 1; } -} - /* Increase sidebar footer action button height for touch */ .agent-sessions-workbench.phone-layout .part.sidebar > .sidebar-footer .sidebar-action-button { min-height: 44px; diff --git a/src/vs/sessions/browser/parts/mobile/mobileTitlebarPart.ts b/src/vs/sessions/browser/parts/mobile/mobileTitlebarPart.ts index 550ac0944a315..38c4cb598138f 100644 --- a/src/vs/sessions/browser/parts/mobile/mobileTitlebarPart.ts +++ b/src/vs/sessions/browser/parts/mobile/mobileTitlebarPart.ts @@ -16,6 +16,7 @@ import { IInstantiationService } from '../../../../platform/instantiation/common import { HiddenItemStrategy, MenuWorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js'; import { ISessionsManagementService } from '../../../services/sessions/common/sessionsManagement.js'; import { IsNewChatSessionContext } from '../../../common/contextkeys.js'; +import { SideBarVisibleContext } from '../../../../workbench/common/contextkeys.js'; import { Menus } from '../../menus.js'; /** @@ -72,13 +73,28 @@ export class MobileTitlebarPart extends Disposable { this._register(toDisposable(() => this.element.remove())); parent.prepend(this.element); - // Hamburger button + // Sidebar toggle button. Uses the same icon as the desktop/web + // agents-app sidebar toggle and reflects open/closed state via the + // SideBarVisibleContext key. const hamburger = append(this.element, $('button.mobile-top-bar-button')); hamburger.setAttribute('aria-label', localize('mobileTopBar.openSessions', "Open sessions")); const hamburgerIcon = append(hamburger, $('span')); - hamburgerIcon.classList.add(...ThemeIcon.asClassNameArray(Codicon.menu)); + const closedIconClasses = ThemeIcon.asClassNameArray(Codicon.layoutSidebarLeftOff); + const openIconClasses = ThemeIcon.asClassNameArray(Codicon.layoutSidebarLeft); + hamburgerIcon.classList.add(...closedIconClasses); this._register(addDisposableListener(hamburger, EventType.CLICK, () => this._onDidClickHamburger.fire())); + const sidebarVisibleKeySet = new Set([SideBarVisibleContext.key]); + const updateSidebarIcon = () => { + const isOpen = !!SideBarVisibleContext.getValue(contextKeyService); + hamburgerIcon.classList.remove(...closedIconClasses, ...openIconClasses); + hamburgerIcon.classList.add(...(isOpen ? openIconClasses : closedIconClasses)); + hamburger.setAttribute('aria-label', isOpen + ? localize('mobileTopBar.closeSessions', "Close sessions") + : localize('mobileTopBar.openSessions', "Open sessions")); + }; + updateSidebarIcon(); + // Center slot: title and/or actions container (mutually exclusive) const center = append(this.element, $('div.mobile-top-bar-center')); @@ -126,6 +142,9 @@ export class MobileTitlebarPart extends Disposable { if (e.affectsSome(newChatKeySet)) { updateCenterMode(); } + if (e.affectsSome(sidebarVisibleKeySet)) { + updateSidebarIcon(); + } })); this._register(toolbar.onDidChangeMenuItems(() => updateCenterMode())); } diff --git a/src/vs/sessions/browser/workbench.ts b/src/vs/sessions/browser/workbench.ts index de3c9c25d0f43..c992176bd935e 100644 --- a/src/vs/sessions/browser/workbench.ts +++ b/src/vs/sessions/browser/workbench.ts @@ -7,7 +7,7 @@ import '../../workbench/browser/style.js'; import './media/style.css'; import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../base/common/lifecycle.js'; import { Emitter, Event, setGlobalLeakWarningThreshold } from '../../base/common/event.js'; -import { getActiveDocument, getActiveElement, getClientArea, getWindowId, getWindows, IDimension, isAncestorUsingFlowTo, isHTMLElement, size, Dimension, runWhenWindowIdle, addDisposableListener, EventType } from '../../base/browser/dom.js'; +import { getActiveDocument, getActiveElement, getClientArea, getWindowId, getWindows, IDimension, isAncestorUsingFlowTo, isHTMLElement, size, Dimension, runWhenWindowIdle } from '../../base/browser/dom.js'; import { DeferredPromise, RunOnceScheduler } from '../../base/common/async.js'; import { isFullscreen, onDidChangeFullscreen, isChrome, isFirefox, isSafari } from '../../base/browser/browser.js'; import { mark } from '../../base/common/performance.js'; @@ -697,9 +697,6 @@ export class Workbench extends Disposable implements IAgentWorkbenchLayoutServic })); } - private sidebarDrawerBackdrop: HTMLElement | undefined; - private readonly sidebarDrawerBackdropDisposables = this._register(new DisposableStore()); - private toggleMobileSidebarDrawer(): void { const isOpen = this.partVisibility.sidebar; if (isOpen) { @@ -710,17 +707,6 @@ export class Workbench extends Disposable implements IAgentWorkbenchLayoutServic } private openMobileSidebarDrawer(): void { - // Show backdrop — created fresh each open so its click listener is - // tracked by a DisposableStore and cleaned up on close. - if (!this.sidebarDrawerBackdrop) { - const backdrop = document.createElement('div'); - backdrop.className = 'mobile-sidebar-backdrop'; - this.sidebarDrawerBackdropDisposables.add(addDisposableListener(backdrop, EventType.CLICK, () => this.closeMobileSidebarDrawer())); - this.sidebarDrawerBackdropDisposables.add(toDisposable(() => backdrop.remove())); - this.sidebarDrawerBackdrop = backdrop; - } - this.mainContainer.appendChild(this.sidebarDrawerBackdrop); - // Push a history entry so the Android back button dismisses the drawer. // Must come before setSideBarHidden(false) so layoutMobileSidebar() sees // the drawer state. @@ -729,16 +715,13 @@ export class Workbench extends Disposable implements IAgentWorkbenchLayoutServic } // Show sidebar in grid — the actual drawer dimensions are applied by - // layoutMobileSidebar() from within layout(), which respects the - // "drawer" shape on phone (85% width, below the mobile top bar). + // layoutMobileSidebar() from within layout(), which uses the full + // viewport width below the mobile top bar on phone. The toggle button + // in the top bar remains visible and is used to close the drawer. this.setSideBarHidden(false); } private closeMobileSidebarDrawer(): void { - // Remove backdrop and dispose its listener. - this.sidebarDrawerBackdropDisposables.clear(); - this.sidebarDrawerBackdrop = undefined; - // Hide sidebar in grid this.setSideBarHidden(true); @@ -1286,10 +1269,11 @@ export class Workbench extends Disposable implements IAgentWorkbenchLayoutServic return; } - // Phone drawer: 85% width (capped at 360px), positioned below the - // mobile top bar (the grid titlebar is hidden on phone). + // Phone drawer: full width, positioned below the mobile top bar so + // the sidebar toggle button stays accessible for dismissal. The grid + // titlebar is hidden on phone so we subtract only the mobile top bar. const topBarHeight = this.mobileTopBarElement?.offsetHeight ?? 48; - const drawerWidth = Math.min(Math.floor(this._mainContainerDimension.width * 0.85), 360); + const drawerWidth = this._mainContainerDimension.width; const drawerHeight = Math.max(0, this._mainContainerDimension.height - topBarHeight); sidebarContainer.classList.add('mobile-overlay-sidebar'); sidebarContainer.style.position = 'fixed'; From 2565d6ccb629761901359d1594d11b19f26b82e7 Mon Sep 17 00:00:00 2001 From: Peng Lyu Date: Fri, 24 Apr 2026 14:52:17 -0700 Subject: [PATCH 3/4] mobile: fix one-frame snap when toggling sidebar drawer The previous code wrote inline position/top/left/width/height/zIndex on the sidebar Part container from layoutMobileSidebar(). Those styles were applied AFTER the grid had already made the wrapper visible and the browser had painted one frame, producing a visible snap on every toggle. The geometry was redundant with the existing layout anyway: the mobile top bar is a flex sibling above the grid, so the grid (and therefore the .split-view-view wrapper which fills the grid via CSS) is already correctly positioned below the top bar. The Part fills the wrapper. No inline positioning is needed. Drop the inline style writes; keep the mobile-overlay-sidebar class toggle (used for background) and the explicit sidebarPart.layout() call so the Part's internal sizing matches the drawer dimensions even on the first paint. --- src/vs/sessions/browser/workbench.ts | 30 +++++++++++----------------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/src/vs/sessions/browser/workbench.ts b/src/vs/sessions/browser/workbench.ts index c992176bd935e..01bd31f30a891 100644 --- a/src/vs/sessions/browser/workbench.ts +++ b/src/vs/sessions/browser/workbench.ts @@ -1255,33 +1255,27 @@ export class Workbench extends Disposable implements IAgentWorkbenchLayoutServic return; } - // Only phone uses the overlay drawer shape. Tablet/desktop let the - // grid position the sidebar normally, so clear any inline styles. + // On phone the sidebar renders as a full-viewport overlay drawer. + // Geometry is fully expressed in CSS — see + // `mobileChatShell.css` (split-view-view fills the grid) and + // `sidebarPart.css` (drawer animation, z-index). We avoid setting + // inline position/size styles here because writing them after the + // grid has already laid out and painted the sidebar causes a + // visible one-frame snap on toggle. const isPhone = this.layoutPolicy.viewportClass.get() === 'phone'; if (!isPhone || !this.partVisibility.sidebar) { sidebarContainer.classList.remove('mobile-overlay-sidebar'); - sidebarContainer.style.position = ''; - sidebarContainer.style.top = ''; - sidebarContainer.style.left = ''; - sidebarContainer.style.width = ''; - sidebarContainer.style.height = ''; - sidebarContainer.style.zIndex = ''; return; } - // Phone drawer: full width, positioned below the mobile top bar so - // the sidebar toggle button stays accessible for dismissal. The grid - // titlebar is hidden on phone so we subtract only the mobile top bar. + sidebarContainer.classList.add('mobile-overlay-sidebar'); + + // Re-layout the sidebar Part with the drawer's content dimensions + // so its internal composite/list sizing matches the CSS-positioned + // drawer (grid area minus the mobile top bar). const topBarHeight = this.mobileTopBarElement?.offsetHeight ?? 48; const drawerWidth = this._mainContainerDimension.width; const drawerHeight = Math.max(0, this._mainContainerDimension.height - topBarHeight); - sidebarContainer.classList.add('mobile-overlay-sidebar'); - sidebarContainer.style.position = 'fixed'; - sidebarContainer.style.top = `${topBarHeight}px`; - sidebarContainer.style.left = '0'; - sidebarContainer.style.width = `${drawerWidth}px`; - sidebarContainer.style.height = `${drawerHeight}px`; - sidebarContainer.style.zIndex = '30'; sidebarPart.layout(drawerWidth, drawerHeight, topBarHeight, 0); } From c43bef19fbd6266962e3b20b3e415aa7fa67f367 Mon Sep 17 00:00:00 2001 From: Peng Lyu Date: Fri, 24 Apr 2026 15:42:12 -0700 Subject: [PATCH 4/4] smooth slide in and less !important --- .../browser/parts/media/sidebarPart.css | 37 +++++++-- .../browser/parts/mobile/mobileChatShell.css | 78 +++++++------------ 2 files changed, 58 insertions(+), 57 deletions(-) diff --git a/src/vs/sessions/browser/parts/media/sidebarPart.css b/src/vs/sessions/browser/parts/media/sidebarPart.css index 5cc1ecf2f89ea..28f63b4d66872 100644 --- a/src/vs/sessions/browser/parts/media/sidebarPart.css +++ b/src/vs/sessions/browser/parts/media/sidebarPart.css @@ -72,7 +72,14 @@ /* On phone, the sidebar is a full-width drawer that slides over the chat. It covers the full viewport below the mobile top bar; the top bar's - sidebar toggle button remains visible and is used to dismiss it. */ + sidebar toggle button remains visible and is used to dismiss it. + + The drawer slides in/out with a transition (not a keyframe animation) so + that interrupted toggles retarget smoothly from the current position + rather than restarting. The split-view wrapper toggles `display: none` + via a `.visible` class; `transition-behavior: allow-discrete` defers + the discrete `display` change until the slide-out completes, and + `@starting-style` provides the offscreen origin for the slide-in. */ .agent-sessions-workbench.phone-layout .split-view-view:has(> .part.sidebar) { position: absolute !important; top: 0 !important; @@ -81,7 +88,24 @@ width: 100% !important; height: 100% !important; z-index: 250; - animation: sidebar-slide-in 200ms ease-out; + transform: translateX(0); + transition: + transform 260ms cubic-bezier(0.32, 0.72, 0, 1), + display 260ms allow-discrete; +} + +/* Slide-in starting state (applies on each transition into .visible) */ +@starting-style { + .agent-sessions-workbench.phone-layout .split-view-view.visible:has(> .part.sidebar) { + transform: translateX(-100%); + } +} + +/* Slide-out target: when `.visible` is removed, splitview's own rule sets + `display: none`. With `allow-discrete` above, the transform animates first + and the discrete `display` swap happens at the end of the transition. */ +.agent-sessions-workbench.phone-layout .split-view-view:not(.visible):has(> .part.sidebar) { + transform: translateX(-100%); } /* The sidebar Part inside fills its container */ @@ -90,12 +114,9 @@ height: 100%; } -@keyframes sidebar-slide-in { - from { - transform: translateX(-100%); - } - to { - transform: translateX(0); +@media (prefers-reduced-motion: reduce) { + .agent-sessions-workbench.phone-layout .split-view-view:has(> .part.sidebar) { + transition: none; } } diff --git a/src/vs/sessions/browser/parts/mobile/mobileChatShell.css b/src/vs/sessions/browser/parts/mobile/mobileChatShell.css index 6bc47480f18d5..2f79a84a73e25 100644 --- a/src/vs/sessions/browser/parts/mobile/mobileChatShell.css +++ b/src/vs/sessions/browser/parts/mobile/mobileChatShell.css @@ -134,10 +134,16 @@ overflow: hidden; } -/* On phone, split-view-views that directly contain a Part fill the full - grid area. Uses :has(> .part) to target only part containers — NOT - nested split-views inside parts' own content. */ -.agent-sessions-workbench.phone-layout .split-view-view:has(> .part) { +/* On phone, all split-view-view wrappers inside the grid — both those + wrapping parts AND those wrapping nested grid branch nodes — fill the + full grid area. This collapses the multi-axis grid into a single + full-screen viewport so parts overlap rather than share horizontal + space. The sidebar uses its own z-index + transform to slide over + the chat (see sidebarPart.css). The :has() conditions scope strictly + to grid wrappers so splitviews used inside individual parts' content + (e.g. a sidebar's view list) are not affected. */ +.agent-sessions-workbench.phone-layout .split-view-view:has(> .part), +.agent-sessions-workbench.phone-layout .split-view-view:has(> .monaco-grid-branch-node) { position: absolute !important; top: 0 !important; left: 0 !important; @@ -145,49 +151,17 @@ height: 100% !important; } -/* The grid's own branch nodes (NOT those inside parts) need full sizing. - Target only direct children of the grid root. */ -.agent-sessions-workbench.phone-layout > .monaco-grid-view > .monaco-grid-branch-node { - position: absolute !important; - top: 0 !important; - left: 0 !important; - width: 100% !important; - height: 100% !important; -} - -/* Split-view-views inside the grid root that contain branch nodes */ -.agent-sessions-workbench.phone-layout > .monaco-grid-view > .monaco-grid-branch-node > .monaco-split-view2 > .split-view-container > .split-view-view:has(> .monaco-grid-branch-node) { - position: absolute !important; - top: 0 !important; - left: 0 !important; - width: 100% !important; - height: 100% !important; -} - -/* Second-level grid branch nodes */ -.agent-sessions-workbench.phone-layout > .monaco-grid-view > .monaco-grid-branch-node > .monaco-split-view2 > .split-view-container > .split-view-view > .monaco-grid-branch-node { - position: absolute !important; - top: 0 !important; - left: 0 !important; - width: 100% !important; - height: 100% !important; -} - -/* Third-level (top-right section) */ -.agent-sessions-workbench.phone-layout > .monaco-grid-view > .monaco-grid-branch-node > .monaco-split-view2 > .split-view-container > .split-view-view > .monaco-grid-branch-node > .monaco-split-view2 > .split-view-container > .split-view-view:has(> .monaco-grid-branch-node) { - position: absolute !important; - top: 0 !important; - left: 0 !important; - width: 100% !important; - height: 100% !important; -} - -.agent-sessions-workbench.phone-layout > .monaco-grid-view > .monaco-grid-branch-node > .monaco-split-view2 > .split-view-container > .split-view-view > .monaco-grid-branch-node > .monaco-split-view2 > .split-view-container > .split-view-view > .monaco-grid-branch-node { - position: absolute !important; - top: 0 !important; - left: 0 !important; - width: 100% !important; - height: 100% !important; +/* All grid branch nodes fill their parent. `.monaco-grid-branch-node` is + specific to the grid widget, so this descendant selector won't hit + splitviews used inside individual parts' content. The grid widget + does not write inline geometry to branch nodes (only to the wrapping + `.split-view-view`), so plain CSS suffices here. */ +.agent-sessions-workbench.phone-layout .monaco-grid-branch-node { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; } /* Remove card appearance from ALL parts on phone. @@ -206,12 +180,18 @@ height: 100%; } -/* Content div matches the inline size set by Part.layoutContents(). */ +/* Pin Part content to the wrapper's full width on phone. + `!important` is required because `Part.layoutContents()` inlines the + width on `.part > .content` based on the splitview size (rule #2 in the + policy above). Without this, opening the sidebar — which makes the + splitview share space between sidebar and chatbar — would shrink the + chatbar's content during the drawer slide animation. */ .agent-sessions-workbench.phone-layout .part.chatbar > .content, .agent-sessions-workbench.phone-layout .part.sidebar > .content, .agent-sessions-workbench.phone-layout .part.auxiliarybar > .content, .agent-sessions-workbench.phone-layout .part.panel > .content { - width: 100%; + width: 100% !important; + height: 100% !important; } /* Hide the session composite bar (Copilot CLI / Approvals / Branch) on phone */