diff --git a/src/vs/platform/agentHost/test/node/agentHostPeerChatStore.test.ts b/src/vs/platform/agentHost/test/node/agentHostPeerChatStore.test.ts index 53b2de67f4caa..3b9e4b66f674b 100644 --- a/src/vs/platform/agentHost/test/node/agentHostPeerChatStore.test.ts +++ b/src/vs/platform/agentHost/test/node/agentHostPeerChatStore.test.ts @@ -1196,15 +1196,30 @@ suite('AgentHostPeerChatStore', () => { const { store, opened } = createPerResourceStore(); const sqlite = await (orchestrator as unknown as { _ensureDatabase(): Promise })._ensureDatabase(); let statements = 0; - sqlite.on('trace', (sql: string) => statements += sql.startsWith('-- TRIGGER') ? 0 : 1); + const drainSql = 'SELECT 1 AS drain'; + let traceDrained: DeferredPromise; + sqlite.on('trace', (sql: string) => { + if (sql === drainSql) { + traceDrained.complete(); + } else if (!sql.startsWith('-- TRIGGER')) { + statements++; + } + }); + const drain = async () => { + // Trace delivery and SQL completion use separate asynchronous callbacks. + traceDrained = new DeferredPromise(); + await Promise.all([ + traceDrained.p, + new Promise((resolve, reject) => sqlite.get(drainSql, error => error ? reject(error) : resolve())), + ]); + }; const measure = async (write: () => Promise) => { - // Traces of finished statements arrive before this one completes. - await new Promise((resolve, reject) => sqlite.get('SELECT 1', error => error ? reject(error) : resolve())); + await drain(); const before = { statements, opened: opened.length }; await write(); await store.whenIdle(); - await new Promise((resolve, reject) => sqlite.get('SELECT 1', error => error ? reject(error) : resolve())); - return { statements: statements - before.statements - 1, opened: opened.slice(before.opened) }; + await drain(); + return { statements: statements - before.statements, opened: opened.slice(before.opened) }; }; const firstWrite = await measure(() => store.setArchived(session, first, true)); const nextWrite = await measure(() => store.setArchived(session, first, false)); diff --git a/src/vs/sessions/contrib/github/browser/githubReferenceList.ts b/src/vs/sessions/contrib/github/browser/githubReferenceList.ts deleted file mode 100644 index d44e55dfae903..0000000000000 --- a/src/vs/sessions/contrib/github/browser/githubReferenceList.ts +++ /dev/null @@ -1,139 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import './media/githubReferenceList.css'; - -import { $, append } from '../../../../base/browser/dom.js'; -import { ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js'; -import { IAction, toAction } from '../../../../base/common/actions.js'; -import { Disposable } from '../../../../base/common/lifecycle.js'; -import { ThemeIcon } from '../../../../base/common/themables.js'; -import { asCssVariable } from '../../../../platform/theme/common/colorUtils.js'; - -/** One row of a GitHub reference list. */ -export interface IGitHubReferenceListEntry { - readonly number: number; - readonly title: string | undefined; - readonly icon: ThemeIcon; - readonly ariaLabel?: string; - /** Actions shown at the trailing edge of the row, e.g. copying its link. */ - readonly toolbarActions?: readonly IAction[]; -} - -interface IGitHubReferenceListRow { - entry: T; - readonly item: HTMLElement; - readonly button: HTMLButtonElement; - readonly icon: HTMLElement; - readonly number: HTMLElement; - readonly title: HTMLElement; - readonly actionBar: ActionBar; - /** The presentation of the rendered actions, so they are only re-rendered when it changes. */ - actionsKey: string; -} - -/** A GitHub reference list whose rows can update without replacing focused buttons. */ -export class GitHubReferenceList extends Disposable { - - readonly element = $('.sessions-github-reference-list', { role: 'list' }); - private readonly _rows: IGitHubReferenceListRow[] = []; - - constructor(entries: readonly T[], private readonly _onDidSelect: (entry: T) => void) { - super(); - this.update(entries); - } - - update(entries: readonly T[]): void { - const numberDigits = entries.reduce((max, entry) => Math.max(max, entry.number.toString().length), 0); - - for (let index = 0; index < entries.length; index++) { - const entry = entries[index]; - const row = this._rows[index] ?? this._createRow(entry); - row.entry = entry; - this._updateRow(row, numberDigits); - } - - for (let index = this._rows.length - 1; index >= entries.length; index--) { - this._store.delete(this._rows[index].actionBar); - this._rows[index].item.remove(); - this._rows.splice(index, 1); - } - } - - private _createRow(entry: T): IGitHubReferenceListRow { - const item = append(this.element, $('.sessions-github-reference-list-item', { role: 'listitem' })); - const button = append(item, document.createElement('button')); - button.className = 'sessions-github-reference-list-entry'; - button.type = 'button'; - const row: IGitHubReferenceListRow = { - entry, - item, - button, - icon: append(button, $('span.sessions-github-reference-list-entry-icon', { 'aria-hidden': 'true' })), - number: append(button, $('span.sessions-github-reference-list-entry-number')), - title: append(button, $('span.sessions-github-reference-list-entry-title')), - actionBar: this._register(new ActionBar(append(item, $('.sessions-github-reference-list-entry-actions')))), - actionsKey: '', - }; - button.onclick = event => { - event.preventDefault(); - event.stopPropagation(); - this._onDidSelect(row.entry); - }; - this._rows.push(row); - return row; - } - - private _updateRow(row: IGitHubReferenceListRow, numberDigits: number): void { - const entry = row.entry; - if (entry.ariaLabel) { - row.button.setAttribute('aria-label', entry.ariaLabel); - } else { - row.button.removeAttribute('aria-label'); - } - - row.icon.className = `sessions-github-reference-list-entry-icon ${ThemeIcon.asClassName(entry.icon)}`; - if (entry.icon.color) { - row.icon.style.color = asCssVariable(entry.icon.color.id); - } else { - row.icon.style.removeProperty('color'); - } - - row.number.textContent = `#${entry.number}`; - row.number.style.width = `calc(${numberDigits}ch + 1em)`; - row.title.textContent = entry.title ?? ''; - row.title.title = entry.title ?? ''; - row.title.hidden = !entry.title; - - this._updateRowActions(row); - } - - /** - * Renders the row's actions, keeping the rendered buttons as long as their presentation - * is unchanged so a focused action survives a state update. The rendered actions run - * against the row's current entry rather than the one they were rendered for. - */ - private _updateRowActions(row: IGitHubReferenceListRow): void { - const actions = row.entry.toolbarActions ?? []; - const actionsKey = actions.map(action => `${action.id}\u0000${action.label}\u0000${action.tooltip}\u0000${action.class}\u0000${action.enabled}\u0000${action.checked}`).join('\u0001'); - if (row.actionsKey === actionsKey) { - return; - } - - row.actionsKey = actionsKey; - row.actionBar.clear(); - if (actions.length) { - row.actionBar.push(actions.map((action, index) => toAction({ - id: action.id, - label: action.label, - tooltip: action.tooltip, - class: action.class, - enabled: action.enabled, - checked: action.checked, - run: (...args: unknown[]) => row.entry.toolbarActions?.[index]?.run(...args), - })), { icon: true, label: false }); - } - } -} diff --git a/src/vs/sessions/contrib/github/browser/media/githubReferenceList.css b/src/vs/sessions/contrib/github/browser/media/githubReferenceList.css deleted file mode 100644 index cfa39cb8b2a77..0000000000000 --- a/src/vs/sessions/contrib/github/browser/media/githubReferenceList.css +++ /dev/null @@ -1,76 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -.sessions-github-reference-list { - box-sizing: border-box; - display: flex; - flex-direction: column; - min-width: 260px; - max-width: 420px; - padding: 0; -} - -.sessions-github-reference-list-item { - display: flex; - align-items: center; - gap: var(--vscode-spacing-size40); - padding-right: var(--vscode-spacing-size40); -} - -.sessions-github-reference-list-entry { - display: flex; - flex: 1; - align-items: center; - gap: var(--vscode-spacing-size80); - min-width: 0; - padding: var(--vscode-spacing-size40) var(--vscode-spacing-size120); - border: none; - border-radius: var(--vscode-cornerRadius-small); - background: none; - color: var(--vscode-editorHoverWidget-foreground); - font-family: inherit; - font-size: var(--vscode-fontSize-body1); - line-height: 1.4; - text-align: left; - cursor: pointer; -} - -.sessions-github-reference-list-entry:hover, -.sessions-github-reference-list-entry:focus { - background-color: var(--vscode-list-hoverBackground); - outline: none; -} - -.sessions-github-reference-list-entry:focus-visible { - outline: var(--vscode-strokeThickness) solid var(--vscode-focusBorder); - outline-offset: -1px; -} - -.sessions-github-reference-list-entry-icon { - flex-shrink: 0; -} - -.sessions-github-reference-list-entry-number { - flex-shrink: 0; - color: var(--vscode-descriptionForeground); - font-variant-numeric: tabular-nums; - text-align: right; -} - -.sessions-github-reference-list-entry-title { - min-width: 0; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; -} - -/* Entry actions stay visible so the hover's focus trap can reach them by keyboard */ -.sessions-github-reference-list-entry-actions { - flex-shrink: 0; -} - -.sessions-github-reference-list-entry-actions .monaco-action-bar .action-label:not(.disabled):hover { - background-color: var(--vscode-toolbar-hoverBackground); -} diff --git a/src/vs/sessions/contrib/github/test/browser/githubContribution.test.ts b/src/vs/sessions/contrib/github/test/browser/githubContribution.test.ts index f353c160376e4..0505e509ba024 100644 --- a/src/vs/sessions/contrib/github/test/browser/githubContribution.test.ts +++ b/src/vs/sessions/contrib/github/test/browser/githubContribution.test.ts @@ -4,13 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; -import { toAction } from '../../../../../base/common/actions.js'; import { Codicon } from '../../../../../base/common/codicons.js'; import { Emitter, Event } from '../../../../../base/common/event.js'; import { IMarkdownString } from '../../../../../base/common/htmlContent.js'; import { DisposableStore, IDisposable, ImmortalReference, IReference, toDisposable } from '../../../../../base/common/lifecycle.js'; import { constObservable, IObservable, ISettableObservable, observableValue } from '../../../../../base/common/observable.js'; -import { ThemeIcon } from '../../../../../base/common/themables.js'; import { NullLogService } from '../../../../../platform/log/common/log.js'; import { Extensions as ConfigurationExtensions, IConfigurationRegistry } from '../../../../../platform/configuration/common/configurationRegistry.js'; import { Registry } from '../../../../../platform/registry/common/platform.js'; @@ -24,173 +22,11 @@ import { mock } from '../../../../../base/test/common/mock.js'; import '../../../../../workbench/contrib/chat/browser/agentSessionsConfiguration.js'; import { AUTO_DELETE_MARKED_AS_DONE_MERGED_SESSIONS_AFTER_DAYS_SETTING, AUTO_MARK_AS_DONE_MERGED_SESSIONS_AFTER_DAYS_SETTING, GitHubPullRequestPollingContribution } from '../../browser/github.contribution.js'; import { AUTOMATIC_MERGED_SESSION_CLEANUP_SETTINGS_QUERY, AUTOMATIC_MERGED_SESSION_CLEANUP_SETTINGS_TAG } from '../../common/sessionLifecycleSettings.js'; -import { GitHubReferenceList, IGitHubReferenceListEntry } from '../../browser/githubReferenceList.js'; import { IGitHubService } from '../../browser/githubService.js'; import { ChatInteractivity, IChat, IGitHubInfo, ISession, ISessionArtifact, ISessionCapabilities, ISessionChangeset, IChatCheckpoints, ISessionFileChange, ISessionWorkspace, SessionArtifactKind, SessionStatus } from '../../../../services/sessions/common/session.js'; import { IActiveSession, ISessionsChangeEvent, ISessionsManagementService } from '../../../../services/sessions/common/sessionsManagement.js'; import { ISessionsService } from '../../../../services/sessions/browser/sessionsService.js'; -suite('GitHubReferenceList', () => { - - const disposables = ensureNoDisposablesAreLeakedInTestSuite(); - - test('updates rows in place so focus is preserved', () => { - const list = disposables.add(new GitHubReferenceList([{ - number: 12345, - title: undefined, - icon: Codicon.gitPullRequest, - ariaLabel: 'Pull Request #12345', - }, { - number: 1, - title: 'Short number', - icon: Codicon.gitPullRequest, - ariaLabel: 'Pull Request #1: Short number', - }], () => { })); - document.body.appendChild(list.element); - - try { - const button = list.element.querySelector('button')!; - const initialNumberWidth = button.querySelector('.sessions-github-reference-list-entry-number')!.style.width; - button.focus(); - - list.update([{ - number: 1, - title: 'Updated title', - icon: Codicon.gitPullRequestDraft, - ariaLabel: 'Draft Pull Request #1: Updated title', - }]); - - assert.deepStrictEqual({ - sameButton: list.element.querySelector('button') === button, - focused: document.activeElement === button, - text: button.textContent, - ariaLabel: button.getAttribute('aria-label'), - iconClasses: [...button.querySelector('.sessions-github-reference-list-entry-icon')!.classList], - initialNumberWidth, - numberWidth: button.querySelector('.sessions-github-reference-list-entry-number')!.style.width, - }, { - sameButton: true, - focused: true, - text: '#1Updated title', - ariaLabel: 'Draft Pull Request #1: Updated title', - iconClasses: ['sessions-github-reference-list-entry-icon', 'codicon', 'codicon-git-pull-request-draft'], - initialNumberWidth: 'calc(5ch + 1em)', - numberWidth: 'calc(1ch + 1em)', - }); - } finally { - list.element.remove(); - } - }); - - test('renders the entry actions in an action bar that does not select the row', () => { - const events: string[] = []; - const copyAction = (target: string) => toAction({ - id: 'test.copyLink', - label: 'Copy Pull Request Link', - class: ThemeIcon.asClassName(Codicon.copy), - run: () => events.push(`copy:${target}`), - }); - const list = disposables.add(new GitHubReferenceList([{ - number: 1, - title: 'Fix the thing', - icon: Codicon.gitPullRequest, - toolbarActions: [copyAction('first')], - }], () => events.push('select'))); - document.body.appendChild(list.element); - - try { - const actionLabel = list.element.querySelector('.sessions-github-reference-list-entry-actions .action-label')!; - actionLabel.focus(); - - // A state update keeps the focused action, but it runs against the latest entry. - list.update([{ - number: 1, - title: 'Fix the thing', - icon: Codicon.gitPullRequestDraft, - toolbarActions: [copyAction('second')], - }]); - actionLabel.click(); - - assert.deepStrictEqual({ - events, - sameAction: list.element.querySelector('.sessions-github-reference-list-entry-actions .action-label') === actionLabel, - focused: document.activeElement === actionLabel, - ariaLabel: actionLabel.getAttribute('aria-label'), - iconClasses: [...actionLabel.classList], - }, { - events: ['copy:second'], - sameAction: true, - focused: true, - ariaLabel: 'Copy Pull Request Link', - iconClasses: ['action-label', 'codicon', 'codicon-copy'], - }); - } finally { - list.element.remove(); - } - }); - - test('row action toolbar preserves tooltip/enablement/checked presentation', () => { - const events: string[] = []; - const copyAction = (target: string, enabled: boolean, checked: boolean, tooltip: string) => toAction({ - id: 'test.copyLink', - label: 'Copy Pull Request Link', - tooltip, - enabled, - checked, - class: ThemeIcon.asClassName(Codicon.copy), - run: () => events.push(`copy:${target}`), - }); - const list = disposables.add(new GitHubReferenceList([{ - number: 1, - title: 'Fix the thing', - icon: Codicon.gitPullRequest, - toolbarActions: [copyAction('first', false, false, 'Cannot copy')], - }], () => events.push('select'))); - document.body.appendChild(list.element); - - try { - const actionLabel = list.element.querySelector('.sessions-github-reference-list-entry-actions .action-label')!; - actionLabel.click(); - const beforeUpdate = { - events: [...events], - ariaDisabled: actionLabel.getAttribute('aria-disabled'), - ariaLabel: actionLabel.getAttribute('aria-label'), - checkedClass: actionLabel.classList.contains('checked'), - }; - - list.update([{ - number: 1, - title: 'Fix the thing', - icon: Codicon.gitPullRequest, - toolbarActions: [copyAction('second', true, true, 'Copy pull request URL')], - }]); - - const updatedActionLabel = list.element.querySelector('.sessions-github-reference-list-entry-actions .action-label')!; - updatedActionLabel.click(); - assert.deepStrictEqual({ - beforeUpdate, - events, - ariaDisabled: updatedActionLabel.getAttribute('aria-disabled'), - ariaLabel: updatedActionLabel.getAttribute('aria-label'), - checkedClass: updatedActionLabel.classList.contains('checked'), - }, { - beforeUpdate: { - events: [], - ariaDisabled: 'true', - ariaLabel: 'Cannot copy', - checkedClass: false, - }, - events: ['copy:second'], - ariaDisabled: null, - ariaLabel: 'Copy pull request URL', - checkedClass: true, - }); - } finally { - list.element.remove(); - } - }); -}); - suite('GitHubPullRequestPollingContribution', () => { // Capture registrations before configuration registry tests clear the global registry.