Skip to content
Open
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 @@ -414,7 +414,7 @@ class CollapsibleChangesSummaryListRenderer implements IListRenderer<IEditSessio

if (templateData.actionBar && this.options?.getRowActions) {
templateData.actionBar.clear();
templateData.actionBar.push(this.options.getRowActions(data), { icon: false, label: true });
templateData.actionBar.push(this.options.getRowActions(data), { icon: true, label: false });
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import * as dom from '../../../../../../base/browser/dom.js';
import { $ } from '../../../../../../base/browser/dom.js';
import { IAction, toAction } from '../../../../../../base/common/actions.js';
import { IAction } from '../../../../../../base/common/actions.js';
import { Codicon } from '../../../../../../base/common/codicons.js';
import { combinedDisposable, Disposable, IDisposable } from '../../../../../../base/common/lifecycle.js';
import { autorun, constObservable, derived, derivedOpts, IObservable } from '../../../../../../base/common/observable.js';
Expand All @@ -26,7 +26,7 @@ import { IEditSessionEntryDiff } from '../../../common/editing/chatEditingServic
import { IChatRendererContent, IChatTurnPillsPart } from '../../../common/model/chatViewModel.js';
import { ChatTreeItem } from '../../chat.js';
import { IChatResponseFileChangesService, IChatResponseFileEdit } from '../../chatResponseFileChangesService.js';
import { diffStatsEqual, EMPTY_DIFF_STATS, IDiffStats, IPreviewFile, observeTurnStatusPillsEnabled, openChatTurnFile, previewFilesEqual, previewKind } from '../chatTurnPills.js';
import { diffStatsEqual, EMPTY_DIFF_STATS, IDiffStats, IPreviewFile, createTurnChangesPreviewActions, observeTurnStatusPillsEnabled, openChatTurnFile, previewFilesEqual, previewKind } from '../chatTurnPills.js';
import { renderChangesSummaryFileList } from './chatChangesSummaryPart.js';
import { ChatCollapsibleContentPart } from './chatCollapsibleContentPart.js';
import { IChatContentPart, IChatContentPartRenderContext } from './chatContentParts.js';
Expand Down Expand Up @@ -254,20 +254,11 @@ export class ChatTurnPillsContentPart extends Disposable implements IChatContent
}

/**
* Row actions for the changed-files list: previewable files get a labelless,
* icon-free action that opens the file.
* Row actions for the changed-files list: previewable files get an icon-only
* Preview action that opens the file.
*/
private _getRowActions(diff: IEditSessionEntryDiff): IAction[] {
const kind = previewKind(diff.modifiedURI);
if (!kind) {
return [];
}
const file: IPreviewFile = { uri: diff.modifiedURI, kind, created: isEqual(diff.originalURI, diff.modifiedURI) };
return [toAction({
id: 'chat.turnChanges.previewFile',
label: localize('chat.turnChanges.preview', "Preview"),
run: () => openChatTurnFile(file, this._openerService, this._configurationService),
})];
return createTurnChangesPreviewActions(diff.modifiedURI, diff.originalURI, this._openerService, this._configurationService);
}

hasSameContent(other: IChatRendererContent, _followingContent: IChatRendererContent[], _element: ChatTreeItem): boolean {
Expand Down
23 changes: 23 additions & 0 deletions src/vs/workbench/contrib/chat/browser/widget/chatTurnPills.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<IDiffStats>;
Expand Down
9 changes: 6 additions & 3 deletions src/vs/workbench/contrib/chat/browser/widget/media/chat.css
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -44,6 +44,60 @@ suite('ChatTurnPills', () => {
});
});

suite('createTurnChangesPreviewActions (#328520)', () => {
const openerService = new class extends mock<IOpenerService>() {
override async open(): Promise<boolean> {
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<IOpenerService>() {
override async open(resource: string | URI, options?: OpenInternalOptions | OpenExternalOptions): Promise<boolean> {
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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down