Skip to content

[WIP] Side-bar agent host#311055

Draft
osortega wants to merge 1 commit intomainfrom
osortega/side-bar-agent-host
Draft

[WIP] Side-bar agent host#311055
osortega wants to merge 1 commit intomainfrom
osortega/side-bar-agent-host

Conversation

@osortega
Copy link
Copy Markdown
Contributor

Co-authored-by: Copilot copilot@github.com

Co-authored-by: Copilot <copilot@github.com>
Copilot AI review requested due to automatic review settings April 17, 2026 17:57
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) + hostBarVisible context 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

Comment on lines +92 to +93
this.ensureRemoteProviderActive();
this.renderContent();
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
this.ensureRemoteProviderActive();
this.renderContent();
const activeProviderIdBeforeEnsure = this.sessionsManagementService.activeProviderId.get();
this.ensureRemoteProviderActive();
if (this.sessionsManagementService.activeProviderId.get() === activeProviderIdBeforeEnsure) {
this.renderContent();
}

Copilot uses AI. Check for mistakes.
Comment on lines +322 to +327
private _getProvidersForPicker(): import('../../../services/sessions/common/sessionsProvider.js').ISessionsProvider[] {
if (this.contextKeyService.getContextKeyValue<boolean>(HostBarVisibleContext.key)) {
return this._getActiveProviders();
}
return this.sessionsProvidersService.getProviders();
}
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +87 to +89
this.hostBarVisibleContextKey = HostBarVisibleContext.bindTo(contextKeyService);
this.hostBarVisibleContextKey.set(true);

Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +558 to 574
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}`);
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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}`);
		}

Copilot uses AI. Check for mistakes.
Comment on lines +88 to +103
// 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);
}
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
// 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);

Copilot uses AI. Check for mistakes.
Comment thread src/vs/sessions/LAYOUT.md

| 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. |
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested 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. |
| 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. |

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants