Skip to content

Agents: Add experimental tabbed workspace picker#312773

Merged
sandy081 merged 20 commits intomainfrom
mrleemurray/new-session-picker-exploration
Apr 30, 2026
Merged

Agents: Add experimental tabbed workspace picker#312773
sandy081 merged 20 commits intomainfrom
mrleemurray/new-session-picker-exploration

Conversation

@mrleemurray
Copy link
Copy Markdown
Contributor

@mrleemurray mrleemurray commented Apr 27, 2026

This pull request introduces a new experimental "Tabbed Workspace Picker" feature for the chat session workspace picker, along with several extensibility and usability improvements to the action widget infrastructure. The main changes include support for rendering a custom header (such as a tab bar) above the action list, new options for constraining the action widget's width, and extensibility hooks in the workspace picker to support filtering and custom layouts. There are also related styling updates and configuration wiring for the new tabbed picker.

Screen.Recording.2026-04-27.at.14.41.36.mov

Tabbed Workspace Picker and Extensibility:

  • Added a new experimental setting sessions.experimental.tabbedWorkspacePicker and implemented the TabbedWorkspacePicker class, enabling a tabbed interface for selecting workspaces in chat sessions. The picker is conditionally enabled based on the configuration setting. [1] [2] [3]
  • Refactored WorkspacePicker to support subclassing, including extensibility hooks for filtering workspaces and browse actions, customizing the header, and controlling minimum/maximum width. [1] [2] [3] [4] [5]
  • Enhanced the logic for collecting and displaying recent workspaces and browse actions to respect subclass-provided filters and layout options. [1] [2] [3] [4]

Action Widget Infrastructure Improvements:

  • Added maxWidth to IActionListOptions, and updated the action list widget to clamp its width between minWidth and maxWidth, ensuring long items are truncated rather than expanding the popup. [1] [2] [3] [4] [5]
  • Enhanced the action widget service to support rendering an optional header element above the action list, used by the tabbed picker to display its tab bar. [1] [2] [3]

Styling Updates:

  • Added CSS for the tabbed workspace picker, including styles for the tab bar and inline browse actions, improving visual clarity and reducing repetition in the UI.

These changes collectively enable a more flexible and user-friendly workspace picker experience, laying the groundwork for further UI enhancements and customization.

Copilot AI review requested due to automatic review settings April 27, 2026 14:12
@mrleemurray mrleemurray self-assigned this Apr 27, 2026
@mrleemurray mrleemurray requested a review from sandy081 April 27, 2026 14:17
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

Introduces an experimental tabbed workspace picker for the Sessions window’s “new session” flow, enabling users to choose workspaces via separate Local/GitHub/Remote tabs and related UI enhancements behind a new configuration setting.

Changes:

  • Add sessions.experimental.tabbedWorkspacePicker setting and wire it into the Sessions window new chat view.
  • Implement TabbedWorkspacePicker with tab UI, category-based filtering, and inline browse actions.
  • Extend the action widget/action list to support an optional header element and a max-width cap used by the picker.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/vs/sessions/contrib/copilotChatSessions/browser/copilotChatSessionsProvider.ts Assign browse action grouping/icon updates to support tab categorization (e.g. repositories).
src/vs/sessions/contrib/configuration/browser/configuration.contribution.ts Register the new experimental setting for enabling the tabbed picker.
src/vs/sessions/contrib/chat/browser/tabbedWorkspacePicker.ts New tabbed picker implementation (tabs + workspace/browse-action categorization).
src/vs/sessions/contrib/chat/browser/sessionWorkspacePicker.ts Add extensibility hooks (header/min/max width) and filtering logic used by the tabbed picker.
src/vs/sessions/contrib/chat/browser/newChatViewPane.ts Select the appropriate picker implementation based on platform and the new setting.
src/vs/sessions/contrib/chat/browser/media/chatWidget.css Styles for the tab bar and inline browse-action rows.
src/vs/sessions/common/configuration.ts Define the new setting ID constant.
src/vs/platform/actionWidget/browser/actionWidget.ts Allow callers to pass an optional header element rendered above the action list/filter.
src/vs/platform/actionWidget/browser/actionList.ts Add maxWidth option and clamp computed list width accordingly.
Comments suppressed due to low confidence (1)

src/vs/sessions/contrib/chat/browser/sessionWorkspacePicker.ts:564

  • Ungrouped browse actions are now rendered without description: action.description, so ISessionWorkspaceBrowseAction.description is effectively ignored in this path despite being intended for display in the workspace picker. Consider restoring the description here (or only suppressing it in the specific tabbed/inline UI variant).
			items.push({
				kind: ActionListItemKind.Action,
				label: localize('workspacePicker.browseSelectAction', "Select {0}...", action.label),
				group: { title: '', icon: action.icon },
				disabled: isUnavailable,
				item: { browseActionIndex: index },
			});

Comment thread src/vs/platform/actionWidget/browser/actionList.ts Outdated
Comment thread src/vs/sessions/contrib/chat/browser/sessionWorkspacePicker.ts Outdated
Comment thread src/vs/sessions/contrib/chat/browser/tabbedWorkspacePicker.ts Outdated
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 28, 2026

Base: 97d25689 Current: e79ec497

No screenshot changes.

@sandy081
Copy link
Copy Markdown
Member

A few architecture concerns:

1. Avoid extending IActionWidgetService with a generic headerElement API

The tab bar UI is specific to the session workspace picker — actionWidget is a generic action-list popup and shouldn't need to know about arbitrary caller-owned DOM. Passing an HTMLElement in creates unclear lifecycle/ownership responsibilities for child widgets and event listeners. The group selection state should live in the workspace picker layer, not in the action widget service.

2. No need for a new setting

If grouped workspace picker is the right UX direction, it should just be the behavior — no need for sessions.experimental.tabbedWorkspacePicker.

3. Groups should be provider-contributable, not hardcoded

The Local / GitHub / Remote grouping is currently hardcoded in TabbedWorkspacePicker based on provider-specific details like remoteAddress, GITHUB_REMOTE_FILE_SCHEME, and group === 'repositories'. The picker shouldn't need to know about these internals.

Instead, the groups should come from the providers themselves. One approach: extend ISessionWorkspaceBrowseAction.group to carry one of the well-known group values (Local, GitHub, Remote), and let the picker render those groups and their data dynamically. Providers contribute browse actions under a group; the picker just renders whatever groups are contributed.

mrleemurray and others added 2 commits April 28, 2026 15:03
…ization

- Updated IActionWidgetService to include an onPopupRendered callback for improved rendering control.
- Modified ActionWidgetService to handle the new onPopupRendered parameter in show method.
- Added category property to IAgentHostSessionWorkspaceOptions for workspace categorization.
- Updated buildAgentHostSessionWorkspace to utilize the new category property.
- Enhanced LocalAgentHostSessionsProvider to categorize workspaces as 'local'.
- Updated CopilotChatSessionsProvider to categorize workspaces as 'local' or 'cloud' based on their source.
- Enhanced RemoteAgentHostSessionsProvider to categorize workspaces as 'remote'.
- Introduced SessionWorkspaceCategory type to standardize workspace categorization across providers.
- Removed the experimental tabbed workspace picker as it is no longer needed.
- Updated WorkspacePicker to support tabbed navigation based on workspace categories (local, cloud, remote).

Co-authored-by: Copilot <copilot@github.com>
@mrleemurray
Copy link
Copy Markdown
Contributor Author

@sandy081 I've made some updates - please review

@sandy081
Copy link
Copy Markdown
Member

After thinking more about the architecture, I think we can avoid extending IActionWidgetService entirely. Here's the approach I'd suggest:

Tab bar as inline DOM, not inside the action widget overlay

The tab bar (Local / GitHub / Remote) should be a normal inline element rendered below the workspace trigger button — owned entirely by the workspace picker, not by the action widget. When a tab is clicked, just re-invoke actionWidgetService.show() with items filtered to that category, passing the tab bar element as the anchor so the dropdown appears directly below the tabs.

Flow:

  1. Click trigger → show inline tab bar underneath + call actionWidgetService.show() anchored to the tab bar
  2. Switch tab → actionWidgetService.hide() + re-call .show() with filtered items (tab bar stays visible)
  3. Dismiss (Escape / click outside) → delegate onHide() hides the tab bar too
  4. Single category available → skip tabs, fall back to flat picker

This keeps IActionWidgetService unchanged — no headerElement, no onPopupRendered, no new parameters. The action widget stays a simple stateless dropdown. Tab state, rendering, and lifecycle are fully owned by the TabbedWorkspacePicker subclass.

For positioning: force the dropdown to always appear below the tab bar (use AnchorPosition.BELOW). The action list already handles overflow with a scrollbar, and the picker sits at the top of the new session view so vertical space is almost always abundant.

@mrleemurray
Copy link
Copy Markdown
Contributor Author

Thanks for the feedback. I gave the inline tab bar approach a try and want to walk through what I hit before settling on the current implementation.

What I tried:

  • Tab bar as inline DOM in the picker slot, sibling to the trigger
  • actionWidgetService.show() anchored to the tab bar (so dropdown opens below it)
  • Tab click → hide()+ re-show() with filtered items
  • delegate.onHide() tears the tab bar down

Where it broke down:

  • Visual disconnect. With the tab bar in normal page flow and the popup floating above content, the two elements read as unrelated UI fragments, the tabs sit flush against surrounding chat-input chrome rather than feeling like part of the picker.
  • Context view dismisses on tab click. The action widget's overlay treats anything outside its rendered bounds as an outside-click and hides immediately. I worked around it with a mousedown stopPropagation on the tab bar, but at that point the tabs are functionally inside the popup's interaction zone while visually outside it, which felt worse than just rendering them inside.
  • Anchor positioning quirks. The dropdown anchored to the tab bar collides with the trigger button when the tab bar height is non-trivial; nudging with margins fights the action widget's own positioning logic.

Where I landed:

Kept the tab bar inside the popup, but exposed it through a generic renderHeader?: (container: HTMLElement) => IDisposable callback on IActionWidgetService.show() instead of the original headerElement: HTMLElement. The differences from your concerns:

  • Action widget no longer holds caller-owned DOM - it just provides an empty container and ties the returned disposable to the popup lifecycle.
  • Tab state, element creation, event listeners, and teardown are all owned by WorkspacePicker. The action widget knows nothing about tabs.
  • No new parameters specific to the picker - renderHeader is generic and reusable.

I think this addresses the ownership concern even if it doesn't fully eliminate the API surface change. Happy to try the inline approach again if you have a trick for the visual/dismissal issues

@sandy081
Copy link
Copy Markdown
Member

Thanks for trying the inline approach and explaining what broke — those are real issues.

I think the right path is a new standalone widget rather than extending IActionWidgetService in any way (headerElement, renderHeader, or otherwise). Here's the idea:

TabbedWorkspacePickerWidget — a composite widget that composes ActionListWidget internally

ActionListWidget can be instantiated directly via IInstantiationService — it has no dependency on IActionWidgetService. ActionWidgetService._renderWidget() already creates it this way internally. So we can build a new widget that:

  1. Owns its own ContextView instance (not the shared IContextViewService singleton — same pattern as SuggestWidget)
  2. Renders a single popup container with tab bar + ActionListWidget as siblings inside it
  3. Manages tab state, item filtering, keyboard navigation, and lifecycle

This solves all three issues you hit:

  • Visual disconnect → tab bar and list are siblings in the same popup — visually unified
  • Outside-click dismissal → provide a custom onDOMEvent on the ContextView delegate so both tabs and list are treated as "inside" the popup bounds
  • Anchor collision → the widget positions itself as a single unit relative to the trigger element

On tab switch: dispose the old ActionListWidget, create a new one with filtered items, call contextView.layout().

No changes needed to actionWidget.ts, actionList.ts, or any platform-level API.

On grouping: I don't think we need a SessionWorkspaceCategory enum at the platform level — the platform shouldn't know about specific categories like Local/GitHub/Remote. There's already an open-ended group property on ISessionWorkspaceBrowseAction. Reuse that: providers set group to whatever group label they want (e.g., 'Local', 'GitHub', 'Remote'), and the picker dynamically discovers and renders tabs from the contributed group values. No closed enum, no platform-level category type.

- Updated ISessionWorkspace and ISessionWorkspaceBrowseAction interfaces to use a generic 'group' property instead of 'category' for workspace categorization.
- Introduced well-known group labels: SESSION_WORKSPACE_GROUP_LOCAL, SESSION_WORKSPACE_GROUP_CLOUD, and SESSION_WORKSPACE_GROUP_REMOTE for consistent workspace grouping across the application.
- Refactored various session providers (LocalAgentHost, RemoteAgentHost, CopilotChat) to utilize the new group labels in their browse actions and workspace definitions.
- Adjusted CSS and picker logic to accommodate the new grouping mechanism, ensuring a seamless user experience when selecting workspaces.
@mrleemurray
Copy link
Copy Markdown
Contributor Author

Thanks for pushing on this. Where it landed:

No IActionWidgetService extension. Reverted actionWidget.ts entirely - no headerElement, no renderHeader, no new parameters.

Picker owns its own popup for the tabbed case. Calls IContextViewService.showContextView() directly, instantiates ActionList<T> via IInstantiationService, and renders a single .action-widget shell with tab bar + filter + list as siblings. Picker manages its own lifecycle, tab state, keyboard nav, and focus tracking. Single tab → falls back to IActionWidgetService.show() for the existing keybindings.

Group-based tab discovery (no closed enum). Dropped SessionWorkspaceCategory. Tabs are derived dynamically from the existing group: string field on ISessionWorkspace and ISessionWorkspaceBrowseAction:

// Union of `group` values across providers' browse actions and recent workspaces.
// Well-known labels (Local/Cloud/Remote) first; custom labels appended.
private _getAvailableTabs(): string[]

Three convenience constants (SESSION_WORKSPACE_GROUP_LOCAL/_CLOUD/_REMOTE) are exported for in-tree providers and resolve to localize(...) at the source. The picker treats them as opaque strings — no more GITHUB_REMOTE_FILE_SCHEME / remoteAddress / 'repositories' checks. Future providers contribute their own labels and they appear as new tabs automatically.

Simplifications that fell out:

  • Removed merge-by-group browse-action logic — within a tab, each action renders as a regular row.
  • Removed the sessions.experimental.tabbedWorkspacePicker setting.
  • Removed _includeWorkspace/_includeBrowseAction predicates in favor of a single tab-derived filter.

19/19 tests pass, including 6 new tests for tab discovery (order, dedup, custom labels, ungrouped actions, recents seeding).

@sandy081
Copy link
Copy Markdown
Member

Select Folders action here does not need Local description

image

Cloud should be called GitHub

image

Manage buttons should be inlined with a separator

image

@sandy081
Copy link
Copy Markdown
Member

Minor: createTestablePicker() in the test helper doesn't stub IContextViewService. Tests pass today because they only exercise _getAvailableTabs(), but calling showPicker() in the tabbed path would crash. Worth adding a stub to make the test helper robust for future test additions.

@sandy081
Copy link
Copy Markdown
Member

sandy081 commented Apr 29, 2026

Consider extracting the tabbed popup logic into a standalone, reusable widget (e.g., TabbedActionListWidget) that is agnostic of the sessions workspace picker. It would take tabs + items + delegate and compose its own ContextView + ActionList — the same pattern you've built here, but without any coupling to workspace picker types. WorkspacePicker would then consume this widget, passing in the tabs discovered from provider group values and the filtered items per tab. This makes the tabbed-dropdown pattern available to other parts of VS Code that might need it, and keeps sessionWorkspacePicker.ts focused on workspace-specific logic.

mrleemurray and others added 3 commits April 30, 2026 11:28
…egrate submenu for manage actions

Co-authored-by: Copilot <copilot@github.com>
…styling

Co-authored-by: Copilot <copilot@github.com>
@sandy081 sandy081 added this to the 1.119.0 milestone Apr 30, 2026
@sandy081 sandy081 marked this pull request as ready for review April 30, 2026 13:21
@sandy081 sandy081 merged commit f06bbac into main Apr 30, 2026
26 checks passed
@sandy081 sandy081 deleted the mrleemurray/new-session-picker-exploration branch April 30, 2026 13:21
@sandy081 sandy081 mentioned this pull request May 3, 2026
2 tasks
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.

3 participants