mirror of
https://github.com/microsoft/vscode.git
synced 2026-02-15 07:28:05 +00:00
add tip for welcome view, first request per session, improve styling (#294653)
This commit is contained in:
@@ -43,14 +43,28 @@ export interface IChatTipService {
|
||||
|
||||
/**
|
||||
* Gets a tip to show for a request, or undefined if a tip has already been shown this session.
|
||||
* Only one tip is shown per VS Code session (resets on reload).
|
||||
* Tips are only shown for requests created after the service was instantiated.
|
||||
* Only one tip is shown per conversation session (resets when switching conversations).
|
||||
* Tips are suppressed if a welcome tip was already shown in this session.
|
||||
* Tips are only shown for requests created after the current session started.
|
||||
* @param requestId The unique ID of the request (used for stable rerenders).
|
||||
* @param requestTimestamp The timestamp when the request was created.
|
||||
* @param contextKeyService The context key service to evaluate tip eligibility.
|
||||
*/
|
||||
getNextTip(requestId: string, requestTimestamp: number, contextKeyService: IContextKeyService): IChatTip | undefined;
|
||||
|
||||
/**
|
||||
* Gets a tip to show on the welcome/getting-started view.
|
||||
* Unlike {@link getNextTip}, this does not require a request and skips request-timestamp checks.
|
||||
* Returns the same tip on repeated calls for stable rerenders.
|
||||
*/
|
||||
getWelcomeTip(contextKeyService: IContextKeyService): IChatTip | undefined;
|
||||
|
||||
/**
|
||||
* Resets tip state for a new conversation.
|
||||
* Call this when the chat widget binds to a new model.
|
||||
*/
|
||||
resetSession(): void;
|
||||
|
||||
/**
|
||||
* Dismisses the current tip and allows a new one to be picked for the same request.
|
||||
* The dismissed tip will not be shown again in this workspace.
|
||||
@@ -133,7 +147,7 @@ const TIP_CATALOG: ITipDefinition[] = [
|
||||
},
|
||||
{
|
||||
id: 'tip.undoChanges',
|
||||
message: localize('tip.undoChanges', "Tip: You can undo Copilot's changes to any point by clicking Restore Checkpoint."),
|
||||
message: localize('tip.undoChanges', "Tip: You can undo chat's changes to any point by clicking Restore Checkpoint."),
|
||||
when: ContextKeyExpr.or(
|
||||
ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Agent),
|
||||
ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Edit),
|
||||
@@ -142,7 +156,7 @@ const TIP_CATALOG: ITipDefinition[] = [
|
||||
},
|
||||
{
|
||||
id: 'tip.customInstructions',
|
||||
message: localize('tip.customInstructions', "Tip: [Generate workspace instructions](command:workbench.action.chat.generateInstructions) so Copilot always has the context it needs when starting a task."),
|
||||
message: localize('tip.customInstructions', "Tip: [Generate workspace instructions](command:workbench.action.chat.generateInstructions) so chat always has the context it needs when starting a task."),
|
||||
enabledCommands: ['workbench.action.chat.generateInstructions'],
|
||||
excludeWhenPromptFilesExist: { promptType: PromptsType.instructions, agentFileType: AgentFileType.copilotInstructionsMd, excludeUntilChecked: true },
|
||||
},
|
||||
@@ -468,16 +482,17 @@ export class ChatTipService extends Disposable implements IChatTipService {
|
||||
readonly onDidDisableTips = this._onDidDisableTips.event;
|
||||
|
||||
/**
|
||||
* Timestamp when this service was instantiated.
|
||||
* Timestamp when the current session started.
|
||||
* Used to only show tips for requests created after this time.
|
||||
* Resets on each {@link resetSession} call.
|
||||
*/
|
||||
private readonly _createdAt = Date.now();
|
||||
private _sessionStartedAt = Date.now();
|
||||
|
||||
/**
|
||||
* Whether a tip has already been shown in this window session.
|
||||
* Only one tip is shown per session.
|
||||
* Whether a chatResponse tip has already been shown in this conversation
|
||||
* session. Only one response tip is shown per session.
|
||||
*/
|
||||
private _hasShownTip = false;
|
||||
private _hasShownRequestTip = false;
|
||||
|
||||
/**
|
||||
* The request ID that was assigned a tip (for stable rerenders).
|
||||
@@ -490,6 +505,7 @@ export class ChatTipService extends Disposable implements IChatTipService {
|
||||
private _shownTip: ITipDefinition | undefined;
|
||||
|
||||
private static readonly _DISMISSED_TIP_KEY = 'chat.tip.dismissed';
|
||||
private static readonly _LAST_TIP_ID_KEY = 'chat.tip.lastTipId';
|
||||
private readonly _tracker: TipEligibilityTracker;
|
||||
|
||||
constructor(
|
||||
@@ -503,13 +519,20 @@ export class ChatTipService extends Disposable implements IChatTipService {
|
||||
this._tracker = this._register(instantiationService.createInstance(TipEligibilityTracker, TIP_CATALOG));
|
||||
}
|
||||
|
||||
resetSession(): void {
|
||||
this._hasShownRequestTip = false;
|
||||
this._shownTip = undefined;
|
||||
this._tipRequestId = undefined;
|
||||
this._sessionStartedAt = Date.now();
|
||||
}
|
||||
|
||||
dismissTip(): void {
|
||||
if (this._shownTip) {
|
||||
const dismissed = this._getDismissedTipIds();
|
||||
dismissed.push(this._shownTip.id);
|
||||
this._storageService.store(ChatTipService._DISMISSED_TIP_KEY, JSON.stringify(dismissed), StorageScope.WORKSPACE, StorageTarget.MACHINE);
|
||||
}
|
||||
this._hasShownTip = false;
|
||||
this._hasShownRequestTip = false;
|
||||
this._shownTip = undefined;
|
||||
this._tipRequestId = undefined;
|
||||
this._onDidDismissTip.fire();
|
||||
@@ -523,14 +546,25 @@ export class ChatTipService extends Disposable implements IChatTipService {
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
this._logService.debug('#ChatTips dismissed:', parsed);
|
||||
return Array.isArray(parsed) ? parsed : [];
|
||||
if (!Array.isArray(parsed)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Safety valve: if every known tip has been dismissed (for example, due to a
|
||||
// past bug that dismissed the current tip on every new session), treat this
|
||||
// as "no tips dismissed" so the feature can recover.
|
||||
if (parsed.length >= TIP_CATALOG.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return parsed;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async disableTips(): Promise<void> {
|
||||
this._hasShownTip = false;
|
||||
this._hasShownRequestTip = false;
|
||||
this._shownTip = undefined;
|
||||
this._tipRequestId = undefined;
|
||||
await this._configurationService.updateValue('chat.tips.enabled', false);
|
||||
@@ -554,33 +588,89 @@ export class ChatTipService extends Disposable implements IChatTipService {
|
||||
}
|
||||
|
||||
// Only show one tip per session
|
||||
if (this._hasShownTip) {
|
||||
if (this._hasShownRequestTip) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Only show tips for requests created after the service was instantiated
|
||||
// This prevents showing tips for old requests being re-rendered after reload
|
||||
if (requestTimestamp < this._createdAt) {
|
||||
// Only show tips for requests created after the current session started.
|
||||
// This prevents showing tips for old requests being re-rendered.
|
||||
if (requestTimestamp < this._sessionStartedAt) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Find eligible tips (excluding dismissed ones)
|
||||
const dismissedIds = new Set(this._getDismissedTipIds());
|
||||
const eligibleTips = TIP_CATALOG.filter(tip => !dismissedIds.has(tip.id) && this._isEligible(tip, contextKeyService));
|
||||
// Record the current mode for future eligibility decisions
|
||||
return this._pickTip(requestId, contextKeyService);
|
||||
}
|
||||
|
||||
getWelcomeTip(contextKeyService: IContextKeyService): IChatTip | undefined {
|
||||
// Check if tips are enabled
|
||||
if (!this._configurationService.getValue<boolean>('chat.tips.enabled')) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Only show tips for Copilot
|
||||
if (!this._isCopilotEnabled()) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Return the already-shown tip for stable rerenders
|
||||
if (this._tipRequestId === 'welcome' && this._shownTip) {
|
||||
return this._createTip(this._shownTip);
|
||||
}
|
||||
|
||||
const tip = this._pickTip('welcome', contextKeyService);
|
||||
|
||||
return tip;
|
||||
}
|
||||
|
||||
private _pickTip(sourceId: string, contextKeyService: IContextKeyService): IChatTip | undefined {
|
||||
// Record the current mode for future eligibility decisions.
|
||||
this._tracker.recordCurrentMode(contextKeyService);
|
||||
|
||||
if (eligibleTips.length === 0) {
|
||||
return undefined;
|
||||
const dismissedIds = new Set(this._getDismissedTipIds());
|
||||
let selectedTip: ITipDefinition | undefined;
|
||||
|
||||
// Determine where to start in the catalog based on the last-shown tip.
|
||||
const lastTipId = this._storageService.get(ChatTipService._LAST_TIP_ID_KEY, StorageScope.WORKSPACE);
|
||||
const lastCatalogIndex = lastTipId ? TIP_CATALOG.findIndex(tip => tip.id === lastTipId) : -1;
|
||||
const startIndex = lastCatalogIndex === -1 ? 0 : (lastCatalogIndex + 1) % TIP_CATALOG.length;
|
||||
|
||||
// Pass 1: walk TIP_CATALOG in a ring, picking the first tip that is both
|
||||
// not dismissed and eligible for the current context.
|
||||
for (let i = 0; i < TIP_CATALOG.length; i++) {
|
||||
const idx = (startIndex + i) % TIP_CATALOG.length;
|
||||
const candidate = TIP_CATALOG[idx];
|
||||
if (!dismissedIds.has(candidate.id) && this._isEligible(candidate, contextKeyService)) {
|
||||
selectedTip = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Pick a random tip from eligible tips
|
||||
const randomIndex = Math.floor(Math.random() * eligibleTips.length);
|
||||
const selectedTip = eligibleTips[randomIndex];
|
||||
// Pass 2: if everything was ineligible (e.g., user has already done all
|
||||
// the suggested actions), still advance through the catalog but only skip
|
||||
// tips that were explicitly dismissed.
|
||||
if (!selectedTip) {
|
||||
for (let i = 0; i < TIP_CATALOG.length; i++) {
|
||||
const idx = (startIndex + i) % TIP_CATALOG.length;
|
||||
const candidate = TIP_CATALOG[idx];
|
||||
if (!dismissedIds.has(candidate.id)) {
|
||||
selectedTip = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Final fallback: if even that fails (all tips dismissed), stick with the
|
||||
// catalog order so rotation still progresses.
|
||||
if (!selectedTip) {
|
||||
selectedTip = TIP_CATALOG[startIndex];
|
||||
}
|
||||
|
||||
// Persist the selected tip id so the next use advances to the following one.
|
||||
this._storageService.store(ChatTipService._LAST_TIP_ID_KEY, selectedTip.id, StorageScope.WORKSPACE, StorageTarget.MACHINE);
|
||||
|
||||
// Record that we've shown a tip this session
|
||||
this._hasShownTip = true;
|
||||
this._tipRequestId = requestId;
|
||||
this._hasShownRequestTip = sourceId !== 'welcome';
|
||||
this._tipRequestId = sourceId;
|
||||
this._shownTip = selectedTip;
|
||||
|
||||
return this._createTip(selectedTip);
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-bottom: 8px;
|
||||
padding: 6px 10px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--vscode-focusBorder);
|
||||
background-color: var(--vscode-editorWidget-background);
|
||||
font-size: var(--vscode-chat-font-size-body-s);
|
||||
font-family: var(--vscode-chat-font-family, inherit);
|
||||
color: var(--vscode-descriptionForeground);
|
||||
@@ -15,13 +19,56 @@
|
||||
|
||||
.interactive-item-container .chat-tip-widget .codicon-lightbulb {
|
||||
font-size: 12px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
color: var(--vscode-notificationsWarningIcon-foreground);
|
||||
}
|
||||
|
||||
.interactive-item-container .chat-tip-widget .rendered-markdown p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.chat-getting-started-tip-container {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
right: 12px;
|
||||
/* Initial vertical position; updated dynamically to align with input top */
|
||||
bottom: 96px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
/* No horizontal padding so width exactly matches input */
|
||||
padding: 0;
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.chat-getting-started-tip-container .chat-tip-widget {
|
||||
pointer-events: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
/* Match input inner padding (6px per side) so content edges align */
|
||||
padding: 6px;
|
||||
/* Darker background for welcome-state tip, distinct from input */
|
||||
background-color: var(--vscode-editorWidget-background);
|
||||
border-radius: 4px 4px 0 0;
|
||||
border: 1px solid var(--vscode-editorWidget-border, var(--vscode-input-border, transparent));
|
||||
border-bottom: none; /* Seamless attachment to input below */
|
||||
box-shadow: none;
|
||||
font-size: var(--vscode-chat-font-size-body-s);
|
||||
font-family: var(--vscode-chat-font-family, inherit);
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
|
||||
.chat-getting-started-tip-container .chat-tip-widget .codicon-lightbulb {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.chat-getting-started-tip-container .chat-tip-widget .rendered-markdown p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Override bubble styling for tip widget's rendered markdown in chat editor */
|
||||
.interactive-session:not(.chat-widget > .interactive-session) {
|
||||
.interactive-item-container.interactive-request .value .chat-tip-widget .rendered-markdown {
|
||||
|
||||
@@ -78,6 +78,9 @@ import { IChatListItemTemplate } from './chatListRenderer.js';
|
||||
import { ChatListWidget } from './chatListWidget.js';
|
||||
import { ChatEditorOptions } from './chatOptions.js';
|
||||
import { ChatViewWelcomePart, IChatSuggestedPrompts, IChatViewWelcomeContent } from '../viewsWelcome/chatViewWelcomeController.js';
|
||||
import { IChatTipService } from '../chatTipService.js';
|
||||
import { ChatTipContentPart } from './chatContentParts/chatTipContentPart.js';
|
||||
import { ChatContentMarkdownRenderer } from './chatContentMarkdownRenderer.js';
|
||||
import { IAgentSessionsService } from '../agentSessions/agentSessionsService.js';
|
||||
|
||||
const $ = dom.$;
|
||||
@@ -233,6 +236,9 @@ export class ChatWidget extends Disposable implements IChatWidget {
|
||||
private welcomeMessageContainer!: HTMLElement;
|
||||
private readonly welcomePart: MutableDisposable<ChatViewWelcomePart> = this._register(new MutableDisposable());
|
||||
|
||||
private gettingStartedTipContainer: HTMLElement | undefined;
|
||||
private readonly _gettingStartedTipPart = this._register(new MutableDisposable<DisposableStore>());
|
||||
|
||||
private readonly chatSuggestNextWidget: ChatSuggestNextWidget;
|
||||
|
||||
private bodyDimension: dom.Dimension | undefined;
|
||||
@@ -368,6 +374,7 @@ export class ChatWidget extends Disposable implements IChatWidget {
|
||||
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
|
||||
@ILifecycleService private readonly lifecycleService: ILifecycleService,
|
||||
@IChatAttachmentResolveService private readonly chatAttachmentResolveService: IChatAttachmentResolveService,
|
||||
@IChatTipService private readonly chatTipService: IChatTipService,
|
||||
) {
|
||||
super();
|
||||
|
||||
@@ -622,6 +629,7 @@ export class ChatWidget extends Disposable implements IChatWidget {
|
||||
} else {
|
||||
this.listContainer = dom.append(this.container, $(`.interactive-list`));
|
||||
dom.append(this.container, this.chatSuggestNextWidget.domNode);
|
||||
this.gettingStartedTipContainer = dom.append(this.container, $('.chat-getting-started-tip-container', { style: 'display: none' }));
|
||||
this.createInput(this.container, { renderFollowups, renderStyle, renderInputToolbarBelowInput });
|
||||
}
|
||||
|
||||
@@ -840,9 +848,33 @@ export class ChatWidget extends Disposable implements IChatWidget {
|
||||
*/
|
||||
private updateChatViewVisibility(): void {
|
||||
if (this.viewModel) {
|
||||
const isStandardLayout = this.viewOptions.renderStyle !== 'compact' && this.viewOptions.renderStyle !== 'minimal';
|
||||
const numItems = this.viewModel.getItems().length;
|
||||
dom.setVisibility(numItems === 0, this.welcomeMessageContainer);
|
||||
dom.setVisibility(numItems !== 0, this.listContainer);
|
||||
|
||||
// Show/hide the getting-started tip container based on empty state.
|
||||
// Only use this in the standard chat layout where the welcome view is shown.
|
||||
if (isStandardLayout && this.gettingStartedTipContainer) {
|
||||
if (numItems === 0) {
|
||||
this.renderGettingStartedTipIfNeeded();
|
||||
this.container.classList.toggle('chat-has-getting-started-tip', !!this._gettingStartedTipPart.value);
|
||||
} else {
|
||||
// Dispose the cached tip part so the next empty state picks a
|
||||
// fresh (rotated) tip instead of re-showing the stale one.
|
||||
this._gettingStartedTipPart.clear();
|
||||
dom.clearNode(this.gettingStartedTipContainer);
|
||||
// Reset inline positioning from layoutGettingStartedTipPosition
|
||||
// so the next render starts from the CSS defaults.
|
||||
this.gettingStartedTipContainer.style.top = '';
|
||||
this.gettingStartedTipContainer.style.bottom = '';
|
||||
this.gettingStartedTipContainer.style.left = '';
|
||||
this.gettingStartedTipContainer.style.right = '';
|
||||
this.gettingStartedTipContainer.style.width = '';
|
||||
dom.setVisibility(false, this.gettingStartedTipContainer);
|
||||
this.container.classList.toggle('chat-has-getting-started-tip', false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Only show welcome getting started until extension is installed
|
||||
@@ -904,6 +936,84 @@ export class ChatWidget extends Disposable implements IChatWidget {
|
||||
}
|
||||
}
|
||||
|
||||
private renderGettingStartedTipIfNeeded(): void {
|
||||
const tipContainer = this.gettingStartedTipContainer;
|
||||
if (!tipContainer) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Already showing a tip
|
||||
if (this._gettingStartedTipPart.value) {
|
||||
dom.setVisibility(true, tipContainer);
|
||||
return;
|
||||
}
|
||||
|
||||
const tip = this.chatTipService.getWelcomeTip(this.contextKeyService);
|
||||
if (!tip) {
|
||||
dom.setVisibility(false, tipContainer);
|
||||
return;
|
||||
}
|
||||
|
||||
const store = new DisposableStore();
|
||||
const renderer = this.instantiationService.createInstance(ChatContentMarkdownRenderer);
|
||||
const tipPart = store.add(this.instantiationService.createInstance(ChatTipContentPart,
|
||||
tip,
|
||||
renderer,
|
||||
() => this.chatTipService.getWelcomeTip(this.contextKeyService),
|
||||
));
|
||||
tipContainer.appendChild(tipPart.domNode);
|
||||
|
||||
store.add(tipPart.onDidHide(() => {
|
||||
tipPart.domNode.remove();
|
||||
this._gettingStartedTipPart.clear();
|
||||
dom.setVisibility(false, tipContainer);
|
||||
this.container.classList.toggle('chat-has-getting-started-tip', false);
|
||||
}));
|
||||
|
||||
this._gettingStartedTipPart.value = store;
|
||||
dom.setVisibility(true, tipContainer);
|
||||
|
||||
// Best-effort synchronous position (works when layout is already settled,
|
||||
// e.g. the very first render after page load).
|
||||
this.layoutGettingStartedTipPosition();
|
||||
|
||||
// Also schedule a deferred correction for cases where the browser
|
||||
// hasn't finished layout yet (e.g. returning to the welcome view
|
||||
// after a conversation).
|
||||
store.add(dom.scheduleAtNextAnimationFrame(dom.getWindow(tipContainer), () => {
|
||||
this.layoutGettingStartedTipPosition();
|
||||
}));
|
||||
}
|
||||
|
||||
private layoutGettingStartedTipPosition(): void {
|
||||
if (!this.container || !this.gettingStartedTipContainer || !this.inputPart) {
|
||||
return;
|
||||
}
|
||||
|
||||
const inputContainer = this.inputPart.inputContainerElement;
|
||||
if (!inputContainer) {
|
||||
return;
|
||||
}
|
||||
|
||||
const containerRect = this.container.getBoundingClientRect();
|
||||
const inputRect = inputContainer.getBoundingClientRect();
|
||||
const tipRect = this.gettingStartedTipContainer.getBoundingClientRect();
|
||||
|
||||
// Align the tip horizontally with the input container.
|
||||
const left = inputRect.left - containerRect.left;
|
||||
this.gettingStartedTipContainer.style.left = `${left}px`;
|
||||
this.gettingStartedTipContainer.style.right = 'auto';
|
||||
this.gettingStartedTipContainer.style.width = `${inputRect.width}px`;
|
||||
|
||||
// Position the tip so its bottom edge sits flush against the input's
|
||||
// top edge for a seamless visual connection.
|
||||
const topOffset = inputRect.top - containerRect.top - tipRect.height;
|
||||
if (topOffset > 0) {
|
||||
this.gettingStartedTipContainer.style.top = `${topOffset}px`;
|
||||
this.gettingStartedTipContainer.style.bottom = 'auto';
|
||||
}
|
||||
}
|
||||
|
||||
private _getGenerateInstructionsMessage(): IMarkdownString {
|
||||
// Start checking for instruction files immediately if not already done
|
||||
if (!this._instructionFilesCheckPromise) {
|
||||
@@ -990,7 +1100,7 @@ export class ChatWidget extends Disposable implements IChatWidget {
|
||||
message: new MarkdownString(DISCLAIMER),
|
||||
icon: Codicon.chatSparkle,
|
||||
additionalMessage,
|
||||
suggestedPrompts: this.getPromptFileSuggestions()
|
||||
suggestedPrompts: this.getPromptFileSuggestions(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1743,6 +1853,9 @@ export class ChatWidget extends Disposable implements IChatWidget {
|
||||
this.layout(this.bodyDimension.height, this.bodyDimension.width);
|
||||
}
|
||||
|
||||
// Keep getting-started tip aligned with the top of the input
|
||||
this.layoutGettingStartedTipPosition();
|
||||
|
||||
this._onDidChangeContentHeight.fire();
|
||||
}));
|
||||
}
|
||||
@@ -1866,6 +1979,7 @@ export class ChatWidget extends Disposable implements IChatWidget {
|
||||
}
|
||||
this.inputPart.clearTodoListWidget(model.sessionResource, false);
|
||||
this.chatSuggestNextWidget.hide();
|
||||
this.chatTipService.resetSession();
|
||||
|
||||
this._codeBlockModelCollection.clear();
|
||||
|
||||
|
||||
@@ -289,12 +289,17 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
|
||||
private chatInputTodoListWidgetContainer!: HTMLElement;
|
||||
private chatQuestionCarouselContainer!: HTMLElement;
|
||||
private chatInputWidgetsContainer!: HTMLElement;
|
||||
private inputContainer!: HTMLElement;
|
||||
private readonly _widgetController = this._register(new MutableDisposable<ChatInputPartWidgetController>());
|
||||
|
||||
private contextUsageWidget?: ChatContextUsageWidget;
|
||||
private contextUsageWidgetContainer!: HTMLElement;
|
||||
private readonly _contextUsageDisposables = this._register(new MutableDisposable<DisposableStore>());
|
||||
|
||||
get inputContainerElement(): HTMLElement | undefined {
|
||||
return this.inputContainer;
|
||||
}
|
||||
|
||||
readonly height = observableValue<number>(this, 0);
|
||||
|
||||
private _inputEditor!: CodeEditorWidget;
|
||||
@@ -1794,6 +1799,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
|
||||
this.followupsContainer = elements.followupsContainer;
|
||||
const inputAndSideToolbar = elements.inputAndSideToolbar; // The chat input and toolbar to the right
|
||||
const inputContainer = elements.inputContainer; // The chat editor, attachments, and toolbars
|
||||
this.inputContainer = inputContainer;
|
||||
const editorContainer = elements.editorContainer;
|
||||
this.attachmentsContainer = elements.attachmentsContainer;
|
||||
this.attachedContextContainer = elements.attachedContextContainer;
|
||||
|
||||
@@ -805,6 +805,12 @@ have to be updated for changes to the rules above, or to support more deeply nes
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* When a getting-started tip is shown, visually attach it to the input */
|
||||
.interactive-session.chat-has-getting-started-tip .chat-input-container {
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
|
||||
/* Context usage widget container - positioned at top right of chat input */
|
||||
.interactive-session .chat-input-container .chat-context-usage-container {
|
||||
position: absolute;
|
||||
|
||||
@@ -479,6 +479,39 @@ suite('ChatTipService', () => {
|
||||
assert.strictEqual(tracker2.isExcluded(tip), true, 'New tracker should read persisted mode exclusion from workspace storage');
|
||||
});
|
||||
|
||||
test('resetSession allows tips in a new conversation', () => {
|
||||
const service = createService();
|
||||
const now = Date.now();
|
||||
|
||||
// Show a tip in the first conversation
|
||||
const tip1 = service.getNextTip('request-1', now + 1000, contextKeyService);
|
||||
assert.ok(tip1, 'First request should get a tip');
|
||||
|
||||
// Second request — no tip (one per session)
|
||||
const tip2 = service.getNextTip('request-2', now + 2000, contextKeyService);
|
||||
assert.strictEqual(tip2, undefined, 'Second request should not get a tip');
|
||||
|
||||
// Start a new conversation
|
||||
service.resetSession();
|
||||
|
||||
// New request after reset should get a tip
|
||||
const tip3 = service.getNextTip('request-3', Date.now() + 1000, contextKeyService);
|
||||
assert.ok(tip3, 'First request after resetSession should get a tip');
|
||||
});
|
||||
|
||||
test('chatResponse tip shows regardless of welcome tip', () => {
|
||||
const service = createService();
|
||||
const now = Date.now();
|
||||
|
||||
// Show a welcome tip (simulating the getting-started view)
|
||||
const welcomeTip = service.getWelcomeTip(contextKeyService);
|
||||
assert.ok(welcomeTip, 'Welcome tip should be shown');
|
||||
|
||||
// First new request should still get a chatResponse tip
|
||||
const tip = service.getNextTip('request-1', now + 1000, contextKeyService);
|
||||
assert.ok(tip, 'ChatResponse tip should show even when welcome tip was shown');
|
||||
});
|
||||
|
||||
test('excludes tip when tracked tool has been invoked', () => {
|
||||
const mockToolsService = createMockToolsService();
|
||||
const tip: ITipDefinition = {
|
||||
|
||||
Reference in New Issue
Block a user