mirror of
https://github.com/stablyai/orca.git
synced 2026-09-30 00:03:15 +00:00
Merge commit 'e4feda6965' into mobile-rearch
This commit is contained in:
@@ -210,9 +210,10 @@ rather than reused.
|
||||
Repeated WebView process loss or a health timeout can promote the compatible
|
||||
verified previous generation. The recovery UI exposes:
|
||||
|
||||
- **Retry** — request the current package from the authenticated Desktop.
|
||||
- **Use previous** — promote this host's verified prior generation.
|
||||
- **Clear cache** — remove this host's package cache and require a verified
|
||||
- **Retry** — request the current package from the authenticated Desktop. This
|
||||
is the only primary button; the rest render as demoted text links.
|
||||
- **Use last version** — promote this host's verified prior generation.
|
||||
- **Reset** — remove this host's package cache and require a verified
|
||||
redownload.
|
||||
- **Switch hosts** — leave the affected host without changing another host's
|
||||
cache or credentials.
|
||||
@@ -302,11 +303,11 @@ result persists after reconnect, restart, or switching hosts.
|
||||
| ------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | -------------------------------------------------------------------------- |
|
||||
| No paired Desktop | Confirm Desktop is running and pairing completed | Scan a new QR code; never copy the encoded credential into a ticket |
|
||||
| Desktop unreachable | Check LAN/Tailscale state, firewall port 6768, sleep state, and Connection Log | Reconnect, wake Desktop, or switch hosts |
|
||||
| “Connect once to cache” | No compatible verified generation exists for that host | Connect to the paired Desktop and choose **Retry** |
|
||||
| “Connect to … to finish setting up.” | No compatible verified generation exists for that host | Connect to the paired Desktop and choose **Retry** |
|
||||
| Refresh warning while UI still works | Healthy cached generation is active but the new package failed | Keep using the cache; correct Desktop delivery, then **Retry** |
|
||||
| Interface fails after one Desktop update | Regression follows the Desktop package | Stop that Desktop rollout and use the rollback runbook |
|
||||
| Repeated WebView termination | Health/crash-loop recovery should select a compatible previous generation | Choose **Use previous** if offered; otherwise switch hosts |
|
||||
| Corrupt or unreadable host cache | Package fails native verification or open | Choose **Clear cache**, then redownload from the authenticated Desktop |
|
||||
| Repeated WebView termination | Health/crash-loop recovery should select a compatible previous generation | Choose **Use last version** if offered; otherwise switch hosts |
|
||||
| Corrupt or unreadable host cache | Package fails native verification or open | Choose **Reset**, then redownload from the authenticated Desktop |
|
||||
| Incompatible bridge | Desktop package does not support the installed shell | Restore a compatible Desktop package or install the required store release |
|
||||
| Pairing, origin, bridge, picker, audio, notification, or recovery failure | Native-owned boundary is affected | Halt the native rollout and ship a corrected store build |
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ boundaries used by this runbook.
|
||||
| ------------------------------------------------------------------------- | ----------------------------- | --------------------------------------------------------------------------- |
|
||||
| Workspace UI regression follows one Desktop version | Desktop web package | Stop that Desktop rollout and restore known-good package content |
|
||||
| New package times out or repeatedly terminates its WebView process | Native verified generation | Let automatic recovery promote the previous verified generation |
|
||||
| One host reports corrupt or unreadable cached assets | Host-scoped native cache | Use **Clear cache** and redownload from an authenticated paired Desktop |
|
||||
| One host reports corrupt or unreadable cached assets | Host-scoped native cache | Use **Reset** and redownload from an authenticated paired Desktop |
|
||||
| Package requires an unsupported bridge version | Desktop/native compatibility | Restore a package compatible with the installed shell; do not force-open it |
|
||||
| Pairing, encrypted connectivity, asset origin, cache, or bridge is broken | Native shell | Halt the store rollout and prepare a corrected native release |
|
||||
| Notification, deep-link, permission, audio, picker, or recovery UI fails | Native shell | Halt the store rollout and prepare a corrected native release |
|
||||
@@ -81,9 +81,9 @@ restarting must not make an affected Desktop safe.
|
||||
manifest and assets and the page reaches the health boundary.
|
||||
- A health timeout or third WebView process loss inside the crash window
|
||||
attempts to promote the compatible verified previous generation.
|
||||
- **Use previous** promotes the verified previous generation and removes the
|
||||
failed generation from the rollback position.
|
||||
- **Clear cache** closes the current package session, removes only the selected
|
||||
- **Use last version** promotes the verified previous generation and removes
|
||||
the failed generation from the rollback position.
|
||||
- **Reset** closes the current package session, removes only the selected
|
||||
paired host's cache, and requires a verified redownload.
|
||||
- An implicit cold open may replace an invalid active generation with a
|
||||
compatible verified previous generation. An explicit build open fails
|
||||
@@ -134,17 +134,23 @@ or native recovery UI.
|
||||
|
||||
Use the least destructive control that addresses the observed failure:
|
||||
|
||||
The recovery UI promotes **Retry** as the single primary button and demotes the
|
||||
remaining controls to text links, so support must name them by these labels:
|
||||
|
||||
- **Retry** asks the authenticated Desktop for its current package again. Use it
|
||||
after connectivity or Desktop package delivery is corrected.
|
||||
- **Use previous** switches to the verified prior generation for the selected
|
||||
host. Use it for a newly activated functional regression or repeated process
|
||||
failure when the previous action is available.
|
||||
- **Clear cache** removes the selected host's verified generations and forces a
|
||||
- **Use last version** switches to the verified prior generation for the
|
||||
selected host. Use it for a newly activated functional regression or repeated
|
||||
process failure when the previous action is available.
|
||||
- **Reset** removes the selected host's verified generations and forces a
|
||||
redownload. Use it for host-scoped corruption or when support explicitly
|
||||
needs to eliminate cached-package state.
|
||||
- **Switch hosts** leaves the affected paired Desktop without changing another
|
||||
host's cache or credentials.
|
||||
|
||||
The shell shows a plain-language notice plus an `Error: <code>` support line;
|
||||
that code is the same stable failure code recorded in diagnostics.
|
||||
|
||||
Cache clearing is not a Desktop rollback. If the Desktop still serves the bad
|
||||
package, a cleared client downloads the same bad package again.
|
||||
|
||||
@@ -173,8 +179,8 @@ rather than a routine rollback.
|
||||
Before production cutover, record iOS and Android evidence for:
|
||||
|
||||
1. Readiness timeout and three-process-loss automatic recovery.
|
||||
2. Manual **Use previous** recovery.
|
||||
3. **Clear cache** followed by an authenticated verified redownload.
|
||||
2. Manual **Use last version** recovery.
|
||||
3. **Reset** followed by an authenticated verified redownload.
|
||||
4. Corrupt active generation with compatible previous-generation fallback.
|
||||
5. Incompatible bridge, disconnected Desktop, pairing removal, and WebView
|
||||
process loss.
|
||||
|
||||
@@ -124,7 +124,7 @@ export default function HybridScreen() {
|
||||
if (sessionId && view) {
|
||||
void view.activateSessionView(sessionId).catch(() => {
|
||||
if (activeSessionIdRef.current === sessionId) {
|
||||
showWarning('Hosted session could not be restored.')
|
||||
showWarning('Couldn’t reopen Orca. Try again.', 'session_reopen_failed')
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -320,7 +320,7 @@ export default function HybridScreen() {
|
||||
routeHandoff: nativeRouteHandoffRef.current,
|
||||
setHostedViewActive,
|
||||
navigateToTerminalSettings: () => router.push('/terminal-settings'),
|
||||
onNavigationFailure: () => showWarning('Terminal settings could not be opened.')
|
||||
onNavigationFailure: () => showWarning('Couldn’t open Terminal settings.')
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -377,14 +377,14 @@ export default function HybridScreen() {
|
||||
onUsePrevious={recoverPrevious}
|
||||
onClearCache={clearCache}
|
||||
onRecoveryFailure={() =>
|
||||
showWarning('The workspace interface recovery action could not be completed.')
|
||||
showWarning('That didn’t work. Try again.', 'recovery_action_failed')
|
||||
}
|
||||
onBridgeMessage={(message) => void handleBridgeMessage(message)}
|
||||
onPageLoaded={() => {
|
||||
hardwareBackHandoff.resetPage()
|
||||
void postInit()
|
||||
}}
|
||||
onNavigationBlocked={() => showWarning('Navigation outside Orca was blocked.')}
|
||||
onNavigationBlocked={() => showWarning('That link can’t be opened here.')}
|
||||
onProcessTerminated={(sessionId) => {
|
||||
hardwareBackHandoff.resetPage()
|
||||
healthDeadlineRef.current.clear()
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
mobileWebShellShowsNativeChrome
|
||||
} from './mobile-web-shell-presentation-state'
|
||||
import type { MobileWebPackageDownloadProgress } from './mobile-web-package-downloader'
|
||||
import { mobileWebShellHostName, type MobileWebShellNotice } from './mobile-web-shell-notice'
|
||||
|
||||
type MobileWebHybridShellPresentationProps = {
|
||||
viewRef: RefObject<MobileWebShellViewRef | null>
|
||||
@@ -25,7 +26,7 @@ type MobileWebHybridShellPresentationProps = {
|
||||
viewEpoch: number
|
||||
packageLoading: boolean
|
||||
packageProgress: MobileWebPackageDownloadProgress | undefined
|
||||
packageWarning: string | undefined
|
||||
packageWarning: MobileWebShellNotice | undefined
|
||||
hostedViewActive: boolean
|
||||
onBack: () => void
|
||||
onShowHosts: () => void
|
||||
@@ -66,6 +67,14 @@ export function MobileWebHybridShellPresentation({
|
||||
packageLoading
|
||||
})
|
||||
const showNativeChrome = mobileWebShellShowsNativeChrome(presentationState)
|
||||
// The notice already reads as a full sentence, so it replaces the generic status line.
|
||||
const statusLine =
|
||||
packageWarning?.message ??
|
||||
(presentationState === 'package-loading'
|
||||
? selectedHost
|
||||
? `Connecting to ${mobileWebShellHostName(selectedHost.name)}…`
|
||||
: 'Getting things ready…'
|
||||
: `Couldn’t connect to ${mobileWebShellHostName(selectedHost?.name)}.`)
|
||||
|
||||
return (
|
||||
<View style={[styles.container, { paddingTop: insets.top }]}>
|
||||
@@ -82,9 +91,8 @@ export function MobileWebHybridShellPresentation({
|
||||
</Pressable>
|
||||
<View style={styles.headerCopy}>
|
||||
<Text numberOfLines={1} style={styles.heading}>
|
||||
{selectedHost?.name ?? 'Hybrid workspace UI'}
|
||||
{selectedHost?.name ?? 'Orca'}
|
||||
</Text>
|
||||
<Text style={styles.headerMeta}>Verified desktop-served interface</Text>
|
||||
</View>
|
||||
{selectedHost ? (
|
||||
<Pressable
|
||||
@@ -105,19 +113,23 @@ export function MobileWebHybridShellPresentation({
|
||||
<MobileWebPackageProgress progress={packageProgress} />
|
||||
) : null}
|
||||
{packageWarning ? (
|
||||
<>
|
||||
<Text accessibilityRole="alert" style={styles.warning}>
|
||||
{packageWarning}
|
||||
<View style={styles.noticeBanner}>
|
||||
<Text accessibilityRole="alert" style={styles.noticeBannerText}>
|
||||
{packageWarning.message}
|
||||
</Text>
|
||||
{packageWarning.code ? (
|
||||
<Text style={styles.noticeCode}>Error: {packageWarning.code}</Text>
|
||||
) : null}
|
||||
<MobileWebRecoveryActions
|
||||
canUsePrevious
|
||||
align="start"
|
||||
onClearCache={onClearCache}
|
||||
onFailure={onRecoveryFailure}
|
||||
onRetry={onRetryRecovery}
|
||||
onShowHosts={onShowHosts}
|
||||
onUsePrevious={onUsePrevious}
|
||||
/>
|
||||
</>
|
||||
</View>
|
||||
) : null}
|
||||
<MobileWebShellView
|
||||
key={`${session.sessionId}:${viewEpoch}`}
|
||||
@@ -141,28 +153,29 @@ export function MobileWebHybridShellPresentation({
|
||||
) : (
|
||||
<MonitorSmartphone size={26} color={colors.textMuted} />
|
||||
)}
|
||||
<Text accessibilityLiveRegion="polite" style={styles.loadingTitle}>
|
||||
{presentationState === 'package-loading'
|
||||
? 'Preparing verified interface…'
|
||||
: 'Workspace interface unavailable'}
|
||||
</Text>
|
||||
{packageLoading && packageProgress ? (
|
||||
<MobileWebPackageProgress progress={packageProgress} />
|
||||
) : (
|
||||
<Text
|
||||
accessibilityLiveRegion="polite"
|
||||
accessibilityRole={packageWarning ? 'alert' : undefined}
|
||||
style={styles.loadingTitle}
|
||||
>
|
||||
{statusLine}
|
||||
</Text>
|
||||
)}
|
||||
{packageWarning?.code ? (
|
||||
<Text style={styles.noticeCode}>Error: {packageWarning.code}</Text>
|
||||
) : null}
|
||||
{packageWarning ? (
|
||||
<>
|
||||
<Text accessibilityRole="alert" style={styles.loadingBody}>
|
||||
{packageWarning}
|
||||
</Text>
|
||||
<MobileWebRecoveryActions
|
||||
canUsePrevious={false}
|
||||
onClearCache={onClearCache}
|
||||
onFailure={onRecoveryFailure}
|
||||
onRetry={onRetryRecovery}
|
||||
onShowHosts={onShowHosts}
|
||||
onUsePrevious={onUsePrevious}
|
||||
/>
|
||||
</>
|
||||
<MobileWebRecoveryActions
|
||||
canUsePrevious={false}
|
||||
onClearCache={onClearCache}
|
||||
onFailure={onRecoveryFailure}
|
||||
onRetry={onRetryRecovery}
|
||||
onShowHosts={onShowHosts}
|
||||
onUsePrevious={onUsePrevious}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
)}
|
||||
|
||||
@@ -13,14 +13,13 @@ export function MobileWebPackageProgress({
|
||||
: 0
|
||||
const label =
|
||||
progress.phase === 'downloading'
|
||||
? `Downloading workspace interface… ${percent}%`
|
||||
: progress.phase === 'verifying'
|
||||
? 'Verifying workspace interface…'
|
||||
: 'Starting workspace interface…'
|
||||
? `Downloading… ${formatBytes(progress.completedBytes)} of ${formatBytes(progress.totalBytes)}`
|
||||
: 'Almost ready…'
|
||||
|
||||
return (
|
||||
<View
|
||||
style={styles.packageProgress}
|
||||
testID="mobile-web-package-progress"
|
||||
accessibilityRole="progressbar"
|
||||
accessibilityLabel={label}
|
||||
accessibilityValue={{ min: 0, max: 100, now: percent }}
|
||||
@@ -31,9 +30,6 @@ export function MobileWebPackageProgress({
|
||||
<View style={styles.packageProgressTrack}>
|
||||
<View style={[styles.packageProgressFill, { width: `${percent}%` }]} />
|
||||
</View>
|
||||
<Text style={styles.packageProgressBytes}>
|
||||
{formatBytes(progress.completedBytes)} of {formatBytes(progress.totalBytes)}
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ type RecoveryAction = 'retry' | 'previous' | 'clear' | 'hosts'
|
||||
|
||||
type MobileWebRecoveryActionsProps = {
|
||||
canUsePrevious: boolean
|
||||
align?: 'center' | 'start'
|
||||
onRetry: () => void | Promise<void>
|
||||
onUsePrevious: () => void | Promise<void>
|
||||
onClearCache: () => void | Promise<void>
|
||||
@@ -16,6 +17,7 @@ type MobileWebRecoveryActionsProps = {
|
||||
|
||||
export function MobileWebRecoveryActions({
|
||||
canUsePrevious,
|
||||
align = 'center',
|
||||
onRetry,
|
||||
onUsePrevious,
|
||||
onClearCache,
|
||||
@@ -48,37 +50,80 @@ export function MobileWebRecoveryActions({
|
||||
}
|
||||
}
|
||||
|
||||
const actions = [
|
||||
{ id: 'retry' as const, label: 'Retry', operation: onRetry },
|
||||
// Retry is the only action most people need; the rest stay reachable as demoted links.
|
||||
const secondaryActions = [
|
||||
...(canUsePrevious
|
||||
? [{ id: 'previous' as const, label: 'Use previous', operation: onUsePrevious }]
|
||||
? [
|
||||
{
|
||||
id: 'previous' as const,
|
||||
label: 'Use last version',
|
||||
testID: 'mobile-web-recovery-previous',
|
||||
operation: onUsePrevious
|
||||
}
|
||||
]
|
||||
: []),
|
||||
{ id: 'clear' as const, label: 'Clear cache', operation: onClearCache },
|
||||
{ id: 'hosts' as const, label: 'Switch hosts', operation: onShowHosts }
|
||||
{
|
||||
id: 'clear' as const,
|
||||
label: 'Reset',
|
||||
testID: 'mobile-web-recovery-reset',
|
||||
operation: onClearCache
|
||||
},
|
||||
{
|
||||
id: 'hosts' as const,
|
||||
label: 'Switch hosts',
|
||||
testID: 'mobile-web-recovery-hosts',
|
||||
operation: onShowHosts
|
||||
}
|
||||
]
|
||||
const busy = Boolean(busyAction)
|
||||
|
||||
return (
|
||||
<View accessibilityRole="toolbar" style={styles.recoveryActions}>
|
||||
{actions.map((action) => (
|
||||
<Pressable
|
||||
key={action.id}
|
||||
accessibilityLabel={`${action.label} workspace interface`}
|
||||
accessibilityRole="button"
|
||||
accessibilityState={{ disabled: Boolean(busyAction) }}
|
||||
disabled={Boolean(busyAction)}
|
||||
style={({ pressed }) => [
|
||||
styles.recoveryButton,
|
||||
pressed && styles.recoveryButtonPressed,
|
||||
busyAction && styles.recoveryButtonDisabled
|
||||
]}
|
||||
onPress={() => void run(action.id, action.operation)}
|
||||
>
|
||||
{busyAction === action.id && showBusy ? (
|
||||
<ActivityIndicator color={colors.textSecondary} size="small" />
|
||||
) : null}
|
||||
<Text style={styles.recoveryButtonText}>{action.label}</Text>
|
||||
</Pressable>
|
||||
))}
|
||||
<View
|
||||
accessibilityRole="toolbar"
|
||||
style={[styles.recoveryActions, align === 'start' && styles.recoveryActionsStart]}
|
||||
>
|
||||
<Pressable
|
||||
accessibilityLabel="Retry"
|
||||
accessibilityRole="button"
|
||||
accessibilityState={{ disabled: busy }}
|
||||
disabled={busy}
|
||||
testID="mobile-web-recovery-retry"
|
||||
style={({ pressed }) => [
|
||||
styles.recoveryPrimaryButton,
|
||||
pressed && styles.recoveryPressed,
|
||||
busy && styles.recoveryDisabled
|
||||
]}
|
||||
onPress={() => void run('retry', onRetry)}
|
||||
>
|
||||
{busyAction === 'retry' && showBusy ? (
|
||||
<ActivityIndicator color={colors.bgBase} size="small" />
|
||||
) : null}
|
||||
<Text style={styles.recoveryPrimaryButtonText}>Retry</Text>
|
||||
</Pressable>
|
||||
<View style={[styles.recoveryLinkRow, align === 'start' && styles.recoveryActionsStart]}>
|
||||
{secondaryActions.map((action) => (
|
||||
<Pressable
|
||||
key={action.id}
|
||||
accessibilityLabel={action.label}
|
||||
accessibilityRole="button"
|
||||
accessibilityState={{ disabled: busy }}
|
||||
disabled={busy}
|
||||
hitSlop={8}
|
||||
testID={action.testID}
|
||||
style={({ pressed }) => [
|
||||
styles.recoveryLink,
|
||||
pressed && styles.recoveryPressed,
|
||||
busy && styles.recoveryDisabled
|
||||
]}
|
||||
onPress={() => void run(action.id, action.operation)}
|
||||
>
|
||||
{busyAction === action.id && showBusy ? (
|
||||
<ActivityIndicator color={colors.accentBlue} size="small" />
|
||||
) : null}
|
||||
<Text style={styles.recoveryLinkText}>{action.label}</Text>
|
||||
</Pressable>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ export const hybridShellStyles = StyleSheet.create({
|
||||
headerButton: { width: 38, height: 38, alignItems: 'center', justifyContent: 'center' },
|
||||
headerCopy: { flex: 1, minWidth: 0 },
|
||||
heading: { color: colors.textPrimary, fontSize: 16, fontWeight: '600' },
|
||||
headerMeta: { color: colors.textMuted, fontSize: 11, marginTop: 1 },
|
||||
hostsButton: { paddingHorizontal: spacing.md, paddingVertical: spacing.sm },
|
||||
hostsButtonText: {
|
||||
color: colors.textSecondary,
|
||||
@@ -24,53 +23,64 @@ export const hybridShellStyles = StyleSheet.create({
|
||||
},
|
||||
webContainer: { flex: 1, minHeight: 0 },
|
||||
webView: { flex: 1, backgroundColor: colors.bgBase },
|
||||
warning: {
|
||||
color: colors.textSecondary,
|
||||
backgroundColor: colors.bgRaised,
|
||||
fontSize: typography.metaSize,
|
||||
lineHeight: 17,
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.sm
|
||||
noticeBanner: {
|
||||
gap: spacing.sm,
|
||||
backgroundColor: colors.bgPanel,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
borderBottomColor: colors.borderSubtle,
|
||||
paddingHorizontal: spacing.lg,
|
||||
paddingVertical: spacing.md
|
||||
},
|
||||
recoveryActions: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
gap: spacing.xs,
|
||||
backgroundColor: colors.bgRaised,
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingBottom: spacing.sm
|
||||
},
|
||||
recoveryButton: {
|
||||
minHeight: 32,
|
||||
noticeBannerText: { color: colors.textPrimary, fontSize: 13, lineHeight: 18 },
|
||||
noticeCode: { color: colors.textMuted, fontSize: 11 },
|
||||
recoveryActions: { alignItems: 'center', gap: spacing.md },
|
||||
recoveryActionsStart: { alignItems: 'flex-start' },
|
||||
recoveryPrimaryButton: {
|
||||
minHeight: 36,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: spacing.xs,
|
||||
justifyContent: 'center',
|
||||
gap: spacing.sm,
|
||||
borderRadius: radii.button,
|
||||
backgroundColor: colors.bgPanel,
|
||||
paddingHorizontal: spacing.sm
|
||||
backgroundColor: colors.textPrimary,
|
||||
paddingHorizontal: spacing.xl,
|
||||
paddingVertical: spacing.sm
|
||||
},
|
||||
recoveryButtonPressed: { backgroundColor: colors.bgBase },
|
||||
recoveryButtonDisabled: { opacity: 0.6 },
|
||||
recoveryButtonText: {
|
||||
color: colors.textPrimary,
|
||||
recoveryPrimaryButtonText: {
|
||||
color: colors.bgBase,
|
||||
fontSize: typography.bodySize,
|
||||
fontWeight: '600'
|
||||
},
|
||||
recoveryLinkRow: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
columnGap: spacing.lg,
|
||||
rowGap: spacing.sm
|
||||
},
|
||||
recoveryLink: { flexDirection: 'row', alignItems: 'center', gap: spacing.xs },
|
||||
recoveryLinkText: {
|
||||
color: colors.accentBlue,
|
||||
fontSize: typography.metaSize,
|
||||
fontWeight: '600'
|
||||
},
|
||||
recoveryPressed: { opacity: 0.7 },
|
||||
recoveryDisabled: { opacity: 0.5 },
|
||||
loadingState: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: spacing.sm,
|
||||
gap: spacing.md,
|
||||
paddingHorizontal: spacing.xl
|
||||
},
|
||||
loadingTitle: { color: colors.textPrimary, fontSize: typography.bodySize, fontWeight: '600' },
|
||||
loadingBody: {
|
||||
color: colors.textSecondary,
|
||||
fontSize: typography.metaSize,
|
||||
lineHeight: 18,
|
||||
loadingTitle: {
|
||||
color: colors.textPrimary,
|
||||
fontSize: typography.bodySize,
|
||||
fontWeight: '600',
|
||||
textAlign: 'center'
|
||||
},
|
||||
packageProgress: { width: '100%', maxWidth: 320, gap: spacing.xs },
|
||||
packageProgress: { width: '100%', maxWidth: 320, gap: spacing.sm },
|
||||
packageProgressLabel: {
|
||||
color: colors.textSecondary,
|
||||
fontSize: typography.metaSize,
|
||||
@@ -86,6 +96,5 @@ export const hybridShellStyles = StyleSheet.create({
|
||||
height: '100%',
|
||||
borderRadius: radii.button,
|
||||
backgroundColor: colors.textSecondary
|
||||
},
|
||||
packageProgressBytes: { color: colors.textMuted, fontSize: 11, textAlign: 'center' }
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,13 +1,23 @@
|
||||
import { mobileWebShellHostName, type MobileWebShellNotice } from './mobile-web-shell-notice'
|
||||
|
||||
export function mobileWebPackageRefreshWarning(
|
||||
failureCode: string,
|
||||
hasHealthyInterface: boolean
|
||||
): string {
|
||||
hasHealthyInterface: boolean,
|
||||
hostName?: string
|
||||
): MobileWebShellNotice {
|
||||
const host = mobileWebShellHostName(hostName)
|
||||
if (failureCode === 'incompatible_bridge') {
|
||||
return hasHealthyInterface
|
||||
? 'Using the last healthy interface because the refreshed interface is not compatible with this Orca Mobile version.'
|
||||
: 'This desktop’s workspace interface is not compatible with this Orca Mobile version.'
|
||||
return {
|
||||
message: hasHealthyInterface
|
||||
? `Update Orca Mobile to get the latest from ${host}.`
|
||||
: `Update Orca Mobile to open ${host}.`,
|
||||
code: failureCode
|
||||
}
|
||||
}
|
||||
return {
|
||||
message: hasHealthyInterface
|
||||
? `Couldn’t update from ${host}. Showing the last version that worked.`
|
||||
: `Couldn’t load ${host}.`,
|
||||
code: failureCode
|
||||
}
|
||||
return hasHealthyInterface
|
||||
? 'Using the last healthy interface because the desktop package could not be refreshed.'
|
||||
: 'The desktop did not provide a valid workspace interface.'
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { MobileWebShellSession } from '@orca/expo-mobile-web-shell'
|
||||
import type { MobileWebPackageDownloadProgress } from './mobile-web-package-downloader'
|
||||
import type { MobileWebShellNotice } from './mobile-web-shell-notice'
|
||||
|
||||
export type MobileWebPackageSession = {
|
||||
session: MobileWebShellSession | null
|
||||
@@ -7,12 +8,12 @@ export type MobileWebPackageSession = {
|
||||
viewEpoch: number
|
||||
packageLoading: boolean
|
||||
packageProgress: MobileWebPackageDownloadProgress | undefined
|
||||
packageWarning: string | undefined
|
||||
packageWarning: MobileWebShellNotice | undefined
|
||||
markHealthy: (sessionId: string) => Promise<void>
|
||||
handleHealthTimeout: (sessionId: string) => Promise<void>
|
||||
handleProcessTerminated: (sessionId: string) => Promise<void>
|
||||
retryPackage: () => void
|
||||
recoverPrevious: () => Promise<void>
|
||||
clearCache: () => Promise<void>
|
||||
showWarning: (warning: string) => void
|
||||
showWarning: (message: string, code?: string) => void
|
||||
}
|
||||
|
||||
@@ -18,7 +18,9 @@ describe('mobile hybrid rollback runbook', () => {
|
||||
})
|
||||
|
||||
it('documents every native recovery action by its product label', () => {
|
||||
for (const label of ['Retry', 'Use previous', 'Clear cache', 'Switch hosts']) {
|
||||
expect(recoveryActions).toContain('accessibilityLabel="Retry"')
|
||||
expect(runbook).toContain('**Retry**')
|
||||
for (const label of ['Use last version', 'Reset', 'Switch hosts']) {
|
||||
expect(recoveryActions).toContain(`label: '${label}'`)
|
||||
expect(runbook).toContain(`**${label}**`)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,10 @@ const recoveryActionsSource = readFileSync(
|
||||
new URL('./MobileWebRecoveryActions.tsx', import.meta.url),
|
||||
'utf8'
|
||||
)
|
||||
const progressSource = readFileSync(
|
||||
new URL('./MobileWebPackageProgress.tsx', import.meta.url),
|
||||
'utf8'
|
||||
)
|
||||
|
||||
describe('mobile web shell accessibility', () => {
|
||||
it('keeps shell navigation controls named and exposed as buttons', () => {
|
||||
@@ -20,24 +24,48 @@ describe('mobile web shell accessibility', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('announces package loading and warnings', () => {
|
||||
expect(shellSource).toMatch(
|
||||
/accessibilityLiveRegion="polite"[\s\S]*?Preparing verified interface…/
|
||||
)
|
||||
expect(shellSource.match(/accessibilityRole="alert"/g)).toHaveLength(2)
|
||||
it('announces status and warnings', () => {
|
||||
expect(shellSource).toMatch(/accessibilityLiveRegion="polite"[\s\S]*?\{statusLine\}/)
|
||||
expect(shellSource).toContain("accessibilityRole={packageWarning ? 'alert' : undefined}")
|
||||
expect(shellSource).toContain('accessibilityRole="alert"')
|
||||
expect(progressSource).toContain('accessibilityRole="progressbar"')
|
||||
expect(progressSource).toContain('accessibilityLiveRegion="polite"')
|
||||
})
|
||||
|
||||
it('names recovery controls', () => {
|
||||
it('names recovery controls and anchors them on stable test ids', () => {
|
||||
expect(recoveryActionsSource).toContain('accessibilityRole="toolbar"')
|
||||
expect(recoveryActionsSource).toContain(
|
||||
'accessibilityLabel={`${action.label} workspace interface`}'
|
||||
)
|
||||
expect(recoveryActionsSource).toContain("label: 'Retry'")
|
||||
expect(recoveryActionsSource).toContain("label: 'Use previous'")
|
||||
expect(recoveryActionsSource).toContain("label: 'Clear cache'")
|
||||
expect(recoveryActionsSource).toContain("label: 'Switch hosts'")
|
||||
expect(recoveryActionsSource).toContain(
|
||||
'accessibilityState={{ disabled: Boolean(busyAction) }}'
|
||||
)
|
||||
expect(recoveryActionsSource).toContain('accessibilityLabel={action.label}')
|
||||
expect(recoveryActionsSource).toContain('accessibilityState={{ disabled: busy }}')
|
||||
for (const [testID, label] of [
|
||||
['mobile-web-recovery-retry', 'Retry'],
|
||||
['mobile-web-recovery-previous', 'Use last version'],
|
||||
['mobile-web-recovery-reset', 'Reset'],
|
||||
['mobile-web-recovery-hosts', 'Switch hosts']
|
||||
]) {
|
||||
expect(recoveryActionsSource).toContain(testID)
|
||||
expect(recoveryActionsSource).toContain(label)
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps implementation vocabulary out of the shell copy', () => {
|
||||
for (const source of [shellSource, recoveryActionsSource, progressSource]) {
|
||||
for (const banned of [
|
||||
'verified',
|
||||
'cache',
|
||||
'hosted session',
|
||||
'workspace interface',
|
||||
'workspace UI',
|
||||
'desktop-served'
|
||||
]) {
|
||||
expect(renderedCopy(source).toLowerCase()).not.toContain(banned.toLowerCase())
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
/** Quoted literals a user can read, excluding prop names and identifiers. */
|
||||
function renderedCopy(source: string): string {
|
||||
return [...source.matchAll(/'([^'\n]*)'/g), ...source.matchAll(/`([^`\n]*)`/g)]
|
||||
.map((match) => match[1])
|
||||
.join('\n')
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
// A shell notice is what the user reads; `code` is the stable support identifier shown
|
||||
// underneath it, so product copy never has to carry an error token.
|
||||
export type MobileWebShellNotice = {
|
||||
message: string
|
||||
code?: string
|
||||
}
|
||||
|
||||
const DEFAULT_HOST_NAME = 'your computer'
|
||||
|
||||
export function mobileWebShellHostName(name: string | undefined): string {
|
||||
return name?.trim() || DEFAULT_HOST_NAME
|
||||
}
|
||||
@@ -32,7 +32,7 @@ export function useMobileWebNavigationIntentHandoff(options: {
|
||||
postMessage: (message: MobileWebBridgeShellMessage) => Promise<void>
|
||||
rememberRoute: (route: MobileWebResumeRoute) => void
|
||||
onNavigationResolved?: (intent: MobileWebNavigationIntent, route: MobileWebResumeRoute) => void
|
||||
showWarning: (warning: string) => void
|
||||
showWarning: (message: string, code?: string) => void
|
||||
}): void {
|
||||
const [intent, setIntent] = useState<MobileWebNavigationIntent | null>(null)
|
||||
const activeIntent =
|
||||
@@ -107,7 +107,8 @@ export function useMobileWebNavigationIntentHandoff(options: {
|
||||
if (!cancelled && MOBILE_WEB_NAVIGATION_INTENTS.consume(activeIntent.sequence)) {
|
||||
setIntent(null)
|
||||
options.showWarning(
|
||||
`${navigationIntentFailureSubject(activeIntent.source)} could not be verified (${mobileWebBridgeErrorCode(error)}).`
|
||||
`${navigationIntentFailureSubject(activeIntent.source)} couldn’t be opened.`,
|
||||
mobileWebBridgeErrorCode(error)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { HostProfile } from '../transport/types'
|
||||
import { mobileWebDiagnosticsStore } from './mobile-web-diagnostics-store'
|
||||
import { removeMobileWebHostCache } from './mobile-web-native-stager'
|
||||
import type { MobileWebProcessFailureTracker } from './mobile-web-process-failure-tracker'
|
||||
import { mobileWebShellHostName, type MobileWebShellNotice } from './mobile-web-shell-notice'
|
||||
|
||||
type PackageRecoveryState = {
|
||||
host: HostProfile | undefined
|
||||
@@ -17,7 +18,7 @@ type PackageRecoveryState = {
|
||||
setSessionHostId: Dispatch<SetStateAction<string | undefined>>
|
||||
setViewEpoch: Dispatch<SetStateAction<number>>
|
||||
setPackageLoading: Dispatch<SetStateAction<boolean>>
|
||||
setPackageWarning: Dispatch<SetStateAction<string | undefined>>
|
||||
setPackageWarning: Dispatch<SetStateAction<MobileWebShellNotice | undefined>>
|
||||
setRefreshEpoch: Dispatch<SetStateAction<number>>
|
||||
}
|
||||
|
||||
@@ -46,7 +47,7 @@ export function useMobileWebPackageRecovery({
|
||||
setRefreshEpoch
|
||||
}: PackageRecoveryState): MobileWebPackageRecoveryActions {
|
||||
const recoverSession = useCallback(
|
||||
async (sessionId: string, warning: string, failureCode: string) => {
|
||||
async (sessionId: string, warning: MobileWebShellNotice, failureCode: string) => {
|
||||
const current = ownedSessionRef.current
|
||||
const hostEpoch = hostEpochRef.current
|
||||
const hostId = activeHostIdRef.current
|
||||
@@ -76,9 +77,10 @@ export function useMobileWebPackageRecovery({
|
||||
ownedSessionRef.current?.sessionId === sessionId
|
||||
) {
|
||||
setViewEpoch((value) => value + 1)
|
||||
setPackageWarning(
|
||||
'The workspace view restarted; no previous healthy interface is available.'
|
||||
)
|
||||
setPackageWarning({
|
||||
message: 'Orca restarted. There’s no earlier version to go back to.',
|
||||
code: 'no_previous_version'
|
||||
})
|
||||
mobileWebDiagnosticsStore.restarted(hostId, current.buildId)
|
||||
}
|
||||
}
|
||||
@@ -123,7 +125,10 @@ export function useMobileWebPackageRecovery({
|
||||
current?.sessionId === sessionId &&
|
||||
current.buildId === owned.buildId
|
||||
) {
|
||||
setPackageWarning('The workspace interface is running but could not be marked healthy.')
|
||||
setPackageWarning({
|
||||
message: 'Orca started, but couldn’t finish its checks.',
|
||||
code: 'health_mark_failed'
|
||||
})
|
||||
const hostId = activeHostIdRef.current
|
||||
if (hostId) {
|
||||
mobileWebDiagnosticsStore.warning(hostId, 'health_mark_failed')
|
||||
@@ -138,7 +143,7 @@ export function useMobileWebPackageRecovery({
|
||||
async (sessionId: string) => {
|
||||
await recoverSession(
|
||||
sessionId,
|
||||
'The refreshed interface did not become healthy; the previous verified version was restored.',
|
||||
{ message: 'The update didn’t start correctly, so the last version that worked is back.' },
|
||||
'health_timeout'
|
||||
)
|
||||
},
|
||||
@@ -153,7 +158,7 @@ export function useMobileWebPackageRecovery({
|
||||
}
|
||||
if (!processFailuresRef.current.record(current.buildId)) {
|
||||
setViewEpoch((value) => value + 1)
|
||||
setPackageWarning('The workspace view stopped and was restarted.')
|
||||
setPackageWarning({ message: 'Orca stopped unexpectedly and restarted.' })
|
||||
const hostId = activeHostIdRef.current
|
||||
if (hostId) {
|
||||
mobileWebDiagnosticsStore.restarted(hostId, current.buildId)
|
||||
@@ -162,7 +167,7 @@ export function useMobileWebPackageRecovery({
|
||||
}
|
||||
await recoverSession(
|
||||
sessionId,
|
||||
'The workspace view stopped repeatedly; the previous verified version was restored.',
|
||||
{ message: 'Orca kept stopping, so the last version that worked is back.' },
|
||||
'webview_crash_loop'
|
||||
)
|
||||
},
|
||||
@@ -188,12 +193,15 @@ export function useMobileWebPackageRecovery({
|
||||
const recoverPrevious = useCallback(async () => {
|
||||
const current = ownedSessionRef.current
|
||||
if (!current) {
|
||||
setPackageWarning('No previous verified workspace interface is available.')
|
||||
setPackageWarning({
|
||||
message: 'There’s no earlier version to go back to.',
|
||||
code: 'no_previous_version'
|
||||
})
|
||||
return
|
||||
}
|
||||
await recoverSession(
|
||||
current.sessionId,
|
||||
'The previous verified workspace interface was restored.',
|
||||
{ message: 'Went back to the last version that worked.' },
|
||||
'manual_recovery'
|
||||
)
|
||||
}, [ownedSessionRef, recoverSession, setPackageWarning])
|
||||
@@ -222,7 +230,10 @@ export function useMobileWebPackageRecovery({
|
||||
} catch {
|
||||
if (hostEpochRef.current === hostEpoch) {
|
||||
setPackageLoading(false)
|
||||
setPackageWarning('The workspace interface cache could not be cleared.')
|
||||
setPackageWarning({
|
||||
message: `Couldn’t reset ${mobileWebShellHostName(host.name)}. Try again.`,
|
||||
code: 'reset_failed'
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import { mobileWebDiagnosticsStore } from './mobile-web-diagnostics-store'
|
||||
import type { MobileWebCachedBuildProbe } from './mobile-web-cached-build-probe'
|
||||
import { mobileWebPackageRefreshWarning } from './mobile-web-package-refresh-warning'
|
||||
import type { MobileWebShellNotice } from './mobile-web-shell-notice'
|
||||
import type { MobileWebPackageCapability } from './use-mobile-web-package-capability'
|
||||
|
||||
type PublishSession = (
|
||||
@@ -35,7 +36,7 @@ export function useMobileWebPackageRefresh(args: {
|
||||
publishSession: PublishSession
|
||||
refreshEpoch: number
|
||||
setPackageLoading: (loading: boolean) => void
|
||||
setPackageWarning: (warning: string | undefined) => void
|
||||
setPackageWarning: (warning: MobileWebShellNotice | undefined) => void
|
||||
setPackageProgress: (progress: MobileWebPackageDownloadProgress | undefined) => void
|
||||
}) {
|
||||
const {
|
||||
@@ -102,10 +103,7 @@ export function useMobileWebPackageRefresh(args: {
|
||||
}
|
||||
if (rejectedBuildIdsRef.current.has(downloaded.commit.buildId)) {
|
||||
mobileWebDiagnosticsStore.warning(host.id, 'rejected_build')
|
||||
finishPackage(
|
||||
false,
|
||||
'Using the previous verified interface until the desktop build changes.'
|
||||
)
|
||||
finishPackage(false, { message: 'Showing the last version that worked.' })
|
||||
return
|
||||
}
|
||||
const activationStartedAt = Date.now()
|
||||
@@ -127,7 +125,7 @@ export function useMobileWebPackageRefresh(args: {
|
||||
console.warn('[mobile-web] package refresh failed', { code: failureCode })
|
||||
finishPackage(
|
||||
false,
|
||||
mobileWebPackageRefreshWarning(failureCode, Boolean(ownedSessionRef.current))
|
||||
mobileWebPackageRefreshWarning(failureCode, Boolean(ownedSessionRef.current), host.name)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -148,7 +146,7 @@ export function useMobileWebPackageRefresh(args: {
|
||||
finishPackage(true)
|
||||
}
|
||||
|
||||
function finishPackage(success: boolean, warning?: string): void {
|
||||
function finishPackage(success: boolean, warning?: MobileWebShellNotice): void {
|
||||
setPackageLoading(false)
|
||||
setPackageProgress(undefined)
|
||||
if (success || warning) {
|
||||
|
||||
@@ -224,9 +224,10 @@ describe('useMobileWebPackageSession', () => {
|
||||
|
||||
expect(packageSession?.session).toBeNull()
|
||||
expect(packageSession?.packageLoading).toBe(false)
|
||||
expect(packageSession?.packageWarning).toBe(
|
||||
'Update Orca on this desktop to use its workspace interface.'
|
||||
)
|
||||
expect(packageSession?.packageWarning).toEqual({
|
||||
message: 'Update Orca on Desktop to continue.',
|
||||
code: 'host_update_required'
|
||||
})
|
||||
expect(native.closeSession).toHaveBeenCalledWith(SESSION_A.sessionId)
|
||||
expect(downloadPackage).not.toHaveBeenCalled()
|
||||
expect(sendRequest).toHaveBeenCalledTimes(1)
|
||||
@@ -242,9 +243,10 @@ describe('useMobileWebPackageSession', () => {
|
||||
await mount('connected')
|
||||
|
||||
expect(packageSession?.session).toBeNull()
|
||||
expect(packageSession?.packageWarning).toBe(
|
||||
'Update Orca on this desktop to use its workspace interface.'
|
||||
)
|
||||
expect(packageSession?.packageWarning).toEqual({
|
||||
message: 'Update Orca on Desktop to continue.',
|
||||
code: 'host_update_required'
|
||||
})
|
||||
expect(native.openSession).not.toHaveBeenCalled()
|
||||
expect(downloadPackage).not.toHaveBeenCalled()
|
||||
})
|
||||
@@ -468,14 +470,8 @@ describe('useMobileWebPackageSession', () => {
|
||||
})
|
||||
|
||||
it.each([
|
||||
[
|
||||
'incompatible_bridge',
|
||||
'Using the last healthy interface because the refreshed interface is not compatible with this Orca Mobile version.'
|
||||
],
|
||||
[
|
||||
'test_failure',
|
||||
'Using the last healthy interface because the desktop package could not be refreshed.'
|
||||
]
|
||||
['incompatible_bridge', 'Update Orca Mobile to get the latest from Desktop.'],
|
||||
['test_failure', 'Couldn’t update from Desktop. Showing the last version that worked.']
|
||||
])('retains a cached session after %s package refresh failure', async (code, warning) => {
|
||||
native.openSession.mockResolvedValue(SESSION_A)
|
||||
downloadFailure.code = code
|
||||
@@ -484,15 +480,12 @@ describe('useMobileWebPackageSession', () => {
|
||||
await mount('connected')
|
||||
|
||||
expect(packageSession?.session).toEqual(SESSION_A)
|
||||
expect(packageSession?.packageWarning).toBe(warning)
|
||||
expect(packageSession?.packageWarning).toEqual({ message: warning, code })
|
||||
})
|
||||
|
||||
it.each([
|
||||
[
|
||||
'incompatible_bridge',
|
||||
'This desktop’s workspace interface is not compatible with this Orca Mobile version.'
|
||||
],
|
||||
['test_failure', 'The desktop did not provide a valid workspace interface.']
|
||||
['incompatible_bridge', 'Update Orca Mobile to open Desktop.'],
|
||||
['test_failure', 'Couldn’t load Desktop.']
|
||||
])('reports %s package refresh failure without a cached session', async (code, warning) => {
|
||||
native.openSession.mockRejectedValue(new Error('cache unavailable'))
|
||||
downloadFailure.code = code
|
||||
@@ -502,7 +495,7 @@ describe('useMobileWebPackageSession', () => {
|
||||
|
||||
expect(packageSession?.session).toBeNull()
|
||||
expect(packageSession?.packageLoading).toBe(false)
|
||||
expect(packageSession?.packageWarning).toBe(warning)
|
||||
expect(packageSession?.packageWarning).toEqual({ message: warning, code })
|
||||
})
|
||||
|
||||
it('remounts after isolated process loss and rolls back a crash loop', async () => {
|
||||
@@ -515,7 +508,9 @@ describe('useMobileWebPackageSession', () => {
|
||||
await packageSession?.markHealthy(SESSION_B.sessionId)
|
||||
})
|
||||
expect(packageSession?.viewEpoch).toBe(1)
|
||||
expect(packageSession?.packageWarning).toBe('The workspace view stopped and was restarted.')
|
||||
expect(packageSession?.packageWarning).toEqual({
|
||||
message: 'Orca stopped unexpectedly and restarted.'
|
||||
})
|
||||
expect(native.recoverSession).not.toHaveBeenCalled()
|
||||
|
||||
await act(async () => {
|
||||
@@ -544,9 +539,9 @@ describe('useMobileWebPackageSession', () => {
|
||||
|
||||
expect(native.recoverSession).toHaveBeenCalledWith(SESSION_B.sessionId)
|
||||
expect(packageSession?.session).toEqual(SESSION_A)
|
||||
expect(packageSession?.packageWarning).toBe(
|
||||
'The previous verified workspace interface was restored.'
|
||||
)
|
||||
expect(packageSession?.packageWarning).toEqual({
|
||||
message: 'Went back to the last version that worked.'
|
||||
})
|
||||
})
|
||||
|
||||
it('clears only the selected host cache and downloads it again', async () => {
|
||||
|
||||
@@ -14,10 +14,9 @@ import { useMobileWebPackageCapability } from './use-mobile-web-package-capabili
|
||||
import { useMobileWebPackageRecovery } from './use-mobile-web-package-recovery'
|
||||
import { useMobileWebPackageRefresh } from './use-mobile-web-package-refresh'
|
||||
import type { MobileWebPackageSession } from './mobile-web-package-session-state'
|
||||
import { mobileWebShellHostName, type MobileWebShellNotice } from './mobile-web-shell-notice'
|
||||
|
||||
export type { MobileWebPackageSession } from './mobile-web-package-session-state'
|
||||
const MOBILE_WEB_PACKAGE_UPDATE_REQUIRED_WARNING =
|
||||
'Update Orca on this desktop to use its workspace interface.'
|
||||
export function useMobileWebPackageSession({
|
||||
client,
|
||||
host,
|
||||
@@ -42,7 +41,7 @@ export function useMobileWebPackageSession({
|
||||
const [viewEpoch, setViewEpoch] = useState(0)
|
||||
const [packageLoading, setPackageLoading] = useState(false)
|
||||
const [packageProgress, setPackageProgress] = useState<MobileWebPackageDownloadProgress>()
|
||||
const [packageWarning, setPackageWarning] = useState<string>()
|
||||
const [packageWarning, setPackageWarning] = useState<MobileWebShellNotice>()
|
||||
const [refreshEpoch, setRefreshEpoch] = useState(0)
|
||||
const packageCapability = useMobileWebPackageCapability({
|
||||
client,
|
||||
@@ -58,7 +57,10 @@ export function useMobileWebPackageSession({
|
||||
const effectivePackageWarning =
|
||||
packageWarning ??
|
||||
(packageCapability.status === 'update-required'
|
||||
? MOBILE_WEB_PACKAGE_UPDATE_REQUIRED_WARNING
|
||||
? {
|
||||
message: `Update Orca on ${mobileWebShellHostName(host?.name)} to continue.`,
|
||||
code: 'host_update_required'
|
||||
}
|
||||
: undefined)
|
||||
|
||||
const publishSession = useCallback(
|
||||
@@ -152,7 +154,9 @@ export function useMobileWebPackageSession({
|
||||
setPackageLoading(false)
|
||||
setPackageWarning(
|
||||
(current) =>
|
||||
current ?? 'Connect to this desktop once to cache its verified workspace UI.'
|
||||
current ?? {
|
||||
message: `Connect to ${mobileWebShellHostName(host.name)} to finish setting up.`
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -200,6 +204,11 @@ export function useMobileWebPackageSession({
|
||||
setPackageProgress
|
||||
})
|
||||
|
||||
const showWarning = useCallback(
|
||||
(message: string, code?: string) => setPackageWarning({ message, code }),
|
||||
[]
|
||||
)
|
||||
|
||||
const {
|
||||
markHealthy,
|
||||
handleHealthTimeout,
|
||||
@@ -236,6 +245,6 @@ export function useMobileWebPackageSession({
|
||||
retryPackage,
|
||||
recoverPrevious,
|
||||
clearCache,
|
||||
showWarning: setPackageWarning
|
||||
showWarning
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user