Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/vs/base/common/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ export namespace Event {
export function debounce<T>(event: Event<T>, merge: (last: T | undefined, event: T) => T, delay?: number | typeof MicrotaskDelay, leading?: boolean, flushOnListenerRemove?: boolean, leakWarningThreshold?: number, disposable?: DisposableStore): Event<T>;
export function debounce<I, O>(event: Event<I>, merge: (last: O | undefined, event: I) => O, delay?: number | typeof MicrotaskDelay, leading?: boolean, flushOnListenerRemove?: boolean, leakWarningThreshold?: number, disposable?: DisposableStore): Event<O>;
export function debounce<I, O>(event: Event<I>, merge: (last: O | undefined, event: I) => O, delay: number | typeof MicrotaskDelay = 100, leading = false, flushOnListenerRemove = false, leakWarningThreshold?: number, disposable?: DisposableStore): Event<O> {
let subscription: IDisposable;
let subscription: IDisposable = Disposable.None;
let output: O | undefined = undefined;
let handle: Timeout | undefined | null = undefined;
let numDebouncedCalls = 0;
Expand Down
8 changes: 8 additions & 0 deletions src/vs/base/test/common/event.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,14 @@ suite('Event utils dispose', function () {

assertDisposablesCount([leaked]); // leaked is still there
});

test('debounce-util can be disposed without listeners', function () {
const store = new DisposableStore();
const emitter = ds.add(new Emitter<number>());
Event.debounce(emitter.event, l => 0, undefined, undefined, undefined, undefined, store);

store.dispose();
});
});

suite('Event', function () {
Expand Down
3 changes: 3 additions & 0 deletions src/vs/sessions/contrib/chat/browser/media/chatInput.css
Original file line number Diff line number Diff line change
Expand Up @@ -663,6 +663,9 @@
padding: 0 0 0 2px;
line-height: 100% !important;
align-self: center;
background-size: contain;
background-position: center;
background-repeat: no-repeat;
}

.sessions-chat-attachment-pill .monaco-icon-label .monaco-icon-label-container {
Expand Down
4 changes: 4 additions & 0 deletions src/vs/sessions/contrib/chat/browser/media/chatWidget.css
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,10 @@
font-size: var(--vscode-codiconFontSize);
}

.sessions-chat-picker-slot.sessions-workspace-category-picker-slot .action-label > .sessions-chat-dropdown-chevron {
font-size: var(--vscode-codiconFontSize-compact);
}

.sessions-workspace-category-picker .sessions-chat-dropdown-label {
margin-left: 0;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { ILanguageService } from '../../../../editor/common/languages/language.j
import { getIconClasses } from '../../../../editor/common/services/getIconClasses.js';
import { basename } from '../../../../base/common/resources.js';
import { Schemas } from '../../../../base/common/network.js';
import { asCssVariable } from '../../../../platform/theme/common/colorUtils.js';
import { DEFAULT_LABELS_CONTAINER, ResourceLabels } from '../../../../workbench/browser/labels.js';

import { IChatRequestVariableEntry, isAgentHostCompletionVariableEntry, isPastedTextArtifact, OmittedState } from '../../../../workbench/contrib/chat/common/attachments/chatVariableEntries.js';
Expand Down Expand Up @@ -167,6 +168,13 @@ export class NewChatContextAttachments extends Disposable implements INewChatAtt
const icon = dom.append(content, renderIcon(Codicon.repo));
icon.setAttribute('aria-hidden', 'true');
dom.append(content, dom.$('span.sessions-chat-attachment-name', undefined, entry.name));
} else if (entry.icon) {
const icon = dom.append(content, renderIcon(entry.icon));
icon.setAttribute('aria-hidden', 'true');
if (entry.icon.color) {
icon.style.color = asCssVariable(entry.icon.color.id);
}
dom.append(content, dom.$('span.sessions-chat-attachment-name', undefined, entry.name));
} else {
const label = this._resourceLabels.create(content, { supportIcons: true });
this._renderDisposables.add(label);
Expand Down
15 changes: 13 additions & 2 deletions src/vs/sessions/contrib/chat/browser/newChatInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ import { IChatSubmitRequestHandlerService } from '../../../../workbench/contrib/
import { INewChatModelPickerService, NewChatModelPickerService } from './newChatModelPicker.js';
import { ModelPicker, ModelPickerActionViewItem } from './modelPicker.js';
import { ISessionModelSelection, SessionModelSelection } from './sessionModelSelection.js';
import { hasSendableModelSelection } from './sessionModelPickerState.js';
import { ISessionContext, SessionContext } from '../../../services/sessions/browser/sessionContext.js';
import { AGENT_SESSIONS_SCOPED_INPUT_HISTORY_SETTING } from './sessionsChatHistory.js';
import { IChatStatusItemService } from '../../../../workbench/contrib/chat/browser/chatStatus/chatStatusItemService.js';
Expand Down Expand Up @@ -510,7 +511,7 @@ export class NewChatInputWidget extends Disposable implements IHistoryNavigation
return true;
}
const modelSelection = this._modelSelection.state.read(reader);
return this.options.canSendRequest.read(reader) && modelSelection.hasSelectableModel && !modelSelection.pendingSelection;
return this.options.canSendRequest.read(reader) && hasSendableModelSelection(modelSelection);
});
this._scopedInstantiationService = this._register(this.instantiationService.createChild(new ServiceCollection(
[INewChatModelPickerService, this._newChatModelPickerService],
Expand All @@ -526,6 +527,7 @@ export class NewChatInputWidget extends Disposable implements IHistoryNavigation
}
}));
}
this._register(this.storageService.onWillSaveState(() => this.saveState()));
this._contextAttachments = this._register(this.instantiationService.createInstance(NewChatContextAttachments));
// Always use the mobile-aware picker. Its overrides bail to the
// desktop behavior when `isPhoneLayout()` is false, so picking
Expand All @@ -534,7 +536,7 @@ export class NewChatInputWidget extends Disposable implements IHistoryNavigation
// phone breakpoint after the chat input mounted.
this.sessionTypePicker = this._register(this.instantiationService.createInstance(MobileSessionTypePicker, this.options.session, this.options.sessionTypePickerOptions));
this._register(this._contextAttachments.onDidChangeContext(() => {
this._updateDraftState();
this._updateAndSaveDraftState();
this._updateSendButtonState();
this.focus();
}));
Expand Down Expand Up @@ -1375,6 +1377,14 @@ export class NewChatInputWidget extends Disposable implements IHistoryNavigation
};
}

private _updateAndSaveDraftState(): void {
if (this._sending) {
return;
}
this._updateDraftState();
this.saveState();
}

private _toHistoryEntry(draft: IDraftState): IChatModelInputState {
return {
...draft,
Expand Down Expand Up @@ -1513,6 +1523,7 @@ export class NewChatInputWidget extends Disposable implements IHistoryNavigation
this._contextAttachments.setAttachments(draft.attachments.map(IChatRequestVariableEntry.fromExport));
}
}
this._updateSendButtonState();
}

private _getDraftState(): IDraftState | undefined {
Expand Down
2 changes: 0 additions & 2 deletions src/vs/sessions/contrib/chat/browser/newChatWidget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,6 @@ export class NewChatWidget extends Disposable {
loading,
historyKey: constObservable(undefined), // no persisted history for the new-session view
placeholder: localize('newSessionPromptPlaceholder', "Pitch your idea"),
sessionTypePickerOptions: { showChevron: false },
supportsBackground: true,
deferredNotificationsEnabled,
petHostPreferred: this.options.petHostPreferred,
Expand Down Expand Up @@ -707,7 +706,6 @@ export class NewChatWidget extends Disposable {
label: localize('newSessionWorkspacePicker.githubContext', "Issue/PR"),
ariaLabel: localize('newSessionWorkspacePicker.githubContextAriaLabel', "Attach a GitHub issue or pull request to the new session"),
tooltip: localize('newSessionWorkspacePicker.githubContextTooltip', "Attach an issue or pull request as context"),
icon: Codicon.add,
hideIconWhenAttached: true,
group: SESSION_WORKSPACE_GROUP_GITHUB,
attachesContext: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ export function hasSelectableModel(
return models.length > 0 || options.showAutoModel;
}

export function hasSendableModelSelection(state: ISessionModelSelectionState): boolean {
return state.hasSelectableModel && (!state.pendingSelection || state.options.showAutoModel);
}

export const EMPTY_MODEL_SELECTION_STATE: ISessionModelSelectionState = {
currentModel: undefined,
pendingSelection: undefined,
Expand Down
23 changes: 12 additions & 11 deletions src/vs/sessions/contrib/chat/browser/sessionTypePicker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import { ISessionsProvidersService } from '../../../services/sessions/browser/se
import { autorun, IObservable, observableValue } from '../../../../base/common/observable.js';
import { ISession, SessionStatus } from '../../../services/sessions/common/session.js';
import { Emitter } from '../../../../base/common/event.js';
import { isWeb } from '../../../../base/common/platform.js';
import { isEqual } from '../../../../base/common/resources.js';
import { URI } from '../../../../base/common/uri.js';
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
Expand Down Expand Up @@ -150,8 +149,7 @@ export class SessionTypePicker extends Disposable {
protected _triggerElement: HTMLElement | undefined;

/**
* Tracks whether the harness picker trigger is currently visible. Mirrors
* the `.hidden` state computed in {@link _updateTriggerLabel}, so the
* Tracks whether the harness picker trigger is currently interactive, so the
* new-session-view onboarding tour can skip the harness step when only a
* single harness can serve the selected workspace.
*/
Expand Down Expand Up @@ -637,18 +635,19 @@ export class SessionTypePicker extends Disposable {

dom.clearNode(this._triggerElement);

// In web (vscode.dev/agents) the host filter already scopes the
// workbench to a single agent host, so when that host advertises only
// one harness there is nothing to pick — hide the trigger entirely.
const hideForSingleHarness = isWeb && this._folderSessionTypes.length <= 1 && this._pickServedByFolder(this._picked);
if (this._folderSessionTypes.length === 0 || hideForSingleHarness) {
if (this._folderSessionTypes.length === 0) {
this._triggerElement.classList.add('hidden');
this._triggerElement.parentElement?.classList.remove('disabled');
this._visibleKey.set(false);
return;
}

const disabled = this._folderSessionTypes.length === 1 && this._pickServedByFolder(this._picked);
this._triggerElement.classList.remove('hidden');
this._visibleKey.set(true);
this._triggerElement.parentElement?.classList.toggle('disabled', disabled);
this._triggerElement.tabIndex = disabled ? -1 : 0;
this._triggerElement.setAttribute('aria-disabled', String(disabled));
this._visibleKey.set(!disabled);
Comment thread
meganrogge marked this conversation as resolved.
Outdated
const currentType = this._folderSessionTypes.find(t =>
t.providerId === this._picked?.providerId && t.sessionType.id === this._picked?.sessionTypeId)?.sessionType
?? this._folderSessionTypes.find(t => t.sessionType.id === this._picked?.sessionTypeId)?.sessionType;
Expand All @@ -659,11 +658,13 @@ export class SessionTypePicker extends Disposable {
const labelSpan = dom.append(this._triggerElement, dom.$('span.sessions-chat-dropdown-label'));
labelSpan.textContent = modeLabel;

if (this._options?.showChevron !== false) {
if (!disabled && this._options?.showChevron !== false) {
const chevron = dom.append(this._triggerElement, renderIcon(Codicon.chevronDownCompact));
chevron.classList.add('sessions-chat-dropdown-chevron');
}

this._triggerElement.ariaLabel = localize('sessionTypePicker.triggerAriaLabel', "Pick Session Type, {0}", modeLabel);
this._triggerElement.ariaLabel = disabled
? localize('sessionTypePicker.disabledTriggerAriaLabel', "Session Type, {0}", modeLabel)
: localize('sessionTypePicker.triggerAriaLabel', "Pick Session Type, {0}", modeLabel);
}
}
11 changes: 9 additions & 2 deletions src/vs/sessions/contrib/chat/browser/sessionWorkspacePicker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export interface IWorkspacePickerTrigger {
readonly label?: string;
readonly ariaLabel: string;
readonly tooltip?: string;
readonly icon: ThemeIcon;
readonly icon?: ThemeIcon;
readonly hideIconWhenAttached?: boolean;
readonly reflectsWorkspace?: boolean;
readonly group?: string;
Expand Down Expand Up @@ -136,6 +136,7 @@ interface IWorkspacePickerTriggerElements {
icon?: HTMLElement;
label?: HTMLElement;
badge?: CountBadge;
chevron?: HTMLElement;
}

type IWorkspacePickerAction = IAction & { icon?: ThemeIcon; hoverContent?: string; onRemove?: () => void };
Expand Down Expand Up @@ -1444,7 +1445,7 @@ export class WorkspacePicker extends Disposable {
trigger.classList.toggle('selected', (reflectsWorkspace && workspace !== undefined) || isSelectedCategory || badgeCount > 0 || relatedGitHubInfo !== undefined);
const icon = (reflectsWorkspace ? workspace?.icon : undefined)
?? (relatedGitHubInfo ? Codicon.repo : (isSelectedCategory && workspace ? workspace.icon : options.icon));
if (options.hideIconWhenAttached === true && badgeCount > 0) {
if (!icon || (options.hideIconWhenAttached === true && badgeCount > 0)) {
contents.icon?.remove();
contents.icon = undefined;
} else {
Expand Down Expand Up @@ -1475,6 +1476,12 @@ export class WorkspacePicker extends Disposable {
contents.badge?.dispose();
contents.badge = undefined;
}
if (!contents.chevron) {
contents.chevron = renderIcon(Codicon.chevronDownCompact);
contents.chevron.classList.add('sessions-chat-dropdown-chevron');
contents.chevron.setAttribute('aria-hidden', 'true');
}
trigger.append(contents.chevron);
return;
}

Expand Down
22 changes: 21 additions & 1 deletion src/vs/sessions/contrib/chat/test/browser/modelPicker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import assert from 'assert';
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
import { ILanguageModelChatMetadataAndIdentifier } from '../../../../../workbench/contrib/chat/common/languageModels.js';
import { hasSelectableModel, normalizeModelPickerOptions } from '../../browser/sessionModelPickerState.js';
import { createModelSelectionState, hasSelectableModel, hasSendableModelSelection, normalizeModelPickerOptions } from '../../browser/sessionModelPickerState.js';

const aModel = { identifier: 'copilot-gpt-4o', metadata: {} } as ILanguageModelChatMetadataAndIdentifier;

Expand Down Expand Up @@ -42,4 +42,24 @@ suite('ModelPicker selectability', () => {
showManageModelsAction: false,
})), true);
});

test('allows an unresolved selection only when Auto is available', () => {
const pendingSelection = { reference: 'pending-model' };
const options = {
useGroupedModelPicker: true,
showFeatured: true,
showUnavailableFeatured: false,
showManageModelsAction: false,
};
const autoOptions = normalizeModelPickerOptions({ ...options, showAutoModel: true });
const explicitModelOptions = normalizeModelPickerOptions({ ...options, showAutoModel: false });

assert.deepStrictEqual({
auto: hasSendableModelSelection(createModelSelectionState([], autoOptions, undefined, pendingSelection)),
explicitModel: hasSendableModelSelection(createModelSelectionState([], explicitModelOptions, undefined, pendingSelection)),
}, {
auto: true,
explicitModel: false,
});
});
});
Loading
Loading