Conversation
There was a problem hiding this comment.
Pull request overview
Adds a first-time-use (FTU) tutorial callout to the Sessions workspace picker and introduces a developer/testing command to reset Sessions FTU-related state so the experience can be re-triggered.
Changes:
- New
WorkspacePickerCalloutUI (dismiss/snooze + FAQ) rendered next to the Sessions workspace picker dropdown. - Workspace picker now emits explicit “show/hide” events so companion UI can react to dropdown visibility.
- Adds a developer command to clear a set of FTU-related storage keys and reload the window for repeatable testing.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentSessionsExperiments.contribution.ts | Adds a developer/testing action to reset Agent Sessions FTU-related storage and reload. |
| src/vs/sessions/contrib/chat/browser/workspacePickerCallout.ts | Introduces the FTU callout widget with dismiss/snooze + FAQ content backed by profile storage. |
| src/vs/sessions/contrib/chat/browser/workspacePicker.ts | Emits onDidShowPicker / onDidHidePicker around the action widget dropdown lifecycle. |
| src/vs/sessions/contrib/chat/browser/newChatViewPane.ts | Renders and wires the callout to workspace picker show/hide events in the welcome UI. |
| src/vs/sessions/contrib/chat/browser/media/workspacePickerCallout.css | Adds styling/animations for the callout. |
| src/vs/sessions/contrib/chat/browser/media/chatWelcomePart.css | Adjusts overflow/positioning to allow the callout to render outside the picker row. |
| src/vs/platform/actionWidget/browser/actionWidget.ts | Attempts to enable interaction with the callout while the action widget is open (requires changes). |
| if (this.storageService.getBoolean(STORAGE_KEY_DISMISSED, StorageScope.PROFILE, false)) { | ||
| return false; | ||
| } | ||
| if (this.storageService.getBoolean(STORAGE_KEY_SNOOZED, StorageScope.PROFILE, false)) { | ||
| const snoozeUntil = this.storageService.getNumber(STORAGE_KEY_SNOOZE_UNTIL, StorageScope.PROFILE, 0); | ||
| if (Date.now() < snoozeUntil) { | ||
| return false; | ||
| } | ||
| // Snooze expired — clear the flag | ||
| this.storageService.remove(STORAGE_KEY_SNOOZED, StorageScope.PROFILE); | ||
| this.storageService.remove(STORAGE_KEY_SNOOZE_UNTIL, StorageScope.PROFILE); | ||
| } |
There was a problem hiding this comment.
This new callout introduces non-trivial state behavior (dismiss vs snooze with expiry, and automatic clearing of expired snooze flags) that is easy to regress. There are existing browser unit tests for related sessions chat pieces (e.g. workspacePicker), so it would be good to add coverage for shouldShow, dismiss, and snooze expiry behavior.
| if (this.storageService.getBoolean(STORAGE_KEY_DISMISSED, StorageScope.PROFILE, false)) { | |
| return false; | |
| } | |
| if (this.storageService.getBoolean(STORAGE_KEY_SNOOZED, StorageScope.PROFILE, false)) { | |
| const snoozeUntil = this.storageService.getNumber(STORAGE_KEY_SNOOZE_UNTIL, StorageScope.PROFILE, 0); | |
| if (Date.now() < snoozeUntil) { | |
| return false; | |
| } | |
| // Snooze expired — clear the flag | |
| this.storageService.remove(STORAGE_KEY_SNOOZED, StorageScope.PROFILE); | |
| this.storageService.remove(STORAGE_KEY_SNOOZE_UNTIL, StorageScope.PROFILE); | |
| } | |
| return this.shouldShowAtTime(Date.now()); | |
| } | |
| /** | |
| * Core implementation of {@link shouldShow}, parameterized by the current | |
| * time (in milliseconds since epoch) to make the snooze expiry behavior | |
| * easier to reason about and test. | |
| */ | |
| private shouldShowAtTime(now: number): boolean { | |
| if (this.storageService.getBoolean(STORAGE_KEY_DISMISSED, StorageScope.PROFILE, false)) { | |
| return false; | |
| } | |
| if (this.storageService.getBoolean(STORAGE_KEY_SNOOZED, StorageScope.PROFILE, false)) { | |
| const snoozeUntil = this.storageService.getNumber(STORAGE_KEY_SNOOZE_UNTIL, StorageScope.PROFILE, 0); | |
| if (now < snoozeUntil) { | |
| return false; | |
| } | |
| // Snooze expired — clear the flag so subsequent calls don't need to | |
| // perform time comparisons against an outdated value. | |
| this.storageService.remove(STORAGE_KEY_SNOOZED, StorageScope.PROFILE); | |
| this.storageService.remove(STORAGE_KEY_SNOOZE_UNTIL, StorageScope.PROFILE); | |
| } |
| import { IStorageService, StorageScope } from '../../../../../../platform/storage/common/storage.js'; | ||
| import { IHostService } from '../../../../../services/host/browser/host.js'; | ||
| import { Categories } from '../../../../../../platform/action/common/actionCommonCategories.js'; | ||
| import { ServicesAccessor } from '../../../../../../platform/instantiation/common/instantiation.js'; | ||
| import { IEditorGroupsService } from '../../../../../services/editor/common/editorGroupsService.js'; | ||
|
|
There was a problem hiding this comment.
New imports are introduced mid-file (after statements). In this codebase, module imports are expected to be grouped at the top for readability and to avoid order-dependent side effects. Please move these imports to the top import section (and keep sorting consistent with the rest of the file).
| // APPLICATION-scoped keys | ||
| storageService.remove('workbench.agentsession.welcomeComplete', StorageScope.APPLICATION); | ||
| storageService.remove('agentSessionsWelcome.privacyNoticeDismissed', StorageScope.APPLICATION); | ||
| storageService.remove('chat.welcomeViewPrefill', StorageScope.APPLICATION); | ||
| storageService.remove('agentSessions.pinnedTaskLabels', StorageScope.APPLICATION); | ||
|
|
There was a problem hiding this comment.
This reset command hard-codes a long list of storage keys across multiple components. That’s brittle (keys/scopes can change independently) and easy to miss when new FTU-related keys are added. Consider centralizing these keys behind a single reset API (e.g. owned by the sessions FTU components) or reusing exported constants so the reset stays correct when storage keys evolve.
| if (user === 'workspacePicker') { | ||
| // Allow interactions with the sessions workspace tutorial callout, | ||
| // which is rendered alongside the action widget while this picker is open. | ||
| block.style.pointerEvents = 'none'; | ||
| } else { | ||
| renderDisposables.add(dom.addDisposableListener(block, dom.EventType.MOUSE_DOWN, e => e.stopPropagation())); | ||
| } |
There was a problem hiding this comment.
Setting pointerEvents = 'none' on the full-screen .context-view-block means clicks outside the action widget will reach (and potentially trigger) the underlying UI before/while the context view hides (ContextView hides on captured click but does not cancel the event). This can cause accidental interactions while the workspace picker is open. Please keep outside clicks blocked and instead make the tutorial callout clickable by rendering it within the context view layer (or otherwise ensuring it sits above the block) rather than disabling pointer events on the block.
| if (user === 'workspacePicker') { | |
| // Allow interactions with the sessions workspace tutorial callout, | |
| // which is rendered alongside the action widget while this picker is open. | |
| block.style.pointerEvents = 'none'; | |
| } else { | |
| renderDisposables.add(dom.addDisposableListener(block, dom.EventType.MOUSE_DOWN, e => e.stopPropagation())); | |
| } | |
| renderDisposables.add(dom.addDisposableListener(block, dom.EventType.MOUSE_DOWN, e => e.stopPropagation())); |
| if (user === 'workspacePicker') { | ||
| // Allow interactions with the sessions workspace tutorial callout, | ||
| // which is rendered alongside the action widget while this picker is open. | ||
| block.style.pointerEvents = 'none'; | ||
| } else { | ||
| renderDisposables.add(dom.addDisposableListener(block, dom.EventType.MOUSE_DOWN, e => e.stopPropagation())); |
There was a problem hiding this comment.
Special-casing behavior based on the magic user === 'workspacePicker' string is fragile (easy to mistype and not discoverable to other callers). Consider using a typed options argument (e.g. { allowCompanionElementSelector: ... } / { allowOutsideInteractionWith: ... }) or a shared constant/enum so this coupling is explicit and maintainable.
|
|
||
| this._renderFaqItem(faq, | ||
| localize('faqCloneQ', "How do I clone a repo?"), | ||
| localize('faqCloneA', "Use the command palette ({0}) and run \"Git: Clone\", then paste a repository URL. The cloned folder will appear in your file explorer and can be opened here.", 'Cmd+Shift+P'), |
There was a problem hiding this comment.
The FAQ answer hard-codes the Command Palette shortcut as "Cmd+Shift+P", which is incorrect on non-macOS and can diverge from user-customized keybindings. Prefer either omitting the shortcut, using a localized generic phrase, or resolving the actual keybinding label via the keybinding service.
| localize('faqCloneA', "Use the command palette ({0}) and run \"Git: Clone\", then paste a repository URL. The cloned folder will appear in your file explorer and can be opened here.", 'Cmd+Shift+P'), | |
| localize('faqCloneA', "Use the Command Palette to run \"Git: Clone\", then paste a repository URL. The cloned folder will appear in your file explorer and can be opened here."), |
No description provided.