Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ export function getAccessibilityHelpText(type: 'panelChat' | 'inlineChat' | 'qui
content.push(localize('chat.modelPicker.pricingDetails', "Pricing Details expands in place without moving the model's controls. Expansion and collapse are immediate when reduced motion is enabled. If the details exceed the available space, use Page Up or Page Down while the model details have focus to scroll."));
content.push(localize('chat.modelPicker.search', "Type while the model list is focused to search across all providers. In the search field, use Up and Down Arrow to navigate results, Enter to select a model, and Escape to close the picker. Left and Right Arrow move the text cursor."));
content.push(localize('chat.fileChangesDisclosure', 'File change summaries show the total files, additions, and deletions. Focus the disclosure and press Enter or Space to show or hide the individual files. Focus an additions and deletions label and press Enter or Space to open the changes in a diff editor.'));
content.push(localize('chat.pendingRequestEditing', "Queued messages can be edited before they are sent. Pending steering messages cannot be edited because they may already have been submitted to the agent."));
}
if (type === 'panelChat' || type === 'quickChat' || type === 'agentView') {
if (type === 'quickChat') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { ChatContextKeys } from '../../common/actions/chatContextKeys.js';
import { ChatRequestQueueKind, IChatService } from '../../common/chatService/chatService.js';
import { IChatSideChatService } from '../../common/chatSideChatService.js';
import { ChatConfiguration } from '../../common/constants.js';
import { isRequestVM } from '../../common/model/chatViewModel.js';
import { isEditableRequestVM, isRequestVM } from '../../common/model/chatViewModel.js';
import { IChatWidgetService } from '../chat.js';
import { captureSideChatSelection } from '../chatSideChat.js';
import { CHAT_CATEGORY } from './chatActions.js';
Expand Down Expand Up @@ -278,7 +278,7 @@ export class ChatEditPendingRequestAction extends Action2 {
group: 'navigation',
order: 2,
when: ContextKeyExpr.and(
ChatContextKeys.isRequest,
ChatContextKeys.isEditableRequest,
ChatContextKeys.isPendingRequest,
ContextKeyExpr.notEquals(`config.${ChatConfiguration.EditRequests}`, 'hover'),
ContextKeyExpr.notEquals(`config.${ChatConfiguration.EditRequests}`, 'input')
Expand All @@ -291,7 +291,7 @@ export class ChatEditPendingRequestAction extends Action2 {
const widgetService = accessor.get(IChatWidgetService);
const [context] = args;

if (!isRequestVM(context) || !context.pendingKind) {
if (!isEditableRequestVM(context) || !context.pendingKind) {
return;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import { isChatViewTitleActionContext } from '../../common/actions/chatActions.j
import { ChatContextKeyExprs, ChatContextKeys } from '../../common/actions/chatContextKeys.js';
import { applyingChatEditsFailedContextKey, CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME, chatEditingResourceContextKey, chatEditingWidgetFileStateContextKey, decidedChatEditingResourceContextKey, hasAppliedChatEditsContextKey, hasUndecidedChatEditingResourceContextKey, IChatEditingService, IChatEditingSession, ModifiedFileEntryState } from '../../common/editing/chatEditingService.js';
import { IChatService } from '../../common/chatService/chatService.js';
import { isChatTreeItem, isRequestVM, isResponseVM } from '../../common/model/chatViewModel.js';
import { isChatTreeItem, isEditableRequestVM, isRequestVM, isResponseVM } from '../../common/model/chatViewModel.js';
import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../../common/constants.js';
import { CHAT_CATEGORY } from '../actions/chatActions.js';
import { ChatTreeItem, IChatWidget, IChatWidgetService } from '../chat.js';
Expand Down Expand Up @@ -671,7 +671,7 @@ registerAction2(class EditAction extends Action2 {
id: MenuId.ChatMessageTitle,
group: 'navigation',
order: 2,
when: ContextKeyExpr.and(ContextKeyExpr.or(ContextKeyExpr.equals(`config.${ChatConfiguration.EditRequests}`, 'hover'), ContextKeyExpr.equals(`config.${ChatConfiguration.EditRequests}`, 'input')), ChatContextKeys.readOnly.negate())
when: ContextKeyExpr.and(ContextKeyExpr.or(ContextKeyExpr.equals(`config.${ChatConfiguration.EditRequests}`, 'hover'), ContextKeyExpr.equals(`config.${ChatConfiguration.EditRequests}`, 'input')), ChatContextKeys.readOnly.negate(), ChatContextKeys.isEditableRequest)
}
]
});
Expand All @@ -689,7 +689,7 @@ registerAction2(class EditAction extends Action2 {
return;
}

if (isRequestVM(item)) {
if (isEditableRequestVM(item)) {
widget?.startEditing(item.id);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ import { ChatQuestionCarouselData } from '../../common/model/chatProgressTypes/c
import { localChatSessionType, SessionType } from '../../common/chatSessionsService.js';
import { getChatSessionType } from '../../common/model/chatUri.js';
import { getExplicitFileOrImageAttachmentSummary, IChatRequestVariableEntry, isExplicitFileOrImageVariableEntry, isPasteVariableEntry } from '../../common/attachments/chatVariableEntries.js';
import { getStickyScrollTargetItem, IChatChangesSummaryPart, IChatCodeCitations, IChatErrorDetailsPart, IChatReferences, IChatRendererContent, IChatRequestViewModel, IChatResponseViewModel, IChatViewModel, IChatWorkingProgress, isRequestVM, isResponseVM, IChatPendingDividerViewModel, isPendingDividerVM, IChatTurnPillsPart } from '../../common/model/chatViewModel.js';
import { getStickyScrollTargetItem, IChatChangesSummaryPart, IChatCodeCitations, IChatErrorDetailsPart, IChatReferences, IChatRendererContent, IChatRequestViewModel, IChatResponseViewModel, IChatViewModel, IChatWorkingProgress, isEditableRequestVM, isRequestVM, isResponseVM, IChatPendingDividerViewModel, isPendingDividerVM, IChatTurnPillsPart } from '../../common/model/chatViewModel.js';
import { getNWords } from '../../common/model/chatWordCounter.js';
import { CHAT_OPEN_AGENT_HOST_CHAT_COMMAND_ID, ChatAgentLocation, ChatConfiguration, ChatModeKind, ChatProgressAnimation, CollapsedToolsDisplayMode, ThinkingDisplayMode } from '../../common/constants.js';
import { getConfiguredProgressAnimation } from './chatWorkingLogo.js';
Expand Down Expand Up @@ -1473,6 +1473,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
}

templateData.currentElement = element;
ChatContextKeys.isEditableRequest.bindTo(templateData.contextKeyService).set(isEditableRequestVM(element));
Comment thread
jruales marked this conversation as resolved.
templateData.renderedReadOnly = readOnly;
templateData.renderedPersistentProgress = persistentProgress;
// Don't update the template map for sticky scroll renders - their templates
Expand Down Expand Up @@ -2380,7 +2381,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
this._onDidRerender.fire(templateData);
}

if (this.configService.getValue<string>('chat.editRequests') !== 'none' && this.rendererOptions.editable) {
if (this.configService.getValue<string>('chat.editRequests') !== 'none' && this.rendererOptions.editable && isEditableRequestVM(element)) {
templateData.elementDisposables.add(dom.addDisposableListener(templateData.rowContainer, dom.EventType.KEY_DOWN, e => {
const ev = new StandardKeyboardEvent(e);
if (ev.equals(KeyCode.Space) || ev.equals(KeyCode.Enter)) {
Expand Down Expand Up @@ -2471,7 +2472,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
}

container.tabIndex = 0;
if (this.configService.getValue<string>('chat.editRequests') === 'inline' && this.rendererOptions.editable) {
if (this.configService.getValue<string>('chat.editRequests') === 'inline' && this.rendererOptions.editable && isEditableRequestVM(element)) {
container.classList.add('clickable');
store.add(dom.addDisposableListener(container, dom.EventType.CLICK, (e: MouseEvent) => {
if (this.viewModel?.editing?.id === element.id) {
Expand Down Expand Up @@ -5151,7 +5152,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
markdownPart.addDisposable(markdownPart.onDidChangeHeight(() => this.fireItemHeightChange(templateData)));
if (isRequestVM(element)) {
markdownPart.domNode.tabIndex = 0;
if (this.configService.getValue<string>('chat.editRequests') === 'inline' && this.rendererOptions.editable) {
if (this.configService.getValue<string>('chat.editRequests') === 'inline' && this.rendererOptions.editable && isEditableRequestVM(element)) {
markdownPart.domNode.classList.add('clickable');
markdownPart.addDisposable(dom.addDisposableListener(markdownPart.domNode, dom.EventType.CLICK, (e: MouseEvent) => {
if (this.viewModel?.editing?.id === element.id) {
Expand Down
28 changes: 25 additions & 3 deletions src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import { ITextResourceEditorInput } from '../../../../../platform/editor/common/
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js';
import { ILogService } from '../../../../../platform/log/common/log.js';
import { INotificationService } from '../../../../../platform/notification/common/notification.js';
import { bindContextKey } from '../../../../../platform/observable/common/platformObservableUtils.js';
import product from '../../../../../platform/product/common/product.js';
import { Progress } from '../../../../../platform/progress/common/progress.js';
Expand Down Expand Up @@ -71,7 +72,7 @@ import { IChatSessionsService, localChatSessionType } from '../../common/chatSes
import { IChatSlashCommandService } from '../../common/participants/chatSlashCommands.js';
import { IChatTodoListService } from '../../common/tools/chatTodoListService.js';
import { ChatRequestVariableSet, IChatRequestTranscriptContextVariableEntry, IChatRequestVariableEntry, isPastedTextArtifact, isPromptFileVariableEntry, isPromptTextVariableEntry, isWorkspaceVariableEntry, PromptFileVariableKind, toPromptFileVariableEntry } from '../../common/attachments/chatVariableEntries.js';
import { ChatViewModel, IChatResponseViewModel, isRequestVM, isResponseVM } from '../../common/model/chatViewModel.js';
import { ChatViewModel, IChatResponseViewModel, isEditableRequestVM, isRequestVM, isResponseVM } from '../../common/model/chatViewModel.js';
import { ChatMessageRole, IChatMessage } from '../../common/languageModels.js';
import { ChatAgentLocation, ChatConfiguration, ChatModeKind, ChatPermissionLevel, IResolvedNewChatSessionType, ThinkingDisplayMode } from '../../common/constants.js';
import { IChatGoalSummaryService } from '../chatGoalSummaryService.js';
Expand Down Expand Up @@ -584,6 +585,7 @@ export class ChatWidget extends Disposable implements IChatWidget {
@IChatPasteTargetService private readonly chatPasteTargetService: IChatPasteTargetService,
@IChatAccessibilityService private readonly chatAccessibilityService: IChatAccessibilityService,
@ILogService private readonly logService: ILogService,
@INotificationService private readonly notificationService: INotificationService,
@IThemeService private readonly themeService: IThemeService,
@IChatSlashCommandService private readonly chatSlashCommandService: IChatSlashCommandService,
@IChatEditingService chatEditingService: IChatEditingService,
Expand Down Expand Up @@ -2224,7 +2226,11 @@ export class ChatWidget extends Disposable implements IChatWidget {
private clickedRequest(item: IChatListItemTemplate) {

const currentElement = item.currentElement;
if (isRequestVM(currentElement) && !this.viewModel?.editing) {
if (!isEditableRequestVM(currentElement)) {
return;
}
Comment thread
jruales marked this conversation as resolved.

if (!this.viewModel?.editing) {

const requests = this.viewModel?.model.getRequests();
if (!requests || !this.viewModel?.sessionResource) {
Expand Down Expand Up @@ -3236,6 +3242,19 @@ export class ChatWidget extends Disposable implements IChatWidget {
return true;
}

private _validateRequestEdit(): boolean {
const editing = this.viewModel?.editing;
if (!editing || editing.pendingKind === undefined) {
return true;
}
if (this.viewModel?.model.getPendingRequests().some(pending => pending.request.id === editing.id && pending.kind === ChatRequestQueueKind.Queued)) {
return true;
}

this.notificationService.warn(localize('chat.editRequest.noLongerQueued', "This message is no longer queued and cannot be edited. Your edits have been kept in the input."));
return false;
}

private async _acceptInput(query: { query: string } | undefined, options: IChatAcceptInputOptions = {}): Promise<IChatResponseModel | undefined> {
if (!query && this.input.generating) {
// if the user submits the input and generation finishes quickly, just submit it for them
Expand All @@ -3251,7 +3270,7 @@ export class ChatWidget extends Disposable implements IChatWidget {
await Event.toPromise(this.onDidChangeViewModel, this._store);
}

if (!this.viewModel) {
if (!this.viewModel || !this._validateRequestEdit()) {
return;
}

Expand Down Expand Up @@ -3316,6 +3335,9 @@ export class ChatWidget extends Disposable implements IChatWidget {
if (await this._executeSlashCommandDuringRequest(requestInputs.input, { attachedContext }, isUserQuery, options.preserveFocus)) {
return;
}
if (!this._validateRequestEdit()) {
return;
}
const isEditing = this.viewModel?.editing;
const submittedFromEditing = shouldUnlockChatPetRequestRevision(isEditing !== undefined, isUserQuery);
// Captured before `finishedEditing` tears the inline editor down, while `this.input` still
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export namespace ChatContextKeys {
export const contextMenuIsBackground = new RawContextKey<boolean>('chatContextMenuIsBackground', false, { type: 'boolean', description: localize('chatContextMenuIsBackground', "Whether the chat context menu was opened from the transcript background rather than chat item content.") });
export const isFirstRequest = new RawContextKey<boolean>('chatFirstRequest', false, { type: 'boolean', description: localize('chatFirstRequest', "The chat item is the first request in the session.") });
export const isPendingRequest = new RawContextKey<boolean>('chatRequestIsPending', false, { type: 'boolean', description: localize('chatRequestIsPending', "True when the chat request item is pending in the queue.") });
export const isEditableRequest = new RawContextKey<boolean>('chatRequestIsEditable', false, { type: 'boolean', description: localize('chatRequestIsEditable', "True when the chat request item can be edited.") });
export const itemId = new RawContextKey<string>('chatItemId', '', { type: 'string', description: localize('chatItemId', "The id of the chat item.") });
export const lastItemId = new RawContextKey<string[]>('chatLastItemId', [], { type: 'string', description: localize('chatLastItemId', "The id of the last chat item.") });

Expand Down
7 changes: 6 additions & 1 deletion src/vs/workbench/contrib/chat/common/model/chatViewModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ export function isRequestVM(item: unknown): item is IChatRequestViewModel {
return !!item && typeof item === 'object' && 'message' in item;
}

/** Pending steering may already be in the agent's input queue and cannot be safely replaced. */
export function isEditableRequestVM(item: unknown): item is IChatRequestViewModel {
return isRequestVM(item) && item.pendingKind !== ChatRequestQueueKind.Steering;
}

export function isResponseVM(item: unknown): item is IChatResponseViewModel {
return !!item && typeof (item as IChatResponseViewModel).setVote !== 'undefined';
}
Expand Down Expand Up @@ -425,7 +430,7 @@ class ChatRequestViewModel implements IChatRequestViewModel {
* An ID that changes when the request should be re-rendered.
*/
get dataId() {
return `${this.id}_${this._model.version + (this._model.response?.isComplete ? 1 : 0)}`;
return `${this.id}_${this._model.version + (this._model.response?.isComplete ? 1 : 0)}_${this._pendingKind ?? ''}`;
}

get sessionResource() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ import { AGENT_SESSION_RENAME_ACTION_ID } from '../../../browser/agentSessions/a
suite('Chat Accessibility Help', () => {
ensureNoDisposablesAreLeakedInTestSuite();

test('distinguishes editable queued messages from pending steering', () => {
const help = getAccessibilityHelpText('agentView', new MockKeybindingService(), true);
assert.deepStrictEqual({
queued: help.includes('Queued messages can be edited before they are sent'),
steering: help.includes('Pending steering messages cannot be edited'),
}, { queued: true, steering: true });
});

for (const type of ['panelChat', 'editsView', 'agentView'] as const) {
test(`documents draft copying, preservation, and invitation dismissal in ${type}`, () => {
const help = getAccessibilityHelpText(type, new MockKeybindingService(), false);
Expand Down
Loading
Loading