mirror of
https://github.com/microsoft/vscode.git
synced 2026-08-16 19:24:27 +01:00
Merge pull request #328211 from microsoft/benibenj/agents/session-view-context-menu-toggle
Add context menu to toggle Aquarium and Pet visibility in session view
This commit is contained in:
@@ -330,6 +330,15 @@ replacement widget restores it when the user returns to the new-session view.
|
||||
Starting a send clears the stored draft before request dispatch and any view
|
||||
replacement.
|
||||
|
||||
The new-session view mounts the aquarium action outside
|
||||
`.new-chat-widget-content`. Its surrounding surface has checked **Aquarium** and
|
||||
**Pet** context-menu items. `AquariumService` owns the application-scoped action
|
||||
visibility preference; `IChatPetService` owns the same persisted pet state used
|
||||
by `/vscode-pet`. Context-menu events from inside `.new-chat-widget-content` are
|
||||
left untouched so the composer retains its own context-menu behavior. The
|
||||
aquarium preference is also keyboard-accessible through the **Developer: Toggle
|
||||
Aquarium Action Visibility** command.
|
||||
|
||||
Agent feedback created while the active session is undefined or uncreated uses
|
||||
one shared new-session feedback scope, so it follows every undefined/uncreated
|
||||
new-session view. The comments belong to the draft's workspace: a draft that has
|
||||
|
||||
@@ -85,3 +85,23 @@ class SimulateFishFeedingStreakAction extends Action2 {
|
||||
}
|
||||
|
||||
registerAction2(SimulateFishFeedingStreakAction);
|
||||
|
||||
class ToggleAquariumActionVisibilityAction extends Action2 {
|
||||
|
||||
static readonly ID = 'sessions.aquarium.toggleActionVisibility';
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: ToggleAquariumActionVisibilityAction.ID,
|
||||
title: localize2('aquarium.toggleActionVisibility', "Toggle Aquarium Action Visibility"),
|
||||
f1: true,
|
||||
category: Categories.Developer,
|
||||
});
|
||||
}
|
||||
|
||||
override run(accessor: ServicesAccessor): void {
|
||||
accessor.get(IAquariumService).toggleActionVisibility();
|
||||
}
|
||||
}
|
||||
|
||||
registerAction2(ToggleAquariumActionVisibilityAction);
|
||||
|
||||
@@ -8,6 +8,7 @@ import { createInstantHoverDelegate } from '../../../../base/browser/ui/hover/ho
|
||||
import { RunOnceScheduler } from '../../../../base/common/async.js';
|
||||
import { Codicon } from '../../../../base/common/codicons.js';
|
||||
import { Disposable, DisposableStore, IDisposable, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
|
||||
import { IObservable, observableValue } from '../../../../base/common/observable.js';
|
||||
import { ThemeIcon } from '../../../../base/common/themables.js';
|
||||
import { localize } from '../../../../nls.js';
|
||||
import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js';
|
||||
@@ -55,6 +56,7 @@ const DART_RATE_PER_SECOND = 0.04;
|
||||
const DART_IMPULSE = 150;
|
||||
|
||||
const ENABLED_STORAGE_KEY = 'sessions.developerJoy.enabled';
|
||||
const ACTION_VISIBLE_STORAGE_KEY = 'sessions.aquarium.action.visible';
|
||||
|
||||
const FISH_HUNGER_ICONS: Record<FishHungerState, ThemeIcon> = {
|
||||
happy: Codicon.fish1Happy,
|
||||
@@ -80,6 +82,8 @@ export const IAquariumService = createDecorator<IAquariumService>('aquariumServi
|
||||
|
||||
export interface IAquariumService {
|
||||
readonly _serviceBrand: undefined;
|
||||
/** Whether the aquarium action is visible on its mounted hosts. */
|
||||
readonly actionVisible: IObservable<boolean>;
|
||||
|
||||
/**
|
||||
* Mount a toggle button into `parent`. Returns a handle that exposes a
|
||||
@@ -90,6 +94,9 @@ export interface IAquariumService {
|
||||
*/
|
||||
mountToggle(parent: HTMLElement): IMountedToggleHandle;
|
||||
|
||||
/** Toggles and persists the aquarium action visibility. */
|
||||
toggleActionVisibility(): boolean;
|
||||
|
||||
/**
|
||||
* Development/demo hook: force the persisted feeding streak into a specific
|
||||
* state and refresh the toggle tooltip(s) live. When `alive` is false the
|
||||
@@ -127,6 +134,8 @@ export class AquariumService extends Disposable implements IAquariumService {
|
||||
private readonly activeContextKey: IContextKey<boolean>;
|
||||
private readonly streak: FishFeedingStreak;
|
||||
private readonly hungerRefreshScheduler: RunOnceScheduler;
|
||||
private readonly _actionVisible = observableValue(this, true);
|
||||
readonly actionVisible: IObservable<boolean> = this._actionVisible;
|
||||
|
||||
constructor(
|
||||
@IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService,
|
||||
@@ -142,10 +151,14 @@ export class AquariumService extends Disposable implements IAquariumService {
|
||||
this.mainContainer = layoutService.mainContainer;
|
||||
this.activeContextKey = SessionsAquariumActiveContext.bindTo(contextKeyService);
|
||||
this.streak = new FishFeedingStreak(storageService);
|
||||
this._actionVisible.set(this.storageService.getBoolean(ACTION_VISIBLE_STORAGE_KEY, StorageScope.APPLICATION, true), undefined);
|
||||
this.hungerRefreshScheduler = this._register(new RunOnceScheduler(() => {
|
||||
this.updateAllToggleButtonsVisual(!!this.activeRef.value);
|
||||
}, 0));
|
||||
|
||||
this._register(this.storageService.onDidChangeValue(StorageScope.APPLICATION, ACTION_VISIBLE_STORAGE_KEY, this._store)(() => {
|
||||
this.setActionVisible(this.storageService.getBoolean(ACTION_VISIBLE_STORAGE_KEY, StorageScope.APPLICATION, true));
|
||||
}));
|
||||
this._register(this.configurationService.onDidChangeConfiguration(e => {
|
||||
if (e.affectsConfiguration(SESSIONS_DEVELOPER_JOY_ENABLED_SETTING)) {
|
||||
this.applyFeatureEnabledState();
|
||||
@@ -202,11 +215,28 @@ export class AquariumService extends Disposable implements IAquariumService {
|
||||
};
|
||||
}
|
||||
|
||||
toggleActionVisibility(): boolean {
|
||||
const visible = !this._actionVisible.get();
|
||||
this.setActionVisible(visible);
|
||||
this.storageService.store(ACTION_VISIBLE_STORAGE_KEY, visible, StorageScope.APPLICATION, StorageTarget.USER);
|
||||
this.accessibilityService.status(visible
|
||||
? localize('aquarium.action.shown', "Aquarium action shown")
|
||||
: localize('aquarium.action.hidden', "Aquarium action hidden"));
|
||||
return visible;
|
||||
}
|
||||
|
||||
simulateStreak(count: number, alive: boolean): void {
|
||||
this.streak.simulate(count, alive);
|
||||
this.updateAllToggleButtonsVisual(!!this.activeRef.value);
|
||||
}
|
||||
|
||||
private setActionVisible(visible: boolean): void {
|
||||
this._actionVisible.set(visible, undefined);
|
||||
for (const mount of this.mounts) {
|
||||
this.applyFeatureEnabledStateForButton(mount.button);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate when at least one mount is host-visible and the user has it on;
|
||||
* otherwise deactivate synchronously (no fade) so the aquarium can't flash
|
||||
@@ -261,7 +291,7 @@ export class AquariumService extends Disposable implements IAquariumService {
|
||||
}
|
||||
|
||||
private applyFeatureEnabledStateForButton(button: HTMLButtonElement): void {
|
||||
button.style.display = this.isFeatureEnabled() ? '' : 'none';
|
||||
button.style.display = this.isFeatureEnabled() && this._actionVisible.get() ? '' : 'none';
|
||||
}
|
||||
|
||||
private updateToggleButtonVisual(button: HTMLButtonElement, active: boolean): void {
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import assert from 'assert';
|
||||
import { IManagedHover } from '../../../../../base/browser/ui/hover/hover.js';
|
||||
import { toDisposable } from '../../../../../base/common/lifecycle.js';
|
||||
import { mock } from '../../../../../base/test/common/mock.js';
|
||||
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
|
||||
import { TestAccessibilityService } from '../../../../../platform/accessibility/test/common/testAccessibilityService.js';
|
||||
import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js';
|
||||
import { IHoverService } from '../../../../../platform/hover/browser/hover.js';
|
||||
import { MockContextKeyService } from '../../../../../platform/keybinding/test/common/mockKeybindingService.js';
|
||||
import { InMemoryStorageService, StorageScope } from '../../../../../platform/storage/common/storage.js';
|
||||
import { NullTelemetryServiceShape } from '../../../../../platform/telemetry/common/telemetryUtils.js';
|
||||
import { IWorkbenchLayoutService } from '../../../../../workbench/services/layout/browser/layoutService.js';
|
||||
import { AquariumService, SESSIONS_DEVELOPER_JOY_ENABLED_SETTING } from '../../browser/aquariumOverlay.js';
|
||||
|
||||
suite('AquariumService', () => {
|
||||
const store = ensureNoDisposablesAreLeakedInTestSuite();
|
||||
|
||||
test('persists and applies aquarium action visibility to mounted buttons', () => {
|
||||
const mainContainer = document.createElement('div');
|
||||
const toggleContainer = document.createElement('div');
|
||||
document.body.append(mainContainer, toggleContainer);
|
||||
store.add(toDisposable(() => {
|
||||
mainContainer.remove();
|
||||
toggleContainer.remove();
|
||||
}));
|
||||
|
||||
const storageService = store.add(new InMemoryStorageService());
|
||||
const layoutService = new class extends mock<IWorkbenchLayoutService>() {
|
||||
override readonly mainContainer = mainContainer;
|
||||
}();
|
||||
const hoverService = new class extends mock<IHoverService>() {
|
||||
override setupManagedHover(): IManagedHover {
|
||||
return {
|
||||
dispose() { },
|
||||
show() { },
|
||||
hide() { },
|
||||
update() { },
|
||||
};
|
||||
}
|
||||
}();
|
||||
const configurationService = new TestConfigurationService({ [SESSIONS_DEVELOPER_JOY_ENABLED_SETTING]: true });
|
||||
store.add(configurationService.onDidChangeConfigurationEmitter);
|
||||
const service = store.add(new AquariumService(
|
||||
layoutService,
|
||||
new MockContextKeyService(),
|
||||
hoverService,
|
||||
storageService,
|
||||
configurationService,
|
||||
new TestAccessibilityService(),
|
||||
new NullTelemetryServiceShape(),
|
||||
));
|
||||
store.add(service.mountToggle(toggleContainer));
|
||||
const button = toggleContainer.querySelector<HTMLButtonElement>('.agents-aquarium-toggle');
|
||||
|
||||
const initial = {
|
||||
visible: service.actionVisible.get(),
|
||||
display: button?.style.display,
|
||||
};
|
||||
const hidden = service.toggleActionVisibility();
|
||||
const afterHide = {
|
||||
visible: service.actionVisible.get(),
|
||||
display: button?.style.display,
|
||||
stored: storageService.getBoolean('sessions.aquarium.action.visible', StorageScope.APPLICATION),
|
||||
};
|
||||
const shown = service.toggleActionVisibility();
|
||||
const afterShow = {
|
||||
visible: service.actionVisible.get(),
|
||||
display: button?.style.display,
|
||||
};
|
||||
|
||||
assert.deepStrictEqual({
|
||||
initial,
|
||||
hidden,
|
||||
afterHide,
|
||||
shown,
|
||||
afterShow,
|
||||
}, {
|
||||
initial: { visible: true, display: '' },
|
||||
hidden: false,
|
||||
afterHide: { visible: false, display: 'none', stored: false },
|
||||
shown: true,
|
||||
afterShow: { visible: true, display: '' },
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -5,6 +5,8 @@
|
||||
|
||||
import './media/chatWidget.css';
|
||||
import * as dom from '../../../../base/browser/dom.js';
|
||||
import { StandardMouseEvent } from '../../../../base/browser/mouseEvent.js';
|
||||
import { Action } from '../../../../base/common/actions.js';
|
||||
import { Event } from '../../../../base/common/event.js';
|
||||
import { Disposable, DisposableMap, DisposableStore, IDisposable, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
|
||||
import { constObservable, derived, derivedObservableWithCache, autorun, IObservable, observableSignalFromEvent } from '../../../../base/common/observable.js';
|
||||
@@ -13,6 +15,7 @@ import { URI } from '../../../../base/common/uri.js';
|
||||
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
|
||||
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
|
||||
import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';
|
||||
import { ILogService } from '../../../../platform/log/common/log.js';
|
||||
import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js';
|
||||
import { localize } from '../../../../nls.js';
|
||||
@@ -35,6 +38,7 @@ import { SessionInputBannerWidget } from '../../sessionInputBanners/browser/sess
|
||||
import { Codicon } from '../../../../base/common/codicons.js';
|
||||
import { ChatTipContentPart } from '../../../../workbench/contrib/chat/browser/widget/chatContentParts/chatTipContentPart.js';
|
||||
import { ChatContentMarkdownRenderer } from '../../../../workbench/contrib/chat/browser/widget/chatContentMarkdownRenderer.js';
|
||||
import { IChatPetService } from '../../../../workbench/contrib/chat/browser/chatPetService.js';
|
||||
import { IChatTipService } from '../../../../workbench/contrib/chat/browser/chatTipService.js';
|
||||
import { ChatContextKeys } from '../../../../workbench/contrib/chat/common/actions/chatContextKeys.js';
|
||||
import { ChatModeKind } from '../../../../workbench/contrib/chat/common/constants.js';
|
||||
@@ -99,6 +103,7 @@ export class NewChatWidget extends Disposable {
|
||||
private readonly options: IChatViewOptions,
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
@IContextKeyService private readonly contextKeyService: IContextKeyService,
|
||||
@IContextMenuService private readonly contextMenuService: IContextMenuService,
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService,
|
||||
@ILogService private readonly logService: ILogService,
|
||||
@ISessionsManagementService private readonly sessionsManagementService: ISessionsManagementService,
|
||||
@@ -107,6 +112,7 @@ export class NewChatWidget extends Disposable {
|
||||
@IAgentHostFilterService private readonly agentHostFilterService: IAgentHostFilterService,
|
||||
@IUriIdentityService private readonly uriIdentityService: IUriIdentityService,
|
||||
@IAgentFeedbackService private readonly agentFeedbackService: IAgentFeedbackService,
|
||||
@IChatPetService private readonly chatPetService: IChatPetService,
|
||||
@IChatTipService private readonly chatTipService: IChatTipService,
|
||||
@IOpenerService private readonly openerService: IOpenerService,
|
||||
) {
|
||||
@@ -272,6 +278,37 @@ export class NewChatWidget extends Disposable {
|
||||
const chatWidgetContent = dom.append(chatWidgetContainer, dom.$('.new-chat-widget-content'));
|
||||
|
||||
this._aquariumToggle = this._register(this.aquariumService.mountToggle(element));
|
||||
const aquariumAction = this._register(new Action(
|
||||
'sessions.aquarium.showAction',
|
||||
localize('aquariumAction', "Aquarium"),
|
||||
undefined,
|
||||
true,
|
||||
() => this.aquariumService.toggleActionVisibility()
|
||||
));
|
||||
const petAction = this._register(new Action(
|
||||
'sessions.chatPet.toggle',
|
||||
localize('petAction', "Pet"),
|
||||
undefined,
|
||||
true,
|
||||
() => this.chatPetService.toggle()
|
||||
));
|
||||
this._register(dom.addDisposableListener(element, dom.EventType.CONTEXT_MENU, (e: MouseEvent) => {
|
||||
const target = e.target as Node | null;
|
||||
if (target && chatWidgetContent.contains(target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
aquariumAction.checked = this.aquariumService.actionVisible.get();
|
||||
petAction.checked = this.chatPetService.enabled.get();
|
||||
const anchor = new StandardMouseEvent(dom.getWindow(element), e);
|
||||
this.contextMenuService.showContextMenu({
|
||||
getAnchor: () => anchor,
|
||||
getActions: () => [aquariumAction, petAction],
|
||||
getCheckedActionsRepresentation: () => 'checkbox',
|
||||
});
|
||||
}));
|
||||
|
||||
const workspacePickerContainer = dom.append(chatWidgetContent, dom.$('.new-session-workspace-picker-container'));
|
||||
// On web (vscode.dev / insiders.vscode.dev) the workspace picker is
|
||||
|
||||
@@ -34,7 +34,8 @@ export class SessionsChatAccessibilityHelp implements IAccessibleViewImplementat
|
||||
content.push(localize('sessionsChat.quickChat', "To start a workspace-less quick chat, use the New Quick Chat command{0} or the plus button on the Chats section in the sessions list. A quick chat has no workspace, so the workspace picker does not apply and the Toggle Side Panel command is disabled.", '<keybinding:sessionsView.newQuickChat>'));
|
||||
content.push(localize('sessionsChat.mobileConfig', "On mobile, the mode and model pickers appear as tappable chips below the input. Tap a chip to open a bottom sheet where you can change the selection."));
|
||||
content.push(localize('sessionsChat.history', "Use up and down arrows to navigate your request history in the input box."));
|
||||
content.push(localize('sessionsChat.vscodePet', "Type /vscode-pet to show or hide the VS Code pet above the input. Drag it horizontally to reposition it, or use Tab to focus it and the left and right arrow keys to move it. Press Enter or Space to show it some love."));
|
||||
content.push(localize('sessionsChat.vscodePet', "Use the checked Pet item in the new-session view context menu, or type /vscode-pet, to show or hide the VS Code pet above the input. Drag it horizontally to reposition it, or use Tab to focus it and the left and right arrow keys to move it. Press Enter or Space to show it some love."));
|
||||
content.push(localize('sessionsChat.aquariumAction', "To show or hide the aquarium action on the new-session view, use the checked Aquarium item in the context menu outside the composer, or run the Toggle Aquarium Action Visibility command."));
|
||||
content.push(localize('sessionsChat.dictation', "When dictation is configured, dictate your message into the input{0}. Tap to start and stop, or hold to dictate only while pressed. If the speech-to-text model is still preparing, activate the dictation control again to cancel.", '<keybinding:sessions.action.chat.toggleDictation>'));
|
||||
content.push(localize('sessionsChat.voiceMode', "Start or stop Voice Mode to interact with the agent using your microphone{0}.", '<keybinding:agentsVoice.startVoiceInChat>'));
|
||||
content.push(localize('sessionsChat.micContextMenu', "To choose a microphone or turn off dictation or Voice Mode, focus the microphone button in the input toolbar and open its context menu (for example Shift+F10)."));
|
||||
|
||||
Reference in New Issue
Block a user