Skip to content
Merged
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
14 changes: 7 additions & 7 deletions src/vs/base/common/glob.ts
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,7 @@ export function isEmptyPattern(pattern: ParsedPattern | ParsedExpression): patte
return false;
}

function parsePattern(arg1: string | IRelativePattern, options: IGlobOptions): ParsedStringPattern {
function parsePattern(arg1: string | IRelativePattern, options: IGlobOptions, cacheKey?: string): ParsedStringPattern {
if (!arg1) {
return NULL;
}
Expand All @@ -359,7 +359,7 @@ function parsePattern(arg1: string | IRelativePattern, options: IGlobOptions): P
};

// Check cache
const patternKey = `${ignoreCase ? pattern.toLowerCase() : pattern}_${!!options.trimForExclusions}_${ignoreCase}`;
const patternKey = `${cacheKey === undefined ? `default:${ignoreCase ? pattern.toLowerCase() : pattern}` : `custom:${cacheKey}`}_${!!options.trimForExclusions}_${ignoreCase}`;
let parsedPattern = CACHE.get(patternKey);
if (parsedPattern) {
return wrapRelativePattern(parsedPattern, arg1, internalOptions);
Expand Down Expand Up @@ -462,7 +462,7 @@ function trivia3(pattern: string, options: IGlobOptionsInternal): ParsedStringPa
const parsedPatterns = aggregateBasenameMatches(pattern.slice(1, -1)
.split(',')
.map(pattern => parsePattern(pattern, options))
.filter(pattern => pattern !== NULL), pattern);
.filter(pattern => pattern !== NULL), pattern, options.ignoreCase);

const patternsLength = parsedPatterns.length;
if (!patternsLength) {
Expand Down Expand Up @@ -617,7 +617,7 @@ export function getPathTerms(patternOrExpression: ParsedPattern | ParsedExpressi
function parsedExpression(expression: IExpression, options: IGlobOptions): ParsedExpression {
const parsedPatterns = aggregateBasenameMatches(Object.getOwnPropertyNames(expression)
.map(pattern => parseExpressionPattern(pattern, expression[pattern], options))
.filter(pattern => pattern !== NULL));
.filter(pattern => pattern !== NULL), undefined, options.ignoreCase);

const patternsLength = parsedPatterns.length;
if (!patternsLength) {
Expand Down Expand Up @@ -750,7 +750,7 @@ function parseExpressionPattern(pattern: string, value: boolean | SiblingClause,
return NULL; // pattern is disabled
}

const parsedPattern = parsePattern(pattern, options);
const parsedPattern = parsePattern(pattern, options, pattern);
if (parsedPattern === NULL) {
return NULL;
}
Expand Down Expand Up @@ -786,7 +786,7 @@ function parseExpressionPattern(pattern: string, value: boolean | SiblingClause,
return parsedPattern;
}

function aggregateBasenameMatches(parsedPatterns: Array<ParsedStringPattern | ParsedExpressionPattern>, result?: string): Array<ParsedStringPattern | ParsedExpressionPattern> {
function aggregateBasenameMatches(parsedPatterns: Array<ParsedStringPattern | ParsedExpressionPattern>, result?: string, ignoreCase?: boolean): Array<ParsedStringPattern | ParsedExpressionPattern> {
const basenamePatterns = parsedPatterns.filter(parsedPattern => !!(<ParsedStringPattern>parsedPattern).basenames);
if (basenamePatterns.length < 2) {
return parsedPatterns;
Expand Down Expand Up @@ -830,7 +830,7 @@ function aggregateBasenameMatches(parsedPatterns: Array<ParsedStringPattern | Pa
basename = path.substring(i);
}

const index = basenames.indexOf(basename);
const index = ignoreCase ? basenames.findIndex(candidate => equalsIgnoreCase(candidate, basename)) : basenames.indexOf(basename);
return index !== -1 ? patterns[index] : null;
};

Expand Down
36 changes: 36 additions & 0 deletions src/vs/base/test/common/glob.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -779,6 +779,40 @@ suite('Glob', () => {
assert.strictEqual(glob.match(expr, 'foo/foo'), null);
});

test('expression with two basename globs ignores case', function () {
const expr = {
'**/BAR': true,
'**/BAZ': true
};

assert.strictEqual(glob.match(expr, 'bar', { ignoreCase: true }), '**/BAR');
assert.strictEqual(glob.match(expr, 'baz', { ignoreCase: true }), '**/BAZ');
assert.strictEqual(glob.match(expr, 'src/bar', { ignoreCase: true }), '**/BAR');
assert.strictEqual(glob.match(expr, 'bar'), null);
});

test('expression with cached basename globs ignores case', function () {
glob.parse('**/bar', { ignoreCase: true });

const expr = {
'**/BAR': true,
'**/BAZ': true
};

assert.strictEqual(glob.match(expr, 'BaR', { ignoreCase: true }), '**/BAR');
});

test('expression cache does not collide with string pattern cache', function () {
glob.parse('**/BAR', { ignoreCase: true });

const expr = {
'**/bar': true,
'**/baz': true
};

assert.strictEqual(glob.match(expr, 'bar', { ignoreCase: true }), '**/bar');
});

test('expression with two basename globs and a siblings expression', function () {
const expr = {
'**/bar': true,
Expand Down Expand Up @@ -1185,6 +1219,8 @@ suite('Glob', () => {
assertNoGlobMatch('{**/*.JS,**/*.TS}', 'bar/foo.js');
assertGlobMatch('{**/*.JS,**/*.TS}', 'bar/foo.ts', true);
assertGlobMatch('{**/*.JS,**/*.TS}', 'bar/foo.js', true);
assertNoGlobMatch('{**/BAR,**/BAZ}', 'bar');
assertGlobMatch('{**/BAR,**/BAZ}', 'bar', true);
// T4
assertNoGlobMatch('**/FOO/Bar', 'bar/foo/bar');
assertGlobMatch('**/FOO/Bar', 'bar/foo/bar', true);
Expand Down
2 changes: 1 addition & 1 deletion src/vs/sessions/SESSIONS_LIST.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ The Open Pull Request action shared by the session context menu and header uses

| Menu | Constant | Where it appears | Use for |
|------|----------|------------------|---------|
| `SessionSectionToolbar` | `SessionSectionToolbarMenuId` | Toolbar on section headers (Pinned, workspace groups, Done) | Section-scoped actions like the workspace `DropdownWithPrimaryActionViewItem` whose fixed primary action is "New Session" and whose dropdown contains actions contributed to `Menus.SessionSectionNewSession`, including the GitHub-backed "Create Session from Pull Request" action. When that menu is empty, the toolbar renders the ordinary "New Session" action. The toolbar also contains the selected "Archive All"/"Mark All as Done" action. The Done section restores/unarchives sessions individually (or via multi-selection) rather than with a section-wide action. Section headers also show a collapsible chevron on hover/focus; while a section action dropdown is open, both the toolbar and chevron remain visible. The chevron uses the same ghost icon hover background token as toolbar icon buttons. |
| `SessionSectionToolbar` | `SessionSectionToolbarMenuId` | Toolbar on section headers (Pinned, workspace groups, Done) | Section-scoped actions like the workspace `DropdownWithPrimaryActionViewItem` whose fixed primary action is "New Session" and whose dropdown contains actions contributed to `Menus.SessionSectionNewSession`, including the GitHub-backed "Create Session from Pull Request" action. When that menu is empty, the toolbar renders the ordinary "New Session" action. The toolbar also contains the selected "Archive All"/"Mark All as Done" action. The Done section restores/unarchives sessions individually (or via multi-selection) rather than with a section-wide action. Section headers also show a collapsible chevron on hover or keyboard focus; while a section action dropdown is open, both the toolbar and chevron remain visible. The chevron uses the same ghost icon hover background token as toolbar icon buttons. |

### Group Header Menu

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,6 @@
}

.new-session-prompt-option-title {
color: var(--vscode-descriptionForeground);
display: flex;
font-size: var(--vscode-agents-fontSize-body1);
font-weight: var(--vscode-agents-fontWeight-semiBold);
Expand Down Expand Up @@ -147,7 +146,7 @@
}

.new-session-prompt-option-description {
color: var(--vscode-agentsChatInput-placeholderForeground, var(--vscode-descriptionForeground));
color: var(--vscode-descriptionForeground);
font-size: var(--vscode-agents-fontSize-label1);
font-weight: var(--vscode-agents-fontWeight-regular);
grid-column: 1 / -1;
Expand Down
20 changes: 17 additions & 3 deletions src/vs/sessions/contrib/sessions/browser/media/sessionsList.css
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@
right: 0;
top: 0;
}

.actions-container {
gap: var(--vscode-spacing-size40);
}
}

.monaco-list-row .session-pending-voice-indicator {
Expand Down Expand Up @@ -522,6 +526,10 @@
display: none;
}

.session-section-toolbar .actions-container {
gap: var(--vscode-spacing-size40);
}

.session-section-toolbar .monaco-action-bar .action-label {
border-radius: var(--vscode-cornerRadius-small);
}
Expand All @@ -531,8 +539,8 @@
display: none;
color: var(--vscode-descriptionForeground);
font-size: var(--vscode-codiconFontSize-compact, 12px);
width: 22px;
height: 22px;
margin-right: 6px;
border-radius: var(--vscode-cornerRadius-small);
justify-content: center;
}
Expand Down Expand Up @@ -563,13 +571,19 @@
}

.monaco-list-row:hover .session-section .session-section-chevron.collapsible,
.monaco-list-row.focused .session-section .session-section-chevron.collapsible,
.sessions-list-control:not(.session-section-focus-from-pointer) .monaco-list:focus-within .monaco-list-row.focused .session-section .session-section-chevron.collapsible,
.monaco-list-row .session-section.dropdown-active .session-section-chevron.collapsible {
display: flex;
align-items: center;
}

.monaco-list-row .session-section .session-section-chevron.collapsible:hover {
.monaco-list-row:hover .session-section .session-section-chevron.collapsible + .session-section-icon,
.sessions-list-control:not(.session-section-focus-from-pointer) .monaco-list:focus-within .monaco-list-row.focused .session-section .session-section-chevron.collapsible + .session-section-icon,
.monaco-list-row .session-section.dropdown-active .session-section-chevron.collapsible + .session-section-icon {
display: none;
}

.session-section.dropdown-active .session-section-toolbar .monaco-dropdown-with-primary {
background-color: var(--vscode-toolbar-hoverBackground);
}

Expand Down
40 changes: 30 additions & 10 deletions src/vs/sessions/contrib/sessions/browser/views/sessionsList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,27 @@ function isSessionSection(item: SessionListItem): item is ISessionSection {
return !isSessionGroupItem(item) && 'sessions' in item && Array.isArray((item as ISessionSection).sessions);
}

function getSessionSectionIcon(sectionId: string): ThemeIcon | undefined {
switch (sectionId) {
case QUICK_CHATS_SECTION_ID:
return Codicon.commentDiscussion;
case 'pinned':
return Codicon.pinned;
case AUTOMATIONS_SECTION_ID:
return Codicon.watch;
case 'archived':
return Codicon.archive;
case 'recent':
return Codicon.history;
case 'older':
return Codicon.calendar;
default:
return sectionId.startsWith('workspace:')
? Codicon.folder
: undefined;
}
}

function isSessionShowMore(item: SessionListItem): item is ISessionShowMore {
return 'showMore' in item && (item as ISessionShowMore).showMore === true;
}
Expand Down Expand Up @@ -970,15 +991,15 @@ export class SessionSectionRenderer implements ITreeRenderer<SessionListItem, Fu
));

container.classList.add('session-section');
const chevron = DOM.append(container, $('span.session-section-chevron'));
chevron.setAttribute('aria-hidden', 'true');
const icon = DOM.append(container, $('span.session-section-icon'));
icon.setAttribute('aria-hidden', 'true');
const label = DOM.append(container, $('span.session-section-label'));
const statusIndicator = DOM.append(container, $('span.session-section-status-indicator'));
statusIndicator.setAttribute('aria-hidden', 'true');
const count = DOM.append(container, $('span.session-section-count'));
const toolbarContainer = DOM.append(container, $('.session-section-toolbar'));
const chevron = DOM.append(container, $('span.session-section-chevron'));
chevron.setAttribute('aria-hidden', 'true');

const contextKeyService = disposables.add(this.contextKeyService.createScoped(container));
const scopedInstantiationService = disposables.add(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, contextKeyService])));
Expand Down Expand Up @@ -1040,12 +1061,7 @@ export class SessionSectionRenderer implements ITreeRenderer<SessionListItem, Fu
template.container.classList.add('session-section-shortcut');
}

// Leading icon for the "Pinned" and "Chats" (quick chats) section headers.
// Templates are reused across rows, so recompute the icon every render.
const sectionIcon = element.id === QUICK_CHATS_SECTION_ID ? Codicon.commentDiscussion
: element.id === 'pinned' ? Codicon.pinned
: element.id === AUTOMATIONS_SECTION_ID ? Codicon.watch
: undefined;
const sectionIcon = getSessionSectionIcon(element.id);
template.icon.className = sectionIcon ? `session-section-icon ${ThemeIcon.asClassName(sectionIcon)}` : 'session-section-icon';
template.icon.style.display = sectionIcon ? '' : 'none';

Expand Down Expand Up @@ -1086,6 +1102,7 @@ export class SessionSectionRenderer implements ITreeRenderer<SessionListItem, Fu
}

this.updateChevron(template, node.collapsible, node.collapsed);
template.chevron.classList.toggle('collapsible', node.collapsible);

// Set context key for section type so toolbar actions can use when clauses
const sectionType = element.id.startsWith('workspace:') ? 'workspace' : element.id;
Expand Down Expand Up @@ -1186,11 +1203,14 @@ class SessionGroupRenderer implements ITreeRenderer<SessionListItem, FuzzyScore,
const disposables = new DisposableStore();

container.classList.add('session-section', 'session-group');
const chevron = DOM.append(container, $('span.session-section-chevron'));
chevron.setAttribute('aria-hidden', 'true');
const icon = DOM.append(container, $('span.session-section-icon'));
icon.classList.add(...ThemeIcon.asClassNameArray(Codicon.collection));
icon.setAttribute('aria-hidden', 'true');
const label = DOM.append(container, $('span.session-section-label'));
const inputContainer = DOM.append(container, $('.session-group-input'));
const toolbarContainer = DOM.append(container, $('.session-section-toolbar'));
const chevron = DOM.append(container, $('span.session-section-chevron'));
chevron.setAttribute('aria-hidden', 'true');

const contextKeyService = disposables.add(this.contextKeyService.createScoped(container));
const scopedInstantiationService = disposables.add(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, contextKeyService])));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@ import { AgentSessionApprovalModel } from './agentSessionApprovalModel.js';
import { FuzzyScore } from '../../../../../base/common/filters.js';
import { IMenuService, MenuId } from '../../../../../platform/actions/common/actions.js';
import { IChatSessionsService } from '../../common/chatSessionsService.js';
import { ICommandService } from '../../../../../platform/commands/common/commands.js';
import { ACTION_ID_NEW_CHAT } from '../actions/chatActions.js';
import { Emitter, Event } from '../../../../../base/common/event.js';
import { Disposable, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js';
import { Throttler } from '../../../../../base/common/async.js';
Expand Down Expand Up @@ -60,6 +58,7 @@ export interface IAgentSessionsControlOptions {
readonly itemHeight?: number;
readonly sectionHeight?: number;

createNewChat(): void;
getHoverPosition(): HoverPosition;
trackActiveEditorSession(): boolean;
collapseOlderSections?(): boolean;
Expand Down Expand Up @@ -116,7 +115,6 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo
@IContextKeyService private readonly contextKeyService: IContextKeyService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IChatSessionsService private readonly chatSessionsService: IChatSessionsService,
@ICommandService private readonly commandService: ICommandService,
@IMenuService private readonly menuService: IMenuService,
@IAgentSessionsService private readonly agentSessionsService: IAgentSessionsService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
Expand Down Expand Up @@ -566,7 +564,7 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo

this._register(list.onMouseDblClick(({ element }) => {
if (element === null) {
this.commandService.executeCommand(ACTION_ID_NEW_CHAT);
this.options.createNewChat();
}
}));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1357,20 +1357,21 @@ export class AICustomizationManagementEditor extends EditorPane {
isWorkspaceFile,
);
};
const renderSelectionCheckbox = (row: HTMLElement, customization: IPromptPath): void => {
const renderSelectionCheckbox = (row: HTMLElement, customization: IPromptPath, onSelectionChange?: () => void): void => {
const checkboxContainer = DOM.append(row, $('.item-sync-checkbox.prompt-migration-checkbox'));
const checkboxTitle = localize('customizationMigrationSelectAriaLabel', "Select {0}", customization.name ?? basename(customization.uri));
const checkbox = this.migrationPageDisposables.add(new Checkbox(checkboxTitle, this.isCustomizationSelectedForMigration(customization), defaultCheckboxStyles));
checkboxContainer.replaceChildren(checkbox.domNode);
this.migrationPageDisposables.add(checkbox.onChange(() => {
this.setCustomizationSelectedForMigration(customization, checkbox.checked);
this.updateCustomizationMigrationActionState();
onSelectionChange?.();
}));
};

const renderItem = (container: HTMLElement, customization: IPromptPath): void => {
const renderItem = (container: HTMLElement, customization: IPromptPath, onSelectionChange?: () => void): void => {
const row = DOM.append(container, $('div.ai-customization-list-item.prompt-migration-item'));
renderSelectionCheckbox(row, customization);
renderSelectionCheckbox(row, customization, onSelectionChange);

const itemLeft = DOM.append(row, $('span.item-left'));
const displayName = customization.name ?? basename(customization.uri);
Expand Down Expand Up @@ -1421,6 +1422,9 @@ export class AICustomizationManagementEditor extends EditorPane {
}
this.renderCustomizationMigrationPage();
}));
const updateGroupCheckboxState = (): void => {
groupCheckbox.checked = customizations.every(customization => this.isCustomizationSelectedForMigration(customization));
};
const groupToggle = DOM.append(groupHeader, $('button.prompt-migration-group-toggle')) as HTMLButtonElement;
groupToggle.type = 'button';
const groupId = `prompt-migration-group-${category.id}-${groupKey}`;
Expand Down Expand Up @@ -1455,7 +1459,7 @@ export class AICustomizationManagementEditor extends EditorPane {
}));

for (const customization of customizations) {
renderItem(groupItems, customization);
renderItem(groupItems, customization, updateGroupCheckboxState);
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -916,14 +916,16 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate {
const newSessionButtonContainer = this.sessionsNewButtonContainer = append(sessionsContainer, $('.agent-sessions-new-button-container'));
const newSessionButton = this._register(new Button(newSessionButtonContainer, { ...defaultButtonStyles, secondary: true }));
newSessionButton.label = localize('newSession', "New Session");
this._register(newSessionButton.onDidClick(() => this.commandService.executeCommand(ACTION_ID_NEW_CHAT, this.getActionsContext())));
const createNewChat = () => this.commandService.executeCommand(ACTION_ID_NEW_CHAT, this.getActionsContext());
this._register(newSessionButton.onDidClick(createNewChat));

// Sessions Control
this.sessionsControlContainer = append(sessionsContainer, $('.agent-sessions-control-container'));
const sessionsControl = this.sessionsControl = this._register(this.instantiationService.createInstance(AgentSessionsControl, this.sessionsControlContainer, {
source: 'chatViewPane',
filter: sessionsFilter,
overrideStyles: this.getLocationBasedColors().listOverrideStyles,
createNewChat,
getHoverPosition: () => this.getSessionHoverPosition(),
trackActiveEditorSession: () => {
return !this._widget || this._widget.isEmpty(); // only track and reveal if chat widget is empty
Expand Down
Loading
Loading