From c4c03e89bb83e698d9dfbda3cc9f30ed18d1e319 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Fri, 16 Oct 2026 16:06:15 -0700 Subject: [PATCH] Compact the Space window layout (#29857) * Compact Space window layout * Make Space summary and workspace rows single line * Expose confirmed single-workspace delete in Space rows --- src/main/window/workspace-space-window.ts | 2 +- src/renderer/space.html | 2 +- .../use-workspace-space-manager-projection.ts | 3 - .../workspace-space-breakdown-list.test.tsx | 21 +++ .../workspace-space-breakdown-list.tsx | 65 ++++++-- .../workspace-space-manager-overview.tsx | 149 ++++++++++-------- .../workspace-space-manager-table.tsx | 22 ++- .../workspace-space-manager-toolbar.tsx | 17 +- .../workspace-space-manager-view.tsx | 2 +- .../status-bar/workspace-space-metrics.tsx | 32 +++- .../status-bar/workspace-space-treemap.tsx | 6 +- .../workspace-space-worktree-row.test.tsx | 71 +++++++-- .../workspace-space-worktree-row.tsx | 124 ++++++++------- .../workspace-space/WorkspaceSpacePage.tsx | 83 ++-------- .../src/i18n/en-runtime-required.json | 4 +- src/renderer/src/i18n/locales/en.json | 12 +- 16 files changed, 363 insertions(+), 252 deletions(-) diff --git a/src/main/window/workspace-space-window.ts b/src/main/window/workspace-space-window.ts index fe53bea7beb..7e1f1a5cc23 100644 --- a/src/main/window/workspace-space-window.ts +++ b/src/main/window/workspace-space-window.ts @@ -46,7 +46,7 @@ export function createOrFocusWorkspaceSpaceWindow(store: Store): BrowserWindow { height: 820, minWidth: 700, minHeight: 520, - title: 'Orca Space', + title: 'Orca Space Beta', show: false, autoHideMenuBar: true, backgroundColor: nativeTheme.shouldUseDarkColors ? '#0a0a0a' : '#ffffff', diff --git a/src/renderer/space.html b/src/renderer/space.html index 617ea525cca..8a057b38c4c 100644 --- a/src/renderer/space.html +++ b/src/renderer/space.html @@ -2,7 +2,7 @@ - Orca Space + Orca Space Beta
diff --git a/src/renderer/src/components/status-bar/use-workspace-space-manager-projection.ts b/src/renderer/src/components/status-bar/use-workspace-space-manager-projection.ts index b28472b0795..5ae16250eed 100644 --- a/src/renderer/src/components/status-bar/use-workspace-space-manager-projection.ts +++ b/src/renderer/src/components/status-bar/use-workspace-space-manager-projection.ts @@ -1,7 +1,6 @@ import { useCallback, useEffect, useMemo } from 'react' import { filterWorkspaceSpaceRows, - getLargestWorkspaceSpaceRowSize, isWorkspaceSpaceRowReadyToDelete, pruneWorkspaceSpaceSelectedIds, resolveWorkspaceSpaceInspectedWorktreeId, @@ -172,7 +171,6 @@ export function useWorkspaceSpaceManagerProjection(args: { (row) => getWorkspaceSpaceWorktreeIdentity(row) === nextTreemapZoomWorktreeId && row.status === 'ok' ) ?? null - const maxSize = getLargestWorkspaceSpaceRowSize(rows) const selectedDeletableRows = useMemo( () => getSelectedDeletableWorkspaceRows(rows, nextSelectedIds, isWorktreeUnavailableForDelete), [isWorktreeUnavailableForDelete, nextSelectedIds, rows] @@ -218,7 +216,6 @@ export function useWorkspaceSpaceManagerProjection(args: { nextTreemapZoomWorktreeId, inspectedWorktree, zoomedWorktree, - maxSize, selectedDeletableRows, selectedDeletableIdentities, visibleDeletableIdentities, diff --git a/src/renderer/src/components/status-bar/workspace-space-breakdown-list.test.tsx b/src/renderer/src/components/status-bar/workspace-space-breakdown-list.test.tsx index 181f9c6e290..2ba6abfca22 100644 --- a/src/renderer/src/components/status-bar/workspace-space-breakdown-list.test.tsx +++ b/src/renderer/src/components/status-bar/workspace-space-breakdown-list.test.tsx @@ -73,6 +73,27 @@ describe('WorkspaceSpaceBreakdownList', () => { expect(revealInFileManager).toHaveBeenCalledWith('/workspace/src', 'local') }) + it('keeps compacted folder details available in the overflow popover', () => { + const { container } = render( + + ) + + expect(renderedRowNames(container)).toEqual(['first', 'second', 'third', 'fourth']) + fireEvent.click(screen.getByText('Show more folder details')) + expect(screen.getAllByText('fifth')).toHaveLength(1) + }) + it('renders one row per counted top-level item, including the omitted aggregate', () => { const { container } = render( +
{isScanning ? : null} {isScanning @@ -74,9 +77,12 @@ export function WorkspaceSpaceBreakdownList({ sizeBytes: worktree.omittedTopLevelSizeBytes } : null + const breakdownItems = omittedItem + ? [...worktree.topLevelItems, omittedItem] + : worktree.topLevelItems return ( -
-
+
+
{worktree.displayName}
@@ -100,7 +106,7 @@ export function WorkspaceSpaceBreakdownList({
{worktree.status !== 'ok' ? ( -
+
{worktree.error ?? @@ -111,28 +117,29 @@ export function WorkspaceSpaceBreakdownList({
) : topLevelItemCount === 0 ? ( -
+
{translate( 'auto.components.status.bar.WorkspaceSpaceManagerPanel.16988df079', 'No files found.' )}
) : worktree.topLevelItems.length === 0 ? ( -
+
{translate( 'workspaceSpace.savedScan.folderDetailsUnavailable', 'Folder details are not saved with past scans. Refresh to inspect this workspace.' )}
) : ( -
-
- {worktree.topLevelItems.slice(0, 12).map((item) => ( +
+
+ {worktree.topLevelItems.slice(0, omittedItem ? 3 : 4).map((item, index) => ( = 2} /> ))} {omittedItem ? ( @@ -140,8 +147,37 @@ export function WorkspaceSpaceBreakdownList({ item={omittedItem} maxSize={maxChildSize} executionHostId={worktree.executionHostId ?? 'unresolved'} + hideWhenShort={worktree.topLevelItems.length >= 2} /> ) : null} + {breakdownItems.length > 2 ? ( + + + + + +
+ {breakdownItems.map((item) => ( + + ))} +
+
+
+ ) : null}
)} @@ -152,14 +188,21 @@ export function WorkspaceSpaceBreakdownList({ function BreakdownRow({ item, maxSize, - executionHostId + executionHostId, + hideWhenShort = false }: { item: WorkspaceSpaceItem maxSize: number executionHostId: ExecutionHostId | 'unresolved' + hideWhenShort?: boolean }): React.JSX.Element { const row = ( -
+
{item.name} diff --git a/src/renderer/src/components/status-bar/workspace-space-manager-overview.tsx b/src/renderer/src/components/status-bar/workspace-space-manager-overview.tsx index b36cc3e885a..9c7377f191b 100644 --- a/src/renderer/src/components/status-bar/workspace-space-manager-overview.tsx +++ b/src/renderer/src/components/status-bar/workspace-space-manager-overview.tsx @@ -1,5 +1,6 @@ import { AlertTriangle, HardDrive, Loader2, RefreshCw, X } from 'lucide-react' import { Button } from '../ui/button' +import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover' import { formatBytes } from './workspace-space-format' import { translate } from '@/i18n/i18n' import { Metric, UpdatedMetric } from './workspace-space-metrics' @@ -56,73 +57,42 @@ export function WorkspaceSpaceManagerOverview({ ) } return ( - <> -
- - - 0 - ? `${analysis.scannedWorktreeCount}/${analysis.worktreeCount}` - : String(analysis.scannedWorktreeCount) - : '—' - } - /> - -
- -
-
- {isScanning ? ( - - ) : ( - - )} - - {analysis - ? isScanning - ? translate( - 'auto.components.status.bar.WorkspaceSpaceManagerPanel.34174bd83d', - '{{value0}}. You can leave this page; the last result stays visible.', - { value0: progressLabel ?? 'Scanning workspace sizes' } - ) - : translate( - 'workspaceSpace.summary.linkedWorktrees', - '{{value0}} is used by linked worktrees. Managed worktrees can be removed here.', - { value0: formatBytes(analysis.reclaimableBytes) } - ) - : isScanning - ? translate( - 'auto.components.status.bar.WorkspaceSpaceManagerPanel.265d956765', - '{{value0}}. You can leave this page.', - { value0: progressLabel ?? 'Scanning workspace sizes' } - ) - : translate( - 'auto.components.status.bar.WorkspaceSpaceManagerPanel.e91dd2a9ae', - 'Run a scan to inspect workspace sizes.' - )} - +
+
+
+ + + 0 + ? `${analysis.scannedWorktreeCount}/${analysis.worktreeCount}` + : String(analysis.scannedWorktreeCount) + : '—' + } + /> +
+ {isScanning ? ( +
+ + + {analysis + ? translate( + 'auto.components.status.bar.WorkspaceSpaceManagerPanel.34174bd83d', + '{{value0}}. You can leave this page; the last result stays visible.', + { value0: progressLabel ?? 'Scanning workspace sizes' } + ) + : translate( + 'auto.components.status.bar.WorkspaceSpaceManagerPanel.265d956765', + '{{value0}}. You can leave this page.', + { value0: progressLabel ?? 'Scanning workspace sizes' } + )} + +
+ ) : null} + {scanError ? ( -
+
{scanError} @@ -171,20 +160,46 @@ export function WorkspaceSpaceManagerOverview({ ) : null} {repoErrors.length > 0 ? ( -
- {repoErrors.map((repo) => ( +
+ {repoErrors.slice(0, 1).map((repo) => (
- + {repo.displayName}: {repo.error} + {repoErrors.length > 1 ? ( + + + + + +
+ {repoErrors.map((entry) => ( +
+ {entry.displayName}: {entry.error} +
+ ))} +
+
+
+ ) : null}
))}
) : null} {hasRows || isInitialScan ? ( -
+
) : null} - +
) } diff --git a/src/renderer/src/components/status-bar/workspace-space-manager-table.tsx b/src/renderer/src/components/status-bar/workspace-space-manager-table.tsx index 920932d63d0..e69c0ae7fab 100644 --- a/src/renderer/src/components/status-bar/workspace-space-manager-table.tsx +++ b/src/renderer/src/components/status-bar/workspace-space-manager-table.tsx @@ -22,7 +22,6 @@ export function WorkspaceSpaceManagerTable({ hasRows, inspectedWorktree, isInitialScan, - maxSize, nextSelectedIds, getDeleteStateForWorktree, rows, @@ -38,11 +37,11 @@ export function WorkspaceSpaceManagerTable({ allVisibleSelected } = model return ( - <> +
{hasRows || isInitialScan ? ( -
-
-
+
+
+
+
-
+
{isInitialScan ? (
@@ -129,7 +136,6 @@ export function WorkspaceSpaceManagerTable({ )} - +
) } diff --git a/src/renderer/src/components/status-bar/workspace-space-manager-toolbar.tsx b/src/renderer/src/components/status-bar/workspace-space-manager-toolbar.tsx index 2bcc2888ea4..5c46c984b7a 100644 --- a/src/renderer/src/components/status-bar/workspace-space-manager-toolbar.tsx +++ b/src/renderer/src/components/status-bar/workspace-space-manager-toolbar.tsx @@ -1,4 +1,4 @@ -import { Check, Search, Trash2 } from 'lucide-react' +import { Check, Trash2 } from 'lucide-react' import { Button } from '../ui/button' import { Input } from '../ui/input' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select' @@ -33,7 +33,7 @@ export function WorkspaceSpaceManagerToolbar({ return ( <> {selectedDeletableIds.length > 0 ? ( -
+
{selectedDeletableIds.length}{' '} @@ -82,9 +82,8 @@ export function WorkspaceSpaceManagerToolbar({ ) : null} {hasRows ? ( -
-
- +
+
setQuery(event.target.value)} @@ -92,7 +91,7 @@ export function WorkspaceSpaceManagerToolbar({ 'auto.components.status.bar.WorkspaceSpaceManagerPanel.6f8f6a6b04', 'Filter workspaces' )} - className="pl-9" + className="h-8" />
@@ -100,7 +99,7 @@ export function WorkspaceSpaceManagerToolbar({ value={sortKey} onValueChange={(value) => selectSortKey(value as WorkspaceSpaceSortKey)} > - + @@ -135,7 +134,7 @@ export function WorkspaceSpaceManagerToolbar({ variant={onlyDeletable ? 'secondary' : 'outline'} size="sm" onClick={() => setOnlyDeletable((current) => !current)} - className="w-32" + className="w-24" aria-label={translate( 'auto.components.status.bar.WorkspaceSpaceManagerPanel.81aaf1de65', 'Show only deletable workspaces' @@ -157,7 +156,7 @@ export function WorkspaceSpaceManagerToolbar({ size="sm" onClick={toggleVisibleSelection} disabled={visibleDeletableIds.length === 0} - className="w-32 gap-1.5" + className="w-24" aria-label={ allVisibleSelected ? translate( diff --git a/src/renderer/src/components/status-bar/workspace-space-manager-view.tsx b/src/renderer/src/components/status-bar/workspace-space-manager-view.tsx index ee6221a3e4a..642882fa137 100644 --- a/src/renderer/src/components/status-bar/workspace-space-manager-view.tsx +++ b/src/renderer/src/components/status-bar/workspace-space-manager-view.tsx @@ -11,7 +11,7 @@ export function WorkspaceSpaceManagerView({ model: WorkspaceSpaceManagerModel }): React.JSX.Element { return ( -
+
diff --git a/src/renderer/src/components/status-bar/workspace-space-metrics.tsx b/src/renderer/src/components/status-bar/workspace-space-metrics.tsx index e0bce006a62..ccd605f8e09 100644 --- a/src/renderer/src/components/status-bar/workspace-space-metrics.tsx +++ b/src/renderer/src/components/status-bar/workspace-space-metrics.tsx @@ -6,23 +6,42 @@ import { getWorkspaceSpaceScanDateTimeLabel, getWorkspaceSpaceScanTimeLabel } from './workspace-space-format' +import { formatShortTimeAgo } from '@/lib/short-time-ago' export function Metric({ label, value, - title + title, + shortLabel, + shortValue }: { label: string value: string title?: string + shortLabel?: string + shortValue?: string }): React.JSX.Element { return ( -
-
- {label} +
+
+ {shortLabel ? ( + <> + {shortLabel} + {label} + + ) : ( + label + )}
-
- {value} +
+ {shortValue ? ( + <> + {shortValue} + {value} + + ) : ( + value + )}
) @@ -55,6 +74,7 @@ export function UpdatedMetric({ 'Updated' )} title={scannedAt === null ? undefined : getWorkspaceSpaceScanDateTimeLabel(scannedAt)} + shortValue={scannedAt === null ? undefined : formatShortTimeAgo(scannedAt, now)} value={ scannedAt === null ? isScanning diff --git a/src/renderer/src/components/status-bar/workspace-space-treemap.tsx b/src/renderer/src/components/status-bar/workspace-space-treemap.tsx index 632ce80889a..be1f12a9300 100644 --- a/src/renderer/src/components/status-bar/workspace-space-treemap.tsx +++ b/src/renderer/src/components/status-bar/workspace-space-treemap.tsx @@ -73,7 +73,7 @@ export function WorkspaceSpaceTreemap({ if (rects.length === 0) { return ( -
+
{zoomedWorktree ? ( + ) : null} {getRevealInFileManagerLabel()} - {canDelete ? ( - <> - - - - {translate( - 'auto.components.status.bar.WorkspaceSpaceManagerPanel.792a214457', - 'Delete workspace' - )} - - - ) : null} + + + {translate( + 'auto.components.status.bar.WorkspaceSpaceManagerPanel.792a214457', + 'Delete workspace' + )} + ) diff --git a/src/renderer/src/components/workspace-space/WorkspaceSpacePage.tsx b/src/renderer/src/components/workspace-space/WorkspaceSpacePage.tsx index b7f01fe5389..20c14d77b10 100644 --- a/src/renderer/src/components/workspace-space/WorkspaceSpacePage.tsx +++ b/src/renderer/src/components/workspace-space/WorkspaceSpacePage.tsx @@ -1,88 +1,23 @@ -import { useCallback, useEffect } from 'react' -import { ArrowLeft, HardDrive, X } from 'lucide-react' -import { Badge } from '../ui/badge' +import { ArrowLeft } from 'lucide-react' import { Button } from '../ui/button' import { WorkspaceSpaceManagerPanel } from '../status-bar/WorkspaceSpaceManagerPanel' import { useAppStore } from '../../store' -import { hasVisibleOverlay } from '@/lib/visible-overlay' import { translate } from '@/i18n/i18n' export default function WorkspaceSpacePage(): React.JSX.Element { const closeSpacePage = useAppStore((state) => state.closeSpacePage) const isSeparateWindow = window.location.pathname.endsWith('/space.html') - const close = useCallback((): void => { - if (isSeparateWindow) { - void window.api.workspaceSpace.closeWindow?.() - } else { - closeSpacePage() - } - }, [closeSpacePage, isSeparateWindow]) - - useEffect(() => { - const handleKeyDown = (event: KeyboardEvent): void => { - if (event.key !== 'Escape') { - return - } - // Why: confirmation dialogs own Escape first; page-level Escape should - // only leave the full Space surface when no modal or popover is active. - if (hasVisibleOverlay()) { - return - } - const target = event.target instanceof HTMLElement ? event.target : null - if ( - target?.matches('input, textarea, select, [contenteditable="true"], [contenteditable=""]') - ) { - return - } - event.preventDefault() - close() - } - - // Why: tooltips can consume Escape before bubble listeners see it. Capture - // keeps the first Escape reliable while still deferring to real overlays. - window.addEventListener('keydown', handleKeyDown, { capture: true }) - return () => window.removeEventListener('keydown', handleKeyDown, { capture: true }) - }, [close]) return ( -
-
- -
-
- -
-
-
-

- {translate( - 'auto.components.workspace.space.WorkspaceSpacePage.45f6302dbc', - 'Space' - )} -

- - {translate('auto.components.workspace.space.WorkspaceSpacePage.e8d6ba11ab', 'Beta')} - -
-

- {translate( - 'auto.components.workspace.space.WorkspaceSpacePage.8d0048e1cb', - 'See what uses space across your workspaces and what you can safely reclaim.' - )} -

-
-
-
- -
-
- -
+ ) : null} +
+
) diff --git a/src/renderer/src/i18n/en-runtime-required.json b/src/renderer/src/i18n/en-runtime-required.json index de89a1255f7..51836cdbca5 100644 --- a/src/renderer/src/i18n/en-runtime-required.json +++ b/src/renderer/src/i18n/en-runtime-required.json @@ -2709,7 +2709,9 @@ }, "space": { "WorkspaceSpacePage": { - "8d0048e1cb": "Workspace disk usage and reclaimable worktree storage." + "45f6302dbc": "Space", + "8d0048e1cb": "Workspace disk usage and reclaimable worktree storage.", + "e8d6ba11ab": "Beta" } } } diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index 27173a60ad8..2dc73ffd6e1 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -20189,6 +20189,13 @@ } }, "workspaceSpace": { + "deleteOne": "Delete {{value0}}", + "table": { + "lastActive": "Last active" + }, + "breakdown": { + "viewAll": "Show more folder details" + }, "compact": { "linkedAndUnavailable": "{{value0}} in linked worktrees · {{value1}} unavailable", "linkedAndScanned": "{{value0}} in linked worktrees · {{value1}} workspaces", @@ -20205,8 +20212,8 @@ "metric": { "linkedWorktrees": "Linked worktrees" }, - "summary": { - "linkedWorktrees": "{{value0}} is used by linked worktrees. Managed worktrees can be removed here." + "scan": { + "moreErrors": "{{value0}} more scan errors" }, "status": { "notManaged": "Not managed" @@ -20217,7 +20224,6 @@ "clickToZoom": "Click a workspace to zoom" }, "window": { - "close": "Close", "errorTitle": "Space hit an error.", "errorDescription": "Close and reopen Space to try again." }