fix(mobile): splash/icon sizing, worktree sleep, host removal persistence

- Add worktree.sleep RPC method with full notifier chain (runtime → IPC → renderer)
- Optimistic UI update for sleeping worktrees in mobile client
- Fix splash screen: configure expo-splash-screen plugin, hide splash only after navigator layout
- Regenerate app icon and splash assets at correct density sizes
- Fix host removal not persisting by disabling Android auto-backup (allowBackup=false)
- Guard against invalid host credentials (base64 crash) and prevent navigating into auth-failed hosts
- Extract ActionSheetContent for inline drawer content switching
- Rewrite BottomDrawer as always-mounted Animated.View overlay (replaces Modal)
- Add ConfirmModal component with inline confirm/cancel buttons

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Jinwoo-H
2026-04-30 18:03:55 -04:00
co-authored by Orca
parent 36005a7af6
commit e668dce4ec
20 changed files with 414 additions and 125 deletions
@@ -95,6 +95,7 @@ describe('fit override integration', () => {
renameTerminal: vi.fn(),
focusTerminal: vi.fn(),
closeTerminal: vi.fn(),
sleepWorktree: vi.fn(),
terminalFitOverrideChanged: (ptyId, mode, cols, rows) => {
notifications.push({ ptyId, mode, cols, rows })
}
@@ -198,6 +199,7 @@ describe('fit override integration', () => {
renameTerminal: vi.fn(),
focusTerminal: vi.fn(),
closeTerminal: vi.fn(),
sleepWorktree: vi.fn(),
terminalFitOverrideChanged: vi.fn()
})
@@ -258,6 +260,7 @@ describe('fit override integration', () => {
renameTerminal: vi.fn(),
focusTerminal: vi.fn(),
closeTerminal: vi.fn(),
sleepWorktree: vi.fn(),
terminalFitOverrideChanged: vi.fn()
})
+4
View File
@@ -1249,6 +1249,7 @@ describe('OrcaRuntimeService', () => {
renameTerminal: vi.fn(),
focusTerminal: vi.fn(),
closeTerminal: vi.fn(),
sleepWorktree: vi.fn(),
terminalFitOverrideChanged: vi.fn()
})
runtime.attachWindow(TEST_WINDOW_ID)
@@ -1274,6 +1275,7 @@ describe('OrcaRuntimeService', () => {
renameTerminal: vi.fn(),
focusTerminal: vi.fn(),
closeTerminal: vi.fn(),
sleepWorktree: vi.fn(),
terminalFitOverrideChanged: vi.fn()
})
runtime.attachWindow(1)
@@ -1442,6 +1444,7 @@ describe('OrcaRuntimeService', () => {
renameTerminal: vi.fn(),
focusTerminal: vi.fn(),
closeTerminal: vi.fn(),
sleepWorktree: vi.fn(),
terminalFitOverrideChanged: vi.fn()
})
@@ -1575,6 +1578,7 @@ describe('OrcaRuntimeService', () => {
renameTerminal: vi.fn(),
focusTerminal: vi.fn(),
closeTerminal: vi.fn(),
sleepWorktree: vi.fn(),
terminalFitOverrideChanged: (ptyId, mode, cols, rows) => {
notifications.push({ ptyId, mode, cols, rows })
}
+10
View File
@@ -199,6 +199,7 @@ type RuntimeNotifier = {
renameTerminal(tabId: string, title: string | null): void
focusTerminal(tabId: string, worktreeId: string): void
closeTerminal(tabId: string, paneRuntimeId?: number): void
sleepWorktree(worktreeId: string): void
terminalFitOverrideChanged(
ptyId: string,
mode: 'mobile-fit' | 'desktop-fit',
@@ -1568,6 +1569,15 @@ export class OrcaRuntimeService {
return await this.resolveWorktreeSelector(worktreeSelector)
}
async sleepManagedWorktree(worktreeSelector: string): Promise<{ worktreeId: string }> {
const worktree = await this.resolveWorktreeSelector(worktreeSelector)
// Why: sleep is renderer-initiated on desktop (it tears down tab state
// before killing PTYs). The notifier tells the renderer to run its own
// sleep flow so all cleanup happens in the correct order.
this.notifier?.sleepWorktree(worktree.id)
return { worktreeId: worktree.id }
}
async activateManagedWorktree(worktreeSelector: string): Promise<{
repoId: string
worktreeId: string
+5
View File
@@ -71,6 +71,11 @@ export const WORKTREE_METHODS: RpcMethod[] = [
worktree: await runtime.showManagedWorktree(params.worktree)
})
}),
defineMethod({
name: 'worktree.sleep',
params: WorktreeSelector,
handler: async (params, { runtime }) => runtime.sleepManagedWorktree(params.worktree)
}),
defineMethod({
name: 'worktree.activate',
params: WorktreeSelector,
@@ -234,6 +234,7 @@ function registerRuntimeWindowLifecycle(
renameTerminal: (tabId, title) => send('ui:renameTerminal', { tabId, title }),
focusTerminal: (tabId, worktreeId) => send('ui:focusTerminal', { tabId, worktreeId }),
closeTerminal: (tabId, paneRuntimeId) => send('ui:closeTerminal', { tabId, paneRuntimeId }),
sleepWorktree: (worktreeId) => send('ui:sleepWorktree', { worktreeId }),
terminalFitOverrideChanged: (ptyId, mode, cols, rows) =>
send('runtime:terminalFitOverrideChanged', { ptyId, mode, cols, rows })
})
+1
View File
@@ -828,6 +828,7 @@ export type PreloadApi = {
onCloseTerminal: (
callback: (data: { tabId: string; paneRuntimeId?: number }) => void
) => () => void
onSleepWorktree: (callback: (data: { worktreeId: string }) => void) => () => void
onTerminalZoom: (callback: (direction: 'in' | 'out' | 'reset') => void) => () => void
readClipboardText: () => Promise<string>
saveClipboardImageAsTempFile: () => Promise<string | null>
+6
View File
@@ -1505,6 +1505,12 @@ const api = {
ipcRenderer.on('ui:closeTerminal', listener)
return () => ipcRenderer.removeListener('ui:closeTerminal', listener)
},
onSleepWorktree: (callback: (data: { worktreeId: string }) => void): (() => void) => {
const listener = (_event: Electron.IpcRendererEvent, data: { worktreeId: string }) =>
callback(data)
ipcRenderer.on('ui:sleepWorktree', listener)
return () => ipcRenderer.removeListener('ui:sleepWorktree', listener)
},
onTerminalZoom: (callback: (direction: 'in' | 'out' | 'reset') => void): (() => void) => {
const listener = (_event: Electron.IpcRendererEvent, direction: 'in' | 'out' | 'reset') =>
callback(direction)
@@ -163,6 +163,7 @@ describe('useIpcEvents updater integration', () => {
onRenameTerminal: () => () => {},
onFocusTerminal: () => () => {},
onCloseTerminal: () => () => {},
onSleepWorktree: () => () => {},
onNewBrowserTab: () => () => {},
onRequestTabCreate: () => () => {},
replyTabCreate: () => {},
@@ -357,6 +358,7 @@ describe('useIpcEvents updater integration', () => {
onRenameTerminal: () => () => {},
onFocusTerminal: () => () => {},
onCloseTerminal: () => () => {},
onSleepWorktree: () => () => {},
onNewBrowserTab: () => () => {},
onRequestTabCreate: () => () => {},
replyTabCreate: () => {},
@@ -549,6 +551,7 @@ describe('useIpcEvents updater integration', () => {
onRenameTerminal: () => () => {},
onFocusTerminal: () => () => {},
onCloseTerminal: () => () => {},
onSleepWorktree: () => () => {},
onNewBrowserTab: () => () => {},
onRequestTabCreate: () => () => {},
replyTabCreate: () => {},
@@ -744,6 +747,7 @@ describe('useIpcEvents browser tab close routing', () => {
onRenameTerminal: () => () => {},
onFocusTerminal: () => () => {},
onCloseTerminal: () => () => {},
onSleepWorktree: () => () => {},
onNewBrowserTab: () => () => {},
onRequestTabCreate: () => () => {},
replyTabCreate: () => {},
@@ -933,6 +937,7 @@ describe('useIpcEvents browser tab close routing', () => {
onRenameTerminal: () => () => {},
onFocusTerminal: () => () => {},
onCloseTerminal: () => () => {},
onSleepWorktree: () => () => {},
onNewBrowserTab: () => () => {},
onRequestTabCreate: () => () => {},
replyTabCreate: () => {},
@@ -1117,6 +1122,7 @@ describe('useIpcEvents browser tab close routing', () => {
onRenameTerminal: () => () => {},
onFocusTerminal: () => () => {},
onCloseTerminal: () => () => {},
onSleepWorktree: () => () => {},
onNewBrowserTab: () => () => {},
onRequestTabCreate: () => () => {},
replyTabCreate: () => {},
@@ -1319,6 +1325,7 @@ describe('useIpcEvents shortcut hint clearing', () => {
onRenameTerminal: () => () => {},
onFocusTerminal: () => () => {},
onCloseTerminal: () => () => {},
onSleepWorktree: () => () => {},
onNewBrowserTab: () => () => {},
onRequestTabCreate: () => () => {},
replyTabCreate: () => {},
@@ -1519,6 +1526,7 @@ describe('useIpcEvents CLI-created worktree activation', () => {
onRenameTerminal: () => () => {},
onFocusTerminal: () => () => {},
onCloseTerminal: () => () => {},
onSleepWorktree: () => () => {},
onNewBrowserTab: () => () => {},
onRequestTabCreate: () => () => {},
replyTabCreate: () => {},
+7
View File
@@ -3,6 +3,7 @@ import { useEffect } from 'react'
import { useAppStore } from '../store'
import { applyUIZoom } from '@/lib/ui-zoom'
import { activateAndRevealWorktree } from '@/lib/worktree-activation'
import { runSleepWorktree } from '@/components/sidebar/sleep-worktree-flow'
import { SPLIT_TERMINAL_PANE_EVENT, CLOSE_TERMINAL_PANE_EVENT } from '@/constants/terminal'
import type { SplitTerminalPaneDetail, CloseTerminalPaneDetail } from '@/constants/terminal'
import { getVisibleWorktreeIds } from '@/components/sidebar/visible-worktrees'
@@ -283,6 +284,12 @@ export function useIpcEvents(): void {
})
)
unsubs.push(
window.api.ui.onSleepWorktree(({ worktreeId }) => {
void runSleepWorktree(worktreeId)
})
)
// Hydrate initial update status then subscribe to changes
window.api.updater.getStatus().then((status) => {
useAppStore.getState().setUpdateStatus(status as UpdateStatus)