mirror of
https://github.com/microsoft/vscode.git
synced 2026-10-01 09:31:50 +01:00
Agents - refactor repository pickers so that all actions are in one toolbar (#336058)
This commit is contained in:
@@ -434,14 +434,15 @@
|
||||
*
|
||||
* IMPLEMENTATION NOTE — specificity over `!important`
|
||||
*
|
||||
* The chips live inside two parallel toolbar wrappers:
|
||||
* The chips live inside two parallel toolbar groups:
|
||||
* .new-chat-controls-container > div > .monaco-toolbar
|
||||
* > .monaco-action-bar > .actions-container > .action-item
|
||||
* > .sessions-chat-picker-slot (Approvals)
|
||||
* .new-chat-repo-config-container > .monaco-toolbar
|
||||
* .new-chat-secondary-controls-container
|
||||
* > .new-chat-repo-config-container > .monaco-toolbar
|
||||
* > .monaco-action-bar > .actions-container > .action-item
|
||||
* > .sessions-chat-agent-host-config
|
||||
* > .sessions-chat-picker-slot[] (Branch + Worktree)
|
||||
* > .sessions-chat-picker-slot (Branch / Worktree)
|
||||
*
|
||||
* The desktop sheet `chatWidget.css` already sets `display: flex`,
|
||||
* `min-width: 0` and `overflow: hidden` at every level so labels can
|
||||
@@ -526,6 +527,7 @@
|
||||
* `.monaco-action-bar > .actions-container > .action-item` chain that's
|
||||
* part of monaco's public structure. */
|
||||
.agent-sessions-workbench.phone-layout .new-chat-widget-container .new-chat-bottom-container .new-chat-controls-container,
|
||||
.agent-sessions-workbench.phone-layout .new-chat-widget-container .new-chat-bottom-container .new-chat-secondary-controls-container,
|
||||
.agent-sessions-workbench.phone-layout .new-chat-widget-container .new-chat-bottom-container .new-chat-repo-config-container,
|
||||
.agent-sessions-workbench.phone-layout .new-chat-widget-container .new-chat-bottom-container .monaco-toolbar,
|
||||
.agent-sessions-workbench.phone-layout .new-chat-widget-container .new-chat-bottom-container .monaco-action-bar,
|
||||
|
||||
@@ -198,7 +198,7 @@ export class SessionSyncChangesContribution extends Disposable implements IWorkb
|
||||
precondition: enabled ? undefined : ContextKeyExpr.false(),
|
||||
},
|
||||
group: 'navigation',
|
||||
order: 4,
|
||||
order: Number.MAX_SAFE_INTEGER,
|
||||
when: ContextKeyExpr.and(IsSessionsWindowContext, ChatContextKeys.enabled, SessionIdContext.isEqualTo(session.sessionId)),
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -131,7 +131,7 @@ suite('Session Sync Changes', () => {
|
||||
second: getVisibleEntries(second.session).length,
|
||||
aiDisabled: getVisibleEntries(first.session, false).length,
|
||||
}, {
|
||||
first: [{ id: commandId, group: 'navigation', order: 4 }],
|
||||
first: [{ id: commandId, group: 'navigation', order: Number.MAX_SAFE_INTEGER }],
|
||||
second: 0,
|
||||
aiDisabled: 0,
|
||||
});
|
||||
|
||||
@@ -124,13 +124,16 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.new-chat-widget-container .new-chat-bottom-container .new-chat-secondary-controls-container,
|
||||
.new-chat-widget-container .new-chat-bottom-container .new-chat-repo-config-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.new-chat-widget-container .new-chat-bottom-container .new-chat-repo-config-container {
|
||||
.sync-changes-action-view-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -141,14 +144,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
.new-chat-status-toolbar.has-no-actions {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.new-chat-widget-container .new-chat-bottom-container .new-chat-status-toolbar.has-no-actions {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Toolbar hosts can shrink, while individual expanded pickers remain intrinsic
|
||||
* and switch to compact form before their labels would truncate. */
|
||||
.new-chat-widget-container .new-chat-bottom-container .new-chat-controls-container > *,
|
||||
.new-chat-widget-container .new-chat-bottom-container .new-chat-secondary-controls-container > *,
|
||||
.new-chat-widget-container .new-chat-bottom-container .new-chat-repo-config-container > * {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
@@ -224,7 +229,7 @@
|
||||
box-sizing: border-box;
|
||||
height: var(--chat-input-control-height);
|
||||
align-items: center;
|
||||
padding: 0 var(--vscode-spacing-size60);
|
||||
padding: 0 var(--vscode-spacing-size40);
|
||||
font-size: var(--vscode-fontSize-label2, 11px);
|
||||
color: var(--vscode-icon-foreground);
|
||||
background-color: var(--session-view-background);
|
||||
|
||||
@@ -794,7 +794,8 @@ export class NewChatInputWidget extends Disposable implements IHistoryNavigation
|
||||
this._register(createNewSessionControlToolbar(sessionControlsContainer, this._scopedInstantiationService));
|
||||
this._register({ dispose: () => sessionControlsContainer.remove() });
|
||||
|
||||
const repoConfigContainer = dom.append(newChatBottomContainer, dom.$('.new-chat-repo-config-container'));
|
||||
const secondaryControlsContainer = dom.append(newChatBottomContainer, dom.$('.new-chat-secondary-controls-container'));
|
||||
const repoConfigContainer = dom.append(secondaryControlsContainer, dom.$('.new-chat-repo-config-container'));
|
||||
if (this.options.renderRepositoryControls !== false) {
|
||||
const session = this.options.session;
|
||||
this._register(this._scopedInstantiationService.createInstance(MenuWorkbenchToolBar, repoConfigContainer, Menus.NewSessionRepositoryConfig, {
|
||||
@@ -816,7 +817,7 @@ export class NewChatInputWidget extends Disposable implements IHistoryNavigation
|
||||
this._register(installMobileChipLaneScroll(newChatBottomContainer, this.layoutService));
|
||||
|
||||
// Generic extension point for status indicators in the new-session view.
|
||||
const statusContainer = dom.append(repoConfigContainer, dom.$('.new-chat-status-toolbar'));
|
||||
const statusContainer = dom.append(secondaryControlsContainer, dom.$('.new-chat-status-toolbar'));
|
||||
this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, statusContainer, MenuId.ChatInputStatus, {
|
||||
hiddenItemStrategy: HiddenItemStrategy.NoHide,
|
||||
toolbarOptions: { primaryGroup: () => true },
|
||||
|
||||
@@ -58,7 +58,7 @@ export class SessionsChatAccessibilityHelp implements IAccessibleViewImplementat
|
||||
content.push(localize('sessionsChat.inputBackground', "Press Alt+Enter to start the session in the background without navigating into it. The started session appears in the Chat Sessions view."));
|
||||
content.push(localize('sessionsChat.workspace', "Shift+Tab to navigate to the workspace picker and choose a workspace for your session. When consolidated remote workspaces are enabled, opening the picker focuses its search input so you can immediately type to filter workspaces. If quick chats are available, you can also choose No workspace to start a workspace-less chat."));
|
||||
content.push(localize('sessionsChat.workspaceHandoff', "Opening Agents from an editor can suggest that editor's folder for a fresh session. Choosing a workspace yourself or starting another session cancels a pending suggestion. If an explicitly requested workspace cannot be selected, a notification offers Retry and Choose Workspace actions."));
|
||||
content.push(localize('sessionsChat.syncChanges', "When available, Sync Changes appears as a separate sync icon with incoming and outgoing commit counts beside the branch picker below the input. Use Tab and the arrow keys to reach it, then Enter or Space to synchronize the session's repository. The action is disabled while synchronization is running."));
|
||||
content.push(localize('sessionsChat.syncChanges', "When available, Sync Changes appears with incoming and outgoing commit counts in the same repository toolbar as the worktree and branch controls below the input. Use Tab and the arrow keys to reach it, then Enter or Space to synchronize the session's repository. The action is disabled while synchronization is running."));
|
||||
content.push(localize('sessionsChat.githubContext', "Use Add Context to attach files, images, and, when available, GitHub issues or pull requests."));
|
||||
content.push(localize('sessionsChat.devContainer', "When Dev Container Agent Host sessions are enabled, Docker is available, and a local folder contains a Dev Container configuration, open the folder's submenu and choose Use Dev Container. The workspace picker then identifies the folder with a Dev Container suffix. Choose Use Local to switch back."));
|
||||
content.push(localize('sessionsChat.pullRequestSession', "In a repository section where New Session is a split button, focus New Session and press Right Arrow to reach its dropdown, then activate New Session from Pull Request to open a searchable pull request picker. Pull requests are grouped by review and assignment status. Use the arrow keys to navigate, Enter to create the session, and Escape to close the picker."));
|
||||
|
||||
+224
-114
@@ -19,7 +19,8 @@ import { autorun, IObservable, observableValue } from '../../../../../base/commo
|
||||
import { ThemeIcon } from '../../../../../base/common/themables.js';
|
||||
import { localize, localize2 } from '../../../../../nls.js';
|
||||
import { IActionViewItemService, type IActionViewItemFactory } from '../../../../../platform/actions/browser/actionViewItemService.js';
|
||||
import { Action2, MenuId, MenuItemAction, registerAction2 } from '../../../../../platform/actions/common/actions.js';
|
||||
import { Action2, MenuId, MenuItemAction, MenuRegistry, registerAction2 } from '../../../../../platform/actions/common/actions.js';
|
||||
import { CommandsRegistry } from '../../../../../platform/commands/common/commands.js';
|
||||
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
|
||||
import { ContextKeyExpr, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';
|
||||
import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js';
|
||||
@@ -40,7 +41,7 @@ import { IViewsService } from '../../../../../workbench/services/views/common/vi
|
||||
import { IAgentWorkbenchLayoutService } from '../../../../browser/workbench.js';
|
||||
import { Menus } from '../../../../browser/menus.js';
|
||||
import { DevContainerWorktreeEnabledSettingId } from '../../../../common/devContainerAgentHostService.js';
|
||||
import { SessionProviderIdContext, IsPhoneLayoutContext, IsQuickChatSessionContext } from '../../../../common/contextkeys.js';
|
||||
import { SessionIdContext, SessionProviderIdContext, IsPhoneLayoutContext, IsQuickChatSessionContext } from '../../../../common/contextkeys.js';
|
||||
import { IWorkbenchLayoutService } from '../../../../../workbench/services/layout/browser/layoutService.js';
|
||||
import { reportNewChatPickerClosed } from '../../../chat/browser/newChatPickerTelemetry.js';
|
||||
import { ISessionChangesService } from '../../../changes/browser/sessionChangesService.js';
|
||||
@@ -73,6 +74,8 @@ import { MarkdownString } from '../../../../../base/common/htmlContent.js';
|
||||
|
||||
const IsActiveSessionRemoteAgentHost = ContextKeyExpr.regex(SessionProviderIdContext.key, REMOTE_AGENT_HOST_PROVIDER_RE);
|
||||
const IsActiveSessionLocalAgentHost = ContextKeyExpr.equals(SessionProviderIdContext.key, LOCAL_AGENT_HOST_PROVIDER_ID);
|
||||
const AGENT_HOST_SESSION_CONFIG_PICKER_ID_PREFIX = 'sessions.agentHost.sessionConfigPicker';
|
||||
const repositoryConfigSequencer = new SequencerByKey<string>();
|
||||
|
||||
function showActiveSessionModePicker(accessor: ServicesAccessor): void {
|
||||
const activeElement = dom.getActiveElement();
|
||||
@@ -86,27 +89,6 @@ function showActiveSessionModePicker(accessor: ServicesAccessor): void {
|
||||
}
|
||||
}
|
||||
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: 'sessions.agentHost.sessionConfigPicker',
|
||||
title: localize2('agentHostSessionConfigPicker', "Session Configuration"),
|
||||
f1: false,
|
||||
menu: [{
|
||||
id: Menus.NewSessionRepositoryConfig,
|
||||
group: 'navigation',
|
||||
order: 3,
|
||||
when: ContextKeyExpr.and(
|
||||
ContextKeyExpr.or(IsActiveSessionLocalAgentHost, IsActiveSessionRemoteAgentHost),
|
||||
IsQuickChatSessionContext.negate(),
|
||||
),
|
||||
}],
|
||||
});
|
||||
}
|
||||
|
||||
override async run(): Promise<void> { }
|
||||
});
|
||||
|
||||
export interface IConfigPickerItem {
|
||||
readonly value: string;
|
||||
readonly label: string;
|
||||
@@ -252,6 +234,58 @@ function applyAutoApproveFiltering(
|
||||
return { items, policyRestricted };
|
||||
}
|
||||
|
||||
function isRenderableSessionConfigProperty(property: string, schema: SessionConfigPropertySchema): boolean {
|
||||
if (schema.type !== 'boolean' && (schema.type !== 'string' || (!schema.enumDynamic && !schema.enum?.length))) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
property === SessionConfigKey.SandboxEnabled ||
|
||||
property === SessionConfigKey.WorktreeBranchTrack ||
|
||||
property === SessionConfigKey.WorktreeCreateNewBranch
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (property === SessionConfigKey.Isolation && !schema.enum?.includes('worktree')) {
|
||||
return false;
|
||||
}
|
||||
if (property === SessionConfigKey.AutoApprove && isWellKnownAutoApproveSchema(schema)) {
|
||||
return false;
|
||||
}
|
||||
if (property === SessionConfigKey.Mode && isWellKnownModeSchema(schema)) {
|
||||
return false;
|
||||
}
|
||||
if (property === ClaudeSessionConfigKey.PermissionMode && isWellKnownClaudePermissionModeSchema(schema)) {
|
||||
return false;
|
||||
}
|
||||
if (property === CodexSessionConfigKey.PermissionsPreset && isWellKnownCodexApprovalsSchema(schema)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function orderSessionConfigProperties(
|
||||
properties: ReadonlyArray<[string, SessionConfigPropertySchema]>,
|
||||
phoneLayout: boolean,
|
||||
): ReadonlyArray<[string, SessionConfigPropertySchema]> {
|
||||
const order = new Map<string, number>(phoneLayout
|
||||
? [
|
||||
[SessionConfigKey.Branch, 0],
|
||||
[SessionConfigKey.Isolation, 1],
|
||||
]
|
||||
: [
|
||||
[SessionConfigKey.Isolation, 0],
|
||||
[SessionConfigKey.Branch, 1],
|
||||
]);
|
||||
return properties
|
||||
.map(([key, schema], index) => ({ key, schema, index }))
|
||||
.sort((a, b) => {
|
||||
const aRank = order.get(a.key) ?? Number.MAX_SAFE_INTEGER;
|
||||
const bRank = order.get(b.key) ?? Number.MAX_SAFE_INTEGER;
|
||||
return aRank - bRank || a.index - b.index;
|
||||
})
|
||||
.map(({ key, schema }) => [key, schema] as [string, SessionConfigPropertySchema]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a confirmation dialog for elevated auto-approve levels (Bypass
|
||||
* or legacy Autopilot). Delegates to the shared
|
||||
@@ -354,8 +388,9 @@ export class AgentHostSessionConfigPicker extends Disposable {
|
||||
private readonly _isolationCheckbox = this._register(new MutableDisposable<ConfigCheckboxControl>());
|
||||
private readonly _hostMarker = this._register(new MutableDisposable());
|
||||
protected readonly _filterDelayer = this._register(new Delayer<readonly IActionListItem<IConfigPickerItem>[]>(200));
|
||||
private readonly _repositoryConfigSequencer = new SequencerByKey<string>();
|
||||
private _container: HTMLElement | undefined;
|
||||
private _focusableElement: HTMLElement | undefined;
|
||||
private _focusable = false;
|
||||
|
||||
/**
|
||||
* Session/property-scoped value→label cache for `enumDynamic`
|
||||
@@ -380,6 +415,7 @@ export class AgentHostSessionConfigPicker extends Disposable {
|
||||
|
||||
constructor(
|
||||
protected readonly _session: IObservable<IActiveSession | undefined>,
|
||||
private readonly _property: string | undefined,
|
||||
@IActionWidgetService protected readonly _actionWidgetService: IActionWidgetService,
|
||||
@IConfigurationService protected readonly _configurationService: IConfigurationService,
|
||||
@IContextKeyService protected readonly _contextKeyService: IContextKeyService,
|
||||
@@ -435,12 +471,27 @@ export class AgentHostSessionConfigPicker extends Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
render(container: HTMLElement): void {
|
||||
render(container: HTMLElement): HTMLElement | undefined {
|
||||
this._isolationCheckbox.clear();
|
||||
container.classList.add('sessions-chat-agent-host-config-host');
|
||||
this._hostMarker.value = toDisposable(() => container.classList.remove('sessions-chat-agent-host-config-host'));
|
||||
this._container = dom.append(container, dom.$('.sessions-chat-agent-host-config'));
|
||||
this._renderConfigPickers();
|
||||
return this._focusableElement;
|
||||
}
|
||||
|
||||
setFocusable(focusable: boolean): void {
|
||||
this._focusable = focusable;
|
||||
if (this._property === undefined) {
|
||||
return;
|
||||
}
|
||||
if (this._focusableElement) {
|
||||
this._focusableElement.tabIndex = focusable ? 0 : -1;
|
||||
}
|
||||
}
|
||||
|
||||
focus(): void {
|
||||
this._focusableElement?.focus();
|
||||
}
|
||||
|
||||
private _renderConfigPickers(): void {
|
||||
@@ -449,6 +500,7 @@ export class AgentHostSessionConfigPicker extends Disposable {
|
||||
}
|
||||
|
||||
this._renderDisposables.clear();
|
||||
this._focusableElement = undefined;
|
||||
const checkboxSlots = new Set([
|
||||
this._isolationCheckbox.value?.slot,
|
||||
]);
|
||||
@@ -484,62 +536,22 @@ export class AgentHostSessionConfigPicker extends Disposable {
|
||||
let renderedIsolationCheckbox = false;
|
||||
|
||||
for (const [property, schema] of properties) {
|
||||
if (!this._isPickable(schema)) {
|
||||
if (this._property !== undefined && property !== this._property) {
|
||||
continue;
|
||||
}
|
||||
if (property === SessionConfigKey.SandboxEnabled) {
|
||||
continue;
|
||||
}
|
||||
// Hidden carrier properties (see `worktreeBranchTrackProperty` in
|
||||
// `worktreeIsolation.ts`) consumed only by the host for worktree
|
||||
// isolation, never edited by the user. Its boolean type otherwise
|
||||
// passes `_isPickable` unlike its string/array carrier siblings
|
||||
// (`worktreeBranchPrefix`/`worktreeIncludeFiles`), which are
|
||||
// filtered out because they lack an `enum`.
|
||||
if (
|
||||
property === SessionConfigKey.WorktreeBranchTrack ||
|
||||
property === SessionConfigKey.WorktreeCreateNewBranch
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if (property === SessionConfigKey.Isolation && !schema.enum?.includes('worktree')) {
|
||||
if (!isRenderableSessionConfigProperty(property, schema)) {
|
||||
continue;
|
||||
}
|
||||
if (!this._shouldRenderProperty(property, schema, isNewSession)) {
|
||||
continue;
|
||||
}
|
||||
// When the autoApprove property uses the well-known schema, the
|
||||
// workbench `PermissionPickerActionItem` (registered separately for
|
||||
// `Menus.NewSessionControl`) handles it — skip it here to avoid
|
||||
// double-rendering. Non-conforming schemas still fall through to
|
||||
// the generic per-property picker below.
|
||||
if (property === SessionConfigKey.AutoApprove && isWellKnownAutoApproveSchema(schema)) {
|
||||
continue;
|
||||
}
|
||||
// When the mode property uses the well-known schema, the dedicated
|
||||
// {@link AgentHostModePicker} (registered separately for
|
||||
// `Menus.NewSessionControl`) handles it. Non-conforming schemas
|
||||
// still fall through to the generic per-property picker below.
|
||||
if (property === SessionConfigKey.Mode && isWellKnownModeSchema(schema)) {
|
||||
continue;
|
||||
}
|
||||
// Claude's permissionMode has a dedicated Claude-native picker so
|
||||
// it doesn't render as a generic enum chip.
|
||||
if (property === ClaudeSessionConfigKey.PermissionMode && isWellKnownClaudePermissionModeSchema(schema)) {
|
||||
continue;
|
||||
}
|
||||
// Codex's permissions preset has a dedicated Codex-native picker
|
||||
// (a single "Approvals" chip) so it doesn't render as a generic
|
||||
// enum chip.
|
||||
if (property === CodexSessionConfigKey.PermissionsPreset && isWellKnownCodexApprovalsSchema(schema)) {
|
||||
continue;
|
||||
}
|
||||
const value = resolvedConfig.values[property] ?? schema.default;
|
||||
const isReadOnly = this._isReadOnlyChip(property, schema, isNewSession)
|
||||
|| (this._requiresBranchCheckout(provider, session.sessionId, property) && !this._getCheckoutChangeset(session.sessionId));
|
||||
// Isolation renders as a Worktree checkbox on desktop; the phone layout keeps the chip for the unified repo sheet.
|
||||
if (property === SessionConfigKey.Isolation && this._shouldRenderIsolationAsCheckbox(schema)) {
|
||||
this._renderIsolationCheckbox(provider, session.sessionId, schema, value, isReadOnly, !isReadOnly && isLoading);
|
||||
this._focusableElement = this._isolationCheckbox.value?.checkbox.domNode;
|
||||
renderedIsolationCheckbox = true;
|
||||
continue;
|
||||
}
|
||||
@@ -554,6 +566,7 @@ export class AgentHostSessionConfigPicker extends Disposable {
|
||||
// the trigger's appearance. The click handler bails when resolving
|
||||
// in `_showPicker`.
|
||||
const trigger = renderPickerTrigger(slot, isReadOnly, this._renderDisposables, () => this._showPicker(provider, session.sessionId, property, schema, trigger));
|
||||
this._focusableElement = trigger;
|
||||
|
||||
// The Branch chip owns its own hover in `_renderTrigger`, because
|
||||
// the content depends on the repository's uncommitted-changes
|
||||
@@ -574,16 +587,7 @@ export class AgentHostSessionConfigPicker extends Disposable {
|
||||
if (!renderedIsolationCheckbox) {
|
||||
this._isolationCheckbox.clear();
|
||||
}
|
||||
}
|
||||
|
||||
private _isPickable(schema: SessionConfigPropertySchema): boolean {
|
||||
if (schema.type === 'boolean') {
|
||||
return true;
|
||||
}
|
||||
if (schema.type !== 'string') {
|
||||
return false;
|
||||
}
|
||||
return !!schema.enumDynamic || (Array.isArray(schema.enum) && schema.enum.length > 0);
|
||||
this.setFocusable(this._focusable);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -595,18 +599,7 @@ export class AgentHostSessionConfigPicker extends Disposable {
|
||||
* (e.g. the mobile chip row groups Approvals | Branch | Worktree).
|
||||
*/
|
||||
protected _orderProperties(properties: ReadonlyArray<[string, SessionConfigPropertySchema]>): ReadonlyArray<[string, SessionConfigPropertySchema]> {
|
||||
const order = new Map<string, number>([
|
||||
[SessionConfigKey.Isolation, 0],
|
||||
[SessionConfigKey.Branch, 1],
|
||||
]);
|
||||
return properties
|
||||
.map(([key, schema], index) => ({ key, schema, index }))
|
||||
.sort((a, b) => {
|
||||
const aRank = order.get(a.key) ?? Number.MAX_SAFE_INTEGER;
|
||||
const bRank = order.get(b.key) ?? Number.MAX_SAFE_INTEGER;
|
||||
return aRank - bRank || a.index - b.index;
|
||||
})
|
||||
.map(({ key, schema }) => [key, schema] as [string, SessionConfigPropertySchema]);
|
||||
return orderSessionConfigProperties(properties, false);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -647,7 +640,7 @@ export class AgentHostSessionConfigPicker extends Disposable {
|
||||
treeish = value;
|
||||
}
|
||||
|
||||
const configOperation = this._repositoryConfigSequencer.queue(sessionId, async () => {
|
||||
const configOperation = repositoryConfigSequencer.queue(sessionId, async () => {
|
||||
const shouldCheckout = this._requiresBranchCheckout(provider, sessionId, property);
|
||||
if (!shouldCheckout) {
|
||||
await provider.setSessionConfigValue(sessionId, property, value);
|
||||
@@ -1126,18 +1119,7 @@ class MobileAgentHostSessionConfigPicker extends AgentHostSessionConfigPicker {
|
||||
* (Isolation first, Branch second) when not on a phone layout.
|
||||
*/
|
||||
protected override _orderProperties(properties: ReadonlyArray<[string, SessionConfigPropertySchema]>): ReadonlyArray<[string, SessionConfigPropertySchema]> {
|
||||
if (!isPhoneLayout(this._layoutService)) {
|
||||
return super._orderProperties(properties);
|
||||
}
|
||||
const order = new Map<string, number>([
|
||||
[SessionConfigKey.Branch, 0],
|
||||
[SessionConfigKey.Isolation, 1],
|
||||
]);
|
||||
return properties.slice().sort(([aKey], [bKey]) => {
|
||||
const a = order.get(aKey) ?? Number.MAX_SAFE_INTEGER;
|
||||
const b = order.get(bKey) ?? Number.MAX_SAFE_INTEGER;
|
||||
return a - b;
|
||||
});
|
||||
return orderSessionConfigProperties(properties, isPhoneLayout(this._layoutService));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1316,6 +1298,7 @@ class MobileAgentHostSessionConfigPicker extends AgentHostSessionConfigPicker {
|
||||
interface IConfigPickerWidget extends IDisposable {
|
||||
render(container: HTMLElement): HTMLElement | void;
|
||||
focus?(): void;
|
||||
setFocusable?(focusable: boolean): void;
|
||||
showPicker?(anchor: HTMLElement, onHide?: () => void): boolean | void;
|
||||
}
|
||||
|
||||
@@ -1323,7 +1306,11 @@ export class PickerActionViewItem extends BaseActionViewItem implements IChatInp
|
||||
private _compact = false;
|
||||
private _focusableElement: HTMLElement | undefined;
|
||||
|
||||
constructor(private readonly _picker: IConfigPickerWidget, disposable?: IDisposable) {
|
||||
constructor(
|
||||
private readonly _picker: IConfigPickerWidget,
|
||||
disposable?: IDisposable,
|
||||
private readonly _manageFocusableElement = false,
|
||||
) {
|
||||
super(undefined, { id: '', label: '', enabled: true, class: undefined, tooltip: '', run: () => { } });
|
||||
if (disposable) {
|
||||
this._register(disposable);
|
||||
@@ -1337,6 +1324,19 @@ export class PickerActionViewItem extends BaseActionViewItem implements IChatInp
|
||||
}
|
||||
|
||||
override focus(): void {
|
||||
if (this._manageFocusableElement) {
|
||||
if (this._picker.setFocusable) {
|
||||
this._picker.setFocusable(true);
|
||||
} else if (this._focusableElement) {
|
||||
this._focusableElement.tabIndex = 0;
|
||||
}
|
||||
if (this._picker.focus) {
|
||||
this._picker.focus();
|
||||
} else {
|
||||
this._focusableElement?.focus();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (this._picker.focus) {
|
||||
this._picker.focus();
|
||||
} else if (this._focusableElement) {
|
||||
@@ -1355,6 +1355,18 @@ export class PickerActionViewItem extends BaseActionViewItem implements IChatInp
|
||||
}
|
||||
|
||||
override blur(): void {
|
||||
if (this._manageFocusableElement) {
|
||||
if (this._picker.setFocusable) {
|
||||
this._picker.setFocusable(false);
|
||||
} else if (this._focusableElement) {
|
||||
this._focusableElement.tabIndex = -1;
|
||||
}
|
||||
const activeElement = dom.getActiveElement();
|
||||
if (this.element && dom.isHTMLElement(activeElement) && dom.isAncestor(activeElement, this.element)) {
|
||||
activeElement.blur();
|
||||
}
|
||||
return;
|
||||
}
|
||||
const activeElement = dom.getActiveElement();
|
||||
if (this.element && dom.isHTMLElement(activeElement) && dom.isAncestor(activeElement, this.element)) {
|
||||
activeElement.blur();
|
||||
@@ -1363,9 +1375,16 @@ export class PickerActionViewItem extends BaseActionViewItem implements IChatInp
|
||||
}
|
||||
}
|
||||
|
||||
override setFocusable(_focusable: boolean): void {
|
||||
override setFocusable(focusable: boolean): void {
|
||||
if (this.element) {
|
||||
this.element.tabIndex = -1;
|
||||
if (this._manageFocusableElement) {
|
||||
if (this._picker.setFocusable) {
|
||||
this._picker.setFocusable(focusable);
|
||||
} else if (this._focusableElement) {
|
||||
this._focusableElement.tabIndex = focusable ? 0 : -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1409,12 +1428,19 @@ export class PickerActionViewItem extends BaseActionViewItem implements IChatInp
|
||||
}
|
||||
}
|
||||
|
||||
class AgentHostSessionConfigPickerContribution extends Disposable implements IWorkbenchContribution {
|
||||
export class AgentHostSessionConfigPickerContribution extends Disposable implements IWorkbenchContribution {
|
||||
static readonly ID = 'sessions.contrib.agentHostSessionConfigPicker';
|
||||
|
||||
private readonly _repositoryMenuItems = this._register(new DisposableStore());
|
||||
private readonly _repositoryPropertyRegistrations = this._register(new DisposableMap<string>());
|
||||
private readonly _providerListeners = this._register(new DisposableMap<string>());
|
||||
|
||||
constructor(
|
||||
@IActionViewItemService actionViewItemService: IActionViewItemService,
|
||||
@IWorkbenchLayoutService private readonly _layoutService: IWorkbenchLayoutService,
|
||||
@IContextKeyService private readonly _contextKeyService: IContextKeyService,
|
||||
@ISessionsProvidersService private readonly _sessionsProvidersService: ISessionsProvidersService,
|
||||
@ISessionsService private readonly _sessionsService: ISessionsService,
|
||||
) {
|
||||
super();
|
||||
// The mode-picker factories below pick the mobile subclass at
|
||||
@@ -1428,14 +1454,23 @@ class AgentHostSessionConfigPickerContribution extends Disposable implements IWo
|
||||
// because the classes are only accessed inside these factory
|
||||
// callbacks, which run at `AfterRestored` — well after both
|
||||
// modules have finished evaluating.
|
||||
this._register(actionViewItemService.register(
|
||||
Menus.NewSessionRepositoryConfig,
|
||||
'sessions.agentHost.sessionConfigPicker',
|
||||
(_action, _options, scopedInstantiationService) => {
|
||||
const { session } = scopedInstantiationService.invokeFunction(accessor => accessor.get(ISessionContext));
|
||||
return new PickerActionViewItem(scopedInstantiationService.createInstance(MobileAgentHostSessionConfigPicker, session));
|
||||
},
|
||||
));
|
||||
this._register(autorun(reader => {
|
||||
this._sessionsService.visibleSessions.read(reader);
|
||||
this._refreshRepositoryMenuItems(actionViewItemService);
|
||||
}));
|
||||
this._watchProviders(this._sessionsProvidersService.getProviders(), actionViewItemService);
|
||||
this._register(this._sessionsProvidersService.onDidChangeProviders(e => {
|
||||
for (const provider of e.removed) {
|
||||
this._providerListeners.deleteAndDispose(provider.id);
|
||||
}
|
||||
this._watchProviders(e.added, actionViewItemService);
|
||||
this._refreshRepositoryMenuItems(actionViewItemService);
|
||||
}));
|
||||
this._register(this._contextKeyService.onDidChangeContext(e => {
|
||||
if (e.affectsSome(new Set([IsPhoneLayoutContext.key]))) {
|
||||
this._refreshRepositoryMenuItems(actionViewItemService);
|
||||
}
|
||||
}));
|
||||
this._register(actionViewItemService.register(
|
||||
Menus.NewSessionControl,
|
||||
NEW_SESSION_MODE_PICKER_ID,
|
||||
@@ -1502,6 +1537,81 @@ class AgentHostSessionConfigPickerContribution extends Disposable implements IWo
|
||||
));
|
||||
}
|
||||
|
||||
private _watchProviders(providers: readonly ISessionsProvider[], actionViewItemService: IActionViewItemService): void {
|
||||
for (const provider of providers) {
|
||||
if (!isAgentHostProvider(provider) || this._providerListeners.has(provider.id)) {
|
||||
continue;
|
||||
}
|
||||
this._providerListeners.set(provider.id, provider.onDidChangeSessionConfig(() => this._refreshRepositoryMenuItems(actionViewItemService)));
|
||||
}
|
||||
}
|
||||
|
||||
private _refreshRepositoryMenuItems(actionViewItemService: IActionViewItemService): void {
|
||||
this._repositoryMenuItems.clear();
|
||||
for (const session of this._sessionsService.visibleSessions.get()) {
|
||||
if (!session) {
|
||||
continue;
|
||||
}
|
||||
const provider = this._sessionsProvidersService.getProvider(session.providerId);
|
||||
if (!provider || !isAgentHostProvider(provider)) {
|
||||
continue;
|
||||
}
|
||||
const config = provider.getSessionConfig(session.sessionId);
|
||||
if (!config) {
|
||||
continue;
|
||||
}
|
||||
const isNewSession = provider.getCreateSessionConfig(session.sessionId) !== undefined;
|
||||
const properties = orderSessionConfigProperties(
|
||||
Object.entries(config.schema.properties),
|
||||
isPhoneLayout(this._layoutService),
|
||||
).filter(([property, schema]) =>
|
||||
isRenderableSessionConfigProperty(property, schema) &&
|
||||
(isNewSession || schema.sessionMutable || property === SessionConfigKey.Isolation || property === SessionConfigKey.Branch)
|
||||
);
|
||||
|
||||
properties.forEach(([property, schema], index) => {
|
||||
const commandId = this._registerRepositoryProperty(property, actionViewItemService);
|
||||
this._repositoryMenuItems.add(MenuRegistry.appendMenuItem(Menus.NewSessionRepositoryConfig, {
|
||||
command: {
|
||||
id: commandId,
|
||||
title: schema.title ?? property,
|
||||
},
|
||||
group: 'navigation',
|
||||
order: index + 1,
|
||||
when: ContextKeyExpr.and(
|
||||
ChatContextKeys.enabled,
|
||||
IsQuickChatSessionContext.negate(),
|
||||
SessionIdContext.isEqualTo(session.sessionId),
|
||||
),
|
||||
}));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _registerRepositoryProperty(property: string, actionViewItemService: IActionViewItemService): string {
|
||||
const commandId = `${AGENT_HOST_SESSION_CONFIG_PICKER_ID_PREFIX}.${encodeURIComponent(property)}`;
|
||||
if (this._repositoryPropertyRegistrations.has(commandId)) {
|
||||
return commandId;
|
||||
}
|
||||
|
||||
const store = new DisposableStore();
|
||||
store.add(CommandsRegistry.registerCommand(commandId, () => { }));
|
||||
store.add(actionViewItemService.register(
|
||||
Menus.NewSessionRepositoryConfig,
|
||||
commandId,
|
||||
(_action, _options, scopedInstantiationService) => {
|
||||
const { session } = scopedInstantiationService.invokeFunction(accessor => accessor.get(ISessionContext));
|
||||
return new PickerActionViewItem(
|
||||
scopedInstantiationService.createInstance(MobileAgentHostSessionConfigPicker, session, property),
|
||||
undefined,
|
||||
true,
|
||||
);
|
||||
},
|
||||
));
|
||||
this._repositoryPropertyRegistrations.set(commandId, store);
|
||||
return commandId;
|
||||
}
|
||||
|
||||
/**
|
||||
* On the new-chat page (left of the toolbar), use the sessions
|
||||
* {@link PermissionPicker} so the styling matches the surrounding sessions
|
||||
|
||||
+118
-13
@@ -13,6 +13,7 @@ import { URI } from '../../../../../../../base/common/uri.js';
|
||||
import { mock } from '../../../../../../../base/test/common/mock.js';
|
||||
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js';
|
||||
import { isIMenuItem, MenuId, MenuRegistry } from '../../../../../../../platform/actions/common/actions.js';
|
||||
import { IActionViewItemService } from '../../../../../../../platform/actions/browser/actionViewItemService.js';
|
||||
import { ActionListItemKind, IActionListDelegate, IActionListItem } from '../../../../../../../platform/actionWidget/browser/actionList.js';
|
||||
import { IActionWidgetService } from '../../../../../../../platform/actionWidget/browser/actionWidget.js';
|
||||
import { AGENT_HOST_CHECKOUT_CHANGESET_OPERATION_ID } from '../../../../../../../platform/agentHost/common/agentHostChangesetOperationService.js';
|
||||
@@ -31,6 +32,7 @@ import { ITelemetryService } from '../../../../../../../platform/telemetry/commo
|
||||
import { NullTelemetryService } from '../../../../../../../platform/telemetry/common/telemetryUtils.js';
|
||||
import { IView } from '../../../../../../../workbench/common/views.js';
|
||||
import { IViewsService } from '../../../../../../../workbench/services/views/common/viewsService.js';
|
||||
import { IWorkbenchLayoutService } from '../../../../../../../workbench/services/layout/browser/layoutService.js';
|
||||
import { IAgentWorkbenchLayoutService } from '../../../../../../browser/workbench.js';
|
||||
import { Menus } from '../../../../../../browser/menus.js';
|
||||
import { IAgentHostSessionsProvider, LOCAL_AGENT_HOST_PROVIDER_ID } from '../../../../../../common/agentHostSessionsProvider.js';
|
||||
@@ -38,10 +40,11 @@ import { DevContainerWorktreeEnabledSettingId } from '../../../../../../common/d
|
||||
import { ISessionChangesService } from '../../../../../../contrib/changes/browser/sessionChangesService.js';
|
||||
import { CHANGES_VIEW_ID } from '../../../../../../contrib/changes/common/changes.js';
|
||||
import { ISessionsProvidersService } from '../../../../../../services/sessions/browser/sessionsProvidersService.js';
|
||||
import { ISessionsService } from '../../../../../../services/sessions/browser/sessionsService.js';
|
||||
import { IActiveSession } from '../../../../../../services/sessions/common/sessionsManagement.js';
|
||||
import { ISessionChangeset, ISessionChangesetOperationTarget, ISessionWorkspace, SessionChangesetOperationScope, SessionChangesetOperationStatus, UNCOMMITTED_CHANGES_CHANGESET_ID } from '../../../../../../services/sessions/common/session.js';
|
||||
import { ISessionsProvider } from '../../../../../../services/sessions/common/sessionsProvider.js';
|
||||
import { AgentHostSessionConfigPicker, IConfigPickerItem, PickerActionViewItem } from '../../../browser/agentHostSessionConfigPicker.js';
|
||||
import { AgentHostSessionConfigPicker, AgentHostSessionConfigPickerContribution, IConfigPickerItem, PickerActionViewItem } from '../../../browser/agentHostSessionConfigPicker.js';
|
||||
|
||||
const SESSION_ID = 'local-agent-host:s1';
|
||||
const SESSION_RESOURCE = URI.parse('agent-session:/s1');
|
||||
@@ -326,12 +329,12 @@ function setupServices(
|
||||
override readonly changesets = changesetsObs;
|
||||
}();
|
||||
const sessionObs = observableValue<IActiveSession | undefined>('activeSession', activeSession);
|
||||
return { instantiationService, provider, sessionObs, workspaceObs, changesetsObs, uncommittedChangeset, actionWidget, checkoutInvocations, branchSelectionEvents, checkoutDialogs };
|
||||
return { instantiationService, provider, activeSession, sessionObs, workspaceObs, changesetsObs, uncommittedChangeset, actionWidget, checkoutInvocations, branchSelectionEvents, checkoutDialogs };
|
||||
}
|
||||
|
||||
/** Create and render a fresh picker instance, as the toolbar does on a rebuild. */
|
||||
function renderPicker(store: Pick<ReturnType<typeof ensureNoDisposablesAreLeakedInTestSuite>, 'add'>, services: ReturnType<typeof setupServices>) {
|
||||
const picker = store.add(services.instantiationService.createInstance(AgentHostSessionConfigPicker, services.sessionObs));
|
||||
const picker = store.add(services.instantiationService.createInstance(AgentHostSessionConfigPicker, services.sessionObs, undefined));
|
||||
const container = document.createElement('div');
|
||||
picker.render(container);
|
||||
return { picker, container };
|
||||
@@ -348,6 +351,70 @@ suite('Agent Host Session Config Picker', () => {
|
||||
assert.strictEqual(container.classList.contains('sessions-chat-agent-host-config-host'), true);
|
||||
});
|
||||
|
||||
test('renders one repository control per toolbar action view item', () => {
|
||||
const services = setupServices(store);
|
||||
const isolationPicker = store.add(services.instantiationService.createInstance(AgentHostSessionConfigPicker, services.sessionObs, SessionConfigKey.Isolation));
|
||||
const branchPicker = store.add(services.instantiationService.createInstance(AgentHostSessionConfigPicker, services.sessionObs, SessionConfigKey.Branch));
|
||||
const isolationContainer = document.createElement('div');
|
||||
const branchContainer = document.createElement('div');
|
||||
isolationPicker.render(isolationContainer);
|
||||
branchPicker.render(branchContainer);
|
||||
isolationPicker.setFocusable(true);
|
||||
branchPicker.setFocusable(false);
|
||||
|
||||
assert.deepStrictEqual({
|
||||
isolation: {
|
||||
controls: isolationContainer.querySelectorAll('.sessions-chat-picker-slot').length,
|
||||
hasIsolation: !!isolationSlot(isolationContainer),
|
||||
hasBranch: !!branchSlot(isolationContainer),
|
||||
tabIndex: isolationContainer.querySelector<HTMLElement>('[tabindex]')?.tabIndex,
|
||||
},
|
||||
branch: {
|
||||
controls: branchContainer.querySelectorAll('.sessions-chat-picker-slot').length,
|
||||
hasIsolation: !!isolationSlot(branchContainer),
|
||||
hasBranch: !!branchSlot(branchContainer),
|
||||
tabIndex: branchContainer.querySelector<HTMLElement>('[tabindex]')?.tabIndex,
|
||||
},
|
||||
}, {
|
||||
isolation: { controls: 1, hasIsolation: true, hasBranch: false, tabIndex: 0 },
|
||||
branch: { controls: 1, hasIsolation: false, hasBranch: true, tabIndex: -1 },
|
||||
});
|
||||
});
|
||||
|
||||
test('contributes one repository toolbar action per renderable property', () => {
|
||||
const services = setupServices(store);
|
||||
services.instantiationService.stub(IActionViewItemService, new class extends mock<IActionViewItemService>() {
|
||||
override readonly onDidChange = Event.None;
|
||||
override register() { return toDisposable(() => { }); }
|
||||
}());
|
||||
services.instantiationService.stub(IWorkbenchLayoutService, new class extends mock<IWorkbenchLayoutService>() {
|
||||
override readonly mainContainer = document.createElement('div');
|
||||
}());
|
||||
services.instantiationService.stub(ISessionsService, new class extends mock<ISessionsService>() {
|
||||
override readonly visibleSessions = constObservable([services.activeSession]);
|
||||
}());
|
||||
store.add(services.instantiationService.createInstance(AgentHostSessionConfigPickerContribution));
|
||||
|
||||
const entries = MenuRegistry.getMenuItems(Menus.NewSessionRepositoryConfig)
|
||||
.filter(isIMenuItem)
|
||||
.filter(item => item.command.id.startsWith('sessions.agentHost.sessionConfigPicker.'))
|
||||
.map(item => ({
|
||||
id: item.command.id,
|
||||
title: typeof item.command.title === 'string' ? item.command.title : item.command.title.value,
|
||||
order: item.order,
|
||||
}));
|
||||
|
||||
assert.deepStrictEqual(entries, [{
|
||||
id: `sessions.agentHost.sessionConfigPicker.${SessionConfigKey.Isolation}`,
|
||||
title: 'Isolation',
|
||||
order: 1,
|
||||
}, {
|
||||
id: `sessions.agentHost.sessionConfigPicker.${SessionConfigKey.Branch}`,
|
||||
title: 'Base Branch',
|
||||
order: 2,
|
||||
}]);
|
||||
});
|
||||
|
||||
test('restores pointer and keyboard focus without leaving pointer focus visible', async () => {
|
||||
const services = setupServices(store);
|
||||
const { container } = renderPicker(store, services);
|
||||
@@ -504,9 +571,46 @@ suite('Agent Host Session Config Picker', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('repository picker action view items use the toolbar roving tab stop', () => {
|
||||
const createItem = () => {
|
||||
const item = store.add(new PickerActionViewItem({
|
||||
render: container => {
|
||||
const trigger = document.createElement('button');
|
||||
trigger.tabIndex = 0;
|
||||
container.appendChild(trigger);
|
||||
return trigger;
|
||||
},
|
||||
dispose: () => { },
|
||||
}, undefined, true));
|
||||
const container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
store.add(toDisposable(() => container.remove()));
|
||||
item.render(container);
|
||||
return { item, trigger: container.querySelector<HTMLElement>('button')! };
|
||||
};
|
||||
const first = createItem();
|
||||
const second = createItem();
|
||||
|
||||
first.item.setFocusable(true);
|
||||
second.item.setFocusable(false);
|
||||
const initialTabIndexes = [first.trigger.tabIndex, second.trigger.tabIndex];
|
||||
first.item.blur();
|
||||
second.item.focus();
|
||||
|
||||
assert.deepStrictEqual({
|
||||
initialTabIndexes,
|
||||
focusedTabIndexes: [first.trigger.tabIndex, second.trigger.tabIndex],
|
||||
secondFocused: document.activeElement === second.trigger,
|
||||
}, {
|
||||
initialTabIndexes: [0, -1],
|
||||
focusedTabIndexes: [-1, 0],
|
||||
secondFocused: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('generic auto-approve chips retain their contextual accessible name', () => {
|
||||
const services = setupServices(store);
|
||||
const picker = store.add(services.instantiationService.createInstance(AlwaysRenderConfigPicker, services.sessionObs));
|
||||
const picker = store.add(services.instantiationService.createInstance(AlwaysRenderConfigPicker, services.sessionObs, undefined));
|
||||
const trigger = document.createElement('span');
|
||||
picker.renderTriggerForTest(trigger, SessionConfigKey.AutoApprove, {
|
||||
title: 'Approval Mode',
|
||||
@@ -768,7 +872,7 @@ suite('Agent Host Session Config Picker', () => {
|
||||
}
|
||||
});
|
||||
services.provider.config = makeDynamicBranchConfig('main', 'folder');
|
||||
const picker = store.add(services.instantiationService.createInstance(AlwaysRenderConfigPicker, services.sessionObs));
|
||||
const picker = store.add(services.instantiationService.createInstance(AlwaysRenderConfigPicker, services.sessionObs, undefined));
|
||||
|
||||
await picker.setSessionConfigValueForTest(services.provider, SessionConfigKey.Branch, 'dev');
|
||||
outcomes.push({
|
||||
@@ -832,7 +936,7 @@ suite('Agent Host Session Config Picker', () => {
|
||||
}
|
||||
});
|
||||
services.provider.config = makeDynamicBranchConfig('main', 'folder');
|
||||
const picker = store.add(services.instantiationService.createInstance(AlwaysRenderConfigPicker, services.sessionObs));
|
||||
const picker = store.add(services.instantiationService.createInstance(AlwaysRenderConfigPicker, services.sessionObs, undefined));
|
||||
const container = document.createElement('div');
|
||||
picker.render(container);
|
||||
|
||||
@@ -937,12 +1041,13 @@ suite('Agent Host Session Config Picker', () => {
|
||||
test('serializes interleaved branch and isolation selections before deciding checkout', async () => {
|
||||
const services = setupServices(store);
|
||||
services.provider.config = makeDynamicBranchConfig('main', 'worktree');
|
||||
const picker = store.add(services.instantiationService.createInstance(AlwaysRenderConfigPicker, services.sessionObs));
|
||||
const branchPicker = store.add(services.instantiationService.createInstance(AlwaysRenderConfigPicker, services.sessionObs, SessionConfigKey.Branch));
|
||||
const isolationPicker = store.add(services.instantiationService.createInstance(AlwaysRenderConfigPicker, services.sessionObs, SessionConfigKey.Isolation));
|
||||
|
||||
await Promise.all([
|
||||
picker.setSessionConfigValueForTest(services.provider, SessionConfigKey.Branch, 'featureA'),
|
||||
picker.setSessionConfigValueForTest(services.provider, SessionConfigKey.Isolation, 'folder'),
|
||||
picker.setSessionConfigValueForTest(services.provider, SessionConfigKey.Branch, 'featureB'),
|
||||
branchPicker.setSessionConfigValueForTest(services.provider, SessionConfigKey.Branch, 'featureA'),
|
||||
isolationPicker.setSessionConfigValueForTest(services.provider, SessionConfigKey.Isolation, 'folder'),
|
||||
branchPicker.setSessionConfigValueForTest(services.provider, SessionConfigKey.Branch, 'featureB'),
|
||||
]);
|
||||
|
||||
assert.deepStrictEqual({
|
||||
@@ -1243,7 +1348,7 @@ suite('Agent Host Session Config Picker', () => {
|
||||
test('does not render configuration controls when the workspace has no Git repository', () => {
|
||||
const services = setupServices(store);
|
||||
services.provider.config = makeNoGitConfig();
|
||||
const picker = store.add(services.instantiationService.createInstance(AlwaysRenderConfigPicker, services.sessionObs));
|
||||
const picker = store.add(services.instantiationService.createInstance(AlwaysRenderConfigPicker, services.sessionObs, undefined));
|
||||
const container = document.createElement('div');
|
||||
picker.render(container);
|
||||
|
||||
@@ -1273,7 +1378,7 @@ suite('Agent Host Session Config Picker', () => {
|
||||
},
|
||||
values: { [SessionConfigKey.Isolation]: 'worktree', [SessionConfigKey.WorktreeBranchTrack]: false, [SessionConfigKey.WorktreeCreateNewBranch]: true },
|
||||
} as ResolveSessionConfigResult;
|
||||
const picker = store.add(services.instantiationService.createInstance(AlwaysRenderConfigPicker, services.sessionObs));
|
||||
const picker = store.add(services.instantiationService.createInstance(AlwaysRenderConfigPicker, services.sessionObs, undefined));
|
||||
const container = document.createElement('div');
|
||||
picker.render(container);
|
||||
|
||||
@@ -1293,7 +1398,7 @@ suite('Agent Host Session Config Picker', () => {
|
||||
},
|
||||
values: { [SessionConfigKey.SandboxEnabled]: 'off' },
|
||||
};
|
||||
const picker = store.add(services.instantiationService.createInstance(AlwaysRenderConfigPicker, services.sessionObs));
|
||||
const picker = store.add(services.instantiationService.createInstance(AlwaysRenderConfigPicker, services.sessionObs, undefined));
|
||||
const container = document.createElement('div');
|
||||
picker.render(container);
|
||||
|
||||
|
||||
@@ -352,22 +352,22 @@
|
||||

|
||||
|
||||
#### sessions/chat/newWidget/newChatWidget/NewSessionAutoModel/Dark
|
||||

|
||||

|
||||
|
||||
#### sessions/chat/newWidget/newChatWidget/NewSessionAutoModel/Light
|
||||

|
||||

|
||||
|
||||
#### sessions/chat/newWidget/newChatWidget/NewSessionChatBackground/Dark
|
||||

|
||||

|
||||
|
||||
#### sessions/chat/newWidget/newChatWidget/NewSessionChatBackground/Light
|
||||

|
||||

|
||||
|
||||
#### sessions/chat/newWidget/newChatWidget/NewSessionCompactAutoModel/Dark
|
||||

|
||||

|
||||
|
||||
#### sessions/chat/newWidget/newChatWidget/NewSessionCompactAutoModel/Light
|
||||

|
||||

|
||||
|
||||
#### sessions/chat/newWidget/newChatWidget/NewSessionGitHubContextPicker/Dark
|
||||

|
||||
|
||||
Reference in New Issue
Block a user