From 0bf70b24b5acb3cb6a70177a6ba8c21e22f1d158 Mon Sep 17 00:00:00 2001 From: Greg Pstrucha <875316+Gricha@users.noreply.github.com> Date: Tue, 6 Jan 2026 17:10:31 +0000 Subject: [PATCH] Fix workspace showing error state during initialization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a workspace is starting up (especially during first-time image pull), the syncWorkspaceStatus function was incorrectly overwriting the 'creating' status with 'error' because the container didn't exist yet. Changes: - syncWorkspaceStatus now preserves 'creating' status instead of overwriting - start() method sets 'creating' status at the beginning for visibility - UI components show "Starting" state with amber indicator when creating 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- mobile/src/screens/WorkspaceDetailScreen.tsx | 19 ++- src/workspace/manager.ts | 128 ++++++++++--------- web/src/pages/WorkspaceDetail.tsx | 117 ++++++++++------- web/src/pages/WorkspaceList.tsx | 12 +- 4 files changed, 169 insertions(+), 107 deletions(-) diff --git a/mobile/src/screens/WorkspaceDetailScreen.tsx b/mobile/src/screens/WorkspaceDetailScreen.tsx index 1681aeb6..bcbdde35 100644 --- a/mobile/src/screens/WorkspaceDetailScreen.tsx +++ b/mobile/src/screens/WorkspaceDetailScreen.tsx @@ -127,6 +127,7 @@ export function WorkspaceDetailScreen({ route, navigation }: any) { }) const isRunning = isHost ? true : workspace?.status === 'running' + const isCreating = isHost ? false : workspace?.status === 'creating' const { data: sessionsData, isLoading: sessionsLoading, refetch } = useQuery({ queryKey: ['sessions', name, agentFilter], @@ -179,7 +180,7 @@ export function WorkspaceDetailScreen({ route, navigation }: any) { {displayName} - + {isHost ? ( @@ -275,10 +276,18 @@ export function WorkspaceDetailScreen({ route, navigation }: any) { )} {!isRunning && !isHost ? ( - - Workspace is not running - Start it from settings to view sessions - + isCreating ? ( + + + Workspace is starting + Please wait while the container starts up + + ) : ( + + Workspace is not running + Start it from settings to view sessions + + ) ) : sessionsLoading ? ( diff --git a/src/workspace/manager.ts b/src/workspace/manager.ts index e5cc7f09..5807b23d 100644 --- a/src/workspace/manager.ts +++ b/src/workspace/manager.ts @@ -419,6 +419,10 @@ export class WorkspaceManager { } private async syncWorkspaceStatus(workspace: Workspace): Promise { + if (workspace.status === 'creating') { + return; + } + const containerName = getContainerName(workspace.name); const exists = await docker.containerExists(containerName); @@ -432,7 +436,7 @@ export class WorkspaceManager { const running = await docker.containerRunning(containerName); const newStatus = running ? 'running' : 'stopped'; - if (workspace.status !== newStatus && workspace.status !== 'creating') { + if (workspace.status !== newStatus) { workspace.status = newStatus; await this.state.setWorkspace(workspace); } @@ -553,76 +557,86 @@ export class WorkspaceManager { throw new Error(`Workspace '${name}' not found`); } - const containerName = getContainerName(name); - const volumeName = `${VOLUME_PREFIX}${name}`; - const exists = await docker.containerExists(containerName); + const previousStatus = workspace.status; + workspace.status = 'creating'; + await this.state.setWorkspace(workspace); - if (!exists) { - const volumeExists = await docker.volumeExists(volumeName); - if (!volumeExists) { - throw new Error( - `Container and volume for workspace '${name}' were deleted. ` + - `Please delete this workspace and create a new one.` - ); - } + try { + const containerName = getContainerName(name); + const volumeName = `${VOLUME_PREFIX}${name}`; + const exists = await docker.containerExists(containerName); + + if (!exists) { + const volumeExists = await docker.volumeExists(volumeName); + if (!volumeExists) { + throw new Error( + `Container and volume for workspace '${name}' were deleted. ` + + `Please delete this workspace and create a new one.` + ); + } - const workspaceImage = await ensureWorkspaceImage(); - const sshPort = await findAvailablePort(SSH_PORT_RANGE_START, SSH_PORT_RANGE_END); + const workspaceImage = await ensureWorkspaceImage(); + const sshPort = await findAvailablePort(SSH_PORT_RANGE_START, SSH_PORT_RANGE_END); - const containerEnv: Record = { - ...this.config.credentials.env, - }; + const containerEnv: Record = { + ...this.config.credentials.env, + }; - if (this.config.agents?.github?.token) { - containerEnv.GITHUB_TOKEN = this.config.agents.github.token; - } - if (this.config.agents?.claude_code?.oauth_token) { - containerEnv.CLAUDE_CODE_OAUTH_TOKEN = this.config.agents.claude_code.oauth_token; - } + if (this.config.agents?.github?.token) { + containerEnv.GITHUB_TOKEN = this.config.agents.github.token; + } + if (this.config.agents?.claude_code?.oauth_token) { + containerEnv.CLAUDE_CODE_OAUTH_TOKEN = this.config.agents.claude_code.oauth_token; + } - if (workspace.repo) { - containerEnv.WORKSPACE_REPO_URL = workspace.repo; + if (workspace.repo) { + containerEnv.WORKSPACE_REPO_URL = workspace.repo; + } + + const containerId = await docker.createContainer({ + name: containerName, + image: workspaceImage, + hostname: name, + privileged: true, + restartPolicy: 'unless-stopped', + env: containerEnv, + volumes: [{ source: volumeName, target: '/home/workspace', readonly: false }], + ports: [{ hostPort: sshPort, containerPort: 22, protocol: 'tcp' }], + labels: { + 'workspace.name': name, + 'workspace.managed': 'true', + }, + }); + + workspace.containerId = containerId; + workspace.ports.ssh = sshPort; + await this.state.setWorkspace(workspace); } - const containerId = await docker.createContainer({ - name: containerName, - image: workspaceImage, - hostname: name, - privileged: true, - restartPolicy: 'unless-stopped', - env: containerEnv, - volumes: [{ source: volumeName, target: '/home/workspace', readonly: false }], - ports: [{ hostPort: sshPort, containerPort: 22, protocol: 'tcp' }], - labels: { - 'workspace.name': name, - 'workspace.managed': 'true', - }, - }); + const running = await docker.containerRunning(containerName); + if (running) { + workspace.status = 'running'; + workspace.lastUsed = new Date().toISOString(); + await this.state.setWorkspace(workspace); + return workspace; + } - workspace.containerId = containerId; - workspace.ports.ssh = sshPort; - await this.state.setWorkspace(workspace); - } + await docker.startContainer(containerName); + await docker.waitForContainerReady(containerName); + await this.setupWorkspaceCredentials(containerName, name); - const running = await docker.containerRunning(containerName); - if (running) { workspace.status = 'running'; workspace.lastUsed = new Date().toISOString(); await this.state.setWorkspace(workspace); - return workspace; - } - - await docker.startContainer(containerName); - await docker.waitForContainerReady(containerName); - await this.setupWorkspaceCredentials(containerName, name); - workspace.status = 'running'; - workspace.lastUsed = new Date().toISOString(); - await this.state.setWorkspace(workspace); - - await this.runPostStartScript(containerName); + await this.runPostStartScript(containerName); - return workspace; + return workspace; + } catch (err) { + workspace.status = previousStatus === 'error' ? 'error' : 'stopped'; + await this.state.setWorkspace(workspace); + throw err; + } } async stop(name: string): Promise { diff --git a/web/src/pages/WorkspaceDetail.tsx b/web/src/pages/WorkspaceDetail.tsx index 1823a3e5..7d137f0f 100644 --- a/web/src/pages/WorkspaceDetail.tsx +++ b/web/src/pages/WorkspaceDetail.tsx @@ -353,6 +353,7 @@ export function WorkspaceDetail() { const isRunning = isHostWorkspace ? (hostInfo?.enabled ?? false) : workspace?.status === 'running' const isError = isHostWorkspace ? false : workspace?.status === 'error' + const isCreating = isHostWorkspace ? false : workspace?.status === 'creating' const displayName = isHostWorkspace ? (hostInfo?.hostname || 'Host') : workspace?.name const tabs = isHostWorkspace @@ -366,50 +367,66 @@ export function WorkspaceDetail() { { id: 'settings' as const, label: 'Settings', icon: Settings }, ] - const renderStartPrompt = () => ( -
-
- {isError ? ( - - ) : ( - - )} -
-

- {isError ? 'Workspace needs recovery' : 'Workspace is stopped'} -

-

- {isError - ? 'The container was deleted externally. Click below to recreate it with existing data.' - : 'Start the workspace to access this feature'} -

- {startMutation.error && ( -
- {(startMutation.error as Error).message || 'Failed to start workspace'} + const renderStartPrompt = () => { + if (isCreating) { + return ( +
+
+ +
+

Workspace is starting

+

+ Please wait while the workspace container starts up. This may take a moment if the Docker image is being downloaded. +

- )} - -
- ) + +
+ ) + } return (
@@ -425,6 +442,8 @@ export function WorkspaceDetail() { ) : isRunning ? ( + ) : isCreating ? ( + ) : isError ? ( error ) : ( @@ -443,6 +462,16 @@ export function WorkspaceDetail() { {stopMutation.isPending ? 'Stopping...' : 'Stop'} + ) : isCreating ? ( + ) : (
@@ -55,6 +60,11 @@ function WorkspaceRow({ workspace, onClick }: { workspace: WorkspaceInfo; onClic Running )} + {isCreating && ( + + Starting + + )} {isError && ( Error