Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
101 changes: 80 additions & 21 deletions src/vs/sessions/contrib/aquarium/browser/aquariumOverlay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,14 +70,29 @@ export interface IAquariumService {
readonly _serviceBrand: undefined;

/**
* Mount a toggle button into `parent`. Returns a disposable that removes
* the button and tears down the active aquarium if it was the last mount.
* Mount a toggle button into `parent`. Returns a handle that exposes a
* {@link IMountedToggleHandle.setHostVisible} hook so callers can keep the
* aquarium tied to their own visibility (e.g. a view pane). Disposing the
* handle removes the button and tears down the active aquarium if it was
* the last mount.
*/
mountToggle(parent: HTMLElement): IDisposable;
mountToggle(parent: HTMLElement): IMountedToggleHandle;
}

export interface IMountedToggleHandle extends IDisposable {
/**
* Inform the service whether this mount's host is currently visible. The
* aquarium is only considered active when at least one mount is visible;
* when the last visible mount goes invisible the aquarium is disposed
* synchronously (no fade-out) so it cannot flash behind a sibling view.
* Hosts that don't care can leave this alone — mounts default to visible.
*/
setHostVisible(visible: boolean): void;
}

interface IMountedToggle {
readonly button: HTMLButtonElement;
hostVisible: boolean;
}

export class AquariumService extends Disposable implements IAquariumService {
Expand Down Expand Up @@ -111,7 +126,7 @@ export class AquariumService extends Disposable implements IAquariumService {
}));
}

mountToggle(parent: HTMLElement): IDisposable {
mountToggle(parent: HTMLElement): IMountedToggleHandle {
const doc = parent.ownerDocument;
const button = doc.createElement('button');
button.className = 'agents-aquarium-toggle';
Expand All @@ -134,25 +149,55 @@ export class AquariumService extends Disposable implements IAquariumService {

parent.appendChild(button);

const mount: IMountedToggle = { button };
const mount: IMountedToggle = { button, hostVisible: true };
this.mounts.add(mount);
this.applyFeatureEnabledStateForButton(button);
this.reconcileActivation();

// First mount with the user's stored preference on — auto-restore.
if (this.isFeatureEnabled() && this.isStoredEnabled() && !this.activeRef.value) {
return {
setHostVisible: (visible: boolean) => {
if (mount.hostVisible === visible) {
return;
}
mount.hostVisible = visible;
this.reconcileActivation();
},
dispose: () => {
store.dispose();
button.remove();
this.mounts.delete(mount);
this.reconcileActivation();
},
};
}

/**
* 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
* behind a sibling view during a view swap.
*/
private reconcileActivation(): void {
const anyHostVisible = this.hasVisibleMount();
if (anyHostVisible && this.isFeatureEnabled() && this.isStoredEnabled() && !this.activeRef.value) {
this.activate(/* persist */ false);
} else if (!anyHostVisible) {
// Host hide: dispose any active aquarium synchronously AND cancel
// any in-flight animated exit (from a prior user toggle-off) so it
// can't keep painting fish behind whatever view took our place.
this.pendingExit.clear();
if (this.activeRef.value) {
this.deactivate(/* persist */ false, /* animate */ false);
}
}
}

return toDisposable(() => {
store.dispose();
button.remove();
this.mounts.delete(mount);
// Last host gone — tear down without persisting so the user's
// preference for next time stays as it was.
if (this.mounts.size === 0 && this.activeRef.value) {
this.deactivate(/* persist */ false);
private hasVisibleMount(): boolean {
for (const m of this.mounts) {
if (m.hostVisible) {
return true;
}
});
}
return false;
}

private isFeatureEnabled(): boolean {
Expand All @@ -174,8 +219,8 @@ export class AquariumService extends Disposable implements IAquariumService {
if (!this.isFeatureEnabled() && this.activeRef.value) {
// Setting turned off — don't persist so the prior preference survives a re-enable.
this.deactivate(/* persist */ false);
} else if (this.isFeatureEnabled() && this.isStoredEnabled() && !this.activeRef.value && this.mounts.size > 0) {
this.activate(/* persist */ false);
} else if (this.isFeatureEnabled()) {
this.reconcileActivation();
}
}

Expand Down Expand Up @@ -209,7 +254,7 @@ export class AquariumService extends Disposable implements IAquariumService {
private toggle(): void {
if (this.activeRef.value) {
this.deactivate(/* persist */ true);
} else {
} else if (this.hasVisibleMount()) {
this.activate(/* persist */ true);
}
}
Expand Down Expand Up @@ -248,8 +293,22 @@ export class AquariumService extends Disposable implements IAquariumService {
}
}

/** @param persist false when tearing down for non-user reasons. */
private deactivate(persist: boolean): void {
/**
* @param persist false when tearing down for non-user reasons.
* @param animate false to dispose synchronously (no fade-out). Used for
* host-driven teardown where running a 900ms fade would let fish stay
* visible while the next view layers on top.
*/
private deactivate(persist: boolean, animate: boolean = true): void {
if (!animate) {
this.activeRef.clear();
this.activeContextKey.set(false);
this.updateAllToggleButtonsVisual(false);
if (persist) {
this.setStoredEnabled(false);
}
return;
}
// Detach from activeRef WITHOUT disposing (clearAndLeak) so the exit
// animation can run; the returned handle from active.exit() is parked
// in `pendingExit` and disposes the underlying store either when the
Expand Down
10 changes: 8 additions & 2 deletions src/vs/sessions/contrib/chat/browser/newChatViewPane.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { IHoverService } from '../../../../platform/hover/browser/hover.js';
import { localize } from '../../../../nls.js';
import { ISessionsManagementService } from '../../../services/sessions/common/sessionsManagement.js';
import { ISessionsProvidersService } from '../../../services/sessions/browser/sessionsProvidersService.js';
import { IAquariumService } from '../../aquarium/browser/aquariumOverlay.js';
import { IAquariumService, IMountedToggleHandle } from '../../aquarium/browser/aquariumOverlay.js';
import { IViewDescriptorService } from '../../../../workbench/common/views.js';
import { IWorkspaceTrustRequestService } from '../../../../platform/workspace/common/workspaceTrust.js';
import { IViewPaneOptions, ViewPane } from '../../../../workbench/browser/parts/views/viewPane.js';
Expand All @@ -36,6 +36,7 @@ class NewChatWidget extends Disposable {

private readonly _workspacePicker: WorkspacePicker;
private readonly _newChatInput: NewChatInputWidget;
private _aquariumToggle: IMountedToggleHandle | undefined;

/** Tracks an in-flight wait for a provider's session types to become available. */
private readonly _pendingSessionTypeWait = new MutableDisposable<IDisposable>();
Expand Down Expand Up @@ -101,7 +102,7 @@ class NewChatWidget extends Disposable {
const chatWidgetContainer = dom.append(element, dom.$('.new-chat-widget-container'));
const chatWidgetContent = dom.append(chatWidgetContainer, dom.$('.new-chat-widget-content'));

this._register(this.aquariumService.mountToggle(element));
this._aquariumToggle = this._register(this.aquariumService.mountToggle(element));

const workspacePickerContainer = dom.append(chatWidgetContent, dom.$('.new-session-workspace-picker-container'));
this._register(this._renderWorkspacePicker(workspacePickerContainer));
Expand Down Expand Up @@ -273,6 +274,10 @@ class NewChatWidget extends Disposable {
this._newChatInput.prefillInput(text);
}

setHostVisible(visible: boolean): void {
this._aquariumToggle?.setHostVisible(visible);
}

sendQuery(text: string): void {
this._newChatInput.sendQuery(text);
}
Expand Down Expand Up @@ -342,6 +347,7 @@ export class NewChatViewPane extends ViewPane {

override setVisible(visible: boolean): void {
super.setVisible(visible);
this._widget?.setHostVisible(visible);
if (visible) {
this._widget?.focusInput();
}
Expand Down
Loading