feat(cloud): asleep Files tab shows the files changed at sleep, read-only, with Wake; neutral note when nothing was saved (RA-10)

This commit is contained in:
Neil
2026-11-10 17:02:24 -08:00
parent 2c638149fd
commit 7a34a50bec
4 changed files with 135 additions and 1 deletions
@@ -0,0 +1,109 @@
import React from 'react'
import { Play } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { useNow } from '@/hooks/use-now'
import { useTranslate } from '@/i18n/use-translate'
import { formatElapsedTime } from '@/components/worktree-creation/worktree-creation-clock'
import { CloudGitChanges } from '../CloudGitChanges'
import { wakeCloudWorkspace } from '../cloud-workspace-actions'
import { useCloudOfflineData } from './use-cloud-offline-data'
import type { CloudOfflineSnapshot } from '../../../../../shared/hosted-sandbox/cloud-workspace-offline-api'
/**
* The Files tab of an Orca Cloud workspace that is asleep (RA-10). There is no file tree without
* the VM, so instead of a read error and Retry: the files that had changed when it went to sleep
* (from its read snapshot or this computer's cache), read-only and labeled as such, then Wake for
* the full view. With no snapshot, a neutral note and Wake.
*/
export function CloudAsleepFilesPanel({ workspaceId }: { workspaceId: string }): React.JSX.Element {
const t = useTranslate()
const { snapshot } = useCloudOfflineData(workspaceId, true)
const now = useNow(30_000)
const git = snapshot?.git ?? null
const wake = (
<Button size="xs" variant="outline" onClick={() => void wakeCloudWorkspace(workspaceId)}>
<Play />
{t('orcaCloudWorkspace.wake', 'Wake')}
</Button>
)
return (
<div
className="flex h-full min-h-0 flex-col gap-3 overflow-y-auto px-3 py-3 text-xs scrollbar-sleek"
data-cloud-asleep-files={git ? 'snapshot' : 'none'}
>
{git ? (
<>
<SavedChanges git={git} at={snapshot?.capturedAt ?? null} now={now} />
<div className="flex flex-col items-start gap-2 border-t border-border/60 pt-3 text-muted-foreground">
<p>
{t(
'orcaCloudWorkspace.files.wakeForAll',
'Wake the workspace to browse all its files.'
)}
</p>
{wake}
</div>
</>
) : (
<div className="flex flex-col items-start gap-2 text-muted-foreground">
<p>
{t(
'orcaCloudWorkspace.files.asleep',
'Files aren’t available while the workspace is asleep.'
)}
</p>
{wake}
</div>
)}
</div>
)
}
function SavedChanges({
git,
at,
now
}: {
git: NonNullable<CloudOfflineSnapshot['git']>
at: number | null
now: number
}): React.JSX.Element {
const t = useTranslate()
return (
<section className="flex flex-col gap-1.5">
<h3 className="font-medium text-foreground">
{at
? t(
'orcaCloudWorkspace.files.changedAtSleep',
'Changed files when this workspace went to sleep · {{time}}',
{
time: t('orcaCloudWorkspace.ago', '{{ago}} ago', {
ago: formatElapsedTime(Math.max(0, now - at))
})
}
)
: t(
'orcaCloudWorkspace.files.changedAtSleepNoTime',
'Changed files when this workspace went to sleep'
)}
</h3>
<CloudGitChanges
now={now}
data={{
branch: git.branch,
changes: git.changes,
changeCount: git.changeCount,
lineTotals:
typeof git.added === 'number' && typeof git.removed === 'number'
? { added: git.added, removed: git.removed }
: null,
commits: git.commits.map((commit) => ({
sha: commit.id,
subject: commit.subject,
at: commit.at
}))
}}
/>
</section>
)
}
@@ -0,0 +1,12 @@
import { useAppStore } from '@/store'
import { useCloudWorkspaceView } from '../use-cloud-workspace-view'
/** The active workspace's id when it is an Orca Cloud workspace that is asleep (or failed to wake). */
export function useAsleepCloudWorkspaceId(): string | null {
const activeWorktreeId = useAppStore((state) => state.activeWorktreeId)
const view = useCloudWorkspaceView(activeWorktreeId ?? '')
if (!activeWorktreeId || !view.isCloud) {
return null
}
return view.state === 'sleeping' || view.state === 'wakeFailed' ? activeWorktreeId : null
}
@@ -9,6 +9,8 @@ import type { RuntimeFileListState } from '@/components/quick-open-file-list'
import type { Repo } from '../../../../../../shared/repo-types'
import type { RightSidebarExplorerView } from '../../../../../../shared/ui-chrome-types'
import { FileExplorerTreeStatus } from './FileExplorerTreeStatus'
import { CloudAsleepFilesPanel } from '@/components/orca-cloud/offline/CloudAsleepFilesPanel'
import { useAsleepCloudWorkspaceId } from '@/components/orca-cloud/offline/use-asleep-cloud-workspace-id'
import { FileExplorerVirtualRows } from '../panel/FileExplorerVirtualRows'
import { canShowAddAsProjectAction } from '../navigation/file-explorer-add-project-action'
import type { FileExplorerNameFilterProjectionSource } from './file-explorer-name-filter-projection'
@@ -132,6 +134,11 @@ export function FileExplorerFilesTreePane({
hasNameFilter && !nameFilterFiles.loadError
? t('auto.components.right.sidebar.FileExplorer.2f4483d6c4', 'No files match this filter')
: undefined
// An asleep Orca Cloud workspace has no tree to read: its saved changes and Wake, not an error.
const asleepCloudId = useAsleepCloudWorkspaceId()
if (asleepCloudId) {
return <CloudAsleepFilesPanel workspaceId={asleepCloudId} />
}
return (
<ScrollArea
+7 -1
View File
@@ -27011,7 +27011,13 @@
"deleteFailedNamed": "Couldn’t delete {{name}}",
"productName": "Orca Cloud",
"cloudMarkLabel": "Cloud workspace · {{state}}",
"statusUnavailableTitle": "Couldn’t get the sandbox status."
"statusUnavailableTitle": "Couldn’t get the sandbox status.",
"files": {
"wakeForAll": "Wake the workspace to browse all its files.",
"asleep": "Files aren’t available while the workspace is asleep.",
"changedAtSleep": "Changed files when this workspace went to sleep · {{time}}",
"changedAtSleepNoTime": "Changed files when this workspace went to sleep"
}
},
"cloudAgentSetup": {
"loading": "Checking what agents can use…",