diff --git a/src/vs/sessions/contrib/changes/browser/changesView.ts b/src/vs/sessions/contrib/changes/browser/changesView.ts index 5b5d05751cf..6c50fec1c5c 100644 --- a/src/vs/sessions/contrib/changes/browser/changesView.ts +++ b/src/vs/sessions/contrib/changes/browser/changesView.ts @@ -12,6 +12,7 @@ import { IObjectTreeElement, ITreeNode } from '../../../../base/browser/ui/tree/ import { Codicon } from '../../../../base/common/codicons.js'; import { Iterable } from '../../../../base/common/iterator.js'; import { Disposable, DisposableStore, MutableDisposable } from '../../../../base/common/lifecycle.js'; +import { Event } from '../../../../base/common/event.js'; import { autorun, constObservable, derived, derivedOpts, IObservable, IObservableWithChange, ISettableObservable, ObservablePromise, observableSignalFromEvent, observableValue, runOnChange } from '../../../../base/common/observable.js'; import { basename, dirname } from '../../../../base/common/path.js'; import { extUriBiasedIgnorePathCase, isEqual } from '../../../../base/common/resources.js'; @@ -62,6 +63,10 @@ import { IGitHubService } from '../../github/browser/githubService.js'; import { CIStatusWidget } from './ciStatusWidget.js'; import { arrayEqualsC } from '../../../../base/common/equals.js'; import { GITHUB_REMOTE_FILE_SCHEME } from '../../sessions/common/sessionData.js'; +import { Orientation } from '../../../../base/browser/ui/sash/sash.js'; +import { IView, Sizing, SplitView } from '../../../../base/browser/ui/splitview/splitview.js'; +import { Color } from '../../../../base/common/color.js'; +import { PANEL_SECTION_BORDER } from '../../../../workbench/common/theme.js'; const $ = dom.$; @@ -302,6 +307,8 @@ export class ChangesViewPane extends ViewPane { private bodyContainer: HTMLElement | undefined; private welcomeContainer: HTMLElement | undefined; + private filesHeaderNode: HTMLElement | undefined; + private filesCountBadge: HTMLElement | undefined; private contentContainer: HTMLElement | undefined; private overviewContainer: HTMLElement | undefined; private summaryContainer: HTMLElement | undefined; @@ -311,6 +318,8 @@ export class ChangesViewPane extends ViewPane { private tree: WorkbenchCompressibleObjectTree | undefined; private ciStatusWidget: CIStatusWidget | undefined; + private splitView: SplitView | undefined; + private splitViewContainer: HTMLElement | undefined; private readonly renderDisposables = this._register(new DisposableStore()); @@ -339,7 +348,7 @@ export class ChangesViewPane extends ViewPane { @ICodeReviewService private readonly codeReviewService: ICodeReviewService, @IGitHubService private readonly gitHubService: IGitHubService, ) { - super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService); + super({ ...options, titleMenuId: MenuId.ChatEditingSessionTitleToolbar }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService); this.viewModel = this.instantiationService.createInstance(ChangesViewModel); this._register(this.viewModel); @@ -394,8 +403,11 @@ export class ChangesViewPane extends ViewPane { // Actions container - positioned outside and above the card this.actionsContainer = dom.append(this.bodyContainer, $('.chat-editing-session-actions.outside-card')); - // Main container with file icons support (the "card") - this.contentContainer = dom.append(this.bodyContainer, $('.chat-editing-session-container.show-file-icons')); + // SplitView container for resizable file tree / CI checks split + this.splitViewContainer = dom.append(this.bodyContainer, $('.changes-splitview-container')); + + // Main container with file icons support (the "card") — top pane + this.contentContainer = dom.append(this.splitViewContainer, $('.chat-editing-session-container.show-file-icons')); this._register(createFileIconThemableTreeContainerScope(this.contentContainer, this.themeService)); // Toggle class based on whether the file icon theme has file icons @@ -405,6 +417,12 @@ export class ChangesViewPane extends ViewPane { updateHasFileIcons(); this._register(this.themeService.onDidFileIconThemeChange(updateHasFileIcons)); + // Files header + this.filesHeaderNode = dom.append(this.contentContainer, $('.changes-files-header')); + const filesTitle = dom.append(this.filesHeaderNode, $('.changes-files-title')); + filesTitle.textContent = localize('changesView.filesTitle', "Changed Files"); + this.filesCountBadge = dom.append(this.filesHeaderNode, $('.changes-files-count')); + // Overview section (header with summary only - actions moved outside card) this.overviewContainer = dom.append(this.contentContainer, $('.chat-editing-session-overview')); this.summaryContainer = dom.append(this.overviewContainer, $('.changes-summary')); @@ -412,9 +430,73 @@ export class ChangesViewPane extends ViewPane { // List container this.listContainer = dom.append(this.contentContainer, $('.chat-editing-session-list')); - // CI Status widget beneath the card - this.ciStatusWidget = this._register(this.instantiationService.createInstance(CIStatusWidget, this.bodyContainer)); - this._register(this.ciStatusWidget.onDidChangeHeight(() => this.layoutTree())); + // CI Status widget — bottom pane + this.ciStatusWidget = this._register(this.instantiationService.createInstance(CIStatusWidget, this.splitViewContainer)); + + // Create SplitView + this.splitView = this._register(new SplitView(this.splitViewContainer, { + orientation: Orientation.VERTICAL, + proportionalLayout: false, + })); + + // Shared constants for pane sizing + const ciMarginTop = 8; // matches margin-top on .ci-status-widget + const ciMinHeight = CIStatusWidget.HEADER_HEIGHT + CIStatusWidget.MIN_BODY_HEIGHT + ciMarginTop; + + // Top pane: file tree + const self = this; + const treePane: IView = { + element: this.contentContainer, + minimumSize: ciMinHeight, + maximumSize: Number.POSITIVE_INFINITY, + onDidChange: Event.None, + layout: (height) => { + self.contentContainer!.style.height = `${height}px`; + self._layoutTreeInPane(height); + }, + }; + + // Bottom pane: CI checks + const ciElement = this.ciStatusWidget.element; + const ciWidget = this.ciStatusWidget; + const ciPane: IView = { + element: ciElement, + minimumSize: ciMinHeight, + maximumSize: Number.POSITIVE_INFINITY, + onDidChange: Event.map(this.ciStatusWidget.onDidChangeHeight, () => undefined), + layout: (height) => { + const innerHeight = Math.max(0, height - ciMarginTop); + ciElement.style.height = `${innerHeight}px`; + const bodyHeight = Math.max(0, innerHeight - CIStatusWidget.HEADER_HEIGHT); + ciWidget.layout(bodyHeight); + }, + }; + + this.splitView.addView(treePane, Sizing.Distribute, 0, true); + this.splitView.addView(ciPane, CIStatusWidget.HEADER_HEIGHT + CIStatusWidget.MIN_BODY_HEIGHT + ciMarginTop, 1, true); + + // Style the sash as a visible separator between sections + const updateSplitViewStyles = () => { + const borderColor = this.themeService.getColorTheme().getColor(PANEL_SECTION_BORDER); + this.splitView!.style({ separatorBorder: borderColor ?? Color.transparent }); + }; + updateSplitViewStyles(); + this._register(this.themeService.onDidColorThemeChange(updateSplitViewStyles)); + + // Initially hide CI pane until checks arrive + this.splitView.setViewVisible(1, false); + + this._register(this.ciStatusWidget.onDidChangeHeight(() => { + if (!this.splitView || !this.ciStatusWidget) { + return; + } + const visible = this.ciStatusWidget.visible; + const isCurrentlyVisible = this.splitView.isViewVisible(1); + if (visible !== isCurrentlyVisible) { + this.splitView.setViewVisible(1, visible); + } + this.layoutSplitView(); + })); this._register(this.onDidChangeBodyVisibility(visible => { if (visible) { @@ -731,14 +813,19 @@ export class ChangesViewPane extends ViewPane { })); } - // Update visibility based on entries + // Update visibility and file count badge based on entries this.renderDisposables.add(autorun(reader => { const { files } = topLevelStats.read(reader); const hasEntries = files > 0; dom.setVisibility(hasEntries, this.contentContainer!); dom.setVisibility(hasEntries, this.actionsContainer!); + dom.setVisibility(hasEntries, this.splitViewContainer!); dom.setVisibility(!hasEntries, this.welcomeContainer!); + + if (this.filesCountBadge) { + this.filesCountBadge.textContent = `${files}`; + } })); // Update summary text (line counts only, file count is shown in badge) @@ -814,7 +901,7 @@ export class ChangesViewPane extends ViewPane { const tree = this.tree; // Re-layout when collapse state changes so the card height adjusts - this.renderDisposables.add(tree.onDidChangeContentHeight(() => this.layoutTree())); + this.renderDisposables.add(tree.onDidChangeContentHeight(() => this.layoutSplitView())); const openFileItem = (item: IChangesFileItem, items: IChangesFileItem[], sideBySide: boolean) => { const { uri: modifiedFileUri, originalUri, isDeletion } = item; @@ -921,85 +1008,45 @@ export class ChangesViewPane extends ViewPane { this.tree.setChildren(null, listChildren); } - this.layoutTree(); + this.layoutSplitView(); })); } - private layoutTree(): void { - if (!this.tree || !this.listContainer) { + /** Layout the tree within its SplitView pane. */ + private _layoutTreeInPane(paneHeight: number): void { + if (!this.tree) { return; } + // Subtract overview/padding within the content container + const overviewHeight = this.overviewContainer?.offsetHeight ?? 0; + const filesHeaderHeight = this.filesHeaderNode?.offsetHeight ?? 0; + const treeHeight = Math.max(0, paneHeight - filesHeaderHeight - overviewHeight); + this.tree.layout(treeHeight, this.currentBodyWidth); + this.tree.getHTMLElement().style.height = `${treeHeight}px`; + } - // Calculate remaining height for the tree by subtracting other elements + /** Layout the SplitView to fill available body space. */ + private layoutSplitView(): void { + if (!this.splitView) { + return; + } const bodyHeight = this.currentBodyHeight; if (bodyHeight <= 0) { return; } - - // Measure non-list elements height (padding, actions, overview) const bodyPadding = 16; // 8px top + 8px bottom from .changes-view-body const actionsHeight = this.actionsContainer?.offsetHeight ?? 0; - const actionsMargin = actionsHeight > 0 ? 8 : 0; // margin-bottom on actions container - const overviewHeight = this.overviewContainer?.offsetHeight ?? 0; - const containerPadding = 8; // 4px top + 4px bottom from .chat-editing-session-container - const containerBorder = 2; // 1px top + 1px bottom border - - const fixedUsed = bodyPadding + actionsHeight + actionsMargin + overviewHeight + containerPadding + containerBorder; - - // Determine CI widget space needs - const ciWidget = this.ciStatusWidget; - const ciVisible = ciWidget?.visible ?? false; - const ciHeaderHeight = ciVisible ? CIStatusWidget.HEADER_HEIGHT : 0; - const ciMargin = ciVisible ? 8 : 0; // margin-top on CI widget - const ciDesiredHeight = ciWidget?.desiredHeight ?? 0; - - const spaceForTreeAndCI = Math.max(0, bodyHeight - fixedUsed - ciMargin); - - // Give the tree priority, then CI gets the rest (with min/max on CI body) - const treeContentHeight = this.tree.contentHeight; - let treeHeight: number; - let ciBodyHeight = 0; - - if (!ciVisible) { - treeHeight = Math.min(spaceForTreeAndCI, treeContentHeight); - } else { - // Reserve space for the CI header - const spaceAfterCIHeader = Math.max(0, spaceForTreeAndCI - ciHeaderHeight); - - // Give the tree what it needs first, up to available space - treeHeight = Math.min(spaceAfterCIHeader, treeContentHeight); - - // Remaining goes to CI body - const remainingForCIBody = Math.max(0, spaceAfterCIHeader - treeHeight); - const ciDesiredBodyHeight = Math.max(0, ciDesiredHeight - ciHeaderHeight); - - ciBodyHeight = Math.min(ciDesiredBodyHeight, remainingForCIBody); - - // Ensure CI body gets at least MIN_BODY_HEIGHT if there's content - if (ciDesiredBodyHeight > 0 && ciBodyHeight < CIStatusWidget.MIN_BODY_HEIGHT) { - const minCIBody = Math.min(CIStatusWidget.MIN_BODY_HEIGHT, ciDesiredBodyHeight); - const needed = minCIBody - ciBodyHeight; - const canTake = Math.max(0, treeHeight - 0); // tree can shrink to 0 - const taken = Math.min(needed, canTake); - treeHeight -= taken; - ciBodyHeight += taken; - } - - // Cap CI body at MAX_BODY_HEIGHT - ciBodyHeight = Math.min(ciBodyHeight, CIStatusWidget.MAX_BODY_HEIGHT); - - ciWidget!.layout(ciBodyHeight); - } - - this.tree.layout(treeHeight, this.currentBodyWidth); - this.tree.getHTMLElement().style.height = `${treeHeight}px`; + const actionsMargin = actionsHeight > 0 ? 8 : 0; + const availableHeight = Math.max(0, bodyHeight - bodyPadding - actionsHeight - actionsMargin); + this.splitViewContainer!.style.height = `${availableHeight}px`; + this.splitView.layout(availableHeight); } protected override layoutBody(height: number, width: number): void { super.layoutBody(height, width); this.currentBodyHeight = height; this.currentBodyWidth = width; - this.layoutTree(); + this.layoutSplitView(); } override focus(): void { diff --git a/src/vs/sessions/contrib/changes/browser/ciStatusWidget.ts b/src/vs/sessions/contrib/changes/browser/ciStatusWidget.ts index 9b81b8bf26c..9d0923012e2 100644 --- a/src/vs/sessions/contrib/changes/browser/ciStatusWidget.ts +++ b/src/vs/sessions/contrib/changes/browser/ciStatusWidget.ts @@ -40,7 +40,7 @@ interface ICICheckCounts { } class CICheckListDelegate implements IListVirtualDelegate { - static readonly ITEM_HEIGHT = 24; + static readonly ITEM_HEIGHT = 28; getHeight(_element: ICICheckListItem): number { return CICheckListDelegate.ITEM_HEIGHT; @@ -136,16 +136,16 @@ class CICheckListRenderer implements IListRenderer; private readonly _labels: ResourceLabels; @@ -154,7 +154,6 @@ export class CIStatusWidget extends Disposable { private readonly _onDidChangeHeight = this._register(new Emitter()); readonly onDidChangeHeight = this._onDidChangeHeight.event; - private _collapsed = true; private _checkCount = 0; private _model: GitHubPullRequestCIModel | undefined; private _sessionResource: URI | undefined; @@ -168,9 +167,6 @@ export class CIStatusWidget extends Disposable { if (this._checkCount === 0) { return 0; } - if (this._collapsed) { - return CIStatusWidget.HEADER_HEIGHT; - } return CIStatusWidget.HEADER_HEIGHT + this._checkCount * CICheckListDelegate.ITEM_HEIGHT; } @@ -194,22 +190,18 @@ export class CIStatusWidget extends Disposable { // Header (always visible) this._headerNode = dom.append(this._domNode, $('.ci-status-widget-header')); this._titleNode = dom.append(this._headerNode, $('.ci-status-widget-title')); - this._titleLabel = this._register(this._labels.create(this._titleNode, { supportIcons: true })); + this._titleLabelNode = dom.append(this._titleNode, $('.ci-status-widget-title-label')); + this._titleLabelNode.textContent = localize('ci.checksLabel', "PR Checks"); + this._countsNode = dom.append(this._titleNode, $('.ci-status-widget-counts')); this._headerActionBarContainer = dom.append(this._headerNode, $('.ci-status-widget-header-actions')); this._headerActionBar = this._register(new ActionBar(this._headerActionBarContainer)); - this._headerActionBarContainer.style.display = 'none'; this._register(dom.addDisposableListener(this._headerActionBarContainer, dom.EventType.CLICK, e => { e.preventDefault(); e.stopPropagation(); })); - this._twistieNode = dom.append(this._headerNode, $('.ci-status-widget-twistie')); - this._updateTwistie(); - this._register(dom.addDisposableListener(this._headerNode, 'click', () => this._toggle())); - - // Body (collapsible list of checks) + // Body (list of checks) this._bodyNode = dom.append(this._domNode, $('.ci-status-widget-body')); - this._bodyNode.style.display = 'none'; const listContainer = $('.ci-status-widget-list'); this._list = this._register(this._instantiationService.createInstance( @@ -278,31 +270,35 @@ export class CIStatusWidget extends Disposable { }); } - private _toggle(): void { - this._collapsed = !this._collapsed; - this._bodyNode.style.display = this._collapsed ? 'none' : ''; - this._updateTwistie(); - this._onDidChangeHeight.fire(); - } + private _renderHeader(checks: readonly IGitHubCICheck[], _overallStatus: GitHubCIOverallStatus): void { + const counts = getCheckCounts(checks); - private _updateTwistie(): void { - dom.clearNode(this._twistieNode); - this._twistieNode.appendChild(renderIcon(this._collapsed ? Codicon.chevronRight : Codicon.chevronDown)); - } + // Update count badges + dom.clearNode(this._countsNode); - private _renderHeader(checks: readonly IGitHubCICheck[], overallStatus: GitHubCIOverallStatus): void { - const { icon, className } = getHeaderIconAndClass(checks, overallStatus); - this._titleNode.className = `ci-status-widget-title ${className}`; + if (counts.running > 0) { + const badge = dom.append(this._countsNode, $('.ci-status-widget-count-badge.ci-status-running')); + badge.appendChild(renderIcon(Codicon.circleFilled)); + dom.append(badge, $('span')).textContent = `${counts.running}`; + } - const summary = getChecksSummary(checks); - const title = localize('ci.headerTitle', "Checks: {0}", summary); - this._titleLabel.setResource({ - name: title, - resource: URI.from({ scheme: 'github-checks', path: '/summary' }), - }, { - icon: icon, - title, - }); + if (counts.failed > 0) { + const badge = dom.append(this._countsNode, $('.ci-status-widget-count-badge.ci-status-failure')); + badge.appendChild(renderIcon(Codicon.error)); + dom.append(badge, $('span')).textContent = `${counts.failed}`; + } + + if (counts.pending > 0) { + const badge = dom.append(this._countsNode, $('.ci-status-widget-count-badge.ci-status-pending')); + badge.appendChild(renderIcon(Codicon.circleFilled)); + dom.append(badge, $('span')).textContent = `${counts.pending}`; + } + + if (counts.successful > 0) { + const badge = dom.append(this._countsNode, $('.ci-status-widget-count-badge.ci-status-success')); + badge.appendChild(renderIcon(Codicon.passFilled)); + dom.append(badge, $('span')).textContent = `${counts.successful}`; + } } private _renderHeaderActions(failedChecks: readonly IGitHubCICheck[]): void { @@ -310,7 +306,7 @@ export class CIStatusWidget extends Disposable { this._headerActionBar.clear(); if (failedChecks.length === 0) { - this._headerActionBarContainer.style.display = 'none'; + this._headerActionBarContainer.classList.remove('has-actions'); return; } @@ -325,7 +321,7 @@ export class CIStatusWidget extends Disposable { )); this._headerActionBar.push([fixChecksAction], { icon: true, label: false }); - this._headerActionBarContainer.style.display = 'flex'; + this._headerActionBarContainer.classList.add('has-actions'); } /** @@ -333,7 +329,7 @@ export class CIStatusWidget extends Disposable { * Called by the parent view after computing available space. */ layout(maxBodyHeight: number): void { - if (this._collapsed || this._checkCount === 0) { + if (this._checkCount === 0) { return; } const contentHeight = this._checkCount * CICheckListDelegate.ITEM_HEIGHT; @@ -417,55 +413,6 @@ function getCheckCounts(checks: readonly IGitHubCICheck[]): ICICheckCounts { return { running, pending, failed, successful }; } -function getChecksSummary(checks: readonly IGitHubCICheck[]): string { - const counts = getCheckCounts(checks); - const parts: string[] = []; - - if (counts.running > 0) { - parts.push(counts.running === 1 - ? localize('ci.oneRunning', "1 running") - : localize('ci.manyRunning', "{0} running", counts.running)); - } - - if (counts.pending > 0) { - parts.push(counts.pending === 1 - ? localize('ci.onePending', "1 pending") - : localize('ci.manyPending', "{0} pending", counts.pending)); - } - - if (counts.failed > 0) { - parts.push(counts.failed === 1 - ? localize('ci.oneFailed', "1 failed") - : localize('ci.manyFailed', "{0} failed", counts.failed)); - } - - if (counts.successful > 0) { - parts.push(counts.successful === 1 - ? localize('ci.oneSuccessful', "1 successful") - : localize('ci.manySuccessful', "{0} successful", counts.successful)); - } - - return parts.join(', '); -} - -function getHeaderIconAndClass(checks: readonly IGitHubCICheck[], overallStatus: GitHubCIOverallStatus): { icon: ThemeIcon; className: string } { - const counts = getCheckCounts(checks); - if (counts.running > 0) { - return { icon: Codicon.clock, className: 'ci-status-running' }; - } - - switch (overallStatus) { - case GitHubCIOverallStatus.Success: - return { icon: Codicon.passFilled, className: 'ci-status-success' }; - case GitHubCIOverallStatus.Failure: - return { icon: Codicon.error, className: 'ci-status-failure' }; - case GitHubCIOverallStatus.Pending: - return { icon: Codicon.circleFilled, className: 'ci-status-pending' }; - default: - return { icon: Codicon.circleFilled, className: 'ci-status-neutral' }; - } -} - function getCheckIcon(check: IGitHubCICheck): ThemeIcon { switch (check.status) { case GitHubCheckStatus.InProgress: diff --git a/src/vs/sessions/contrib/changes/browser/media/changesView.css b/src/vs/sessions/contrib/changes/browser/media/changesView.css index ce34dce6c7c..1c3c0d76403 100644 --- a/src/vs/sessions/contrib/changes/browser/media/changesView.css +++ b/src/vs/sessions/contrib/changes/browser/media/changesView.css @@ -15,6 +15,13 @@ box-sizing: border-box; } +/* SplitView container */ +.changes-view-body .changes-splitview-container { + flex: 1; + min-height: 0; + overflow: hidden; +} + /* Welcome/Empty state */ .changes-view-body .changes-welcome { display: flex; @@ -38,19 +45,46 @@ font-size: 12px; } -/* Main container - matches chat editing session styling */ +/* Main container */ .changes-view-body .chat-editing-session-container { - padding: 4px 3px; box-sizing: border-box; - background-color: var(--vscode-editor-background); - border: 1px solid var(--vscode-input-border, transparent); - border-radius: 4px; display: flex; flex-direction: column; gap: 2px; overflow: hidden; } +/* Files header */ +.changes-view-body .changes-files-header { + display: flex; + align-items: center; + gap: 6px; + padding: 2px 8px; + min-height: 22px; + font-weight: 500; + font-size: 12px; +} + +.changes-view-body .changes-files-title { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.changes-view-body .changes-files-count { + flex-shrink: 0; + font-size: 11px; + padding: 2px 0; + border-radius: 4px; + background-color: var(--vscode-badge-background); + color: var(--vscode-badge-foreground); + line-height: 1; + font-weight: 600; + min-width: 16px; + text-align: center; +} + /* Overview section (header) - hidden since actions moved outside card */ .changes-view-body .chat-editing-session-overview { display: none; @@ -260,6 +294,21 @@ color: var(--vscode-chat-linesRemovedForeground); } +/* Line counts in buttons */ +.changes-view-body .chat-editing-session-actions .monaco-button.working-set-diff-stats { + flex-shrink: 0; + padding-left: 4px; + padding-right: 8px; +} + +.changes-view-body .chat-editing-session-actions .monaco-button .working-set-lines-added { + color: var(--vscode-chat-linesAddedForeground); +} + +.changes-view-body .chat-editing-session-actions .monaco-button .working-set-lines-removed { + color: var(--vscode-chat-linesRemovedForeground); +} + .changes-view-body .chat-editing-session-actions .monaco-button.code-review-comments, .changes-view-body .chat-editing-session-actions .monaco-button.code-review-loading { padding-left: 4px; diff --git a/src/vs/sessions/contrib/changes/browser/media/ciStatusWidget.css b/src/vs/sessions/contrib/changes/browser/media/ciStatusWidget.css index 451457dd543..cf4aa36e6f5 100644 --- a/src/vs/sessions/contrib/changes/browser/media/ciStatusWidget.css +++ b/src/vs/sessions/contrib/changes/browser/media/ciStatusWidget.css @@ -3,30 +3,25 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -/* CI Status Widget - beneath the changes tree */ +/* CI Status Widget - beneath the files list */ .ci-status-widget { + display: flex; + flex-direction: column; + flex-shrink: 0; + box-sizing: border-box; margin-top: 8px; - border: 1px solid var(--vscode-input-border, transparent); - border-radius: 4px; - background-color: var(--vscode-editor-background); overflow: hidden; font-size: 12px; } -/* Header - always visible, clickable */ +/* Header */ .ci-status-widget-header { display: flex; align-items: center; gap: 6px; - padding: 4px 8px 4px 4px; - cursor: pointer; - -webkit-user-select: none; - user-select: none; + padding: 2px 0; min-height: 22px; -} - -.ci-status-widget-header:hover { - background-color: var(--vscode-list-hoverBackground); + font-weight: 500; } /* Title - single line, overflow ellipsis */ @@ -34,27 +29,60 @@ flex: 1; display: flex; align-items: center; + gap: 6px; overflow: hidden; color: var(--vscode-foreground); + padding-left: 8px; } -.ci-status-widget-title .monaco-icon-label { - width: 100%; - height: 18px; -} - -.ci-status-widget-title .monaco-icon-label-container, -.ci-status-widget-title .monaco-icon-name-container { - display: block; - overflow: hidden; -} - -.ci-status-widget-title .label-name { +.ci-status-widget-title-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +/* Status count badges in the header */ +.ci-status-widget-counts { + display: flex; + align-items: center; + gap: 8px; + flex-shrink: 0; + margin-left: auto; + padding-right: 8px; +} + +.ci-status-widget:hover .ci-status-widget-counts { + padding-right: 0; +} + +.ci-status-widget-count-badge { + display: inline-flex; + align-items: center; + gap: 2px; + font-size: 11px; + line-height: 1; +} + +.ci-status-widget-count-badge .codicon { + font-size: 14px; +} + +.ci-status-widget-count-badge.ci-status-success .codicon { + color: var(--vscode-testing-iconPassed, #73c991); +} + +.ci-status-widget-count-badge.ci-status-failure .codicon { + color: var(--vscode-testing-iconFailed, #f14c4c); +} + +.ci-status-widget-count-badge.ci-status-running .codicon { + color: var(--vscode-testing-iconQueued, var(--vscode-editorWarning-foreground)); +} + +.ci-status-widget-count-badge.ci-status-pending .codicon { + color: var(--vscode-descriptionForeground); +} + .ci-status-widget-header-actions { flex: 0 0 auto; display: none; @@ -62,6 +90,10 @@ margin-left: auto; } +.ci-status-widget:hover .ci-status-widget-header-actions.has-actions { + display: flex; +} + .ci-status-widget-header-actions .monaco-action-bar { display: flex; align-items: center; @@ -72,18 +104,11 @@ height: 16px; } -/* Twistie icon on the right */ -.ci-status-widget-twistie { - flex: 0 0 auto; - display: flex; - align-items: center; - color: var(--vscode-foreground); - opacity: 0.7; -} - -/* Body - collapsible list */ +/* Body - check list */ .ci-status-widget-body { - border-top: 1px solid var(--vscode-input-border, transparent); + flex: 1; + min-height: 0; + overflow: hidden; } .ci-status-widget-list { @@ -96,23 +121,21 @@ } /* Individual check row */ +.ci-status-widget-list .monaco-list-row { + border-radius: 4px !important; +} + .ci-status-widget-check { display: flex; align-items: center; gap: 6px; - padding: 4px 8px 4px 4px; + padding: 4px 8px; height: 100%; width: 100%; box-sizing: border-box; min-width: 0; } -.ci-status-widget-list .monaco-list-row:hover .ci-status-widget-check, -.ci-status-widget-list .monaco-list-row.focused .ci-status-widget-check, -.ci-status-widget-list .monaco-list-row.selected .ci-status-widget-check { - background-color: var(--vscode-list-hoverBackground); -} - .ci-status-widget-check-label { display: flex; flex: 1; @@ -143,19 +166,19 @@ color: var(--vscode-foreground); } -.ci-status-widget-title.ci-status-success .monaco-icon-label::before, +.ci-status-widget-title.ci-status-success .codicon, .ci-status-widget-check.ci-status-success .monaco-icon-label::before { color: var(--vscode-testing-iconPassed, #73c991); } -.ci-status-widget-title.ci-status-failure .monaco-icon-label::before, +.ci-status-widget-title.ci-status-failure .codicon, .ci-status-widget-check.ci-status-failure .monaco-icon-label::before { color: var(--vscode-testing-iconFailed, #f14c4c); } -.ci-status-widget-title.ci-status-running .monaco-icon-label::before, +.ci-status-widget-title.ci-status-running .codicon, .ci-status-widget-check.ci-status-running .monaco-icon-label::before, -.ci-status-widget-title.ci-status-pending .monaco-icon-label::before { +.ci-status-widget-title.ci-status-pending .codicon { color: var(--vscode-testing-iconQueued, var(--vscode-editorWarning-foreground)); }