-
Notifications
You must be signed in to change notification settings - Fork 39.6k
sessions: Add mobile-compatible layout for agent sessions #309344
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 12 commits
c4fcd1c
b9adf4e
84f9322
cf8c57c
5e2ca68
ffe7c95
1d1804b
973e3fa
a053290
19f3f0a
6e0dbc9
543d44b
41760a2
43ffabf
7b663f5
6dcfd00
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,154 @@ | ||
| # Mobile Agent Sessions — Architecture | ||
|
|
||
| ## Core Principle | ||
|
|
||
| **Every feature accessible in the desktop window must be accessible on mobile — same functionality, different presentation.** Mobile is NOT "desktop minus stuff." It is a parallel UI layer where the same services, views, and actions are rendered through mobile-native interaction patterns. | ||
|
|
||
| ## Architecture | ||
|
|
||
| ### Mobile Part Subclasses | ||
|
|
||
| Desktop Parts (`ChatBarPart`, `SidebarPart`, `PanelPart`, `AuxiliaryBarPart`) remain unchanged. Each has a **mobile subclass** that extends it and overrides only `layout()` and/or `updateStyles()`. `AgenticPaneCompositePartService` conditionally instantiates the mobile or desktop variant at startup based on viewport width (`< 640px` → phone). | ||
|
|
||
| Each mobile Part checks the current layout class (via `isPhoneLayout(layoutService)`) at every call. When the viewport is phone it applies mobile behavior (full-cell layout, no card chrome, no session-bar subtraction). When the viewport is tablet/desktop — which happens when a real phone rotates past the 640px breakpoint — it delegates to the desktop `super` implementation. This means a `Mobile*Part` instance is safe to keep through a viewport-class transition without producing wrong layout math. | ||
|
|
||
| This means: | ||
| - Desktop code has **zero** phone-layout checks — all mobile logic lives in mobile subclasses, `MobileTopBar`, and CSS. | ||
| - Phone-instantiated parts adapt correctly to rotation across the 640px breakpoint by delegating to `super`. | ||
|
|
||
| After a viewport-class transition the workbench calls `updateStyles()` on each pane composite part so card-chrome inline styles get re-applied (desktop) or cleared (phone) for the new class. | ||
|
|
||
| ### View & Action Gating | ||
|
|
||
| Views, menu items, and actions use `when` clauses with the `sessionsIsPhoneLayout` context key to control visibility in phone layout. This follows a **default-deny** approach for phone: | ||
|
|
||
| - **Desktop-only features** add `when: IsPhoneLayoutContext.negate()` to their view descriptors and menu registrations. They simply don't appear on phone. | ||
| - **Phone-compatible features** (chat, sessions list) have no phone gate — they render on all viewports. | ||
| - **Phone-specific replacements** (when ready) register with `when: IsPhoneLayoutContext` and live in separate files under `parts/mobile/contributions/`. | ||
|
|
||
| Tablet and larger viewports currently fall back to the desktop layout; no separate tablet design exists yet. | ||
|
|
||
| Two registrations can target the same slot with opposite `when` clauses, pointing to different view classes in different files — giving full file separation with no internal branching. | ||
|
|
||
| #### Current Gating Status | ||
|
|
||
| | Feature | Phone Status | Mechanism | | ||
| |---------|--------------|-----------| | ||
| | Sessions list (sidebar) | ✅ Compatible | No gate | | ||
| | Chat views (ChatBar) | ✅ Compatible | No gate | | ||
| | Changes view (AuxiliaryBar) | ❌ Gated | `when: !sessionsIsPhoneLayout` on view descriptor | | ||
| | Files view (AuxiliaryBar) | ❌ Gated | `when: !sessionsIsPhoneLayout` on view descriptor | | ||
| | Logs view (Panel) | ❌ Gated | `when: !sessionsIsPhoneLayout` on view descriptor | | ||
| | Terminal actions | ❌ Gated | `when: !sessionsIsPhoneLayout` on menu item | | ||
| | "Open in VS Code" action | ❌ Gated | `when: !sessionsIsPhoneLayout` on menu item | | ||
| | Code review toolbar | ❌ Gated | `when: !sessionsIsPhoneLayout` on menu item | | ||
| | Customizations toolbar | ❌ Hidden | CSS `display: none` on phone | | ||
| | Titlebar | ❌ Hidden | Grid `visible: false` + CSS + MobileTopBar replacement | | ||
|
|
||
| ### Phone Layout | ||
|
|
||
| On phone-sized viewports (`< 640px` width): | ||
|
|
||
| ``` | ||
| ┌──────────────────────────────────┐ | ||
| │ [☰] Session Title [+] │ ← MobileTopBar (prepended before grid) | ||
| ├──────────────────────────────────┤ | ||
| │ │ | ||
| │ Chat (edge-to-edge) │ ← Grid: ChatBarPart fills 100% | ||
| │ │ | ||
| │ │ | ||
| │ │ | ||
| │ ┌──────────────────────────┐ │ | ||
| │ │ Chat input │ │ ← Pinned to bottom | ||
| │ └──────────────────────────┘ │ | ||
| └──────────────────────────────────┘ | ||
| ``` | ||
|
|
||
| - **MobileTopBar** is a DOM element prepended above the grid. It has a hamburger (☰), session title, and new session (+) button. | ||
| - **Sidebar** is hidden by default and opens as an **85% width drawer overlay** with a backdrop when the hamburger is tapped. CSS makes its `split-view-view` absolutely positioned with `z-index: 250`. The workbench manually calls `sidebarPart.layout()` with drawer dimensions after opening. Closing the drawer clears the navigation stack. | ||
| - **Titlebar** is hidden in the grid (`visible: false`) and via CSS — replaced by MobileTopBar. | ||
| - **SessionCompositeBar** (chat tabs) is hidden via CSS. | ||
| - The grid uses `display: flex; flex-direction: column` and all `split-view-view:has(> .part)` containers are positioned absolutely at `100% width/height`. | ||
|
|
||
| ### Viewport Classification | ||
|
|
||
| `SessionsLayoutPolicy` classifies the viewport: | ||
| - **phone**: `width < 640px` | ||
| - **tablet**: `640px ≤ width < 1024px` (treated as desktop; no phone-specific chrome) | ||
| - **desktop**: `width ≥ 1024px` | ||
|
|
||
| The workbench toggles the `phone-layout` CSS class on `layout()` and creates/destroys mobile components when the viewport class changes at runtime (e.g., DevTools device emulation, or a real phone rotating across the 640px breakpoint). MobileTopBar lifecycle is managed via a `DisposableStore` that is cleared on viewport transitions to prevent leaks. | ||
|
|
||
| ### Context Keys | ||
|
|
||
| | Key | Type | Purpose | | ||
| |-----|------|---------| | ||
| | `sessionsIsPhoneLayout` | `boolean` | `true` when the viewport is phone (< 640px) | | ||
| | `sessionsKeyboardVisible` | `boolean` | `true` when the virtual keyboard is visible | | ||
|
|
||
| ### Desktop → Mobile Component Mapping | ||
|
|
||
| | Desktop Component | Mobile Equivalent | How Accessed | | ||
| |---|---|---| | ||
| | **Titlebar** (3-section toolbar) | **MobileTopBar** (☰ / title / +) | Always visible at top | | ||
| | **Sidebar** (sessions list) | Drawer overlay (85% width) | Hamburger button (☰) | | ||
| | **ChatBar** (chat widget) | Same Part, edge-to-edge, no card chrome | Default view (always visible) | | ||
| | **AuxiliaryBar** (files, changes) | Gated — not shown on mobile | Planned: mobile-specific view | | ||
| | **Panel** (terminal, output) | Gated — not shown on mobile | Planned: mobile-specific view | | ||
| | **SessionCompositeBar** (chat tabs) | Hidden on phone | — | | ||
| | **New Session** (sidebar button) | + button in MobileTopBar | Always visible in top bar | | ||
|
|
||
| ## File Map | ||
|
|
||
| ### Mobile Part Subclasses | ||
|
|
||
| | File | Purpose | | ||
| |------|---------| | ||
| | `src/vs/sessions/browser/parts/mobile/mobileChatBarPart.ts` | Extends `ChatBarPart`. Overrides `layout()` (no card margins) and `updateStyles()` (no inline card styles). | | ||
| | `src/vs/sessions/browser/parts/mobile/mobileSidebarPart.ts` | Extends `SidebarPart`. Overrides `updateStyles()` (no inline card/title styles). | | ||
| | `src/vs/sessions/browser/parts/mobile/mobileAuxiliaryBarPart.ts` | Extends `AuxiliaryBarPart`. Overrides `layout()` and `updateStyles()` (no card margins or inline styles). | | ||
| | `src/vs/sessions/browser/parts/mobile/mobilePanelPart.ts` | Extends `PanelPart`. Overrides `layout()` and `updateStyles()` (no card margins or inline styles). | | ||
|
|
||
| ### Mobile Chrome Components | ||
|
|
||
| | File | Purpose | | ||
| |------|---------| | ||
| | `src/vs/sessions/browser/parts/mobile/mobileTopBar.ts` | Phone top bar: hamburger (☰), session title, new session (+). Emits `onDidClickHamburger`, `onDidClickNewSession`, `onDidClickTitle`. | | ||
| | `src/vs/sessions/browser/parts/mobile/mobileChatShell.css` | **Single source of truth** for all phone-layout CSS: flex column layout, split-view-view absolute positioning, card chrome removal, part/content width overrides, sidebar title hiding, composite bar hiding, welcome page layout, sash hiding, button focus overrides, mobile pickers. | | ||
|
|
||
| ### Layout & Navigation | ||
|
|
||
| | File | Purpose | | ||
| |------|---------| | ||
| | `src/vs/sessions/browser/layoutPolicy.ts` | `SessionsLayoutPolicy`: observable viewport classification (phone/tablet/desktop), platform flags (isIOS, isAndroid, isTouchDevice), part visibility and size defaults. | | ||
| | `src/vs/sessions/browser/mobileNavigationStack.ts` | `MobileNavigationStack`: Android back button integration via `history.pushState` / `popstate`. Supports `push()`, `pop()`, and `clear()`. | | ||
| | `src/vs/sessions/common/contextkeys.ts` | Phone context keys: `IsPhoneLayoutContext`, `KeyboardVisibleContext`. | | ||
|
|
||
| ### Part Instantiation | ||
|
|
||
| | File | Purpose | | ||
| |------|---------| | ||
| | `src/vs/sessions/browser/paneCompositePartService.ts` | `AgenticPaneCompositePartService`: checks viewport width at construction time and instantiates `Mobile*Part` vs desktop `*Part` classes accordingly. | | ||
|
|
||
| ### Workbench Integration | ||
|
|
||
| | File | Key Changes | | ||
| |------|-------------| | ||
| | `src/vs/sessions/browser/workbench.ts` | Layout policy integration, MobileTopBar creation/destruction (via `DisposableStore`), sidebar drawer open/close with backdrop, viewport-class-change detection, window resize listener, grid height calculation (subtracts MobileTopBar height), titlebar grid visibility toggle, `ISessionsManagementService` for new session button. | | ||
| | `src/vs/sessions/browser/parts/chatBarPart.ts` | `_lastLayout` changed from `private` to `protected` for mobile subclass access. | | ||
|
|
||
| ### Styling | ||
|
|
||
| | File | Purpose | | ||
| |------|---------| | ||
| | `src/vs/sessions/browser/parts/mobile/mobileChatShell.css` | All phone-layout CSS (see above). | | ||
| | `src/vs/sessions/browser/parts/media/sidebarPart.css` | Sidebar drawer overlay CSS: 85% width, z-index 250, slide-in animation, backdrop. | | ||
| | `src/vs/sessions/browser/media/style.css` | Mobile overscroll containment, 44px touch targets, quick pick bottom sheets, context menu action sheets, dialog sizing, notification positioning, hover card suppression, editor modal full-screen. | | ||
|
|
||
| ## Remaining Work | ||
|
|
||
| - **Session title sync**: MobileTopBar shows hardcoded "New Session" — needs to subscribe to `sessionsManagementService.activeSession` and update title when session changes. | ||
| - **Files & Terminal access**: Should become phone-specific views gated with `when: IsPhoneLayoutContext`. | ||
| - **iOS keyboard handling**: Adjust layout when virtual keyboard appears (context key exists, but no layout response yet). | ||
| - **Session list inline actions**: Make always-visible on touch devices (no hover-to-reveal). | ||
| - **Customizations on mobile**: Currently hidden — needs a mobile-friendly alternative. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,159 @@ | ||
| /*--------------------------------------------------------------------------------------------- | ||
| * Copyright (c) Microsoft Corporation. All rights reserved. | ||
| * Licensed under the MIT License. See License.txt in the project root for license information. | ||
| *--------------------------------------------------------------------------------------------*/ | ||
|
|
||
| import { Disposable } from '../../base/common/lifecycle.js'; | ||
| import { observableValue, derived, IObservable } from '../../base/common/observable.js'; | ||
| import { isIOS } from '../../base/common/platform.js'; | ||
| import { isAndroid } from '../../base/browser/browser.js'; | ||
| import { Gesture } from '../../base/browser/touch.js'; | ||
|
|
||
| /** Viewport classification based on container width. */ | ||
| export type ViewportClass = 'phone' | 'tablet' | 'desktop'; | ||
|
|
||
| /** Default visibility for each workbench part. */ | ||
| export interface IPartVisibilityDefaults { | ||
| readonly sidebar: boolean; | ||
| readonly auxiliaryBar: boolean; | ||
| readonly panel: boolean; | ||
| readonly chatBar: boolean; | ||
| readonly editor: boolean; | ||
| } | ||
|
|
||
| /** Default sizes (in pixels) for each workbench part. */ | ||
| export interface IPartSizeDefaults { | ||
| readonly sideBarSize: number; | ||
| readonly auxiliaryBarSize: number; | ||
| readonly panelSize: number; | ||
| readonly chatBarWidth: number; | ||
| } | ||
|
|
||
| const PHONE_MAX_WIDTH = 640; | ||
| const TABLET_MAX_WIDTH = 1024; | ||
|
|
||
| /** | ||
| * Whether the current platform is a phone/tablet OS. The phone layout is | ||
| * only applied on actual mobile devices so that resizing a desktop window | ||
| * below 640px does not switch the agents workbench into phone mode. | ||
| */ | ||
| const isMobilePlatform = isIOS || isAndroid; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we may want to use platform.isMobile.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Updated in commit Screenshot: https://github.com/user-attachments/assets/538e657c-c808-429c-9c64-a6b8f0eae021 |
||
|
|
||
| /** | ||
| * Classifies the viewport into one of three classes based on width. | ||
| * Phone and tablet classifications are gated on a mobile OS; desktop | ||
| * browsers and Electron always report `desktop` regardless of width. | ||
| */ | ||
| function classifyViewport(width: number): ViewportClass { | ||
| if (!isMobilePlatform) { | ||
| return 'desktop'; | ||
| } | ||
| if (width < PHONE_MAX_WIDTH) { | ||
| return 'phone'; | ||
| } | ||
| if (width < TABLET_MAX_WIDTH) { | ||
| return 'tablet'; | ||
| } | ||
| return 'desktop'; | ||
| } | ||
|
|
||
| /** | ||
| * Observable-based viewport classification and layout policy for | ||
| * the Sessions workbench. Consumed by `SessionsWorkbench` to drive | ||
| * part visibility, sizing, and behavior based on viewport dimensions | ||
| * and platform. | ||
| */ | ||
| export class SessionsLayoutPolicy extends Disposable { | ||
|
|
||
| // --- Platform flags (static, read once) --- | ||
|
|
||
| /** Whether the current platform is iOS. */ | ||
| readonly isIOS: boolean; | ||
|
|
||
| /** Whether the current platform is Android. */ | ||
| readonly isAndroid: boolean; | ||
|
|
||
| /** Whether the current device supports touch input. */ | ||
| readonly isTouchDevice: boolean; | ||
|
|
||
| // --- Observables --- | ||
|
|
||
| private readonly _viewportClass = observableValue<ViewportClass>(this, 'desktop'); | ||
|
|
||
| /** Current viewport class derived from the most recent `update()` call. */ | ||
| readonly viewportClass: IObservable<ViewportClass> = this._viewportClass; | ||
|
|
||
| /** `true` when the viewport class is `phone`. */ | ||
| readonly isPhoneLayout: IObservable<boolean> = derived(this, reader => { | ||
| return this._viewportClass.read(reader) === 'phone'; | ||
| }); | ||
|
|
||
| constructor() { | ||
| super(); | ||
|
|
||
| this.isIOS = isIOS; | ||
| this.isAndroid = isAndroid; | ||
| this.isTouchDevice = Gesture.isTouchDevice(); | ||
| } | ||
|
|
||
| /** | ||
| * Update the viewport classification. Call this from the workbench | ||
| * `layout()` method whenever the container dimensions change. | ||
| * | ||
| * @param width Container width in pixels. | ||
| * @param height Container height in pixels (reserved for future use). | ||
| */ | ||
| update(width: number, _height: number): void { | ||
| const next = classifyViewport(width); | ||
| if (this._viewportClass.get() !== next) { | ||
| this._viewportClass.set(next, undefined); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Returns the default part visibility for the given viewport class. | ||
| * If no class is supplied the current observed class is used. | ||
| */ | ||
| getPartVisibilityDefaults(viewportClass?: ViewportClass): IPartVisibilityDefaults { | ||
| const vc = viewportClass ?? this._viewportClass.get(); | ||
| switch (vc) { | ||
| case 'phone': | ||
| return { sidebar: false, auxiliaryBar: false, panel: false, chatBar: true, editor: false }; | ||
| case 'tablet': | ||
| case 'desktop': | ||
| // Tablet and desktop share the standard multi-part workbench defaults. | ||
| // A dedicated tablet layout has not been designed yet. | ||
| return { sidebar: true, auxiliaryBar: true, panel: false, chatBar: true, editor: false }; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Returns the default part sizes for the given viewport dimensions. | ||
| * If no viewport class is supplied the current observed class is used. | ||
| * | ||
| * @param width Container width in pixels. | ||
| * @param height Container height in pixels (reserved for future use). | ||
| * @param viewportClass Optional explicit viewport class override. | ||
| */ | ||
| getPartSizes(width: number, _height: number, viewportClass?: ViewportClass): IPartSizeDefaults { | ||
| const vc = viewportClass ?? this._viewportClass.get(); | ||
| switch (vc) { | ||
| case 'phone': | ||
| return { | ||
| sideBarSize: 0, | ||
| auxiliaryBarSize: 0, | ||
| panelSize: 0, | ||
| chatBarWidth: width, | ||
| }; | ||
| case 'tablet': | ||
| case 'desktop': | ||
| // Tablet currently falls back to desktop sizing. | ||
| return { | ||
| sideBarSize: 300, | ||
| auxiliaryBarSize: 380, | ||
| panelSize: 300, | ||
| chatBarWidth: width - 300, | ||
| }; | ||
| } | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.