Add braces to mobile control flow (#4351)

This commit is contained in:
Neil
2026-05-31 21:32:37 -07:00
committed by GitHub
parent 16e0ec3e11
commit 0bb62b9d29
50 changed files with 2069 additions and 696 deletions
+3 -1
View File
@@ -52,7 +52,9 @@ export default function RootLayout() {
}
void Linking.getInitialURL().then((url) => {
if (url) handleUrl(url)
if (url) {
handleUrl(url)
}
})
const sub = Linking.addEventListener('url', ({ url }) => handleUrl(url))
+21 -7
View File
@@ -39,10 +39,14 @@ export default function AccountsScreen() {
const [busyAccountId, setBusyAccountId] = useState<string | null>(null)
useEffect(() => {
if (!hostId) return
if (!hostId) {
return
}
let stale = false
void loadHosts().then((hosts) => {
if (stale) return
if (stale) {
return
}
const host = hosts.find((h) => h.id === hostId)
if (!host) {
setError('Host not found')
@@ -60,9 +64,13 @@ export default function AccountsScreen() {
// when the user switches accounts. Falls back to a one-shot accounts.list
// if the subscription stream errors.
useEffect(() => {
if (!client || connState !== 'connected') return
if (!client || connState !== 'connected') {
return
}
const unsubscribe = client.subscribe('accounts.subscribe', null, (payload) => {
if (!payload || typeof payload !== 'object') return
if (!payload || typeof payload !== 'object') {
return
}
const evt = payload as { type?: string; snapshot?: AccountsSnapshot }
if ((evt.type === 'ready' || evt.type === 'snapshot') && evt.snapshot) {
setSnapshot(evt.snapshot)
@@ -73,7 +81,9 @@ export default function AccountsScreen() {
}, [client, connState])
const refresh = useCallback(async () => {
if (!client) return
if (!client) {
return
}
setRefreshing(true)
try {
const res = await client.sendRequest('accounts.list')
@@ -92,7 +102,9 @@ export default function AccountsScreen() {
const selectAccount = useCallback(
async (provider: ProviderKey, accountId: string | null) => {
if (!client) return
if (!client) {
return
}
setBusyAccountId(accountId ?? `${provider}:default`)
const method = provider === 'claude' ? 'accounts.selectClaude' : 'accounts.selectCodex'
try {
@@ -115,7 +127,9 @@ export default function AccountsScreen() {
)
const renderProviderSection = (provider: ProviderKey, title: string) => {
if (!snapshot) return null
if (!snapshot) {
return null
}
const state = provider === 'claude' ? snapshot.claude : snapshot.codex
const activeUsage = getActiveProviderRateLimits(snapshot, provider)
const Icon = provider === 'claude' ? ClaudeIcon : OpenAIIcon
+3 -1
View File
@@ -175,7 +175,9 @@ export default function MobileFileExplorerScreen() {
const openFile = useCallback(
async (relativePath: string, kind: 'text' | 'binary') => {
if (!client || kind === 'binary') return
if (!client || kind === 'binary') {
return
}
setOpeningPath(relativePath)
try {
const response = await client.sendRequest('files.open', {
+150 -53
View File
@@ -130,8 +130,12 @@ const GROUP_OPTIONS: PickerOption<GroupMode>[] = [
]
function getWorktreeStatus(w: Worktree): 'working' | 'active' | 'permission' | 'done' | 'inactive' {
if (w.status) return w.status
if (w.liveTerminalCount > 0) return 'active'
if (w.status) {
return w.status
}
if (w.liveTerminalCount > 0) {
return 'active'
}
return 'inactive'
}
@@ -139,9 +143,15 @@ function getWorktreeStatus(w: Worktree): 'working' | 'active' | 'permission' | '
// worktrees with idle terminal prompts had no recent output and were excluded.
// Any worktree with live terminals or unread output counts as "active".
function isWorktreeActive(w: Worktree): boolean {
if (w.unread) return true
if (w.status) return w.status !== 'inactive'
if (w.liveTerminalCount > 0) return true
if (w.unread) {
return true
}
if (w.status) {
return w.status !== 'inactive'
}
if (w.liveTerminalCount > 0) {
return true
}
return false
}
@@ -163,21 +173,29 @@ const WORKSPACE_STATUS_ORDER: ReturnType<typeof getWorktreeStatus>[] = [
function sortWorktrees(worktrees: Worktree[], mode: SortMode): Worktree[] {
return [...worktrees].sort((a, b) => {
if (mode === 'name') return (a.displayName || a.repo).localeCompare(b.displayName || b.repo)
if (mode === 'recent') return (b.lastOutputAt ?? 0) - (a.lastOutputAt ?? 0)
if (mode === 'name') {
return (a.displayName || a.repo).localeCompare(b.displayName || b.repo)
}
if (mode === 'recent') {
return (b.lastOutputAt ?? 0) - (a.lastOutputAt ?? 0)
}
if (mode === 'repo') {
const repoComparison = a.repo.localeCompare(b.repo, undefined, { sensitivity: 'base' })
return repoComparison || (a.displayName || a.repo).localeCompare(b.displayName || b.repo)
}
// 'smart' — attention-first
if (a.unread !== b.unread) return a.unread ? -1 : 1
if (a.unread !== b.unread) {
return a.unread ? -1 : 1
}
const aStatus = getWorktreeStatus(a)
const bStatus = getWorktreeStatus(b)
const statusOrder = { permission: 0, working: 1, done: 2, active: 3, inactive: 4 }
if (statusOrder[aStatus] !== statusOrder[bStatus])
if (statusOrder[aStatus] !== statusOrder[bStatus]) {
return statusOrder[aStatus] - statusOrder[bStatus]
if ((a.lastOutputAt ?? 0) !== (b.lastOutputAt ?? 0))
}
if ((a.lastOutputAt ?? 0) !== (b.lastOutputAt ?? 0)) {
return (b.lastOutputAt ?? 0) - (a.lastOutputAt ?? 0)
}
return (a.displayName || a.repo).localeCompare(b.displayName || b.repo)
})
}
@@ -218,11 +236,19 @@ const PR_GROUP_LABELS: Record<PRGroupKey, string> = {
const PR_GROUP_ORDER: PRGroupKey[] = ['done', 'in-review', 'in-progress', 'closed']
function getPRGroupKey(w: Worktree): PRGroupKey {
if (!w.linkedPR) return 'in-progress'
if (!w.linkedPR) {
return 'in-progress'
}
const s = w.linkedPR.state.toLowerCase()
if (s === 'merged') return 'done'
if (s === 'closed') return 'closed'
if (s === 'draft') return 'in-progress'
if (s === 'merged') {
return 'done'
}
if (s === 'closed') {
return 'closed'
}
if (s === 'draft') {
return 'in-progress'
}
return 'in-review'
}
@@ -265,8 +291,11 @@ function buildSections(
for (const w of unpinned) {
const key = w.repo || 'Unknown'
const list = byRepo.get(key)
if (list) list.push(w)
else byRepo.set(key, [w])
if (list) {
list.push(w)
} else {
byRepo.set(key, [w])
}
}
for (const [repo, items] of byRepo) {
sections.push({ title: repo, data: items })
@@ -276,8 +305,11 @@ function buildSections(
for (const w of unpinned) {
const key = getWorktreeStatus(w)
const list = byStatus.get(key)
if (list) list.push(w)
else byStatus.set(key, [w])
if (list) {
list.push(w)
} else {
byStatus.set(key, [w])
}
}
for (const status of WORKSPACE_STATUS_ORDER) {
const items = byStatus.get(status)
@@ -290,8 +322,11 @@ function buildSections(
for (const w of unpinned) {
const key = getPRGroupKey(w)
const list = byGroup.get(key)
if (list) list.push(w)
else byGroup.set(key, [w])
if (list) {
list.push(w)
} else {
byGroup.set(key, [w])
}
}
for (const groupKey of PR_GROUP_ORDER) {
const items = byGroup.get(groupKey)
@@ -368,11 +403,15 @@ export default function HostScreen() {
// Load persisted pins and preferences
useEffect(() => {
if (!hostId) return
if (!hostId) {
return
}
let stale = false
void (async () => {
const [pins, prefs] = await Promise.all([loadPinnedIds(hostId), loadPreferences(hostId)])
if (stale) return
if (stale) {
return
}
setPinnedIds(pins)
setSortMode(prefs.sortMode as SortMode)
setFilters({
@@ -413,10 +452,14 @@ export default function HostScreen() {
setWorktrees([])
setLastKnownWorktrees([])
}
if (!hostId) return
if (!hostId) {
return
}
let stale = false
void loadHosts().then((hosts) => {
if (stale) return
if (stale) {
return
}
const host = hosts.find((h) => h.id === hostId)
if (!host) {
setError('Host not found')
@@ -431,13 +474,17 @@ export default function HostScreen() {
}, [hostId])
const fetchWorktrees = useCallback(async () => {
if (!client || connState !== 'connected') return
if (!client || connState !== 'connected') {
return
}
const requestClient = client
const requestHostId = hostId
try {
const response = await requestClient.sendRequest('worktree.ps')
if (clientRef.current !== requestClient || hostId !== requestHostId) return
if (clientRef.current !== requestClient || hostId !== requestHostId) {
return
}
if (response.ok) {
const result = (response as RpcSuccess).result as { worktrees: Worktree[] }
setWorktrees(result.worktrees)
@@ -447,8 +494,12 @@ export default function HostScreen() {
void requestClient
.sendRequest('repo.list')
.then((repoResponse) => {
if (clientRef.current !== requestClient || hostId !== requestHostId) return
if (!repoResponse.ok) return
if (clientRef.current !== requestClient || hostId !== requestHostId) {
return
}
if (!repoResponse.ok) {
return
}
const repoResult = (repoResponse as RpcSuccess).result as { repos: RepoSummary[] }
setRepoColorsByName(
new Map(
@@ -486,7 +537,9 @@ export default function HostScreen() {
if (serverPinned.size === prev.size && [...serverPinned].every((id) => prev.has(id))) {
return prev
}
if (hostId) void savePinnedIds(hostId, serverPinned)
if (hostId) {
void savePinnedIds(hostId, serverPinned)
}
return serverPinned
})
}
@@ -502,14 +555,20 @@ export default function HostScreen() {
// Today's compat constants are wide-open so this never blocks; the wire
// format is in place to flip a switch in a future release.
useEffect(() => {
if (connState !== 'connected' || !client) return
if (connState !== 'connected' || !client) {
return
}
let cancelled = false
const requestClient = client
void (async () => {
try {
const response = await requestClient.sendRequest('status.get')
if (cancelled || clientRef.current !== requestClient) return
if (!response.ok) return
if (cancelled || clientRef.current !== requestClient) {
return
}
if (!response.ok) {
return
}
const status = (response as RpcSuccess).result as DesktopStatus
const verdict = evaluateCompat({
desktopProtocolVersion: status.protocolVersion,
@@ -538,7 +597,9 @@ export default function HostScreen() {
useFocusEffect(
useCallback(() => {
if (connState !== 'connected') return
if (connState !== 'connected') {
return
}
void fetchWorktrees()
// Why: React Navigation keeps previous stack screens mounted; only
// poll the host list while this route is visible.
@@ -553,9 +614,14 @@ export default function HostScreen() {
(worktreeId: string, pinned: boolean) => {
setPinnedIds((prev) => {
const next = new Set(prev)
if (pinned) next.add(worktreeId)
else next.delete(worktreeId)
if (hostId) void savePinnedIds(hostId, next)
if (pinned) {
next.add(worktreeId)
} else {
next.delete(worktreeId)
}
if (hostId) {
void savePinnedIds(hostId, next)
}
return next
})
},
@@ -593,7 +659,9 @@ export default function HostScreen() {
const handleDeleteWorktree = useCallback(
async (item: Worktree) => {
if (!client) return
if (!client) {
return
}
const removeFromList = (list: Worktree[]) =>
list.filter((w) => w.worktreeId !== item.worktreeId)
@@ -619,7 +687,9 @@ export default function HostScreen() {
)
const handleRemoveHost = useCallback(async () => {
if (!hostId) return
if (!hostId) {
return
}
// Why: close the shared client first so its WebSocket is gone before
// the host record disappears; otherwise the next loadHosts() the
// provider does (e.g. on remount) wouldn't find this host but the
@@ -648,7 +718,9 @@ export default function HostScreen() {
const handleSortChange = useCallback(
(value: SortMode) => {
setSortMode(value)
if (hostId) void savePreferences(hostId, { sortMode: value })
if (hostId) {
void savePreferences(hostId, { sortMode: value })
}
},
[hostId]
)
@@ -656,10 +728,11 @@ export default function HostScreen() {
const toggleActiveFilter = useCallback(() => {
setFilters((prev) => {
const next = { ...prev, activeOnly: !prev.activeOnly }
if (hostId)
if (hostId) {
void savePreferences(hostId, {
filterMode: next.activeOnly ? 'active' : 'all'
})
}
return next
})
}, [hostId])
@@ -668,10 +741,15 @@ export default function HostScreen() {
(repo: string) => {
setFilters((prev) => {
const next = new Set(prev.selectedRepos)
if (next.has(repo)) next.delete(repo)
else next.add(repo)
if (next.has(repo)) {
next.delete(repo)
} else {
next.add(repo)
}
const updated = { ...prev, selectedRepos: next }
if (hostId) void savePreferences(hostId, { selectedRepos: [...next] })
if (hostId) {
void savePreferences(hostId, { selectedRepos: [...next] })
}
return updated
})
},
@@ -680,12 +758,16 @@ export default function HostScreen() {
const clearFilters = useCallback(() => {
setFilters({ activeOnly: false, selectedRepos: new Set() })
if (hostId) void savePreferences(hostId, { filterMode: 'all', selectedRepos: [] })
if (hostId) {
void savePreferences(hostId, { filterMode: 'all', selectedRepos: [] })
}
}, [hostId])
const activeFilterCount = useMemo(() => {
let count = 0
if (filters.activeOnly) count++
if (filters.activeOnly) {
count++
}
count += filters.selectedRepos.size
return count
}, [filters])
@@ -693,7 +775,9 @@ export default function HostScreen() {
const handleGroupChange = useCallback(
(value: GroupMode) => {
setGroupMode(value)
if (hostId) void savePreferences(hostId, { groupMode: value })
if (hostId) {
void savePreferences(hostId, { groupMode: value })
}
},
[hostId]
)
@@ -716,7 +800,9 @@ export default function HostScreen() {
const uniqueRepos = useMemo(() => {
const repos = new Map<string, string>()
for (const w of displayWorktrees) {
if (!repos.has(w.repo)) repos.set(w.repo, repoColorsByName.get(w.repo) ?? repoColor(w.repo))
if (!repos.has(w.repo)) {
repos.set(w.repo, repoColorsByName.get(w.repo) ?? repoColor(w.repo))
}
}
return [...repos.entries()].map(([name, color]) => ({ name, color }))
}, [displayWorktrees, repoColorsByName])
@@ -730,9 +816,14 @@ export default function HostScreen() {
(title: string) => {
setCollapsedGroups((prev) => {
const next = new Set(prev)
if (next.has(title)) next.delete(title)
else next.add(title)
if (hostId) void savePreferences(hostId, { collapsedGroups: [...next] })
if (next.has(title)) {
next.delete(title)
} else {
next.add(title)
}
if (hostId) {
void savePreferences(hostId, { collapsedGroups: [...next] })
}
return next
})
},
@@ -799,7 +890,9 @@ export default function HostScreen() {
const verdict = headerVerdict
const isError = isErrorVerdict(verdict)
const showReconnectButton = isError && hostId && verdict.kind !== 'auth-failed'
if (!showReconnectButton) return null
if (!showReconnectButton) {
return null
}
return (
<Pressable
style={styles.reconnectButton}
@@ -979,7 +1072,9 @@ export default function HostScreen() {
isWideLayout && { maxWidth: contentMaxWidth, width: '100%', alignSelf: 'center' }
]}
renderSectionHeader={({ section }) => {
if (!section.title) return null
if (!section.title) {
return null
}
const isCollapsed = collapsedGroups.has(section.title)
const rawSection = rawSections.find((s) => s.title === section.title)
const count = rawSection?.data.length ?? 0
@@ -1272,7 +1367,9 @@ function ListSeparator() {
function repoColor(name: string): string {
const palette = ['#f97316', '#8b5cf6', '#06b6d4', '#ec4899', '#84cc16', '#f59e0b', '#6366f1']
let hash = 0
for (let i = 0; i < name.length; i++) hash = (hash * 31 + name.charCodeAt(i)) | 0
for (let i = 0; i < name.length; i++) {
hash = (hash * 31 + name.charCodeAt(i)) | 0
}
return palette[Math.abs(hash) % palette.length]!
}
File diff suppressed because it is too large Load Diff
@@ -236,8 +236,12 @@ function formatBranchEntryMeta(entry: MobileGitBranchChangeEntry): string | null
}
function diffLinePrefix(kind: MobileDiffLine['kind']): string {
if (kind === 'add') return '+'
if (kind === 'delete') return '-'
if (kind === 'add') {
return '+'
}
if (kind === 'delete') {
return '-'
}
return ' '
}
@@ -336,7 +340,9 @@ export default function MobileSourceControlScreen() {
setBranchCompareState((prev) => (prev.kind === 'ready' ? prev : { kind: 'loading' }))
try {
const baseRef = await resolveMobileBranchCompareBaseRef(client, worktreeId)
if (!isCurrentLoad()) return false
if (!isCurrentLoad()) {
return false
}
if (!baseRef) {
setBranchCompareState({ kind: 'idle' })
return true
@@ -345,7 +351,9 @@ export default function MobileSourceControlScreen() {
worktree: `id:${worktreeId}`,
baseRef
})
if (!isCurrentLoad()) return false
if (!isCurrentLoad()) {
return false
}
if (!response.ok) {
if (isMobileGitUnavailable(response.error?.code, response.error?.message)) {
setBranchCompareState({ kind: 'idle' })
@@ -359,7 +367,9 @@ export default function MobileSourceControlScreen() {
})
return true
} catch (err) {
if (!isCurrentLoad()) return false
if (!isCurrentLoad()) {
return false
}
const message = err instanceof Error ? err.message : 'Unable to load committed changes'
setBranchCompareState((prev) => {
if (options?.preserveReadyOnFailure && prev.kind === 'ready') {
@@ -404,14 +414,18 @@ export default function MobileSourceControlScreen() {
}
return false
}
if (!isCurrentLoad()) return false
if (!isCurrentLoad()) {
return false
}
setScreenState((prev) => (prev.kind === 'ready' ? prev : { kind: 'loading' }))
try {
for (let attempt = 0; attempt <= SELECTOR_RETRY_COUNT; attempt += 1) {
const response = await client.sendRequest('git.status', {
worktree: `id:${worktreeId}`
})
if (!isCurrentLoad()) return false
if (!isCurrentLoad()) {
return false
}
if (response.ok) {
const result = (response as RpcSuccess).result as MobileGitStatusResult
setScreenState({ kind: 'ready', status: result })
@@ -433,13 +447,17 @@ export default function MobileSourceControlScreen() {
isMobileGitTransientRefreshError(response.error?.code, response.error?.message)
if (shouldRetry && attempt < SELECTOR_RETRY_COUNT) {
await wait(SELECTOR_RETRY_DELAY_MS)
if (!isCurrentLoad()) return false
if (!isCurrentLoad()) {
return false
}
continue
}
throw new Error(response.error?.message || 'Unable to load source control')
}
} catch (err) {
if (!isCurrentLoad()) return false
if (!isCurrentLoad()) {
return false
}
const message = err instanceof Error ? err.message : 'Unable to load source control'
setScreenState((prev) => {
// Why: git mutations can succeed while the immediate status refresh
@@ -590,13 +608,17 @@ export default function MobileSourceControlScreen() {
runner: () => Promise<void>,
options?: { clearCommitMessage?: boolean }
) => {
if (busyActionRef.current) return false
if (busyActionRef.current) {
return false
}
busyActionRef.current = actionId
setBusyAction(actionId)
setActionError(null)
try {
await runner()
if (!mountedRef.current) return false
if (!mountedRef.current) {
return false
}
if (options?.clearCommitMessage) {
setCommitMessage('')
}
@@ -604,7 +626,9 @@ export default function MobileSourceControlScreen() {
await loadStatus({ preserveReadyOnFailure: true, force: true })
return true
} catch (err) {
if (!mountedRef.current) return false
if (!mountedRef.current) {
return false
}
triggerError()
setActionError(err instanceof Error ? err.message : 'Source control action failed')
return false
@@ -655,19 +679,25 @@ export default function MobileSourceControlScreen() {
const stageAll = useCallback(async () => {
const filePaths = stageablePaths
if (filePaths.length === 0) return
if (filePaths.length === 0) {
return
}
await runGitAction('stage-all', 'git.bulkStage', { filePaths })
}, [runGitAction, stageablePaths])
const unstageAll = useCallback(async () => {
const filePaths = unstageablePaths
if (filePaths.length === 0) return
if (filePaths.length === 0) {
return
}
await runGitAction('unstage-all', 'git.bulkUnstage', { filePaths })
}, [runGitAction, unstageablePaths])
const commit = useCallback(async () => {
const message = commitMessage.trim()
if (!message) return false
if (!message) {
return false
}
return await runGitWorkflow(
'commit',
async () => {
@@ -680,8 +710,12 @@ export default function MobileSourceControlScreen() {
const runCommitFollowUps = useCallback(
async (actionId: string, afterCommit: () => Promise<void>) => {
const message = commitMessage.trim()
if (!message) return false
if (busyActionRef.current) return false
if (!message) {
return false
}
if (busyActionRef.current) {
return false
}
busyActionRef.current = actionId
setBusyAction(actionId)
setActionError(null)
@@ -690,13 +724,17 @@ export default function MobileSourceControlScreen() {
await sendCommitRequest(message)
didCommit = true
await afterCommit()
if (!mountedRef.current) return false
if (!mountedRef.current) {
return false
}
setCommitMessage('')
triggerSuccess()
await loadStatus({ preserveReadyOnFailure: true, force: true })
return true
} catch (err) {
if (!mountedRef.current) return false
if (!mountedRef.current) {
return false
}
triggerError()
const message = err instanceof Error ? err.message : 'Source control action failed'
if (didCommit) {
@@ -778,10 +816,16 @@ export default function MobileSourceControlScreen() {
const openFile = useCallback(
async (entry: MobileGitStatusEntry) => {
if (entry.status === 'deleted' || entry.conflictStatus === 'unresolved') return
if (openingPathRef.current || busyActionRef.current) return
if (entry.status === 'deleted' || entry.conflictStatus === 'unresolved') {
return
}
if (openingPathRef.current || busyActionRef.current) {
return
}
if (!client || connState !== 'connected') {
if (!mountedRef.current) return
if (!mountedRef.current) {
return
}
setActionError('Waiting for desktop...')
return
}
@@ -803,7 +847,9 @@ export default function MobileSourceControlScreen() {
if (!response.ok) {
throw new Error(response.error?.message || 'Unable to open diff')
}
if (!mountedRef.current) return
if (!mountedRef.current) {
return
}
triggerSelection()
if (origin === 'session') {
router.back()
@@ -818,7 +864,9 @@ export default function MobileSourceControlScreen() {
`/h/${encodeURIComponent(hostId)}/session/${encodeURIComponent(worktreeId)}${query ? `?${query}` : ''}`
)
} catch (err) {
if (!mountedRef.current) return
if (!mountedRef.current) {
return
}
triggerError()
setActionError(err instanceof Error ? err.message : 'Unable to open diff')
} finally {
@@ -835,9 +883,13 @@ export default function MobileSourceControlScreen() {
const openBranchDiff = useCallback(
async (entry: MobileGitBranchChangeEntry) => {
if (openingBranchPathRef.current || openingPathRef.current || busyActionRef.current) return
if (openingBranchPathRef.current || openingPathRef.current || busyActionRef.current) {
return
}
if (!client || connState !== 'connected') {
if (!mountedRef.current) return
if (!mountedRef.current) {
return
}
setActionError('Waiting for desktop...')
return
}
@@ -872,7 +924,9 @@ export default function MobileSourceControlScreen() {
throw new Error('Binary branch diff preview unavailable on mobile')
}
const diff = buildMobileDiffLines(result.originalContent, result.modifiedContent)
if (!mountedRef.current) return
if (!mountedRef.current) {
return
}
setBranchDiffPreview({
kind: 'ready',
entry,
@@ -882,7 +936,9 @@ export default function MobileSourceControlScreen() {
})
triggerSelection()
} catch (err) {
if (!mountedRef.current) return
if (!mountedRef.current) {
return
}
triggerError()
setBranchDiffPreview({
kind: 'error',
File diff suppressed because it is too large Load Diff
+81 -27
View File
@@ -105,9 +105,13 @@ function formatDuration(ms: number): string {
const totalHours = Math.floor(totalMinutes / 60)
const days = Math.floor(totalHours / 24)
const hours = totalHours % 24
if (days > 0) return `${days}d ${hours}h`
if (days > 0) {
return `${days}d ${hours}h`
}
const minutes = totalMinutes % 60
if (totalHours > 0) return `${totalHours}h ${minutes}m`
if (totalHours > 0) {
return `${totalHours}h ${minutes}m`
}
return `${totalMinutes}m`
}
@@ -134,7 +138,9 @@ function fetchStats(
client
.sendRequest('stats.summary')
.then((response) => {
if (disposed()) return
if (disposed()) {
return
}
if (response.ok) {
setStats(response.result as StatsSummary)
}
@@ -157,7 +163,9 @@ function fetchWorktreeInfo(
// momentarily flip to "0 worktrees" / disappear during reconnects.
const markLoadedIfMissing = () => {
setInfo((prev) => {
if (prev[hostId]) return prev
if (prev[hostId]) {
return prev
}
return {
...prev,
[hostId]: {
@@ -173,7 +181,9 @@ function fetchWorktreeInfo(
client
.sendRequest('worktree.ps')
.then((response) => {
if (disposed()) return
if (disposed()) {
return
}
if (response.ok) {
const result = response.result as { worktrees: WorktreeSummary[] }
const worktrees = result.worktrees ?? []
@@ -195,7 +205,9 @@ function fetchWorktreeInfo(
}
})
.catch(() => {
if (!disposed()) markLoadedIfMissing()
if (!disposed()) {
markLoadedIfMissing()
}
})
}
@@ -210,7 +222,9 @@ function fetchAccountsSnapshot(
client
.sendRequest('accounts.list')
.then((response) => {
if (disposed()) return
if (disposed()) {
return
}
if (response.ok) {
const snapshot = response.result as AccountsSnapshot
setSnapshots((prev) => ({ ...prev, [hostId]: snapshot }))
@@ -233,7 +247,9 @@ function fetchTaskProviders(
client.sendRequest('linear.status')
])
.then(([settingsResponse, preflightResponse, linearResponse]) => {
if (disposed()) return
if (disposed()) {
return
}
const settings = settingsResponse.ok
? (((settingsResponse.result as { settings?: HomeTaskSettings }).settings ??
{}) as HomeTaskSettings)
@@ -252,7 +268,9 @@ function fetchTaskProviders(
setProviders((prev) => ({ ...prev, [hostId]: providers }))
})
.catch(() => {
if (disposed()) return
if (disposed()) {
return
}
setProviders((prev) => (prev[hostId] ? prev : { ...prev, [hostId]: ['github'] }))
})
}
@@ -302,7 +320,9 @@ export default function HomeScreen() {
// openEntry on cold start (which serialised behind the first one and
// showed up as multi-second connect latency).
useEffect(() => {
if (hosts.length > 0) primeHosts(hosts)
if (hosts.length > 0) {
primeHosts(hosts)
}
}, [hosts, primeHosts])
const allClientsRef = useRef<Array<{ hostId: string; client: RpcClient }>>([])
// Why: the focus callback stays stable to avoid refetching on every
@@ -318,11 +338,15 @@ export default function HomeScreen() {
// Stream/list responses overwrite this seed in place when they arrive.
const hydratedRef = useRef(false)
useEffect(() => {
if (hydratedRef.current) return
if (hydratedRef.current) {
return
}
hydratedRef.current = true
let cancelled = false
void loadHomeSnapshot().then((snap) => {
if (cancelled || !snap) return
if (cancelled || !snap) {
return
}
setWorktreeInfo((prev) => (Object.keys(prev).length > 0 ? prev : snap.worktreeInfo))
setAccountsByHost((prev) => (Object.keys(prev).length > 0 ? prev : snap.accountsByHost))
for (const [hostId, info] of Object.entries(snap.worktreeInfo)) {
@@ -357,10 +381,14 @@ export default function HomeScreen() {
useCallback(() => {
let stale = false
void loadHosts().then((h) => {
if (!stale) setHosts(h)
if (!stale) {
setHosts(h)
}
})
void AsyncStorage.getItem('orca:last-visited-worktree').then((raw) => {
if (stale || !raw) return
if (stale || !raw) {
return
}
try {
setLastVisited(JSON.parse(raw))
} catch {}
@@ -427,7 +455,9 @@ export default function HomeScreen() {
// already tracked — otherwise the initial-acquire frame (entry not
// yet materialised) would briefly flip every host to 'disconnected'.
for (const host of hosts) {
if (liveIds.has(host.id)) continue
if (liveIds.has(host.id)) {
continue
}
if (!host.publicKeyB64 || !host.deviceToken) {
if (next[host.id] !== 'auth-failed') {
next[host.id] = 'auth-failed'
@@ -470,7 +500,9 @@ export default function HomeScreen() {
}
if (!unsubAccounts) {
unsubAccounts = entry.client.subscribe('accounts.subscribe', null, (payload) => {
if (!payload || typeof payload !== 'object') return
if (!payload || typeof payload !== 'object') {
return
}
const evt = payload as { type?: string; snapshot?: AccountsSnapshot }
if ((evt.type === 'ready' || evt.type === 'snapshot') && evt.snapshot) {
setAccountsByHost((prev) => ({ ...prev, [entry.hostId]: evt.snapshot! }))
@@ -503,7 +535,9 @@ export default function HomeScreen() {
})
}
return () => {
for (const c of cleanups) c()
for (const c of cleanups) {
c()
}
}
// Why: depend on the host-id set AND each entry's client identity, so
// resubscriptions don't fire on every render that produces a new
@@ -538,10 +572,14 @@ export default function HomeScreen() {
if (lastVisited && hostStates[lastVisited.hostId] === 'connected') {
const cached = getCachedWorktrees(lastVisited.hostId) as WorktreeSummary[] | null
const match = cached?.find((w) => w.worktreeId === lastVisited.worktreeId)
if (match) return { hostId: lastVisited.hostId, worktree: match }
if (match) {
return { hostId: lastVisited.hostId, worktree: match }
}
}
for (const host of sortedHosts) {
if (hostStates[host.id] !== 'connected') continue
if (hostStates[host.id] !== 'connected') {
continue
}
const info = worktreeInfo[host.id]
if (info?.lastActiveWorktree) {
return { hostId: host.id, worktree: info.lastActiveWorktree }
@@ -556,12 +594,18 @@ export default function HomeScreen() {
const accountsHosts = useMemo(() => {
const items: Array<{ host: HostProfile; snapshot: AccountsSnapshot }> = []
for (const host of sortedHosts) {
if (hostStates[host.id] !== 'connected') continue
if (hostStates[host.id] !== 'connected') {
continue
}
const snap = accountsByHost[host.id]
if (!snap) continue
if (!snap) {
continue
}
const hasClaude = snap.claude.accounts.length > 0
const hasCodex = snap.codex.accounts.length > 0
if (hasClaude || hasCodex) items.push({ host, snapshot: snap })
if (hasClaude || hasCodex) {
items.push({ host, snapshot: snap })
}
}
return items
}, [sortedHosts, hostStates, accountsByHost])
@@ -575,7 +619,9 @@ export default function HomeScreen() {
: []
const openTasks = useCallback(
(provider?: TaskProvider) => {
if (!primaryConnectedHost) return
if (!primaryConnectedHost) {
return
}
const suffix = provider ? `?taskSource=${provider}` : ''
router.push(`/h/${primaryConnectedHost.id}/tasks${suffix}`)
},
@@ -636,7 +682,9 @@ export default function HomeScreen() {
)
async function handleRename(newName: string) {
if (!renameTarget) return
if (!renameTarget) {
return
}
try {
await renameHost(renameTarget.id, newName)
setRenameTarget(null)
@@ -647,7 +695,9 @@ export default function HomeScreen() {
}
async function handleRemove() {
if (!confirmRemove) return
if (!confirmRemove) {
return
}
try {
// Why: close the shared client first so the WebSocket is gone
// before the host record disappears from loadHosts().
@@ -921,7 +971,9 @@ export default function HomeScreen() {
provider === 'claude'
? snapshot.claude.accounts
: snapshot.codex.accounts
if (accounts.length === 0) return null
if (accounts.length === 0) {
return null
}
const limits = getActiveProviderRateLimits(snapshot, provider)
const isFetching =
limits?.status === 'fetching' || limits?.status === 'idle'
@@ -977,7 +1029,9 @@ export default function HomeScreen() {
message={actionTarget ? endpointLabel(actionTarget.endpoint) : undefined}
actions={(() => {
const host = actionTarget
if (!host) return []
if (!host) {
return []
}
const state = hostStates[host.id] ?? 'connecting'
const isLive =
state === 'connected' ||
+21 -7
View File
@@ -73,7 +73,9 @@ export default function PairConfirmScreen() {
}, [])
async function confirm() {
if (!offer) return
if (!offer) {
return
}
setStatus('connecting')
logsRef.current = []
setLogs([])
@@ -92,7 +94,9 @@ export default function PairConfirmScreen() {
try {
client = connect(offer.endpoint, offer.deviceToken, offer.publicKeyB64, {
onLog: (entry) => {
if (!mountedRef.current || activePairingAttemptRef.current !== attempt) return
if (!mountedRef.current || activePairingAttemptRef.current !== attempt) {
return
}
logsRef.current = [...logsRef.current, entry]
setLogs(logsRef.current)
}
@@ -103,7 +107,9 @@ export default function PairConfirmScreen() {
if (activePairingAttemptRef.current === attempt) {
activePairingAttemptRef.current = null
}
if (!mountedRef.current || !attemptIsCurrent) return
if (!mountedRef.current || !attemptIsCurrent) {
return
}
} catch (err) {
const timedOut = attempt.timedOut
const attemptIsCurrent = activePairingAttemptRef.current === attempt
@@ -111,7 +117,9 @@ export default function PairConfirmScreen() {
if (activePairingAttemptRef.current === attempt) {
activePairingAttemptRef.current = null
}
if (!mountedRef.current || !attemptIsCurrent) return
if (!mountedRef.current || !attemptIsCurrent) {
return
}
console.warn('[pair-confirm] connect failed', err)
setStatus('error')
setErrorMessage(
@@ -123,7 +131,9 @@ export default function PairConfirmScreen() {
}
if (!response.ok) {
if (!mountedRef.current) return
if (!mountedRef.current) {
return
}
setStatus('error')
setErrorMessage(
response.error.code === 'unauthorized'
@@ -144,10 +154,14 @@ export default function PairConfirmScreen() {
publicKeyB64: offer.publicKeyB64,
lastConnected: Date.now()
})
if (!mountedRef.current) return
if (!mountedRef.current) {
return
}
router.replace(`/h/${hostId}`)
} catch (err) {
if (!mountedRef.current) return
if (!mountedRef.current) {
return
}
console.warn('[pair-confirm] save failed', err)
setStatus('error')
setErrorMessage(
+24 -8
View File
@@ -59,7 +59,9 @@ export default function PairScanScreen() {
const handleBarCodeScanned = useCallback(
({ data }: { data: string }) => {
if (processingRef.current) return
if (processingRef.current) {
return
}
processingRef.current = true
const offer = decodePairingUrl(data)
@@ -77,7 +79,9 @@ export default function PairScanScreen() {
const handlePasteSubmit = useCallback((input: string) => {
setPasteVisible(false)
if (processingRef.current) return
if (processingRef.current) {
return
}
processingRef.current = true
const offer = parsePairingCode(input)
@@ -111,7 +115,9 @@ export default function PairScanScreen() {
try {
client = connect(offer.endpoint, offer.deviceToken, offer.publicKeyB64, {
onLog: (entry) => {
if (!mountedRef.current || activePairingAttemptRef.current !== attempt) return
if (!mountedRef.current || activePairingAttemptRef.current !== attempt) {
return
}
logsRef.current = [...logsRef.current, entry]
setLogs(logsRef.current)
}
@@ -122,7 +128,9 @@ export default function PairScanScreen() {
if (activePairingAttemptRef.current === attempt) {
activePairingAttemptRef.current = null
}
if (!mountedRef.current || !attemptIsCurrent) return
if (!mountedRef.current || !attemptIsCurrent) {
return
}
} catch (err) {
const timedOut = attempt.timedOut
const attemptIsCurrent = activePairingAttemptRef.current === attempt
@@ -130,7 +138,9 @@ export default function PairScanScreen() {
if (activePairingAttemptRef.current === attempt) {
activePairingAttemptRef.current = null
}
if (!mountedRef.current || !attemptIsCurrent) return
if (!mountedRef.current || !attemptIsCurrent) {
return
}
console.warn('[pair] connect failed', err)
setStatus('error')
setErrorMessage(
@@ -143,7 +153,9 @@ export default function PairScanScreen() {
}
if (!response.ok) {
if (!mountedRef.current) return
if (!mountedRef.current) {
return
}
if (response.error.code === 'unauthorized') {
setStatus('error')
setErrorMessage('Authentication failed — token may be expired')
@@ -167,10 +179,14 @@ export default function PairScanScreen() {
publicKeyB64: offer.publicKeyB64,
lastConnected: Date.now()
})
if (!mountedRef.current) return
if (!mountedRef.current) {
return
}
router.replace(`/h/${hostId}`)
} catch (err) {
if (!mountedRef.current) return
if (!mountedRef.current) {
return
}
console.warn('[pair] save failed', err)
setStatus('error')
setErrorMessage(
+33 -11
View File
@@ -47,9 +47,13 @@ const AUTO_RESTORE_FIT_OPTIONS: (PickerOption<RestoreValue> & { ms: number | nul
]
function valueFromMs(ms: number | null | undefined): RestoreValue {
if (ms == null) return 'indefinite'
if (ms == null) {
return 'indefinite'
}
const exact = AUTO_RESTORE_FIT_OPTIONS.find((o) => o.ms === ms)
if (exact) return exact.value
if (exact) {
return exact.value
}
// Why: server may return a non-preset ms (custom value, future preset,
// or server-side clamp). Snap to the closest finite preset so the
// picker's selected radio agrees with the row sublabel rendered by
@@ -57,7 +61,9 @@ function valueFromMs(ms: number | null | undefined): RestoreValue {
let closest: (typeof AUTO_RESTORE_FIT_OPTIONS)[number] | null = null
let bestDelta = Infinity
for (const opt of AUTO_RESTORE_FIT_OPTIONS) {
if (opt.ms == null) continue
if (opt.ms == null) {
continue
}
const delta = Math.abs(opt.ms - ms)
if (delta < bestDelta) {
bestDelta = delta
@@ -68,8 +74,12 @@ function valueFromMs(ms: number | null | undefined): RestoreValue {
}
function autoRestoreSummary(ms: number | null | undefined): string {
if (ms === undefined) return '…'
if (ms === null) return AUTO_RESTORE_FIT_OPTIONS[0]!.label
if (ms === undefined) {
return '…'
}
if (ms === null) {
return AUTO_RESTORE_FIT_OPTIONS[0]!.label
}
const exact = AUTO_RESTORE_FIT_OPTIONS.find((o) => o.ms === ms)
return exact ? exact.label : `After ${Math.round(ms / 1000)}s`
}
@@ -174,7 +184,9 @@ export default function TerminalSettingsScreen() {
const refreshShortcutLayout = useCallback(() => {
const refreshSeq = layoutWriteSeqRef.current
void loadTerminalAccessoryLayout().then((layout) => {
if (pendingLayoutWritesRef.current > 0 || refreshSeq !== layoutWriteSeqRef.current) return
if (pendingLayoutWritesRef.current > 0 || refreshSeq !== layoutWriteSeqRef.current) {
return
}
setVisibleBuiltInIds(layout.visibleBuiltInIds)
})
}, [])
@@ -230,11 +242,15 @@ export default function TerminalSettingsScreen() {
let cancelled = false
for (const host of hosts) {
const client = hostClientsById.get(host.id) ?? null
if (!client) continue
if (!client) {
continue
}
void client
.sendRequest('terminal.getAutoRestoreFit')
.then((resp) => {
if (cancelled) return
if (cancelled) {
return
}
const value = (resp as { ms?: number | null } | null)?.ms
// Why: reconnect/status ticks can replay the same value; preserving
// object identity avoids rerendering every settings row again.
@@ -253,9 +269,13 @@ export default function TerminalSettingsScreen() {
async function selectValue(hostId: string, value: RestoreValue) {
const client = hostClientsById.get(hostId) ?? null
if (!client) return
if (!client) {
return
}
const opt = AUTO_RESTORE_FIT_OPTIONS.find((o) => o.value === value)
if (!opt) return
if (!opt) {
return
}
setHostMs((prev) => setTerminalAutoRestoreFitMsForHost(prev, hostId, opt.ms))
try {
const resp = (await client.sendRequest('terminal.setAutoRestoreFit', {
@@ -397,7 +417,9 @@ export default function TerminalSettingsScreen() {
options={AUTO_RESTORE_FIT_OPTIONS}
selected={valueFromMs(pickerHost ? hostMs[pickerHost.id] : null)}
onSelect={(v) => {
if (pickerHost) void selectValue(pickerHost.id, v)
if (pickerHost) {
void selectValue(pickerHost.id, v)
}
}}
onClose={() => setPickerHostId(null)}
/>
+21 -7
View File
@@ -160,7 +160,9 @@ export default function TroubleshootScreen() {
results.push({ label: 'Paired hosts', status: 'warn', detail: 'Could not read host data' })
}
if (!isCurrentRun()) return
if (!isCurrentRun()) {
return
}
setChecks([...results])
const internetCheck = startDiagnosticFetchTimeout(5000)
@@ -169,14 +171,18 @@ export default function TroubleshootScreen() {
const resp = await fetch('https://dns.google/resolve?name=example.com&type=A', {
signal: internetCheck.signal
})
if (!isCurrentRun()) return
if (!isCurrentRun()) {
return
}
results.push(
resp.ok
? { label: 'Internet', status: 'pass', detail: 'Connected' }
: { label: 'Internet', status: 'warn', detail: 'Unexpected response' }
)
} catch {
if (!isCurrentRun()) return
if (!isCurrentRun()) {
return
}
results.push({ label: 'Internet', status: 'fail', detail: 'No connection' })
} finally {
internetCheck.dispose()
@@ -185,15 +191,21 @@ export default function TroubleshootScreen() {
}
}
if (!isCurrentRun()) return
if (!isCurrentRun()) {
return
}
setChecks([...results])
try {
const hosts = await loadHosts()
for (const host of hosts) {
if (!isCurrentRun()) return
if (!isCurrentRun()) {
return
}
const reachable = await testHostReachability(host.endpoint)
if (!isCurrentRun()) return
if (!isCurrentRun()) {
return
}
results.push({
label: host.name,
status: reachable ? 'pass' : 'fail',
@@ -207,7 +219,9 @@ export default function TroubleshootScreen() {
results.push({ label: 'Hosts', status: 'warn', detail: 'Could not test' })
}
if (!isCurrentRun()) return
if (!isCurrentRun()) {
return
}
results.push({
label: 'Platform',
+12 -4
View File
@@ -36,11 +36,15 @@ function e2eeEncrypt(plaintext: string, sharedKey: Uint8Array): string {
function e2eeDecrypt(encrypted: string, sharedKey: Uint8Array): string | null {
const bundle = Uint8Array.from(Buffer.from(encrypted, 'base64'))
if (bundle.length < nacl.box.nonceLength + nacl.box.overheadLength) return null
if (bundle.length < nacl.box.nonceLength + nacl.box.overheadLength) {
return null
}
const nonce = bundle.slice(0, nacl.box.nonceLength)
const ciphertext = bundle.slice(nacl.box.nonceLength)
const plaintext = nacl.box.open.after(ciphertext, nonce, sharedKey)
if (!plaintext) return null
if (!plaintext) {
return null
}
return new TextDecoder().decode(plaintext)
}
@@ -147,7 +151,9 @@ function toGitStatusEntry(entry: FakeGitEntry): MobileGitStatusEntry {
}
function stageFakeGitEntry(entry: FakeGitEntry, filePaths: Set<string>): FakeGitEntry {
if (!filePaths.has(entry.path)) return entry
if (!filePaths.has(entry.path)) {
return entry
}
if (entry.area === 'untracked') {
return { ...entry, area: 'staged', status: 'added', stagedFromUntracked: true }
}
@@ -155,7 +161,9 @@ function stageFakeGitEntry(entry: FakeGitEntry, filePaths: Set<string>): FakeGit
}
function unstageFakeGitEntry(entry: FakeGitEntry, filePaths: Set<string>): FakeGitEntry {
if (!filePaths.has(entry.path)) return entry
if (!filePaths.has(entry.path)) {
return entry
}
if (entry.stagedFromUntracked) {
return { ...entry, area: 'untracked', status: 'untracked', stagedFromUntracked: false }
}
+12 -4
View File
@@ -140,7 +140,9 @@ async function listHandles(
async function ensureSecondHandle(ws: WebSocket, handleA: string): Promise<string> {
const terminals = await listHandles(ws)
const existing = terminals.find((terminal) => terminal.handle !== handleA)
if (existing) return existing.handle
if (existing) {
return existing.handle
}
const created = await send(ws, 'terminal.create', {
worktree: worktreeSelector,
@@ -174,7 +176,9 @@ async function captureSnapshot(ws: WebSocket, label: string, handle: string): Pr
}
})
streamListeners.set(id, (result) => {
if (result.type !== 'scrollback') return
if (result.type !== 'scrollback') {
return
}
clearTimeout(timeout)
pending.delete(id)
streamListeners.delete(id)
@@ -333,7 +337,9 @@ ws.on('message', (data) => {
return
}
const plaintext = decrypt(raw)
if (!plaintext) return
if (!plaintext) {
return
}
const response = JSON.parse(plaintext) as RpcResponse
const result = response.result
@@ -344,7 +350,9 @@ ws.on('message', (data) => {
}
const request = pending.get(response.id)
if (!request || request.method === 'terminal.subscribe') return
if (!request || request.method === 'terminal.subscribe') {
return
}
pending.delete(response.id)
request.resolve(response)
})
@@ -338,7 +338,9 @@ ws.on('message', (data) => {
return
}
const plaintext = decrypt(raw)
if (!plaintext) return
if (!plaintext) {
return
}
const response = JSON.parse(plaintext) as RpcResponse
const result = response.result
@@ -349,7 +351,9 @@ ws.on('message', (data) => {
}
const request = pending.get(response.id)
if (!request || request.method === 'terminal.subscribe') return
if (!request || request.method === 'terminal.subscribe') {
return
}
pending.delete(response.id)
request.resolve(response)
})
+6 -2
View File
@@ -114,7 +114,9 @@ function formatResponse(response: RpcResponse): string {
}
async function chooseWorktree(ws: WebSocket): Promise<string> {
if (worktreeSelector) return worktreeSelector
if (worktreeSelector) {
return worktreeSelector
}
const response = await send(ws, 'worktree.ps')
if (!response.ok) {
@@ -275,7 +277,9 @@ ws.on('message', (data) => {
// connect flow above. The global listener only cares about encrypted RPC.
return
}
if (!plaintext) return
if (!plaintext) {
return
}
const response = JSON.parse(plaintext) as RpcResponse
if (response._meta?.runtimeId) {
runtimeId = response._meta.runtimeId
+15 -5
View File
@@ -430,7 +430,9 @@ export function MobileBrowserPane({
busyRef.current = true
setBusy(true)
let startupTimer: ReturnType<typeof setTimeout> | null = setTimeout(() => {
if (streamGenerationRef.current !== generation) return
if (streamGenerationRef.current !== generation) {
return
}
busyRef.current = false
setBusy(false)
setError('Browser stream timed out.')
@@ -449,7 +451,9 @@ export function MobileBrowserPane({
...streamRequest
},
(payload) => {
if (streamGenerationRef.current !== generation) return
if (streamGenerationRef.current !== generation) {
return
}
const event = payload as {
type?: string
message?: string
@@ -509,7 +513,9 @@ export function MobileBrowserPane({
},
{
onBinaryFrame: (frame) => {
if (streamGenerationRef.current !== generation) return
if (streamGenerationRef.current !== generation) {
return
}
clearStartupTimer()
if (cacheKey) {
applyFrameThrottled(frame, cacheKey)
@@ -758,9 +764,13 @@ export function MobileBrowserPane({
clearLongPressTimer()
longPressTimerRef.current = setTimeout(() => {
const start = startPointRef.current
if (!start) return
if (!start) {
return
}
const point = mapTouchPoint(start.x, start.y)
if (!point) return
if (!point) {
return
}
rightClickSentRef.current = true
void sendPointerClick(point, 'right')
onToast('Right click')
+9 -3
View File
@@ -34,10 +34,14 @@ let memoryCache: HomeSnapshot | null = null
let writeTimer: ReturnType<typeof setTimeout> | null = null
export async function loadHomeSnapshot(): Promise<HomeSnapshot | null> {
if (memoryCache) return memoryCache
if (memoryCache) {
return memoryCache
}
try {
const raw = await AsyncStorage.getItem(STORAGE_KEY)
if (!raw) return null
if (!raw) {
return null
}
const parsed = JSON.parse(raw) as HomeSnapshot
if (
typeof parsed !== 'object' ||
@@ -58,7 +62,9 @@ export async function loadHomeSnapshot(): Promise<HomeSnapshot | null> {
// (one per provider fetch finishing) doesn't hammer AsyncStorage.
export function saveHomeSnapshot(snapshot: HomeSnapshot): void {
memoryCache = snapshot
if (writeTimer) clearTimeout(writeTimer)
if (writeTimer) {
clearTimeout(writeTimer)
}
writeTimer = setTimeout(() => {
writeTimer = null
void AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(snapshot)).catch(() => {})
+6 -2
View File
@@ -20,13 +20,17 @@ export function setCachedWorktrees(hostId: string, worktrees: unknown[]): void {
cache.set(hostId, { worktrees, at: Date.now() })
if (cache.size > MAX_ENTRIES) {
const oldest = cache.keys().next().value
if (oldest) cache.delete(oldest)
if (oldest) {
cache.delete(oldest)
}
}
}
export function getCachedWorktrees(hostId: string): unknown[] | null {
const entry = cache.get(hostId)
if (!entry) return null
if (!entry) {
return null
}
if (Date.now() - entry.at > MAX_AGE_MS) {
cache.delete(hostId)
return null
+6 -2
View File
@@ -23,8 +23,12 @@ type Props = {
}
function iconForAction(label: string, destructive?: boolean, icon?: LucideIcon): LucideIcon {
if (icon) return icon
if (destructive || /delete|remove/i.test(label)) return Trash2
if (icon) {
return icon
}
if (destructive || /delete|remove/i.test(label)) {
return Trash2
}
return Edit3
}
+12 -4
View File
@@ -61,7 +61,9 @@ export function BottomDrawer({
// Why: hidden drawers are rendered by parent screens even while closed; keep
// their Reanimated/Gesture setup out of hot paths like commit-message typing.
if (!resolvedMounted) return null
if (!resolvedMounted) {
return null
}
return (
<MountedBottomDrawer
@@ -121,7 +123,9 @@ function MountedBottomDrawer({
// useAnimatedKeyboard). Keyboard event listeners work on both platforms
// and give us the exact height to shift the drawer by.
useEffect(() => {
if (!visible) return
if (!visible) {
return
}
const showEvent = Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow'
const hideEvent = Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide'
@@ -142,7 +146,9 @@ function MountedBottomDrawer({
}, [visible, insets.bottom])
useEffect(() => {
if (!visible) return
if (!visible) {
return
}
const sub = BackHandler.addEventListener('hardwareBackPress', () => {
onClose()
@@ -215,7 +221,9 @@ function MountedBottomDrawer({
}
})
.onEnd((e) => {
if (!contentDragCanDismiss.value || scrollOffsetY.value > TOP_SCROLL_EPSILON) return
if (!contentDragCanDismiss.value || scrollOffsetY.value > TOP_SCROLL_EPSILON) {
return
}
const translationY = e.translationY - contentDragStartY.value
if (translationY > DISMISS_THRESHOLD || e.velocityY > 500) {
+9 -3
View File
@@ -28,15 +28,21 @@ function formatTime(ts: number, baseTs: number): string {
// Why: show elapsed seconds since the first entry — absolute wall-clock
// time isn't actionable when debugging "why is connecting stuck".
const elapsed = Math.max(0, ts - baseTs) / 1000
if (elapsed < 10) return `+${elapsed.toFixed(2)}s`
if (elapsed < 100) return `+${elapsed.toFixed(1)}s`
if (elapsed < 10) {
return `+${elapsed.toFixed(2)}s`
}
if (elapsed < 100) {
return `+${elapsed.toFixed(1)}s`
}
return `+${Math.round(elapsed)}s`
}
export function ConnectionLog({ entries, title }: Props) {
const scrollRef = useRef<ScrollView | null>(null)
if (entries.length === 0) return null
if (entries.length === 0) {
return null
}
const baseTs = entries[0]!.ts
return (
+12 -4
View File
@@ -119,7 +119,9 @@ export function CustomKeyModal({ visible, onClose, onKeysChanged, onManageShortc
const previewKeyLabel = useMemo(() => {
const special = SPECIAL_KEY_BY_ID[shortcutKey]
if (special) return special.label
if (special) {
return special.label
}
return shortcutKey.length === 1 ? shortcutKey.toUpperCase() : shortcutKey
}, [shortcutKey])
@@ -156,14 +158,18 @@ export function CustomKeyModal({ visible, onClose, onKeysChanged, onManageShortc
const handleShortcutSave = useCallback(() => {
const built = buildTerminalShortcutKey({ key: shortcutKey, modifiers: shortcutModifiers })
if (!built) return
if (!built) {
return
}
void addKey({ label: built.label, bytes: built.bytes, enter: false })
}, [addKey, shortcutKey, shortcutModifiers])
const handleMacroSave = useCallback(() => {
const label = macroLabel.trim() || macroText.trim().slice(0, 12)
const text = macroText
if (!label || !text) return
if (!label || !text) {
return
}
const bytes = macroEnter ? `${text}\r` : text
void addKey({ label, bytes, enter: false })
}, [addKey, macroLabel, macroText, macroEnter])
@@ -324,7 +330,9 @@ export function CustomKeyModal({ visible, onClose, onKeysChanged, onManageShortc
<View style={styles.keyGrid}>
{group.ids.map((id) => {
const key = SPECIAL_KEY_BY_ID[id]
if (!key) return null
if (!key) {
return null
}
const selected = shortcutKey === id
const flexBasis = `${100 / group.columns}%` as const
return (
+15 -5
View File
@@ -79,11 +79,21 @@ function AgentLetterIcon({ letter, size = 16 }: { letter: string; size?: number
}
export function MobileAgentIcon({ agentId, size = 16 }: { agentId: string; size?: number }) {
if (agentId === 'claude') return <ClaudeIcon size={size} />
if (agentId === 'codex') return <OpenAIIcon size={size} />
if (agentId === 'pi') return <PiIcon size={size} />
if (agentId === 'omp') return <OmpIcon size={size} />
if (agentId === 'aider') return <AiderIcon size={size} />
if (agentId === 'claude') {
return <ClaudeIcon size={size} />
}
if (agentId === 'codex') {
return <OpenAIIcon size={size} />
}
if (agentId === 'pi') {
return <PiIcon size={size} />
}
if (agentId === 'omp') {
return <OmpIcon size={size} />
}
if (agentId === 'aider') {
return <AiderIcon size={size} />
}
if (agentId === '__blank__' || agentId === 'blank') {
return <Terminal size={size} color={colors.textMuted} />
}
@@ -29,13 +29,21 @@ const EDITOR_DOCUMENT_URL = `${EDITOR_DOCUMENT_ORIGIN}/rich-markdown-editor`
function normalizeExternalEditorUrl(value: string): string | null {
const url = value.trim()
if (!url) return null
if (!url) {
return null
}
for (let index = 0; index < url.length; index += 1) {
const code = url.charCodeAt(index)
if (code <= 32 || code === 127) return null
if (code <= 32 || code === 127) {
return null
}
}
if (/^mailto:/i.test(url)) {
return url
}
if (!/^https?:\/\//i.test(url)) {
return null
}
if (/^mailto:/i.test(url)) return url
if (!/^https?:\/\//i.test(url)) return null
try {
const parsed = new URL(url)
return parsed.protocol === 'http:' || parsed.protocol === 'https:' ? parsed.toString() : null
@@ -128,7 +136,9 @@ function MobileRichMarkdownEditorInner({ content, editable, onChange }: Props) {
)
useEffect(() => {
if (!readyRef.current) return
if (!readyRef.current) {
return
}
if (currentWebViewContentRef.current !== content) {
applyContent(content)
}
@@ -148,7 +158,9 @@ function MobileRichMarkdownEditorInner({ content, editable, onChange }: Props) {
} catch {
return
}
if (!message || typeof message !== 'object') return
if (!message || typeof message !== 'object') {
return
}
const editorMessage = message as Partial<EditorWebViewMessage>
if ('type' in message && message.type === 'ready') {
readyRef.current = true
+48 -16
View File
@@ -79,7 +79,9 @@ type SetupTrustPrompt = {
function repoColor(name: string): string {
const palette = ['#f97316', '#8b5cf6', '#06b6d4', '#ec4899', '#84cc16', '#f59e0b', '#6366f1']
let hash = 0
for (let i = 0; i < name.length; i += 1) hash = (hash * 31 + name.charCodeAt(i)) | 0
for (let i = 0; i < name.length; i += 1) {
hash = (hash * 31 + name.charCodeAt(i)) | 0
}
return palette[Math.abs(hash) % palette.length]!
}
@@ -230,7 +232,9 @@ export function NewWorktreeModal({
setShowAgentPicker(false)
return
}
if (!client) return
if (!client) {
return
}
let stale = false
setName('')
setNote('')
@@ -262,7 +266,9 @@ export function NewWorktreeModal({
client.sendRequest('settings.get'),
client.sendRequest('ui.get')
])
if (stale) return
if (stale) {
return
}
if (settingsResponse.ok) {
const result = (settingsResponse as RpcSuccess).result as { settings: RuntimeSettings }
setRuntimeSettings(result.settings)
@@ -283,9 +289,13 @@ export function NewWorktreeModal({
}
}
} catch {
if (!stale) setRepos([])
if (!stale) {
setRepos([])
}
} finally {
if (!stale) setLoading(false)
if (!stale) {
setLoading(false)
}
}
})()
return () => {
@@ -303,7 +313,9 @@ export function NewWorktreeModal({
void client
.sendRequest('ssh.getState', { targetId: selectedRepoConnectionId })
.then((response) => {
if (stale) return
if (stale) {
return
}
if (!response.ok) {
throw new Error(response.error.message)
}
@@ -333,7 +345,9 @@ export function NewWorktreeModal({
}, [client, selectedRepoConnectionId, visible])
useEffect(() => {
if (!visible || !client) return
if (!visible || !client) {
return
}
if (selectedRepoConnectionId && sshGate.status !== 'connected') {
setDetectedAgentIds(null)
return
@@ -347,12 +361,16 @@ export function NewWorktreeModal({
connectionId: selectedRepoConnectionId
})
: await client.sendRequest('preflight.detectAgents')
if (stale) return
if (stale) {
return
}
setDetectedAgentIds(
response.ok ? new Set((response as RpcSuccess).result as string[]) : new Set()
)
} catch {
if (!stale) setDetectedAgentIds(new Set())
if (!stale) {
setDetectedAgentIds(new Set())
}
}
})()
return () => {
@@ -373,7 +391,9 @@ export function NewWorktreeModal({
const response = await client.sendRequest('repo.hooks', {
repo: `id:${selectedRepo.id}`
})
if (stale) return
if (stale) {
return
}
if (response.ok) {
const result = (response as RpcSuccess).result as RepoHooksResponse
const cmd = result.hooks?.scripts?.setup?.trim() || null
@@ -404,7 +424,9 @@ export function NewWorktreeModal({
}, [client, selectedRepo])
async function connectSelectedSshRepo(): Promise<void> {
if (!client || !selectedRepoConnectionId) return
if (!client || !selectedRepoConnectionId) {
return
}
setSshConnecting(true)
setSshState({
targetId: selectedRepoConnectionId,
@@ -447,7 +469,9 @@ export function NewWorktreeModal({
contentHash: string,
alwaysTrust: boolean
): Promise<void> {
if (!client) return
if (!client) {
return
}
const next = trustedOrcaHooksWithSetupApproval({
trust: trustedOrcaHooks,
repoId,
@@ -462,7 +486,9 @@ export function NewWorktreeModal({
}
async function handleCreate(options: CreateOptions = {}) {
if (!client || !selectedRepo) return
if (!client || !selectedRepo) {
return
}
setCreating(true)
setError('')
@@ -566,8 +592,12 @@ export function NewWorktreeModal({
setupDecision,
name: candidateName
}
if (selectedAgent.id !== '__blank__') params.createdWithAgent = selectedAgent.id
if (note.trim()) params.comment = note.trim()
if (selectedAgent.id !== '__blank__') {
params.createdWithAgent = selectedAgent.id
}
if (note.trim()) {
params.comment = note.trim()
}
const response = await client.sendRequest('worktree.create', params, {
timeoutMs: WORKTREE_CREATE_TIMEOUT_MS
@@ -715,7 +745,9 @@ export function NewWorktreeModal({
autoFocus={repos.length <= 1}
returnKeyType="done"
onSubmitEditing={() => {
if (canCreate) void handleCreate()
if (canCreate) {
void handleCreate()
}
}}
/>
</View>
+6 -2
View File
@@ -79,14 +79,18 @@ function PickerModalContent<T extends string = string>({
opt.disabled && styles.rowDisabled
]}
onPress={() => {
if (opt.disabled) return
if (opt.disabled) {
return
}
onSelect(opt.value)
onClose()
}}
onLongPress={
onLongSelect
? () => {
if (opt.disabled) return
if (opt.disabled) {
return
}
onLongSelect(opt.value)
onClose()
}
@@ -42,7 +42,9 @@ export function parseMobileMarkdown(content: string): MobileMarkdownBlock[] {
code.push(lines[index] ?? '')
index += 1
}
if (index < lines.length) index += 1
if (index < lines.length) {
index += 1
}
blocks.push({ type: 'code', text: code.join('\n'), language: fence[1] })
continue
}
@@ -18,10 +18,14 @@ function extractFunctionSource(script: string, name: string): string {
let depth = 0
for (let index = bodyStart; index < script.length; index += 1) {
const char = script[index]
if (char === '{') depth += 1
if (char === '{') {
depth += 1
}
if (char === '}') {
depth -= 1
if (depth === 0) return script.slice(start, index + 1)
if (depth === 0) {
return script.slice(start, index + 1)
}
}
}
throw new Error(`Could not extract ${name}`)
@@ -39,13 +39,17 @@ export function getSuggestedCreatureName(
}
// Lowercased to match branch-name convention (fix/seahorse, not fix/Seahorse).
const available = MARINE_CREATURES.map(normalize).filter((name) => !used.has(name))
if (available.length > 0) return pickRandom(available, random)
if (available.length > 0) {
return pickRandom(available, random)
}
let suffix = 2
while (true) {
const numbered = MARINE_CREATURES.map((name) => `${normalize(name)}-${suffix}`).filter(
(name) => !used.has(name)
)
if (numbered.length > 0) return pickRandom(numbered, random)
if (numbered.length > 0) {
return pickRandom(numbered, random)
}
suffix += 1
}
}
@@ -15,7 +15,9 @@ export function startDiagnosticFetchTimeout(timeoutMs: number): DiagnosticFetchT
}, timeoutMs)
function dispose() {
if (disposed) return
if (disposed) {
return
}
disposed = true
if (timer) {
clearTimeout(timer)
@@ -58,10 +58,14 @@ function configureNotificationChannel(): void {
async function showLocalNotification(event: NotificationEvent, hostId: string): Promise<void> {
const enabled = await loadPushNotificationsEnabled()
if (!enabled) return
if (!enabled) {
return
}
const granted = await ensureNotificationPermissions()
if (!granted) return
if (!granted) {
return
}
await Notifications.scheduleNotificationAsync({
content: {
@@ -99,10 +103,14 @@ export function subscribeToDesktopNotifications(client: RpcClient, hostId: strin
return
}
if (event.type === 'end') {
if (disposed) unsubscribeStream()
if (disposed) {
unsubscribeStream()
}
return
}
if (disposed) {
return
}
if (disposed) return
if (event.type === 'notification') {
void showLocalNotification(event as NotificationEvent, hostId)
}
+15 -5
View File
@@ -43,15 +43,21 @@ export function formatDiffComments(comments: readonly DiffComment[]): string {
}
export function normalizeMobileDiffComments(value: unknown, worktreeId: string): DiffComment[] {
if (!Array.isArray(value)) return []
if (!Array.isArray(value)) {
return []
}
return value.flatMap((candidate): DiffComment[] => {
if (!isRecord(candidate)) return []
if (!isRecord(candidate)) {
return []
}
const id = typeof candidate.id === 'string' ? candidate.id : ''
const filePath = typeof candidate.filePath === 'string' ? candidate.filePath : ''
const lineNumber = typeof candidate.lineNumber === 'number' ? candidate.lineNumber : NaN
const body = typeof candidate.body === 'string' ? candidate.body.trim() : ''
const createdAt = typeof candidate.createdAt === 'number' ? candidate.createdAt : Date.now()
if (!id || !filePath || !Number.isFinite(lineNumber) || !body) return []
if (!id || !filePath || !Number.isFinite(lineNumber) || !body) {
return []
}
return [
{
id,
@@ -103,7 +109,9 @@ export function removeMobileDiffComments(
comments: readonly DiffComment[],
ids: ReadonlySet<string>
): DiffComment[] {
if (ids.size === 0) return [...comments]
if (ids.size === 0) {
return [...comments]
}
return comments.filter((comment) => !ids.has(comment.id))
}
@@ -123,7 +131,9 @@ export function removeDeliveredMobileDiffComments(
comments: readonly DiffComment[],
delivered: readonly DiffComment[]
): DiffComment[] {
if (delivered.length === 0) return [...comments]
if (delivered.length === 0) {
return [...comments]
}
const deliveredById = new Map(delivered.map((comment) => [comment.id, comment]))
return comments.filter((comment) => {
const snapshot = deliveredById.get(comment.id)
+24 -8
View File
@@ -172,14 +172,30 @@ function tokenKindForClasses(className: unknown): MobileSyntaxTokenKind | null {
: []
const tokens = new Set(classes.map((value) => value.replace(/^hljs-/, '')))
if (hasAny(tokens, ['comment', 'quote'])) return 'comment'
if (hasAny(tokens, ['keyword', 'selector-tag', 'tag', 'name'])) return 'keyword'
if (hasAny(tokens, ['string', 'regexp', 'symbol', 'bullet'])) return 'string'
if (hasAny(tokens, ['number', 'literal'])) return 'number'
if (hasAny(tokens, ['type', 'built_in', 'class', 'title.class'])) return 'type'
if (hasAny(tokens, ['title.function', 'function', 'title'])) return 'function'
if (hasAny(tokens, ['attr', 'attribute', 'property', 'variable', 'params'])) return 'variable'
if (hasAny(tokens, ['meta', 'doctag', 'subst', 'section'])) return 'meta'
if (hasAny(tokens, ['comment', 'quote'])) {
return 'comment'
}
if (hasAny(tokens, ['keyword', 'selector-tag', 'tag', 'name'])) {
return 'keyword'
}
if (hasAny(tokens, ['string', 'regexp', 'symbol', 'bullet'])) {
return 'string'
}
if (hasAny(tokens, ['number', 'literal'])) {
return 'number'
}
if (hasAny(tokens, ['type', 'built_in', 'class', 'title.class'])) {
return 'type'
}
if (hasAny(tokens, ['title.function', 'function', 'title'])) {
return 'function'
}
if (hasAny(tokens, ['attr', 'attribute', 'property', 'variable', 'params'])) {
return 'variable'
}
if (hasAny(tokens, ['meta', 'doctag', 'subst', 'section'])) {
return 'meta'
}
return null
}
@@ -44,8 +44,12 @@ function compareGitStatusEntries(a: MobileGitStatusEntry, b: MobileGitStatusEntr
}
function getConflictSortRank(entry: MobileGitStatusEntry): number {
if (entry.conflictStatus === 'unresolved') return 0
if (entry.conflictStatus === 'resolved_locally') return 1
if (entry.conflictStatus === 'unresolved') {
return 0
}
if (entry.conflictStatus === 'resolved_locally') {
return 1
}
return 2
}
+9 -3
View File
@@ -12,7 +12,9 @@ const NOTIF_KEY = 'orca:pushNotificationsEnabled'
export async function loadPushNotificationsEnabled(): Promise<boolean> {
try {
const raw = await AsyncStorage.getItem(NOTIF_KEY)
if (raw === null) return false
if (raw === null) {
return false
}
return raw === 'true'
} catch {
return false
@@ -55,7 +57,9 @@ function allowedString(value: unknown, allowed: Set<string>, fallback: string):
export async function loadPinnedIds(hostId: string): Promise<Set<string>> {
try {
const raw = await AsyncStorage.getItem(PINS_PREFIX + hostId)
if (!raw) return new Set()
if (!raw) {
return new Set()
}
return new Set(stringArray(JSON.parse(raw)))
} catch {
return new Set()
@@ -69,7 +73,9 @@ export async function savePinnedIds(hostId: string, ids: Set<string>): Promise<v
export async function loadPreferences(hostId: string): Promise<HostPreferences> {
try {
const raw = await AsyncStorage.getItem(PREFS_PREFIX + hostId)
if (!raw) return DEFAULT_PREFS
if (!raw) {
return DEFAULT_PREFS
}
const parsed = JSON.parse(raw) as Partial<HostPreferences>
return {
sortMode: allowedString(parsed.sortMode, SORT_MODES, DEFAULT_PREFS.sortMode),
+21 -7
View File
@@ -16,9 +16,13 @@ type CachedSlugState =
export function normalizeGitHubRepositorySlug(value: string | null | undefined): string | null {
const trimmed = value?.trim()
if (!trimmed) return null
if (!trimmed) {
return null
}
const [owner, repo, extra] = trimmed.split('/')
if (!owner || !repo || extra) return null
if (!owner || !repo || extra) {
return null
}
return `${owner}/${repo}`.toLowerCase()
}
@@ -27,8 +31,12 @@ function cachedSlugStateForRepo(
slugsByRepoId: Record<string, GitHubRepoSlugCacheEntry | undefined>
): CachedSlugState {
const cached = slugsByRepoId[repo.id]
if (!cached) return { status: 'missing' }
if (cached.path !== repo.path) return { status: 'stale' }
if (!cached) {
return { status: 'missing' }
}
if (cached.path !== repo.path) {
return { status: 'stale' }
}
return { status: 'resolved', slug: normalizeGitHubRepositorySlug(cached.slug) }
}
@@ -38,7 +46,9 @@ export function findRepoForGitHubProjectRepository(
slugsByRepoId: Record<string, GitHubRepoSlugCacheEntry | undefined> = {}
): GitHubProjectRepoMatch | null {
const slug = normalizeGitHubRepositorySlug(repository)
if (!slug) return null
if (!slug) {
return null
}
const slugStates = new Map(
repos.map((repo) => [repo.id, cachedSlugStateForRepo(repo, slugsByRepoId)])
@@ -47,8 +57,12 @@ export function findRepoForGitHubProjectRepository(
const state = slugStates.get(repo.id)
return state?.status === 'resolved' && state.slug === slug
})
if (slugMatches.length === 1) return slugMatches[0]!
if (slugMatches.length > 1) return null
if (slugMatches.length === 1) {
return slugMatches[0]!
}
if (slugMatches.length > 1) {
return null
}
return (
repos.find((repo) => {
+21 -7
View File
@@ -12,13 +12,27 @@ function isWorkspaceSshConnectInProgress(status: SshConnectionStatus | null): bo
}
export function workspaceSshStatusLabel(status: SshConnectionStatus | null): string {
if (status === 'connected') return 'Connected'
if (status === 'connecting') return 'Connecting'
if (status === 'deploying-relay') return 'Deploying relay'
if (status === 'reconnecting') return 'Reconnecting'
if (status === 'auth-failed') return 'Authentication failed'
if (status === 'reconnection-failed') return 'Reconnect failed'
if (status === 'error') return 'Connection failed'
if (status === 'connected') {
return 'Connected'
}
if (status === 'connecting') {
return 'Connecting'
}
if (status === 'deploying-relay') {
return 'Deploying relay'
}
if (status === 'reconnecting') {
return 'Reconnecting'
}
if (status === 'auth-failed') {
return 'Authentication failed'
}
if (status === 'reconnection-failed') {
return 'Reconnect failed'
}
if (status === 'error') {
return 'Connection failed'
}
return 'Disconnected'
}
+9 -3
View File
@@ -2059,7 +2059,9 @@ export const TerminalWebView = forwardRef<TerminalWebViewHandle, Props>(function
})
} else if (msg.type === 'terminal-input') {
const bytes = typeof msg.bytes === 'string' ? msg.bytes : ''
if (bytes.length > 0) onTerminalInput?.(bytes)
if (bytes.length > 0) {
onTerminalInput?.(bytes)
}
} else if (msg.type === 'terminal-tap') {
onTerminalTap?.()
} else if (msg.type === 'keyboard-avoidance-metrics') {
@@ -2144,7 +2146,9 @@ export const TerminalWebView = forwardRef<TerminalWebViewHandle, Props>(function
measureFitDimensions(
containerHeight?: number
): Promise<{ cols: number; rows: number } | null> {
if (!isWebReadyRef.current) return Promise.resolve(null)
if (!isWebReadyRef.current) {
return Promise.resolve(null)
}
return new Promise((resolve) => {
measureResolveRef.current?.(null)
let timeout: ReturnType<typeof setTimeout> | null = null
@@ -2184,7 +2188,9 @@ export const TerminalWebView = forwardRef<TerminalWebViewHandle, Props>(function
// immediately if no init is pending. Capped at 3s so a stuck
// WebView doesn't hang the caller.
const p = readyPromiseRef.current
if (!p) return
if (!p) {
return
}
await new Promise<void>((resolve) => {
let settled = false
const timeout = setTimeout(() => {
@@ -23,7 +23,9 @@ function defaultPreference(ids = builtInIds()): TerminalAccessoryLayoutPreferenc
}
function stringArray(value: unknown): string[] | null {
if (!Array.isArray(value)) return null
if (!Array.isArray(value)) {
return null
}
return value.every((item): item is string => typeof item === 'string') ? value : null
}
@@ -31,7 +33,9 @@ function dedupeKnownIds(ids: string[], builtInSet: Set<string>): string[] {
const seen = new Set<string>()
const out: string[] = []
for (const id of ids) {
if (!builtInSet.has(id) || seen.has(id)) continue
if (!builtInSet.has(id) || seen.has(id)) {
continue
}
seen.add(id)
out.push(id)
}
@@ -52,7 +56,9 @@ export function normalizeTerminalAccessoryLayoutPreference(
currentBuiltInIds = builtInIds()
): TerminalAccessoryLayoutPreference {
const fallback = defaultPreference(currentBuiltInIds)
if (!value || typeof value !== 'object') return fallback
if (!value || typeof value !== 'object') {
return fallback
}
const candidate = value as {
version?: unknown
@@ -61,7 +67,9 @@ export function normalizeTerminalAccessoryLayoutPreference(
}
const visibleInput = stringArray(candidate.visibleBuiltInIds)
const knownInput = stringArray(candidate.knownBuiltInIds)
if (candidate.version !== 1 || !visibleInput || !knownInput) return fallback
if (candidate.version !== 1 || !visibleInput || !knownInput) {
return fallback
}
const builtInSet = new Set(currentBuiltInIds)
const knownInputSet = new Set(knownInput.filter((id) => builtInSet.has(id)))
@@ -129,7 +137,9 @@ export function getVisibleTerminalAccessoryKeys(
export async function loadTerminalAccessoryLayout(): Promise<TerminalAccessoryLayoutPreference> {
try {
const raw = await AsyncStorage.getItem(TERMINAL_ACCESSORY_LAYOUT_STORAGE_KEY)
if (!raw) return defaultPreference()
if (!raw) {
return defaultPreference()
}
return normalizeTerminalAccessoryLayoutPreference(JSON.parse(raw))
} catch {
return defaultPreference()
@@ -37,7 +37,9 @@ function isSgrMouseGestureSequence(bytes: string, offset: number): number | null
}
const sequence = bytes.slice(offset, end + 1)
const match = SGR_MOUSE_GESTURE_SEQUENCE_RE.exec(sequence)
if (!match) return null
if (!match) {
return null
}
const button = match[1]
const col = Number(match[2])
const row = Number(match[3])
@@ -40,8 +40,12 @@ export type BrowserScreencastFrame = {
}
function byteToFormat(value: number): BrowserScreencastFormat | null {
if (value === 1) return 'jpeg'
if (value === 2) return 'png'
if (value === 1) {
return 'jpeg'
}
if (value === 2) {
return 'png'
}
return null
}
+69 -23
View File
@@ -74,17 +74,25 @@ export function RpcClientProvider({ children }: { children: ReactNode }) {
function notifyHostState(hostId: string, state: ConnectionState) {
const set = stateListenersRef.current.get(hostId)
if (!set) return
for (const listener of set) listener(state)
if (!set) {
return
}
for (const listener of set) {
listener(state)
}
}
function notifyAllHosts() {
for (const listener of allHostsListenersRef.current) listener()
for (const listener of allHostsListenersRef.current) {
listener()
}
}
const closeEntry = useCallback((hostId: string) => {
const entry = storeRef.current.get(hostId)
if (!entry) return
if (!entry) {
return
}
entry.unsubState()
entry.client.close()
storeRef.current.delete(hostId)
@@ -123,12 +131,16 @@ export function RpcClientProvider({ children }: { children: ReactNode }) {
notifyAllHosts()
return null
}
if (!host) return null
if (!host) {
return null
}
}
// Re-check after any await — another acquire() may have completed.
const after = storeRef.current.get(hostId)
if (after) return after
if (after) {
return after
}
let client: RpcClient
try {
@@ -143,7 +155,9 @@ export function RpcClientProvider({ children }: { children: ReactNode }) {
}
const unsubState = client.onStateChange((state) => {
const cur = storeRef.current.get(hostId)
if (!cur) return
if (!cur) {
return
}
cur.state = state
notifyHostState(hostId, state)
})
@@ -171,7 +185,9 @@ export function RpcClientProvider({ children }: { children: ReactNode }) {
// pass inside openEntry.
const acquire = useCallback(
(hostId: string, host?: HostProfile): RpcClient | null => {
if (host) primedHostsRef.current.set(hostId, host)
if (host) {
primedHostsRef.current.set(hostId, host)
}
const existing = storeRef.current.get(hostId)
if (existing) {
existing.refCount += 1
@@ -181,7 +197,9 @@ export function RpcClientProvider({ children }: { children: ReactNode }) {
// try again once the state listener fires; consumers are expected to
// call acquire() inside an effect that re-runs on state changes.
void openEntry(hostId).then((entry) => {
if (!entry) return
if (!entry) {
return
}
entry.refCount += 1
})
return null
@@ -190,7 +208,9 @@ export function RpcClientProvider({ children }: { children: ReactNode }) {
)
const primeHosts = useCallback((hosts: HostProfile[]) => {
for (const host of hosts) primedHostsRef.current.set(host.id, host)
for (const host of hosts) {
primedHostsRef.current.set(host.id, host)
}
}, [])
// Why: refcount dropping to 0 no longer triggers an idle-close. The
@@ -203,7 +223,9 @@ export function RpcClientProvider({ children }: { children: ReactNode }) {
// unmount (app shutdown).
const release = useCallback((hostId: string) => {
const entry = storeRef.current.get(hostId)
if (!entry) return
if (!entry) {
return
}
entry.refCount = Math.max(0, entry.refCount - 1)
}, [])
@@ -222,7 +244,9 @@ export function RpcClientProvider({ children }: { children: ReactNode }) {
storeRef.current.delete(hostId)
}
const fresh = await openEntry(hostId)
if (fresh) fresh.refCount = savedRefCount
if (fresh) {
fresh.refCount = savedRefCount
}
},
[openEntry]
)
@@ -249,9 +273,13 @@ export function RpcClientProvider({ children }: { children: ReactNode }) {
set.add(listener)
return () => {
const s = stateListenersRef.current.get(hostId)
if (!s) return
if (!s) {
return
}
s.delete(listener)
if (s.size === 0) stateListenersRef.current.delete(hostId)
if (s.size === 0) {
stateListenersRef.current.delete(hostId)
}
}
},
[]
@@ -286,7 +314,9 @@ export function RpcClientProvider({ children }: { children: ReactNode }) {
useEffect(() => {
const store = storeRef.current
return () => {
for (const [hostId] of store) closeEntry(hostId)
for (const [hostId] of store) {
closeEntry(hostId)
}
}
}, [])
@@ -324,7 +354,9 @@ export function RpcClientProvider({ children }: { children: ReactNode }) {
function useCtx(): ContextValue {
const ctx = useContext(Ctx)
if (!ctx) throw new Error('useHostClient must be used inside <RpcClientProvider>')
if (!ctx) {
throw new Error('useHostClient must be used inside <RpcClientProvider>')
}
return ctx
}
@@ -351,7 +383,9 @@ export function useHostClient(hostId: string | undefined): {
let cancelled = false
// Subscribe before acquire so any state change during open is captured.
const unsub = ctx.subscribeHostState(hostId, (next) => {
if (cancelled) return
if (cancelled) {
return
}
setState(next)
// Why: if the client was null at first acquire (async open), the
// first state change ('connecting'/'handshaking'/'connected') is our
@@ -395,16 +429,24 @@ export function useAllHostClients(hostIds: string[]): Array<{
const [tick, setTick] = useState(0)
useEffect(() => {
if (hostIds.length === 0) return
for (const id of hostIds) ctx.acquire(id)
if (hostIds.length === 0) {
return
}
for (const id of hostIds) {
ctx.acquire(id)
}
const unsubs: Array<() => void> = []
for (const id of hostIds) {
unsubs.push(ctx.subscribeHostState(id, () => setTick((n) => n + 1)))
}
unsubs.push(ctx.subscribeAllHosts(() => setTick((n) => n + 1)))
return () => {
for (const u of unsubs) u()
for (const id of hostIds) ctx.release(id)
for (const u of unsubs) {
u()
}
for (const id of hostIds) {
ctx.release(id)
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [key])
@@ -454,7 +496,9 @@ export function useReconnectAttempt(hostId: string | undefined): number {
const ctx = useCtx()
const [, force] = useState(0)
useEffect(() => {
if (!hostId) return
if (!hostId) {
return
}
return ctx.subscribeHostState(hostId, () => force((n) => n + 1))
}, [ctx, hostId])
return hostId ? ctx.getReconnectAttempt(hostId) : 0
@@ -469,7 +513,9 @@ export function useLastConnectedAt(hostId: string | undefined): number | null {
const ctx = useCtx()
const [, force] = useState(0)
useEffect(() => {
if (!hostId) return
if (!hostId) {
return
}
return ctx.subscribeHostState(hostId, () => force((n) => n + 1))
}, [ctx, hostId])
return hostId ? ctx.getLastConnectedAt(hostId) : null
+3 -1
View File
@@ -43,7 +43,9 @@ export function classifyConnection(args: {
}
// Connected / connecting / handshaking are normal.
if (state === 'connected') return { kind: 'normal', label: 'Connected' }
if (state === 'connected') {
return { kind: 'normal', label: 'Connected' }
}
if (state === 'connecting' || state === 'handshaking') {
return { kind: 'normal', label: 'Connecting…' }
}
+3 -1
View File
@@ -9,7 +9,9 @@ export function getNextHostNameFromHosts(hosts: readonly HostNameSource[]): stri
for (const host of hosts) {
const match = HOST_NUMBER_PATTERN.exec(host.name)
if (!match) continue
if (!match) {
continue
}
const hostNumber = Number.parseInt(match[1]!, 10)
if (hostNumber > largestHostNumber) {
+21 -7
View File
@@ -39,7 +39,9 @@ let inflightLoad: Promise<HostProfile[]> | null = null
export async function loadHosts(): Promise<HostProfile[]> {
// Why: deduplicate concurrent loadHosts() calls so multiple screens
// mounting simultaneously share one Keychain read pass.
if (inflightLoad) return inflightLoad
if (inflightLoad) {
return inflightLoad
}
inflightLoad = doLoadHosts().finally(() => {
inflightLoad = null
})
@@ -48,14 +50,18 @@ export async function loadHosts(): Promise<HostProfile[]> {
async function doLoadHosts(): Promise<HostProfile[]> {
const raw = await AsyncStorage.getItem(STORAGE_KEY)
if (!raw) return []
if (!raw) {
return []
}
let parsed: unknown
try {
parsed = JSON.parse(raw)
} catch {
return []
}
if (!Array.isArray(parsed)) return []
if (!Array.isArray(parsed)) {
return []
}
const out: HostProfile[] = []
for (const item of parsed) {
@@ -67,7 +73,9 @@ async function doLoadHosts(): Promise<HostProfile[]> {
continue
}
const stored = StoredHostProfileSchema.safeParse(item)
if (!stored.success) continue
if (!stored.success) {
continue
}
let token = tokenCache.get(stored.data.id)
if (!token) {
@@ -97,14 +105,20 @@ async function doLoadHosts(): Promise<HostProfile[]> {
async function loadStoredHosts(): Promise<StoredHostProfile[]> {
const raw = await AsyncStorage.getItem(STORAGE_KEY)
if (!raw) return []
if (!raw) {
return []
}
try {
const parsed = JSON.parse(raw) as unknown
if (!Array.isArray(parsed)) return []
if (!Array.isArray(parsed)) {
return []
}
return parsed.flatMap((item) => {
// Why: same drop-old-records rule as loadHosts; keeps internal
// mutators from re-persisting pre-v0.0.3 entries.
if (item && typeof item === 'object' && 'deviceToken' in item) return []
if (item && typeof item === 'object' && 'deviceToken' in item) {
return []
}
const result = StoredHostProfileSchema.safeParse(item)
return result.success ? [result.data] : []
})
@@ -16,13 +16,17 @@ export function startPairingConnectionAttempt({
let timer: ReturnType<typeof setTimeout> | null = null
function closeClientOnce() {
if (clientClosed) return
if (clientClosed) {
return
}
clientClosed = true
closeClient()
}
function dispose() {
if (disposed) return
if (disposed) {
return
}
disposed = true
if (timer) {
clearTimeout(timer)
+9 -3
View File
@@ -8,7 +8,9 @@ import { PairingOfferSchema, type PairingOffer } from './types'
export function decodePairingUrl(url: string): PairingOffer | null {
try {
const code = extractPairingCodeFromUrl(url)
if (!code) return null
if (!code) {
return null
}
return decodePairingBase64(code)
} catch {
return null
@@ -19,7 +21,9 @@ export function decodePairingUrl(url: string): PairingOffer | null {
// extraction here makes QR scan, paste, and external deep-link flows
// accept the same URL shapes.
export function extractPairingCodeFromUrl(url: string): string | null {
if (!url.startsWith('orca://pair')) return null
if (!url.startsWith('orca://pair')) {
return null
}
const queryIndex = url.indexOf('?')
if (queryIndex !== -1) {
const query = url.slice(queryIndex + 1).split('#')[0] ?? ''
@@ -41,7 +45,9 @@ export function extractPairingCodeFromUrl(url: string): string | null {
// copied from desktop.
export function parsePairingCode(input: string): PairingOffer | null {
const trimmed = input.trim()
if (!trimmed) return null
if (!trimmed) {
return null
}
try {
if (trimmed.startsWith('orca://pair')) {
return decodePairingUrl(trimmed)
+3 -1
View File
@@ -34,7 +34,9 @@ class MockWebSocket {
emitCloseOnClose = true
sent: string[] = []
close = vi.fn(() => {
if (this.readyState === MockWebSocket.CLOSED) return
if (this.readyState === MockWebSocket.CLOSED) {
return
}
this.readyState = MockWebSocket.CLOSED
if (this.emitCloseOnClose) {
this.onclose?.()
+36 -12
View File
@@ -149,7 +149,9 @@ export function connect(
const onLog = options.onLog
let logCounter = 0
function emitLog(level: ConnectionLogLevel, message: string, detail?: string) {
if (!onLog) return
if (!onLog) {
return
}
onLog({
id: `log-${++logCounter}-${Date.now()}`,
ts: Date.now(),
@@ -216,7 +218,9 @@ export function connect(
}
function setState(next: ConnectionState) {
if (state === next) return
if (state === next) {
return
}
const prev = state
const dwelt = Date.now() - stateEnteredAt
state = next
@@ -258,8 +262,12 @@ export function connect(
}
function waitForConnected(timeoutMs?: number): Promise<void> {
if (state === 'connected') return Promise.resolve()
if (intentionallyClosed) return Promise.reject(new Error('Client closed'))
if (state === 'connected') {
return Promise.resolve()
}
if (intentionallyClosed) {
return Promise.reject(new Error('Client closed'))
}
if (state === 'reconnecting' && reconnectAttempt >= GIVE_UP_AFTER_ATTEMPTS && !reconnectTimer) {
// Why: after the retry cap there is no future state transition to
// release callers waiting before their per-request timeout starts.
@@ -290,7 +298,9 @@ export function connect(
}
function openConnection() {
if (intentionallyClosed) return
if (intentionallyClosed) {
return
}
const now = Date.now()
wsConstructionCounter++
@@ -644,10 +654,14 @@ export function connect(
event,
(_k, v) => {
if (typeof v === 'object' && v !== null) {
if (seen.has(v as object)) return '[circular]'
if (seen.has(v as object)) {
return '[circular]'
}
seen.add(v as object)
}
if (typeof v === 'function') return '[fn]'
if (typeof v === 'function') {
return '[fn]'
}
return v
},
0
@@ -696,10 +710,14 @@ export function connect(
event,
(_k, v) => {
if (typeof v === 'object' && v !== null) {
if (seen.has(v as object)) return '[circular]'
if (seen.has(v as object)) {
return '[circular]'
}
seen.add(v as object)
}
if (typeof v === 'function') return '[fn]'
if (typeof v === 'function') {
return '[fn]'
}
return v
},
0
@@ -798,7 +816,9 @@ export function connect(
// Why: only probe while the channel is actually in 'connected'. The
// sendRequest path itself waits for connected, but a probe scheduled
// during a reconnect would just stack up timeouts and confuse logs.
if (state !== 'connected' || !ws) return
if (state !== 'connected' || !ws) {
return
}
const probeWs = ws
// Why: short timeout (8s) — server's heartbeat is 15s, so if we
// don't see *anything* back within 8s the link is almost certainly
@@ -823,11 +843,15 @@ export function connect(
}, 8_000)
pending.set(id, {
resolve: () => {
if (timedOut) return
if (timedOut) {
return
}
clearTimeout(timeout)
},
reject: () => {
if (timedOut) return
if (timedOut) {
return
}
clearTimeout(timeout)
}
})