diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatChangesSummaryPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatChangesSummaryPart.ts index 5e002c0ec1a019..d55f7e7e4fa22c 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatChangesSummaryPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatChangesSummaryPart.ts @@ -414,7 +414,7 @@ class CollapsibleChangesSummaryListRenderer implements IListRenderer openChatTurnFile(file, this._openerService, this._configurationService), - })]; + return createTurnChangesPreviewActions(diff.modifiedURI, diff.originalURI, this._openerService, this._configurationService); } hasSameContent(other: IChatRendererContent, _followingContent: IChatRendererContent[], _element: ChatTreeItem): boolean { diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatTurnPills.ts b/src/vs/workbench/contrib/chat/browser/widget/chatTurnPills.ts index f9d2dd2199ec77..200d99171e5d96 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatTurnPills.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatTurnPills.ts @@ -107,6 +107,29 @@ export async function openChatTurnFile(file: IPreviewFile, openerService: IOpene }); } +/** + * Row actions for the expanded turn-changes file list. + * Previewable files get an icon-only Preview action (`Codicon.openPreview`). + */ +export function createTurnChangesPreviewActions( + modifiedURI: URI, + originalURI: URI, + openerService: IOpenerService, + configurationService: IConfigurationService, +): IAction[] { + const kind = previewKind(modifiedURI); + if (!kind) { + return []; + } + const file: IPreviewFile = { uri: modifiedURI, kind, created: isEqual(originalURI, modifiedURI) }; + return [toAction({ + id: 'chat.turnChanges.previewFile', + label: localize('chat.turnChanges.preview', "Preview"), + class: ThemeIcon.asClassName(Codicon.openPreview), + run: () => openChatTurnFile(file, openerService, configurationService), + })]; +} + /** The data and interactions a {@link ChatTurnPillsWidget} reflects. */ export interface IChatTurnPillsModel { readonly stats: IObservable; diff --git a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css index 7a7b0635da72c6..c2d906e1be015e 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css @@ -3217,7 +3217,7 @@ have to be updated for changes to the rules above, or to support more deeply nes width: 16px; } -/* Per-row action bar in the changed-files list (e.g. the "Preview" action). The +/* Per-row action bar in the changed-files list (e.g. the Preview icon). The row becomes a flex row so the file label fills the remaining width, followed by the actions and the right-aligned change counts. Scoped via the class the renderer adds only when a row-action provider is supplied. */ @@ -3242,12 +3242,15 @@ have to be updated for changes to the rules above, or to support more deeply nes } .interactive-session .chat-summary-list-actions .action-label { - color: var(--vscode-textLink-foreground); + color: var(--vscode-icon-foreground); cursor: pointer; + border-radius: 4px; + padding: 2px; } .interactive-session .chat-summary-list-actions .action-label:hover { - color: var(--vscode-textLink-activeForeground); + background-color: var(--vscode-toolbar-hoverBackground); + color: var(--vscode-icon-foreground); } .interactive-session .chat-used-context { diff --git a/src/vs/workbench/contrib/chat/test/browser/widget/chatTurnPills.test.ts b/src/vs/workbench/contrib/chat/test/browser/widget/chatTurnPills.test.ts index 3eb911ed98657f..1b0985c041b875 100644 --- a/src/vs/workbench/contrib/chat/test/browser/widget/chatTurnPills.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/widget/chatTurnPills.test.ts @@ -10,7 +10,7 @@ import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/ import { TestConfigurationService } from '../../../../../../platform/configuration/test/common/testConfigurationService.js'; import { IOpenerService, OpenExternalOptions, OpenInternalOptions } from '../../../../../../platform/opener/common/opener.js'; import { BrowserViewEditorId } from '../../../../browserView/common/browserView.js'; -import { openChatTurnFile, previewKind } from '../../../browser/widget/chatTurnPills.js'; +import { createTurnChangesPreviewActions, openChatTurnFile, previewKind } from '../../../browser/widget/chatTurnPills.js'; import { ChatConfiguration } from '../../../common/constants.js'; suite('ChatTurnPills', () => { @@ -44,6 +44,60 @@ suite('ChatTurnPills', () => { }); }); + suite('createTurnChangesPreviewActions (#328520)', () => { + const openerService = new class extends mock() { + override async open(): Promise { + return true; + } + }; + const configurationService = new TestConfigurationService(); + + test('returns an icon-only Preview action for markdown files', () => { + const modified = URI.file('/workspace/docs/AI_CUSTOMIZATIONS.md'); + const actions = createTurnChangesPreviewActions(modified, URI.file('/workspace/other.md'), openerService, configurationService); + + assert.strictEqual(actions.length, 1); + assert.strictEqual(actions[0].id, 'chat.turnChanges.previewFile'); + assert.strictEqual(actions[0].label, 'Preview'); + assert.ok(actions[0].class?.includes('codicon-open-preview'), `expected open-preview icon class, got: ${actions[0].class}`); + }); + + test('returns no actions for non-previewable files', () => { + const modified = URI.file('/workspace/src/service.ts'); + const actions = createTurnChangesPreviewActions(modified, modified, openerService, configurationService); + assert.deepStrictEqual(actions, []); + }); + + test('run opens the markdown file via chat editor associations', async () => { + const resource = URI.file('/workspace/README.md'); + let opened: { resource: string; options: OpenInternalOptions | OpenExternalOptions | undefined } | undefined; + const trackingOpener = new class extends mock() { + override async open(resource: string | URI, options?: OpenInternalOptions | OpenExternalOptions): Promise { + opened = { resource: resource.toString(), options }; + return true; + } + }; + const config = new TestConfigurationService({ + [ChatConfiguration.EditorAssociations]: { + '*.md': 'vscode.markdown.editor', + }, + }); + + const actions = createTurnChangesPreviewActions(resource, resource, trackingOpener, config); + await actions[0].run(); + + assert.deepStrictEqual(opened, { + resource: resource.toString(), + options: { + fromUserGesture: true, + editorOptions: { + override: 'vscode.markdown.editor', + }, + }, + }); + }); + }); + test('classifies supported preview resources', () => { assert.deepStrictEqual([ previewKind(URI.file('/workspace/README.md'), true), diff --git a/src/vs/workbench/test/browser/componentFixtures/chat/chatTurnPills.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/chat/chatTurnPills.fixture.ts index ccea8c11e84c18..b5316ade6b0538 100644 --- a/src/vs/workbench/test/browser/componentFixtures/chat/chatTurnPills.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/chat/chatTurnPills.fixture.ts @@ -155,12 +155,14 @@ export default defineThemedFixtureGroup({ path: 'chat/' }, { }), }), - // The external README remains in the preview pill and out of the expanded - // workspace changes list. + // Expanded workspace changes list. External README stays in the preview + // pill (not this list). Workspace markdown rows get the per-row Preview + // icon; non-md rows have no row action. ChangesAndExternalPreview_Expanded: defineComponentFixture({ render: (ctx) => renderTurnPills(ctx, { expanded: true, diffs: [ + fileDiff('docs.md', 12, 2, true), fileDiff('index.html', 30, 4, true), fileDiff('app.ts', 8, 3, false), fileDiff('styles.css', 4, 1, false),