Skip to content

Commit 2b01d20

Browse files
osortegaCopilot
andcommitted
sessions: add mobile-compatible PWA layout for agent sessions
Introduce a responsive mobile layout for the agent sessions window, enabling a native app-like experience when accessed via mobile browsers or installed as a PWA. The implementation uses mobile Part subclasses, a MobileTopBar chrome component, CSS overrides, and when-clause gating to progressively enhance the desktop sessions workbench for phone viewports. Key changes: Layout policy & viewport detection: - SessionsLayoutPolicy with observable viewport classification (phone <640px, tablet 640-1024, desktop >1024) - Context keys: ViewportClassContext, IsMobileLayoutContext, KeyboardVisibleContext - Runtime viewport class change detection in layout() Mobile chrome components: - MobileTopBar: hamburger + session title + new session (+) - MobileNavigationStack: history.pushState integration for Android back button - Sidebar drawer overlay with backdrop dismiss Mobile Part subclasses: - MobileChatBarPart, MobileSidebarPart, MobileAuxiliaryBarPart, MobilePanelPart override layout()/updateStyles() only - AgenticPaneCompositePartService conditionally instantiates mobile vs desktop Parts View gating: - Desktop-only views hidden on mobile via IsMobileLayoutContext: Changes, Files, Logs, Terminal, Code Review, Open in VS Code - Customization toolbar hidden via CSS on phone CSS & PWA: - Edge-to-edge chat (no card chrome on phone) - Safe area insets, touch targets (44px min), overscroll containment - PWA manifest with standalone display mode - viewport-fit=cover meta tag - Dynamic theme-color meta created programmatically Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 2bd737e commit 2b01d20

26 files changed

Lines changed: 1640 additions & 23 deletions

extensions/copilot/package-lock.json

Lines changed: 6 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

resources/server/manifest.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
"lang": "en-US",
66
"display": "standalone",
77
"display_override": ["window-controls-overlay"],
8+
"background_color": "#1e1e1e",
9+
"theme_color": "#1e1e1e",
10+
"orientation": "any",
811
"icons": [
912
{
1013
"src": "code-192.png",

src/vs/code/browser/workbench/workbench.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@
1212
<meta name="apple-mobile-web-app-capable" content="yes" />
1313
<meta name="apple-mobile-web-app-title" content="Code">
1414
<link rel="apple-touch-icon" href="{{WORKBENCH_WEB_BASE_URL}}/resources/server/code-192.png" />
15+
<link rel="apple-touch-startup-image" href="{{WORKBENCH_WEB_BASE_URL}}/favicon.ico">
1516

1617
<!-- Disable pinch zooming -->
17-
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no">
18+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no, viewport-fit=cover">
1819

1920
<!-- Workbench Configuration -->
2021
<meta id="vscode-workbench-web-configuration" data-settings="{{WORKBENCH_WEB_CONFIGURATION}}">

src/vs/sessions/MOBILE.md

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
# Mobile Agent Sessions — Architecture
2+
3+
## Core Principle
4+
5+
**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.
6+
7+
## Architecture
8+
9+
### Mobile Part Subclasses
10+
11+
Desktop Parts (`ChatBarPart`, `SidebarPart`, `PanelPart`, `AuxiliaryBarPart`) remain unchanged. Each has a **mobile subclass** that extends it and overrides only `layout()` and/or `updateStyles()` to remove card margins, border insets, and inline theme styles. `AgenticPaneCompositePartService` conditionally instantiates the mobile or desktop variant at startup based on viewport width (`< 640px` → phone).
12+
13+
This means:
14+
- Desktop code has **zero** phone-layout checks — all mobile logic lives in mobile subclasses, `MobileTopBar`, and CSS.
15+
16+
**Known limitation:** Part classes are chosen once at construction and never swapped at runtime. If the viewport changes class (e.g., device rotation from portrait to landscape), the original Part implementations remain. This is acceptable because real mobile devices don't switch between phone and desktop — the scenario only occurs in DevTools emulation.
17+
18+
### View & Action Gating
19+
20+
Views, menu items, and actions use `when` clauses with the `sessionsIsMobileLayout` context key to control visibility per viewport class. This follows a **default-deny** approach for mobile:
21+
22+
- **Desktop-only features** add `when: IsMobileLayoutContext.negate()` to their view descriptors and menu registrations. They simply don't appear on mobile.
23+
- **Mobile-compatible features** (chat, sessions list) have no mobile gate — they render on all viewports.
24+
- **Mobile-specific replacements** (when ready) register with `when: IsMobileLayoutContext` and live in separate files under `parts/mobile/contributions/`.
25+
26+
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.
27+
28+
#### Current Gating Status
29+
30+
| Feature | Mobile Status | Mechanism |
31+
|---------|--------------|-----------|
32+
| Sessions list (sidebar) | ✅ Compatible | No gate |
33+
| Chat views (ChatBar) | ✅ Compatible | No gate |
34+
| Changes view (AuxiliaryBar) | ❌ Gated | `when: !sessionsIsMobileLayout` on view descriptor |
35+
| Files view (AuxiliaryBar) | ❌ Gated | `when: !sessionsIsMobileLayout` on view descriptor |
36+
| Logs view (Panel) | ❌ Gated | `when: !sessionsIsMobileLayout` on view descriptor |
37+
| Terminal actions | ❌ Gated | `when: !sessionsIsMobileLayout` on menu item |
38+
| "Open in VS Code" action | ❌ Gated | `when: !sessionsIsMobileLayout` on menu item |
39+
| Code review toolbar | ❌ Gated | `when: !sessionsIsMobileLayout` on menu item |
40+
| Customizations toolbar | ❌ Hidden | CSS `display: none` on phone |
41+
| Titlebar | ❌ Hidden | Grid `visible: false` + CSS + MobileTopBar replacement |
42+
43+
### Phone Layout
44+
45+
On phone-sized viewports (`< 640px` width):
46+
47+
```
48+
┌──────────────────────────────────┐
49+
│ [☰] Session Title [+] │ ← MobileTopBar (prepended before grid)
50+
├──────────────────────────────────┤
51+
│ │
52+
│ Chat (edge-to-edge) │ ← Grid: ChatBarPart fills 100%
53+
│ │
54+
│ │
55+
│ │
56+
│ ┌──────────────────────────┐ │
57+
│ │ Chat input │ │ ← Pinned to bottom
58+
│ └──────────────────────────┘ │
59+
└──────────────────────────────────┘
60+
```
61+
62+
- **MobileTopBar** is a DOM element prepended above the grid. It has a hamburger (☰), session title, and new session (+) button.
63+
- **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.
64+
- **Titlebar** is hidden in the grid (`visible: false`) and via CSS — replaced by MobileTopBar.
65+
- **SessionCompositeBar** (chat tabs) is hidden via CSS.
66+
- The grid uses `display: flex; flex-direction: column` and all `split-view-view:has(> .part)` containers are positioned absolutely at `100% width/height`.
67+
68+
### Viewport Classification
69+
70+
`SessionsLayoutPolicy` classifies the viewport:
71+
- **phone**: `width < 640px`
72+
- **tablet**: `640px ≤ width < 1024px`
73+
- **desktop**: `width ≥ 1024px`
74+
75+
The workbench toggles CSS classes (`phone-layout`, `mobile-layout`) on `layout()` and creates/destroys mobile components when the viewport class changes at runtime (e.g., DevTools device emulation). MobileTopBar lifecycle is managed via a `DisposableStore` that is cleared on viewport transitions to prevent leaks.
76+
77+
### Context Keys
78+
79+
| Key | Type | Purpose |
80+
|-----|------|---------|
81+
| `sessionsViewportClass` | `string` | `'phone'`, `'tablet'`, or `'desktop'` |
82+
| `sessionsIsMobileLayout` | `boolean` | `true` when phone or tablet |
83+
| `sessionsKeyboardVisible` | `boolean` | `true` when virtual keyboard is visible |
84+
85+
### Desktop → Mobile Component Mapping
86+
87+
| Desktop Component | Mobile Equivalent | How Accessed |
88+
|---|---|---|
89+
| **Titlebar** (3-section toolbar) | **MobileTopBar** (☰ / title / +) | Always visible at top |
90+
| **Sidebar** (sessions list) | Drawer overlay (85% width) | Hamburger button (☰) |
91+
| **ChatBar** (chat widget) | Same Part, edge-to-edge, no card chrome | Default view (always visible) |
92+
| **AuxiliaryBar** (files, changes) | Gated — not shown on mobile | Planned: mobile-specific view |
93+
| **Panel** (terminal, output) | Gated — not shown on mobile | Planned: mobile-specific view |
94+
| **SessionCompositeBar** (chat tabs) | Hidden on phone ||
95+
| **New Session** (sidebar button) | + button in MobileTopBar | Always visible in top bar |
96+
97+
## File Map
98+
99+
### Mobile Part Subclasses
100+
101+
| File | Purpose |
102+
|------|---------|
103+
| `src/vs/sessions/browser/parts/mobile/mobileChatBarPart.ts` | Extends `ChatBarPart`. Overrides `layout()` (no card margins) and `updateStyles()` (no inline card styles). |
104+
| `src/vs/sessions/browser/parts/mobile/mobileSidebarPart.ts` | Extends `SidebarPart`. Overrides `updateStyles()` (no inline card/title styles). |
105+
| `src/vs/sessions/browser/parts/mobile/mobileAuxiliaryBarPart.ts` | Extends `AuxiliaryBarPart`. Overrides `layout()` and `updateStyles()` (no card margins or inline styles). |
106+
| `src/vs/sessions/browser/parts/mobile/mobilePanelPart.ts` | Extends `PanelPart`. Overrides `layout()` and `updateStyles()` (no card margins or inline styles). |
107+
108+
### Mobile Chrome Components
109+
110+
| File | Purpose |
111+
|------|---------|
112+
| `src/vs/sessions/browser/parts/mobile/mobileTopBar.ts` | Phone top bar: hamburger (☰), session title, new session (+). Emits `onDidClickHamburger`, `onDidClickNewSession`, `onDidClickTitle`. |
113+
| `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. |
114+
115+
### Layout & Navigation
116+
117+
| File | Purpose |
118+
|------|---------|
119+
| `src/vs/sessions/browser/layoutPolicy.ts` | `SessionsLayoutPolicy`: observable viewport classification (phone/tablet/desktop), platform flags (isIOS, isAndroid, isTouchDevice), part visibility and size defaults. |
120+
| `src/vs/sessions/browser/mobileNavigationStack.ts` | `MobileNavigationStack`: Android back button integration via `history.pushState` / `popstate`. Supports `push()`, `pop()`, and `clear()`. |
121+
| `src/vs/sessions/common/contextkeys.ts` | Mobile context keys: `ViewportClassContext`, `IsMobileLayoutContext`, `KeyboardVisibleContext`. |
122+
123+
### Part Instantiation
124+
125+
| File | Purpose |
126+
|------|---------|
127+
| `src/vs/sessions/browser/paneCompositePartService.ts` | `AgenticPaneCompositePartService`: checks viewport width at construction time and instantiates `Mobile*Part` vs desktop `*Part` classes accordingly. |
128+
129+
### Workbench Integration
130+
131+
| File | Key Changes |
132+
|------|-------------|
133+
| `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. |
134+
| `src/vs/sessions/browser/parts/chatBarPart.ts` | `_lastLayout` changed from `private` to `protected` for mobile subclass access. |
135+
136+
### Styling
137+
138+
| File | Purpose |
139+
|------|---------|
140+
| `src/vs/sessions/browser/parts/mobile/mobileChatShell.css` | All phone-layout CSS (see above). |
141+
| `src/vs/sessions/browser/parts/media/sidebarPart.css` | Sidebar drawer overlay CSS: 85% width, z-index 250, slide-in animation, backdrop. |
142+
| `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. |
143+
144+
### PWA & Viewport
145+
146+
| File | Purpose |
147+
|------|---------|
148+
| `src/vs/code/browser/workbench/workbench.html` | `viewport-fit=cover` meta tag, `theme-color` meta tag. |
149+
| `resources/server/manifest.json` | PWA manifest: `background_color`, `theme_color`, `orientation`. |
150+
151+
## Remaining Work
152+
153+
- **Session title sync**: MobileTopBar shows hardcoded "New Session" — needs to subscribe to `sessionsManagementService.activeSession` and update title when session changes.
154+
- **Files & Terminal access**: Should become mobile-specific views gated with `when: IsMobileLayoutContext`.
155+
- **iOS keyboard handling**: Adjust layout when virtual keyboard appears (context key exists, but no layout response yet).
156+
- **Session list inline actions**: Make always-visible on touch devices (no hover-to-reveal).
157+
- **Customizations on mobile**: Currently hidden — needs a mobile-friendly alternative.
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { Disposable } from '../../base/common/lifecycle.js';
7+
import { observableValue, derived, IObservable } from '../../base/common/observable.js';
8+
import { isIOS } from '../../base/common/platform.js';
9+
import { isAndroid } from '../../base/browser/browser.js';
10+
import { Gesture } from '../../base/browser/touch.js';
11+
12+
/** Viewport classification based on container width. */
13+
export type ViewportClass = 'phone' | 'tablet' | 'desktop';
14+
15+
/** Default visibility for each workbench part. */
16+
export interface IPartVisibilityDefaults {
17+
readonly sidebar: boolean;
18+
readonly auxiliaryBar: boolean;
19+
readonly panel: boolean;
20+
readonly chatBar: boolean;
21+
readonly editor: boolean;
22+
}
23+
24+
/** Default sizes (in pixels) for each workbench part. */
25+
export interface IPartSizeDefaults {
26+
readonly sideBarSize: number;
27+
readonly auxiliaryBarSize: number;
28+
readonly panelSize: number;
29+
readonly chatBarWidth: number;
30+
}
31+
32+
const PHONE_MAX_WIDTH = 640;
33+
const TABLET_MAX_WIDTH = 1024;
34+
35+
/**
36+
* Classifies the viewport into one of three classes based on width.
37+
*/
38+
function classifyViewport(width: number): ViewportClass {
39+
if (width < PHONE_MAX_WIDTH) {
40+
return 'phone';
41+
}
42+
if (width < TABLET_MAX_WIDTH) {
43+
return 'tablet';
44+
}
45+
return 'desktop';
46+
}
47+
48+
/**
49+
* Observable-based viewport classification and layout policy for
50+
* the Sessions workbench. Consumed by `SessionsWorkbench` to drive
51+
* part visibility, sizing, and behavior based on viewport dimensions
52+
* and platform.
53+
*/
54+
export class SessionsLayoutPolicy extends Disposable {
55+
56+
// --- Platform flags (static, read once) ---
57+
58+
/** Whether the current platform is iOS. */
59+
readonly isIOS: boolean;
60+
61+
/** Whether the current platform is Android. */
62+
readonly isAndroid: boolean;
63+
64+
/** Whether the current device supports touch input. */
65+
readonly isTouchDevice: boolean;
66+
67+
// --- Observables ---
68+
69+
private readonly _viewportClass = observableValue<ViewportClass>(this, 'desktop');
70+
71+
/** Current viewport class derived from the most recent `update()` call. */
72+
readonly viewportClass: IObservable<ViewportClass> = this._viewportClass;
73+
74+
/** `true` when the viewport class is `phone` or `tablet`. */
75+
readonly isMobileLayout: IObservable<boolean> = derived(this, reader => {
76+
const vc = this._viewportClass.read(reader);
77+
return vc === 'phone' || vc === 'tablet';
78+
});
79+
80+
constructor() {
81+
super();
82+
83+
this.isIOS = isIOS;
84+
this.isAndroid = isAndroid;
85+
this.isTouchDevice = Gesture.isTouchDevice();
86+
}
87+
88+
/**
89+
* Update the viewport classification. Call this from the workbench
90+
* `layout()` method whenever the container dimensions change.
91+
*
92+
* @param width Container width in pixels.
93+
* @param height Container height in pixels (reserved for future use).
94+
*/
95+
update(width: number, _height: number): void {
96+
const next = classifyViewport(width);
97+
if (this._viewportClass.get() !== next) {
98+
this._viewportClass.set(next, undefined);
99+
}
100+
}
101+
102+
/**
103+
* Returns the default part visibility for the given viewport class.
104+
* If no class is supplied the current observed class is used.
105+
*/
106+
getPartVisibilityDefaults(viewportClass?: ViewportClass): IPartVisibilityDefaults {
107+
const vc = viewportClass ?? this._viewportClass.get();
108+
switch (vc) {
109+
case 'phone':
110+
return { sidebar: false, auxiliaryBar: false, panel: false, chatBar: true, editor: false };
111+
case 'tablet':
112+
return { sidebar: true, auxiliaryBar: false, panel: false, chatBar: true, editor: false };
113+
case 'desktop':
114+
return { sidebar: true, auxiliaryBar: false, panel: false, chatBar: true, editor: false };
115+
}
116+
}
117+
118+
/**
119+
* Returns the default part sizes for the given viewport dimensions.
120+
* If no viewport class is supplied the current observed class is used.
121+
*
122+
* @param width Container width in pixels.
123+
* @param height Container height in pixels (reserved for future use).
124+
* @param viewportClass Optional explicit viewport class override.
125+
*/
126+
getPartSizes(width: number, _height: number, viewportClass?: ViewportClass): IPartSizeDefaults {
127+
const vc = viewportClass ?? this._viewportClass.get();
128+
switch (vc) {
129+
case 'phone':
130+
return {
131+
sideBarSize: 0,
132+
auxiliaryBarSize: 0,
133+
panelSize: 0,
134+
chatBarWidth: width,
135+
};
136+
case 'tablet':
137+
return {
138+
sideBarSize: 250,
139+
auxiliaryBarSize: 300,
140+
panelSize: 250,
141+
chatBarWidth: width - 250,
142+
};
143+
case 'desktop':
144+
return {
145+
sideBarSize: 300,
146+
auxiliaryBarSize: 380,
147+
panelSize: 300,
148+
chatBarWidth: width - 300,
149+
};
150+
}
151+
}
152+
}

0 commit comments

Comments
 (0)