Conversation
Co-authored-by: Copilot <copilot@github.com>
There was a problem hiding this comment.
Pull request overview
Adds a web-only “Host Bar” rail to the Sessions workbench so users can switch the active remote agent host and have key UI surfaces scope to that host, while also reducing duplicate tunnel host entries by deduping cached tunnels by name.
Changes:
- Introduce
HostBarPart(48px left rail) +hostBarVisiblecontext key, and wire the part into the Sessions grid layout (web +chat.remoteAgentHostsEnabled). - Scope the Sessions list and workspace picker to the active provider when the Host Bar is visible.
- Deduplicate cached tunnel entries (by normalized tunnel
name) and consolidate provider tiles to avoid duplicates; update layout documentation.
Show a summary per file
| File | Description |
|---|---|
| src/vs/sessions/contrib/sessions/browser/views/sessionsList.ts | Scopes session list filtering to the active provider when the Host Bar is visible. |
| src/vs/sessions/contrib/remoteAgentHost/electron-browser/tunnelAgentHostServiceImpl.ts | Dedupes cached tunnels on read and prevents duplicate entries on write (by name and id). |
| src/vs/sessions/contrib/remoteAgentHost/browser/webTunnelAgentHostService.ts | Same as electron impl: dedupe cached tunnels and avoid writing duplicate entries. |
| src/vs/sessions/contrib/remoteAgentHost/browser/tunnelAgentHost.contribution.ts | Reconciles tunnel-backed providers using a consolidated cached-tunnel list. |
| src/vs/sessions/contrib/chat/browser/sessionWorkspacePicker.ts | Scopes picker providers/browse actions to the active provider when the Host Bar is visible. |
| src/vs/sessions/common/contextkeys.ts | Adds HostBarVisibleContext context key. |
| src/vs/sessions/browser/workbench.ts | Instantiates and places HostBarPart into the Sessions grid layout (web-only gate). |
| src/vs/sessions/browser/parts/parts.ts | Adds AgenticParts.HOSTBAR_PART identifier. |
| src/vs/sessions/browser/parts/media/hostBarPart.css | Styling for the Host Bar rail and its entries. |
| src/vs/sessions/browser/parts/hostBarPart.ts | Implements Host Bar UI, provider selection, removal flow, and hover/status rendering. |
| src/vs/sessions/LAYOUT.md | Documents Host Bar placement and updates the layout diagram/history. |
| src/vs/platform/agentHost/common/tunnelAgentHost.ts | Introduces shared dedupeCachedTunnels helper for consolidating tunnel cache entries. |
Copilot's findings
- Files reviewed: 12/12 changed files
- Comments generated: 6
| this.ensureRemoteProviderActive(); | ||
| this.renderContent(); |
There was a problem hiding this comment.
onDidChangeProviders always calls renderContent(), but ensureRemoteProviderActive() can synchronously change activeProviderId, which will trigger the autorun below and cause a second renderContent() call. Consider only rendering when ensureRemoteProviderActive() didn’t change the active provider, or defer rendering to a single reaction to avoid duplicate work.
| this.ensureRemoteProviderActive(); | |
| this.renderContent(); | |
| const activeProviderIdBeforeEnsure = this.sessionsManagementService.activeProviderId.get(); | |
| this.ensureRemoteProviderActive(); | |
| if (this.sessionsManagementService.activeProviderId.get() === activeProviderIdBeforeEnsure) { | |
| this.renderContent(); | |
| } |
| private _getProvidersForPicker(): import('../../../services/sessions/common/sessionsProvider.js').ISessionsProvider[] { | ||
| if (this.contextKeyService.getContextKeyValue<boolean>(HostBarVisibleContext.key)) { | ||
| return this._getActiveProviders(); | ||
| } | ||
| return this.sessionsProvidersService.getProviders(); | ||
| } |
There was a problem hiding this comment.
The new _getProvidersForPicker return type uses an inline import('...').ISessionsProvider[], which makes refactors harder and duplicates the same type expression already used elsewhere in this class. Consider adding a type ISessionsProvider import at the top and using it for _getActiveProviders / _getProvidersForPicker for clarity.
| this.hostBarVisibleContextKey = HostBarVisibleContext.bindTo(contextKeyService); | ||
| this.hostBarVisibleContextKey.set(true); | ||
|
|
There was a problem hiding this comment.
HostBarVisibleContext is set to true in the constructor but never reset. Other features that toggle visibility context keys typically reset them via a disposable (e.g. toDisposable(() => key.reset())) so tests / reinitialization don’t leave stale context. Please reset this context key when the part is disposed.
| const partDescriptors: { id: string; role: string; classes: string[] }[] = [ | ||
| { id: Parts.TITLEBAR_PART, role: 'none', classes: ['titlebar'] }, | ||
| { id: Parts.SIDEBAR_PART, role: 'none', classes: ['sidebar', 'left'] }, | ||
| { id: Parts.AUXILIARYBAR_PART, role: 'none', classes: ['auxiliarybar', 'basepanel', 'right'] }, | ||
| { id: Parts.CHATBAR_PART, role: 'main', classes: ['chatbar', 'basepanel', 'right'] }, | ||
| { id: Parts.PANEL_PART, role: 'none', classes: ['panel', 'basepanel', positionToString(this.getPanelPosition())] }, | ||
| ]) { | ||
| ]; | ||
| if (this.hostBarEnabled) { | ||
| partDescriptors.push({ id: AgenticParts.HOSTBAR_PART, role: 'none', classes: ['hostbar', 'left'] }); | ||
| } | ||
|
|
||
| for (const { id, role, classes } of partDescriptors) { | ||
| const partContainer = this.createPartContainer(id, role, classes); | ||
|
|
||
| mark(`code/willCreatePart/${id}`); | ||
| this.getPart(id).create(partContainer); | ||
| this.getPart(id as Parts).create(partContainer); | ||
| mark(`code/didCreatePart/${id}`); |
There was a problem hiding this comment.
Workbench.getPart is typed to only accept Parts, so this loop is forced to cast id as Parts even when id is AgenticParts.HOSTBAR_PART. This weakens type-safety and could mask real ID mistakes. Consider widening getPart (and related helpers) to accept string | Parts | AgenticParts or adding a dedicated getPartById(id: string) to avoid the cast.
See below for a potential fix:
let hostBarPart: HostBarPart | undefined;
if (this.hostBarEnabled) {
// Instantiating the part auto-registers it with this layout service via Part base class.
hostBarPart = instantiationService.createInstance(HostBarPart);
}
// Create Parts (editor starts hidden and is shown when an editor opens)
const partDescriptors: { id: Parts; role: string; classes: string[] }[] = [
{ id: Parts.TITLEBAR_PART, role: 'none', classes: ['titlebar'] },
{ id: Parts.SIDEBAR_PART, role: 'none', classes: ['sidebar', 'left'] },
{ id: Parts.AUXILIARYBAR_PART, role: 'none', classes: ['auxiliarybar', 'basepanel', 'right'] },
{ id: Parts.CHATBAR_PART, role: 'main', classes: ['chatbar', 'basepanel', 'right'] },
{ id: Parts.PANEL_PART, role: 'none', classes: ['panel', 'basepanel', positionToString(this.getPanelPosition())] },
];
for (const { id, role, classes } of partDescriptors) {
const partContainer = this.createPartContainer(id, role, classes);
mark(`code/willCreatePart/${id}`);
this.getPart(id).create(partContainer);
mark(`code/didCreatePart/${id}`);
}
if (hostBarPart) {
const id = AgenticParts.HOSTBAR_PART;
const role = 'none';
const classes = ['hostbar', 'left'];
const partContainer = this.createPartContainer(id, role, classes);
mark(`code/willCreatePart/${id}`);
hostBarPart.create(partContainer);
mark(`code/didCreatePart/${id}`);
}
| // Consolidate by name (case-insensitive). The cache-read path already | ||
| // dedupes, but we dedupe again here to guarantee a single tile per | ||
| // logical machine even if legacy entries with a missing or blank name | ||
| // somehow survive (those fall back to per-tunnelId entries). | ||
| const seenNames = new Set<string>(); | ||
| const consolidated: typeof cached = []; | ||
| for (const tunnel of cached) { | ||
| const key = tunnel.name?.trim().toLowerCase(); | ||
| if (key) { | ||
| if (seenNames.has(key)) { | ||
| continue; | ||
| } | ||
| seenNames.add(key); | ||
| } | ||
| consolidated.push(tunnel); | ||
| } |
There was a problem hiding this comment.
The dedupe logic here duplicates the newly introduced dedupeCachedTunnels helper and only dedupes by normalized name (it won’t collapse duplicate tunnelIds if name is empty/whitespace). Reusing dedupeCachedTunnels(cached) would keep behavior consistent with the storage read-path and avoid maintaining two slightly different implementations.
| // Consolidate by name (case-insensitive). The cache-read path already | |
| // dedupes, but we dedupe again here to guarantee a single tile per | |
| // logical machine even if legacy entries with a missing or blank name | |
| // somehow survive (those fall back to per-tunnelId entries). | |
| const seenNames = new Set<string>(); | |
| const consolidated: typeof cached = []; | |
| for (const tunnel of cached) { | |
| const key = tunnel.name?.trim().toLowerCase(); | |
| if (key) { | |
| if (seenNames.has(key)) { | |
| continue; | |
| } | |
| seenNames.add(key); | |
| } | |
| consolidated.push(tunnel); | |
| } | |
| // Reuse the shared cache dedupe logic so provider reconciliation stays | |
| // consistent with the storage read-path, including unnamed entries that | |
| // must fall back to per-tunnelId deduplication. | |
| const consolidated = dedupeCachedTunnels(cached); |
|
|
||
| | Date | Change | | ||
| |------|--------| | ||
| | 2026-04-16 | Added the Host Bar (`HostBarPart`) — a 48px left rail rendered in web builds when `chat.remoteAgentHostsEnabled` is true. Lists registered sessions providers as letter-initial avatars with a connection-status dot; selecting an entry calls `ISessionsManagementService.setActiveProvider`, which scopes both the sessions list and the workspace picker to that provider. An "+" footer button runs `sessions.remoteAgentHost.add`. Wired as the first leaf in the grid root (`[hostBarNode, sideBarNode, rightSection]`). Sets the `hostBarVisible` context key. | |
There was a problem hiding this comment.
This changelog entry claims the Host Bar renders “letter-initial avatars with a connection-status dot”, but the current HostBarPart implementation renders vm codicons (vm-active/vm-outline) and doesn’t show a separate status dot or letter avatars. Please update the description to match the actual UI to keep LAYOUT.md accurate.
| | 2026-04-16 | Added the Host Bar (`HostBarPart`) — a 48px left rail rendered in web builds when `chat.remoteAgentHostsEnabled` is true. Lists registered sessions providers as letter-initial avatars with a connection-status dot; selecting an entry calls `ISessionsManagementService.setActiveProvider`, which scopes both the sessions list and the workspace picker to that provider. An "+" footer button runs `sessions.remoteAgentHost.add`. Wired as the first leaf in the grid root (`[hostBarNode, sideBarNode, rightSection]`). Sets the `hostBarVisible` context key. | | |
| | 2026-04-16 | Added the Host Bar (`HostBarPart`) — a 48px left rail rendered in web builds when `chat.remoteAgentHostsEnabled` is true. Lists registered sessions providers with VM codicons that reflect active/inactive state (`vm-active` / `vm-outline`); selecting an entry calls `ISessionsManagementService.setActiveProvider`, which scopes both the sessions list and the workspace picker to that provider. An "+" footer button runs `sessions.remoteAgentHost.add`. Wired as the first leaf in the grid root (`[hostBarNode, sideBarNode, rightSection]`). Sets the `hostBarVisible` context key. | |
Co-authored-by: Copilot copilot@github.com