mirror of
https://github.com/stablyai/orca.git
synced 2026-10-13 08:02:52 +00:00
+105
-10
@@ -2,37 +2,130 @@
|
||||
|
||||
React Native companion app for Orca. Monitor worktrees, view terminal output, and send commands from your phone.
|
||||
|
||||
Local development uses two processes:
|
||||
|
||||
- Orca desktop/Electron from the repo root. This hosts the mobile WebSocket RPC server on port `6768`.
|
||||
- Expo Metro from `mobile/`. This serves the React Native app on port `8081`.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js 24+
|
||||
- pnpm
|
||||
- Expo Go app on your phone (from App Store / Google Play), **or** a dev client build
|
||||
- Xcode and/or Android Studio tooling for simulator or device builds
|
||||
- Expo Go on your phone, or a development client build when native modules are needed
|
||||
- Phone and desktop on the same LAN when testing a physical phone
|
||||
|
||||
## Quick Start
|
||||
## Start Desktop Orca
|
||||
|
||||
From the repository root:
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
Confirm the mobile RPC server is listening:
|
||||
|
||||
```bash
|
||||
lsof -nP -iTCP:6768 -sTCP:LISTEN
|
||||
```
|
||||
|
||||
Restart `pnpm dev` after changing Electron main-process code. Metro hot reload only applies to the mobile JavaScript bundle.
|
||||
|
||||
## Start The Mobile App
|
||||
|
||||
```bash
|
||||
cd mobile
|
||||
pnpm install
|
||||
pnpm start # starts Expo dev server
|
||||
pnpm start
|
||||
```
|
||||
|
||||
Scan the QR code shown in the terminal with your phone's camera (iOS) or the Expo Go app (Android).
|
||||
Scan the Expo QR code with your phone's camera on iOS, or Expo Go on Android.
|
||||
|
||||
For a native dev-client build:
|
||||
|
||||
```bash
|
||||
pnpm exec expo run:android
|
||||
pnpm exec expo run:ios
|
||||
pnpm start --dev-client
|
||||
```
|
||||
|
||||
## Pair With Desktop Orca
|
||||
|
||||
1. Open Orca desktop.
|
||||
2. Go to Settings > Mobile.
|
||||
3. Scan the pairing QR code from the mobile app.
|
||||
4. Confirm the mobile host endpoint is `ws://<desktop-ip>:6768`.
|
||||
|
||||
For the Android emulator, use `ws://10.0.2.2:6768`. For a physical phone, use the desktop LAN IP, for example `ws://192.168.0.179:6768`.
|
||||
|
||||
If the phone has a stale host entry, remove it from the app and pair again.
|
||||
|
||||
## Development Paths
|
||||
|
||||
### I have an Android phone
|
||||
### Android Phone
|
||||
|
||||
1. Install Expo Go from Google Play
|
||||
2. Run `pnpm start`, scan QR with Expo Go
|
||||
3. For native modules (camera): `npx expo prebuild --platform android && cd android && ./gradlew assembleDebug`
|
||||
4. Install APK: `adb install android/app/build/outputs/apk/debug/app-debug.apk`
|
||||
5. Run with `pnpm start --dev-client`
|
||||
3. For native modules: `pnpm exec expo run:android`
|
||||
4. Run with `pnpm start --dev-client`
|
||||
|
||||
### I only have a Mac (iOS Simulator)
|
||||
### iOS Simulator
|
||||
|
||||
1. Install Xcode from the App Store
|
||||
2. Run `pnpm start --ios` to open in iOS Simulator
|
||||
|
||||
## Physical Phone Debugging
|
||||
|
||||
The phone can be inspected through the connected device tooling:
|
||||
|
||||
```bash
|
||||
orca snapshot --json
|
||||
orca click --element @e3 --json
|
||||
orca fill --element @e1 --value "ls" --json
|
||||
orca screenshot --json
|
||||
```
|
||||
|
||||
Use `snapshot` first to find the current element refs, then click/fill those refs. After mobile file edits, Metro usually hot reloads automatically, but navigating out of and back into the session screen can be useful because it re-runs `terminal.subscribe`.
|
||||
|
||||
## Terminal Streaming Repro Without A Phone
|
||||
|
||||
Use this when terminal output does not render on device and you need to split server streaming bugs from WebView/UI bugs:
|
||||
|
||||
```bash
|
||||
cd mobile
|
||||
ORCA_MOBILE_WS_URL=ws://127.0.0.1:6768 pnpm exec tsx scripts/test-subscribe.ts <deviceToken>
|
||||
```
|
||||
|
||||
You can pass a worktree selector as the second argument:
|
||||
|
||||
```bash
|
||||
pnpm exec tsx scripts/test-subscribe.ts <deviceToken> "id:<worktreeId>"
|
||||
pnpm exec tsx scripts/test-subscribe.ts <deviceToken> "path:/absolute/worktree/path"
|
||||
pnpm exec tsx scripts/test-subscribe.ts <deviceToken> "name:my-worktree"
|
||||
```
|
||||
|
||||
The expected result includes:
|
||||
|
||||
```text
|
||||
streamSawMarker: true
|
||||
readSawMarker: true
|
||||
```
|
||||
|
||||
If this repro fails, debug the desktop runtime/PTY path before the mobile WebView. If it passes but the phone is blank, debug the session screen or `TerminalWebView` readiness/queueing path.
|
||||
|
||||
## Validation
|
||||
|
||||
Run these checks before committing mobile terminal changes:
|
||||
|
||||
```bash
|
||||
cd mobile
|
||||
pnpm exec tsc --noEmit
|
||||
pnpm lint
|
||||
cd ..
|
||||
pnpm typecheck:node
|
||||
```
|
||||
|
||||
## Mock Server
|
||||
|
||||
Develop the mobile app without a running Orca desktop instance:
|
||||
@@ -58,8 +151,10 @@ mobile/
|
||||
│ ├── index.tsx # Home screen — paired hosts list
|
||||
│ └── pair-scan.tsx # QR code scanning screen
|
||||
├── src/
|
||||
│ └── transport/ # WebSocket RPC client (TBD)
|
||||
│ ├── terminal/ # Terminal WebView and xterm bridge
|
||||
│ └── transport/ # WebSocket RPC client
|
||||
├── scripts/
|
||||
│ ├── test-subscribe.ts # Desktop streaming repro without a phone
|
||||
│ └── mock-server.ts # Standalone mock WebSocket server
|
||||
└── assets/ # App icons and splash screen
|
||||
```
|
||||
|
||||
@@ -13,6 +13,10 @@ import { useLocalSearchParams } from 'expo-router'
|
||||
import { connect, type RpcClient } from '../../../../src/transport/rpc-client'
|
||||
import { loadHosts } from '../../../../src/transport/host-store'
|
||||
import type { ConnectionState, RpcSuccess } from '../../../../src/transport/types'
|
||||
import {
|
||||
TerminalWebView,
|
||||
type TerminalWebViewHandle
|
||||
} from '../../../../src/terminal/TerminalWebView'
|
||||
|
||||
type Terminal = {
|
||||
handle: string
|
||||
@@ -25,11 +29,12 @@ export default function SessionScreen() {
|
||||
const [client, setClient] = useState<RpcClient | null>(null)
|
||||
const [connState, setConnState] = useState<ConnectionState>('disconnected')
|
||||
const [terminals, setTerminals] = useState<Terminal[]>([])
|
||||
const [output, setOutput] = useState('')
|
||||
const [input, setInput] = useState('')
|
||||
const [activeHandle, setActiveHandle] = useState<string | null>(null)
|
||||
const scrollRef = useRef<ScrollView>(null)
|
||||
const termRef = useRef<TerminalWebViewHandle>(null)
|
||||
const unsubRef = useRef<(() => void) | null>(null)
|
||||
const activeHandleRef = useRef<string | null>(null)
|
||||
const terminalSizeRef = useRef({ cols: 80, rows: 24 })
|
||||
|
||||
useEffect(() => {
|
||||
let rpcClient: RpcClient | null = null
|
||||
@@ -49,6 +54,62 @@ export default function SessionScreen() {
|
||||
}
|
||||
}, [hostId])
|
||||
|
||||
useEffect(() => {
|
||||
unsubRef.current?.()
|
||||
unsubRef.current = null
|
||||
activeHandleRef.current = null
|
||||
setActiveHandle(null)
|
||||
setTerminals([])
|
||||
termRef.current?.clear()
|
||||
}, [worktreeId])
|
||||
|
||||
const writeLines = useCallback((lines: string[] | string | undefined) => {
|
||||
const text = Array.isArray(lines) ? lines.join('\r\n') : (lines ?? '')
|
||||
if (text) {
|
||||
termRef.current?.write(text)
|
||||
if (Array.isArray(lines)) {
|
||||
termRef.current?.write('\r\n')
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
const subscribeToTerminal = useCallback(
|
||||
(handle: string) => {
|
||||
unsubRef.current?.()
|
||||
if (!client) return
|
||||
|
||||
termRef.current?.clear()
|
||||
|
||||
const unsub = client.subscribe('terminal.subscribe', { terminal: handle }, (result) => {
|
||||
const data = result as Record<string, unknown>
|
||||
if (data.type === 'scrollback') {
|
||||
// Why: init xterm at the desktop's exact cols/rows so escape
|
||||
// codes with absolute cursor positioning render correctly.
|
||||
// CSS transform: scale() in TerminalWebView shrinks the canvas
|
||||
// to fit the phone viewport, producing a 1:1 miniature.
|
||||
const cols = (data.cols as number) || 80
|
||||
const rows = (data.rows as number) || 24
|
||||
terminalSizeRef.current = { cols, rows }
|
||||
termRef.current?.init(cols, rows)
|
||||
// Why: prefer serialized xterm buffer (ANSI escape string that
|
||||
// reconstructs the exact screen state) over line-based tail
|
||||
// because TUI apps use absolute cursor positioning that only
|
||||
// works at the original terminal dimensions.
|
||||
if (typeof data.serialized === 'string' && data.serialized.length > 0) {
|
||||
termRef.current?.write(data.serialized)
|
||||
} else {
|
||||
writeLines(data.lines as string[] | string | undefined)
|
||||
}
|
||||
} else if (data.type === 'data') {
|
||||
termRef.current?.write(data.chunk as string)
|
||||
}
|
||||
})
|
||||
|
||||
unsubRef.current = unsub
|
||||
},
|
||||
[client, writeLines]
|
||||
)
|
||||
|
||||
const fetchTerminals = useCallback(async () => {
|
||||
if (!client) return
|
||||
|
||||
@@ -60,16 +121,26 @@ export default function SessionScreen() {
|
||||
const result = (response as RpcSuccess).result as { terminals: Terminal[] }
|
||||
setTerminals(result.terminals)
|
||||
|
||||
const active = result.terminals.find((t) => t.isActive) ?? result.terminals[0]
|
||||
if (active && active.handle !== activeHandle) {
|
||||
setActiveHandle(active.handle)
|
||||
subscribeToTerminal(active.handle)
|
||||
const current = activeHandleRef.current
|
||||
if (!current || !result.terminals.some((t) => t.handle === current)) {
|
||||
const active = result.terminals.find((t) => t.isActive) ?? result.terminals[0]
|
||||
if (active) {
|
||||
activeHandleRef.current = active.handle
|
||||
setActiveHandle(active.handle)
|
||||
subscribeToTerminal(active.handle)
|
||||
} else {
|
||||
unsubRef.current?.()
|
||||
unsubRef.current = null
|
||||
activeHandleRef.current = null
|
||||
setActiveHandle(null)
|
||||
termRef.current?.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Will retry on reconnect
|
||||
// Failed to list terminals
|
||||
}
|
||||
}, [client, worktreeId, activeHandle])
|
||||
}, [client, worktreeId, subscribeToTerminal])
|
||||
|
||||
useEffect(() => {
|
||||
if (connState === 'connected') {
|
||||
@@ -77,32 +148,14 @@ export default function SessionScreen() {
|
||||
}
|
||||
}, [connState, fetchTerminals])
|
||||
|
||||
function subscribeToTerminal(handle: string) {
|
||||
unsubRef.current?.()
|
||||
|
||||
if (!client) return
|
||||
|
||||
setOutput('')
|
||||
|
||||
const unsub = client.subscribe('terminal.subscribe', { terminal: handle }, (result) => {
|
||||
const data = result as Record<string, unknown>
|
||||
if (data.type === 'scrollback') {
|
||||
setOutput(data.lines as string)
|
||||
scrollToBottom()
|
||||
} else if (data.type === 'data') {
|
||||
setOutput((prev) => prev + (data.chunk as string))
|
||||
scrollToBottom()
|
||||
}
|
||||
})
|
||||
|
||||
unsubRef.current = unsub
|
||||
}
|
||||
|
||||
function scrollToBottom() {
|
||||
setTimeout(() => {
|
||||
scrollRef.current?.scrollToEnd({ animated: true })
|
||||
}, 50)
|
||||
}
|
||||
const switchTab = useCallback(
|
||||
(handle: string) => {
|
||||
activeHandleRef.current = handle
|
||||
setActiveHandle(handle)
|
||||
subscribeToTerminal(handle)
|
||||
},
|
||||
[subscribeToTerminal]
|
||||
)
|
||||
|
||||
async function handleSend() {
|
||||
if (!client || !activeHandle || !input.trim()) return
|
||||
@@ -142,10 +195,7 @@ export default function SessionScreen() {
|
||||
<Pressable
|
||||
key={t.handle}
|
||||
style={[styles.tab, t.handle === activeHandle && styles.tabActive]}
|
||||
onPress={() => {
|
||||
setActiveHandle(t.handle)
|
||||
subscribeToTerminal(t.handle)
|
||||
}}
|
||||
onPress={() => switchTab(t.handle)}
|
||||
>
|
||||
<Text
|
||||
style={[styles.tabText, t.handle === activeHandle && styles.tabTextActive]}
|
||||
@@ -158,15 +208,7 @@ export default function SessionScreen() {
|
||||
</ScrollView>
|
||||
)}
|
||||
|
||||
<ScrollView
|
||||
ref={scrollRef}
|
||||
style={styles.outputScroll}
|
||||
contentContainerStyle={styles.outputContent}
|
||||
>
|
||||
<Text style={styles.outputText} selectable>
|
||||
{output || 'Waiting for output...'}
|
||||
</Text>
|
||||
</ScrollView>
|
||||
<TerminalWebView ref={termRef} />
|
||||
|
||||
<View style={styles.inputBar}>
|
||||
<TextInput
|
||||
@@ -238,18 +280,6 @@ const styles = StyleSheet.create({
|
||||
tabTextActive: {
|
||||
color: '#e0e0e0'
|
||||
},
|
||||
outputScroll: {
|
||||
flex: 1
|
||||
},
|
||||
outputContent: {
|
||||
padding: 12
|
||||
},
|
||||
outputText: {
|
||||
color: '#d4d4d4',
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 13,
|
||||
lineHeight: 18
|
||||
},
|
||||
inputBar: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
"react-native-safe-area-context": "^5.6.2",
|
||||
"react-native-screens": "^4.23.0",
|
||||
"react-native-web": "^0.21.2",
|
||||
"react-native-webview": "^13.16.1",
|
||||
"react-native-worklets": "^0.7.4",
|
||||
"ws": "^8.18.0",
|
||||
"zod": "^4.3.6",
|
||||
|
||||
Generated
+38
-21
@@ -13,7 +13,7 @@ importers:
|
||||
version: 2.2.0(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))
|
||||
expo:
|
||||
specifier: ^55.0.17
|
||||
version: 55.0.17(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.13)(react-dom@19.2.5(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
version: 55.0.17(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.13)(react-dom@19.2.5(react@19.2.0))(react-native-webview@13.16.1(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
expo-camera:
|
||||
specifier: ^55.0.16
|
||||
version: 55.0.16(@types/emscripten@1.41.5)(expo@55.0.17)(react-native-web@0.21.2(react-dom@19.2.5(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
|
||||
@@ -50,6 +50,9 @@ importers:
|
||||
react-native-web:
|
||||
specifier: ^0.21.2
|
||||
version: 0.21.2(react-dom@19.2.5(react@19.2.0))(react@19.2.0)
|
||||
react-native-webview:
|
||||
specifier: ^13.16.1
|
||||
version: 13.16.1(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
|
||||
react-native-worklets:
|
||||
specifier: ^0.7.4
|
||||
version: 0.7.4(@babel/core@7.29.0)(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
|
||||
@@ -4665,6 +4668,12 @@ packages:
|
||||
react: ^18.0.0 || ^19.0.0
|
||||
react-dom: ^18.0.0 || ^19.0.0
|
||||
|
||||
react-native-webview@13.16.1:
|
||||
resolution: {integrity: sha512-If0eHhoEdOYDcHsX+xBFwHMbWBGK1BvGDQDQdVkwtSIXiq1uiqjkpWVP2uQ1as94J0CzvFE9PUNDuhiX0Z6ubw==}
|
||||
peerDependencies:
|
||||
react: '*'
|
||||
react-native: '*'
|
||||
|
||||
react-native-worklets@0.7.4:
|
||||
resolution: {integrity: sha512-NYOdM1MwBb3n+AtMqy1tFy3Mn8DliQtd8sbzAVRf9Gc+uvQ0zRfxN7dS8ZzoyX7t6cyQL5THuGhlnX+iFlQTag==}
|
||||
peerDependencies:
|
||||
@@ -6652,7 +6661,7 @@ snapshots:
|
||||
connect: 3.7.0
|
||||
debug: 4.4.3
|
||||
dnssd-advertise: 1.1.4
|
||||
expo: 55.0.17(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.13)(react-dom@19.2.5(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
expo: 55.0.17(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.13)(react-dom@19.2.5(react@19.2.0))(react-native-webview@13.16.1(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
expo-server: 55.0.8
|
||||
fetch-nodeshim: 0.4.10
|
||||
getenv: 2.0.0
|
||||
@@ -6750,7 +6759,7 @@ snapshots:
|
||||
|
||||
'@expo/dom-webview@55.0.5(expo@55.0.17)(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)':
|
||||
dependencies:
|
||||
expo: 55.0.17(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.13)(react-dom@19.2.5(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
expo: 55.0.17(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.13)(react-dom@19.2.5(react@19.2.0))(react-native-webview@13.16.1(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
react: 19.2.0
|
||||
react-native: 0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0)
|
||||
|
||||
@@ -6808,7 +6817,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@expo/dom-webview': 55.0.5(expo@55.0.17)(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
|
||||
anser: 1.4.10
|
||||
expo: 55.0.17(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.13)(react-dom@19.2.5(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
expo: 55.0.17(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.13)(react-dom@19.2.5(react@19.2.0))(react-native-webview@13.16.1(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
react: 19.2.0
|
||||
react-native: 0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0)
|
||||
stacktrace-parser: 0.1.11
|
||||
@@ -6835,7 +6844,7 @@ snapshots:
|
||||
postcss: 8.4.49
|
||||
resolve-from: 5.0.0
|
||||
optionalDependencies:
|
||||
expo: 55.0.17(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.13)(react-dom@19.2.5(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
expo: 55.0.17(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.13)(react-dom@19.2.5(react@19.2.0))(react-native-webview@13.16.1(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- supports-color
|
||||
@@ -6846,7 +6855,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@expo/log-box': 55.0.11(@expo/dom-webview@55.0.5)(expo@55.0.17)(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
|
||||
anser: 1.4.10
|
||||
expo: 55.0.17(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.13)(react-dom@19.2.5(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
expo: 55.0.17(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.13)(react-dom@19.2.5(react@19.2.0))(react-native-webview@13.16.1(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
pretty-format: 29.7.0
|
||||
react: 19.2.0
|
||||
react-native: 0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0)
|
||||
@@ -6910,7 +6919,7 @@ snapshots:
|
||||
'@expo/json-file': 10.0.13
|
||||
'@react-native/normalize-colors': 0.83.6
|
||||
debug: 4.4.3
|
||||
expo: 55.0.17(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.13)(react-dom@19.2.5(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
expo: 55.0.17(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.13)(react-dom@19.2.5(react@19.2.0))(react-native-webview@13.16.1(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
resolve-from: 5.0.0
|
||||
semver: 7.7.4
|
||||
xml2js: 0.6.0
|
||||
@@ -6931,7 +6940,7 @@ snapshots:
|
||||
'@expo/router-server@55.0.15(@expo/metro-runtime@55.0.10)(expo-constants@55.0.15)(expo-font@55.0.6)(expo-router@55.0.13)(expo-server@55.0.8)(expo@55.0.17)(react-dom@19.2.5(react@19.2.0))(react@19.2.0)':
|
||||
dependencies:
|
||||
debug: 4.4.3
|
||||
expo: 55.0.17(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.13)(react-dom@19.2.5(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
expo: 55.0.17(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.13)(react-dom@19.2.5(react@19.2.0))(react-native-webview@13.16.1(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
expo-constants: 55.0.15(expo@55.0.17)(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))
|
||||
expo-font: 55.0.6(expo@55.0.17)(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
|
||||
expo-server: 55.0.8
|
||||
@@ -8364,7 +8373,7 @@ snapshots:
|
||||
resolve-from: 5.0.0
|
||||
optionalDependencies:
|
||||
'@babel/runtime': 7.29.2
|
||||
expo: 55.0.17(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.13)(react-dom@19.2.5(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
expo: 55.0.17(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.13)(react-dom@19.2.5(react@19.2.0))(react-native-webview@13.16.1(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
transitivePeerDependencies:
|
||||
- '@babel/core'
|
||||
- supports-color
|
||||
@@ -9190,7 +9199,7 @@ snapshots:
|
||||
expo-asset@55.0.16(expo@55.0.17)(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3):
|
||||
dependencies:
|
||||
'@expo/image-utils': 0.8.13(typescript@5.9.3)
|
||||
expo: 55.0.17(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.13)(react-dom@19.2.5(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
expo: 55.0.17(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.13)(react-dom@19.2.5(react@19.2.0))(react-native-webview@13.16.1(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
expo-constants: 55.0.15(expo@55.0.17)(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))
|
||||
react: 19.2.0
|
||||
react-native: 0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0)
|
||||
@@ -9201,7 +9210,7 @@ snapshots:
|
||||
expo-camera@55.0.16(@types/emscripten@1.41.5)(expo@55.0.17)(react-native-web@0.21.2(react-dom@19.2.5(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0):
|
||||
dependencies:
|
||||
barcode-detector: 3.1.2(@types/emscripten@1.41.5)
|
||||
expo: 55.0.17(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.13)(react-dom@19.2.5(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
expo: 55.0.17(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.13)(react-dom@19.2.5(react@19.2.0))(react-native-webview@13.16.1(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
react: 19.2.0
|
||||
react-native: 0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0)
|
||||
optionalDependencies:
|
||||
@@ -9212,32 +9221,32 @@ snapshots:
|
||||
expo-constants@55.0.15(expo@55.0.17)(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0)):
|
||||
dependencies:
|
||||
'@expo/env': 2.1.1
|
||||
expo: 55.0.17(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.13)(react-dom@19.2.5(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
expo: 55.0.17(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.13)(react-dom@19.2.5(react@19.2.0))(react-native-webview@13.16.1(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
react-native: 0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
expo-file-system@55.0.17(expo@55.0.17)(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0)):
|
||||
dependencies:
|
||||
expo: 55.0.17(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.13)(react-dom@19.2.5(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
expo: 55.0.17(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.13)(react-dom@19.2.5(react@19.2.0))(react-native-webview@13.16.1(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
react-native: 0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0)
|
||||
|
||||
expo-font@55.0.6(expo@55.0.17)(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0):
|
||||
dependencies:
|
||||
expo: 55.0.17(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.13)(react-dom@19.2.5(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
expo: 55.0.17(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.13)(react-dom@19.2.5(react@19.2.0))(react-native-webview@13.16.1(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
fontfaceobserver: 2.3.0
|
||||
react: 19.2.0
|
||||
react-native: 0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0)
|
||||
|
||||
expo-glass-effect@55.0.10(expo@55.0.17)(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0):
|
||||
dependencies:
|
||||
expo: 55.0.17(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.13)(react-dom@19.2.5(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
expo: 55.0.17(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.13)(react-dom@19.2.5(react@19.2.0))(react-native-webview@13.16.1(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
react: 19.2.0
|
||||
react-native: 0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0)
|
||||
|
||||
expo-image@55.0.9(expo@55.0.17)(react-native-web@0.21.2(react-dom@19.2.5(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0):
|
||||
dependencies:
|
||||
expo: 55.0.17(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.13)(react-dom@19.2.5(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
expo: 55.0.17(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.13)(react-dom@19.2.5(react@19.2.0))(react-native-webview@13.16.1(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
react: 19.2.0
|
||||
react-native: 0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0)
|
||||
sf-symbols-typescript: 2.2.0
|
||||
@@ -9246,7 +9255,7 @@ snapshots:
|
||||
|
||||
expo-keep-awake@55.0.6(expo@55.0.17)(react@19.2.0):
|
||||
dependencies:
|
||||
expo: 55.0.17(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.13)(react-dom@19.2.5(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
expo: 55.0.17(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.13)(react-dom@19.2.5(react@19.2.0))(react-native-webview@13.16.1(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
react: 19.2.0
|
||||
|
||||
expo-linking@55.0.14(expo@55.0.17)(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0):
|
||||
@@ -9336,7 +9345,7 @@ snapshots:
|
||||
client-only: 0.0.1
|
||||
debug: 4.4.3
|
||||
escape-string-regexp: 4.0.0
|
||||
expo: 55.0.17(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.13)(react-dom@19.2.5(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
expo: 55.0.17(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.13)(react-dom@19.2.5(react@19.2.0))(react-native-webview@13.16.1(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
expo-constants: 55.0.15(expo@55.0.17)(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))
|
||||
expo-glass-effect: 55.0.10(expo@55.0.17)(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
|
||||
expo-image: 55.0.9(expo@55.0.17)(react-native-web@0.21.2(react-dom@19.2.5(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
|
||||
@@ -9383,13 +9392,13 @@ snapshots:
|
||||
expo-symbols@55.0.7(expo-font@55.0.6)(expo@55.0.17)(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0):
|
||||
dependencies:
|
||||
'@expo-google-fonts/material-symbols': 0.4.34
|
||||
expo: 55.0.17(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.13)(react-dom@19.2.5(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
expo: 55.0.17(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.13)(react-dom@19.2.5(react@19.2.0))(react-native-webview@13.16.1(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
expo-font: 55.0.6(expo@55.0.17)(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
|
||||
react: 19.2.0
|
||||
react-native: 0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0)
|
||||
sf-symbols-typescript: 2.2.0
|
||||
|
||||
expo@55.0.17(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.13)(react-dom@19.2.5(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3):
|
||||
expo@55.0.17(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.13)(react-dom@19.2.5(react@19.2.0))(react-native-webview@13.16.1(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.29.2
|
||||
'@expo/cli': 55.0.26(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-constants@55.0.15)(expo-font@55.0.6)(expo-router@55.0.13)(expo@55.0.17)(react-dom@19.2.5(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
@@ -9419,6 +9428,7 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@expo/dom-webview': 55.0.5(expo@55.0.17)(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
|
||||
'@expo/metro-runtime': 55.0.10(@expo/dom-webview@55.0.5)(expo@55.0.17)(react-dom@19.2.5(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
|
||||
react-native-webview: 13.16.1(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)
|
||||
transitivePeerDependencies:
|
||||
- '@babel/core'
|
||||
- bufferutil
|
||||
@@ -10099,7 +10109,7 @@ snapshots:
|
||||
'@jest/create-cache-key-function': 29.7.0
|
||||
'@jest/globals': 29.7.0
|
||||
babel-jest: 29.7.0(@babel/core@7.29.0)
|
||||
expo: 55.0.17(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.13)(react-dom@19.2.5(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
expo: 55.0.17(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.13)(react-dom@19.2.5(react@19.2.0))(react-native-webview@13.16.1(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
|
||||
jest-environment-jsdom: 29.7.0
|
||||
jest-snapshot: 29.7.0
|
||||
jest-watch-select-projects: 2.0.0
|
||||
@@ -11370,6 +11380,13 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
|
||||
react-native-webview@13.16.1(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0):
|
||||
dependencies:
|
||||
escape-string-regexp: 4.0.0
|
||||
invariant: 2.2.4
|
||||
react: 19.2.0
|
||||
react-native: 0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0)
|
||||
|
||||
react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.6(@babel/core@7.29.0)(@react-native/metro-config@0.85.2(@babel/core@7.29.0))(@types/react@19.2.14)(react@19.2.0))(react@19.2.0):
|
||||
dependencies:
|
||||
'@babel/core': 7.29.0
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
/**
|
||||
* Lightweight terminal streaming repro for the mobile WebSocket RPC.
|
||||
*
|
||||
* Usage:
|
||||
* pnpm exec tsx scripts/test-subscribe.ts <deviceToken> [worktreeSelector]
|
||||
*
|
||||
* Example:
|
||||
* pnpm exec tsx scripts/test-subscribe.ts "$TOKEN" \
|
||||
* "id:repo-id::/path/to/worktree"
|
||||
*/
|
||||
import WebSocket from 'ws'
|
||||
|
||||
const WS_URL = process.env.ORCA_MOBILE_WS_URL ?? 'ws://127.0.0.1:6768'
|
||||
const token = process.argv[2]
|
||||
const worktreeSelector = process.argv[3]
|
||||
const marker = `MOBILE_STREAM_${Date.now()}`
|
||||
|
||||
type RpcResponse = {
|
||||
id: string
|
||||
ok: boolean
|
||||
streaming?: true
|
||||
result?: Record<string, unknown>
|
||||
error?: { code: string; message: string }
|
||||
_meta?: { runtimeId: string }
|
||||
}
|
||||
|
||||
type PendingRequest = {
|
||||
method: string
|
||||
resolve: (response: RpcResponse) => void
|
||||
reject: (error: Error) => void
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
console.error('Usage: pnpm exec tsx scripts/test-subscribe.ts <deviceToken> [worktreeSelector]')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
let reqId = 0
|
||||
const pending = new Map<string, PendingRequest>()
|
||||
let streamSawMarker = false
|
||||
let readSawMarker = false
|
||||
let activeHandle: string | null = null
|
||||
let runtimeId = ''
|
||||
|
||||
function nextId(): string {
|
||||
reqId += 1
|
||||
return `test-${reqId}`
|
||||
}
|
||||
|
||||
function send(ws: WebSocket, method: string, params?: unknown): Promise<RpcResponse> {
|
||||
const id = nextId()
|
||||
ws.send(JSON.stringify({ id, deviceToken: token, method, params }))
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
pending.delete(id)
|
||||
reject(new Error(`Timed out waiting for ${method}`))
|
||||
}, 10_000)
|
||||
pending.set(id, {
|
||||
method,
|
||||
resolve: (response) => {
|
||||
clearTimeout(timeout)
|
||||
resolve(response)
|
||||
},
|
||||
reject: (error) => {
|
||||
clearTimeout(timeout)
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function formatResponse(response: RpcResponse): string {
|
||||
return JSON.stringify(response.result ?? response.error ?? response).slice(0, 1000)
|
||||
}
|
||||
|
||||
async function chooseWorktree(ws: WebSocket): Promise<string> {
|
||||
if (worktreeSelector) return worktreeSelector
|
||||
|
||||
const response = await send(ws, 'worktree.ps')
|
||||
if (!response.ok) {
|
||||
throw new Error(`worktree.ps failed: ${formatResponse(response)}`)
|
||||
}
|
||||
|
||||
const worktrees = (response.result?.worktrees ?? []) as Array<{
|
||||
worktreeId: string
|
||||
liveTerminalCount: number
|
||||
branch: string
|
||||
path: string
|
||||
}>
|
||||
const selected = worktrees.find((w) => w.liveTerminalCount > 0) ?? worktrees[0]
|
||||
if (!selected) {
|
||||
throw new Error('No worktrees returned by worktree.ps')
|
||||
}
|
||||
|
||||
console.log(`worktree: ${selected.branch || '(no branch)'} ${selected.path}`)
|
||||
return `id:${selected.worktreeId}`
|
||||
}
|
||||
|
||||
async function chooseTerminal(ws: WebSocket, worktree: string): Promise<string> {
|
||||
const list = await send(ws, 'terminal.list', { worktree })
|
||||
if (!list.ok) {
|
||||
throw new Error(`terminal.list failed: ${formatResponse(list)}`)
|
||||
}
|
||||
|
||||
const terminals = (list.result?.terminals ?? []) as Array<{
|
||||
handle: string
|
||||
title: string
|
||||
preview: string
|
||||
lastOutputAt: number | null
|
||||
}>
|
||||
|
||||
if (terminals.length > 0) {
|
||||
const selected = terminals[0]!
|
||||
console.log(`terminal: ${selected.title || selected.handle} ${selected.handle}`)
|
||||
return selected.handle
|
||||
}
|
||||
|
||||
const created = await send(ws, 'terminal.create', {
|
||||
worktree,
|
||||
title: 'mobile-stream-repro'
|
||||
})
|
||||
if (!created.ok) {
|
||||
throw new Error(`terminal.create failed: ${formatResponse(created)}`)
|
||||
}
|
||||
|
||||
const terminal = created.result?.terminal as { handle?: string; title?: string } | undefined
|
||||
if (!terminal?.handle) {
|
||||
throw new Error(`terminal.create returned no handle: ${formatResponse(created)}`)
|
||||
}
|
||||
console.log(`terminal: ${terminal.title || terminal.handle} ${terminal.handle}`)
|
||||
return terminal.handle
|
||||
}
|
||||
|
||||
async function run(ws: WebSocket): Promise<void> {
|
||||
console.log(`connected: ${WS_URL}`)
|
||||
const worktree = await chooseWorktree(ws)
|
||||
const handle = await chooseTerminal(ws, worktree)
|
||||
activeHandle = handle
|
||||
|
||||
void send(ws, 'terminal.subscribe', { terminal: handle }).catch((error) => {
|
||||
console.error(`subscribe failed: ${error.message}`)
|
||||
})
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
|
||||
const sendResponse = await send(ws, 'terminal.send', {
|
||||
terminal: handle,
|
||||
text: `echo ${marker}`,
|
||||
enter: true
|
||||
})
|
||||
if (!sendResponse.ok) {
|
||||
throw new Error(`terminal.send failed: ${formatResponse(sendResponse)}`)
|
||||
}
|
||||
console.log(`sent marker: ${marker}`)
|
||||
|
||||
const deadline = Date.now() + 5_000
|
||||
while (Date.now() < deadline && !streamSawMarker && !readSawMarker) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
const read = await send(ws, 'terminal.read', { terminal: handle })
|
||||
if (!read.ok) {
|
||||
throw new Error(`terminal.read failed: ${formatResponse(read)}`)
|
||||
}
|
||||
const terminal = read.result?.terminal as { tail?: string[]; preview?: string } | undefined
|
||||
const text = [...(terminal?.tail ?? []), terminal?.preview ?? ''].join('\n')
|
||||
if (text.includes(marker)) {
|
||||
readSawMarker = true
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`runtime: ${runtimeId || '(unknown)'}`)
|
||||
console.log(`streamSawMarker: ${streamSawMarker}`)
|
||||
console.log(`readSawMarker: ${readSawMarker}`)
|
||||
|
||||
if (!streamSawMarker && !readSawMarker) {
|
||||
throw new Error('terminal output did not reach stream or terminal.read')
|
||||
}
|
||||
}
|
||||
|
||||
const ws = new WebSocket(WS_URL)
|
||||
|
||||
ws.on('open', () => {
|
||||
run(ws)
|
||||
.then(() => {
|
||||
ws.close()
|
||||
process.exit(0)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error.message)
|
||||
ws.close()
|
||||
process.exit(1)
|
||||
})
|
||||
})
|
||||
|
||||
ws.on('message', (data) => {
|
||||
const response = JSON.parse(data.toString()) as RpcResponse
|
||||
if (response._meta?.runtimeId) {
|
||||
runtimeId = response._meta.runtimeId
|
||||
}
|
||||
|
||||
const result = response.result
|
||||
if (response.streaming && result?.type === 'data') {
|
||||
const chunk = typeof result.chunk === 'string' ? result.chunk : ''
|
||||
if (chunk.includes(marker)) {
|
||||
streamSawMarker = true
|
||||
}
|
||||
}
|
||||
|
||||
if (response.streaming && result?.type === 'scrollback') {
|
||||
const lines = Array.isArray(result.lines) ? result.lines.join('\n') : String(result.lines ?? '')
|
||||
if (lines.includes(marker)) {
|
||||
streamSawMarker = true
|
||||
}
|
||||
}
|
||||
|
||||
const pendingRequest = pending.get(response.id)
|
||||
if (!pendingRequest) {
|
||||
return
|
||||
}
|
||||
|
||||
if (response.streaming && pendingRequest.method === 'terminal.subscribe') {
|
||||
return
|
||||
}
|
||||
|
||||
pending.delete(response.id)
|
||||
pendingRequest.resolve(response)
|
||||
})
|
||||
|
||||
ws.on('close', () => {
|
||||
for (const request of pending.values()) {
|
||||
request.reject(new Error('WebSocket closed'))
|
||||
}
|
||||
pending.clear()
|
||||
})
|
||||
|
||||
ws.on('error', (error) => {
|
||||
console.error(`WebSocket error: ${error.message}`)
|
||||
if (activeHandle) {
|
||||
console.error(`active terminal: ${activeHandle}`)
|
||||
}
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -0,0 +1,272 @@
|
||||
import { useRef, useCallback, forwardRef, useImperativeHandle } from 'react'
|
||||
import { StyleSheet } from 'react-native'
|
||||
import { WebView } from 'react-native-webview'
|
||||
import type { WebViewMessageEvent } from 'react-native-webview'
|
||||
|
||||
export type TerminalWebViewHandle = {
|
||||
write: (data: string) => void
|
||||
init: (cols: number, rows: number) => void
|
||||
clear: () => void
|
||||
}
|
||||
|
||||
type Props = object
|
||||
|
||||
type TerminalMessage =
|
||||
| { type: 'write'; data: string }
|
||||
| { type: 'init'; cols: number; rows: number }
|
||||
| { type: 'clear' }
|
||||
|
||||
// Why: TUI apps (Claude Code / Ink) emit escape codes with absolute cursor
|
||||
// positioning designed for the desktop's terminal dimensions (~150 cols).
|
||||
// Rendering at phone-native dimensions (55 cols) garbles TUI output because
|
||||
// row/col coordinates don't match. Instead, we initialize xterm at the
|
||||
// desktop's exact cols/rows and use CSS transform: scale() to shrink the
|
||||
// canvas to fit the phone viewport. This produces an accurate miniature of
|
||||
// the desktop screen. Pinch-to-zoom is enabled for readability.
|
||||
const XTERM_HTML = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=0.5, maximum-scale=5, user-scalable=yes">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.min.css">
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
html, body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
background: #1a1b26;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
#terminal-container {
|
||||
transform-origin: top left;
|
||||
overflow: visible;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="terminal-container"></div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min.js"></script>
|
||||
<script>
|
||||
(function() {
|
||||
var FONT_SIZE = 13;
|
||||
|
||||
var container = document.getElementById('terminal-container');
|
||||
var term = null;
|
||||
var writeQueue = [];
|
||||
var ready = false;
|
||||
var currentScale = 1;
|
||||
|
||||
function applyScale() {
|
||||
if (!term) return;
|
||||
var screen = container.querySelector('.xterm-screen');
|
||||
if (!screen) return;
|
||||
var termWidth = screen.offsetWidth;
|
||||
var viewWidth = window.innerWidth;
|
||||
if (termWidth > viewWidth) {
|
||||
currentScale = viewWidth / termWidth;
|
||||
container.style.transform = 'scale(' + currentScale + ')';
|
||||
container.style.width = (100 / currentScale) + '%';
|
||||
} else {
|
||||
currentScale = 1;
|
||||
container.style.transform = 'none';
|
||||
container.style.width = '100%';
|
||||
}
|
||||
}
|
||||
|
||||
function init(cols, rows) {
|
||||
ready = false;
|
||||
writeQueue = [];
|
||||
if (term) term.dispose();
|
||||
container.style.transform = 'none';
|
||||
container.style.width = '100%';
|
||||
term = new Terminal({
|
||||
cols: cols || 80,
|
||||
rows: rows || 24,
|
||||
theme: {
|
||||
background: '#1a1b26',
|
||||
foreground: '#c0caf5',
|
||||
cursor: '#c0caf5',
|
||||
cursorAccent: '#1a1b26',
|
||||
selectionBackground: '#33467c',
|
||||
black: '#15161e',
|
||||
red: '#f7768e',
|
||||
green: '#9ece6a',
|
||||
yellow: '#e0af68',
|
||||
blue: '#7aa2f7',
|
||||
magenta: '#bb9af7',
|
||||
cyan: '#7dcfff',
|
||||
white: '#a9b1d6',
|
||||
brightBlack: '#414868',
|
||||
brightRed: '#f7768e',
|
||||
brightGreen: '#9ece6a',
|
||||
brightYellow: '#e0af68',
|
||||
brightBlue: '#7aa2f7',
|
||||
brightMagenta: '#bb9af7',
|
||||
brightCyan: '#7dcfff',
|
||||
brightWhite: '#c0caf5'
|
||||
},
|
||||
fontFamily: '"Menlo", "Consolas", "DejaVu Sans Mono", monospace',
|
||||
fontSize: FONT_SIZE,
|
||||
scrollback: 5000,
|
||||
disableStdin: true,
|
||||
cursorBlink: false,
|
||||
cursorStyle: 'bar',
|
||||
cursorInactiveStyle: 'none',
|
||||
convertEol: false,
|
||||
allowProposedApi: true
|
||||
});
|
||||
term.open(container);
|
||||
requestAnimationFrame(function() {
|
||||
applyScale();
|
||||
ready = true;
|
||||
for (var i = 0; i < writeQueue.length; i++) {
|
||||
term.write(writeQueue[i]);
|
||||
}
|
||||
writeQueue = [];
|
||||
notify({ type: 'ready' });
|
||||
});
|
||||
}
|
||||
|
||||
function write(data) {
|
||||
if (ready && term) {
|
||||
term.write(data);
|
||||
} else {
|
||||
writeQueue.push(data);
|
||||
}
|
||||
}
|
||||
|
||||
function notify(msg) {
|
||||
if (window.ReactNativeWebView) {
|
||||
window.ReactNativeWebView.postMessage(JSON.stringify(msg));
|
||||
}
|
||||
}
|
||||
|
||||
function handleMsg(msg) {
|
||||
if (msg.type === 'init') {
|
||||
init(msg.cols, msg.rows);
|
||||
} else if (msg.type === 'write') {
|
||||
write(msg.data);
|
||||
} else if (msg.type === 'clear') {
|
||||
writeQueue = [];
|
||||
if (term) { term.clear(); term.reset(); }
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('message', function(e) {
|
||||
try {
|
||||
handleMsg(typeof e.data === 'string' ? JSON.parse(e.data) : e.data);
|
||||
} catch(ex) {}
|
||||
});
|
||||
|
||||
document.addEventListener('message', function(e) {
|
||||
try {
|
||||
handleMsg(typeof e.data === 'string' ? JSON.parse(e.data) : e.data);
|
||||
} catch(ex) {}
|
||||
});
|
||||
|
||||
if (window.Terminal) {
|
||||
notify({ type: 'web-ready' });
|
||||
} else {
|
||||
notify({ type: 'error', message: 'xterm failed to load' });
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
export const TerminalWebView = forwardRef<TerminalWebViewHandle, Props>(
|
||||
function TerminalWebView(_props, ref) {
|
||||
const webViewRef = useRef<WebView>(null)
|
||||
const isWebReadyRef = useRef(false)
|
||||
const pendingMessagesRef = useRef<TerminalMessage[]>([])
|
||||
|
||||
const sendToWebView = useCallback((msg: TerminalMessage) => {
|
||||
webViewRef.current?.postMessage(JSON.stringify(msg))
|
||||
}, [])
|
||||
|
||||
const flushPendingMessages = useCallback(() => {
|
||||
const pending = pendingMessagesRef.current
|
||||
pendingMessagesRef.current = []
|
||||
for (const msg of pending) {
|
||||
sendToWebView(msg)
|
||||
}
|
||||
}, [sendToWebView])
|
||||
|
||||
const postMessage = useCallback(
|
||||
(msg: TerminalMessage) => {
|
||||
// Why: React Native drops postMessage calls made before the WebView page
|
||||
// has installed its message handlers, so terminal output must wait here.
|
||||
if (!isWebReadyRef.current) {
|
||||
pendingMessagesRef.current.push(msg)
|
||||
return
|
||||
}
|
||||
|
||||
sendToWebView(msg)
|
||||
},
|
||||
[sendToWebView]
|
||||
)
|
||||
|
||||
const handleMessage = useCallback(
|
||||
(event: WebViewMessageEvent) => {
|
||||
let msg: Record<string, unknown>
|
||||
try {
|
||||
msg = JSON.parse(event.nativeEvent.data) as Record<string, unknown>
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
if (msg.type === 'web-ready') {
|
||||
isWebReadyRef.current = true
|
||||
flushPendingMessages()
|
||||
}
|
||||
},
|
||||
[flushPendingMessages]
|
||||
)
|
||||
|
||||
const handleLoadStart = useCallback(() => {
|
||||
isWebReadyRef.current = false
|
||||
}, [])
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
write(data: string) {
|
||||
postMessage({ type: 'write', data })
|
||||
},
|
||||
init(cols: number, rows: number) {
|
||||
postMessage({ type: 'init', cols, rows })
|
||||
},
|
||||
clear() {
|
||||
postMessage({ type: 'clear' })
|
||||
}
|
||||
}),
|
||||
[postMessage]
|
||||
)
|
||||
|
||||
return (
|
||||
<WebView
|
||||
ref={webViewRef}
|
||||
source={{ html: XTERM_HTML }}
|
||||
style={styles.webview}
|
||||
originWhitelist={['*']}
|
||||
javaScriptEnabled
|
||||
scrollEnabled
|
||||
nestedScrollEnabled
|
||||
showsVerticalScrollIndicator={false}
|
||||
setBuiltInZoomControls={false}
|
||||
scalesPageToFit={false}
|
||||
androidLayerType="none"
|
||||
onLoadStart={handleLoadStart}
|
||||
onMessage={handleMessage}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
webview: {
|
||||
flex: 1,
|
||||
backgroundColor: '#1a1b26'
|
||||
}
|
||||
})
|
||||
@@ -7,6 +7,12 @@ type PendingRequest = {
|
||||
|
||||
type StreamingListener = (result: unknown) => void
|
||||
|
||||
type StreamRequest = {
|
||||
method: string
|
||||
params: unknown
|
||||
listener: StreamingListener
|
||||
}
|
||||
|
||||
export type RpcClient = {
|
||||
sendRequest: (method: string, params?: unknown) => Promise<RpcResponse>
|
||||
subscribe: (method: string, params: unknown, onData: StreamingListener) => () => void
|
||||
@@ -31,7 +37,7 @@ export function connect(
|
||||
let intentionallyClosed = false
|
||||
|
||||
const pending = new Map<string, PendingRequest>()
|
||||
const streamListeners = new Map<string, StreamingListener>()
|
||||
const streamListeners = new Map<string, StreamRequest>()
|
||||
const stateListeners = new Set<(state: ConnectionState) => void>()
|
||||
// Callers that are waiting for the socket to reach 'connected' state
|
||||
const connectWaiters: Array<{ resolve: () => void; reject: (e: Error) => void }> = []
|
||||
@@ -75,6 +81,9 @@ export function connect(
|
||||
ws.onopen = () => {
|
||||
reconnectAttempt = 0
|
||||
setState('connected')
|
||||
for (const [id, stream] of streamListeners) {
|
||||
sendRaw({ id, deviceToken, method: stream.method, params: stream.params })
|
||||
}
|
||||
}
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
@@ -88,9 +97,9 @@ export function connect(
|
||||
const isStreaming = response.ok && (response as RpcSuccess).streaming === true
|
||||
|
||||
if (isStreaming) {
|
||||
const listener = streamListeners.get(response.id)
|
||||
if (listener && response.ok) {
|
||||
listener((response as RpcSuccess).result)
|
||||
const stream = streamListeners.get(response.id)
|
||||
if (stream && response.ok) {
|
||||
stream.listener((response as RpcSuccess).result)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -100,18 +109,18 @@ export function connect(
|
||||
if (response.ok) {
|
||||
const result = (response as RpcSuccess).result as Record<string, unknown> | null
|
||||
if (result && result.type === 'end') {
|
||||
const listener = streamListeners.get(response.id)
|
||||
if (listener) {
|
||||
listener(result)
|
||||
const stream = streamListeners.get(response.id)
|
||||
if (stream) {
|
||||
stream.listener(result)
|
||||
streamListeners.delete(response.id)
|
||||
return
|
||||
}
|
||||
}
|
||||
// Scrollback (first message from terminal.subscribe) also goes to stream listener
|
||||
if (result && result.type === 'scrollback') {
|
||||
const listener = streamListeners.get(response.id)
|
||||
if (listener) {
|
||||
listener(result)
|
||||
const stream = streamListeners.get(response.id)
|
||||
if (stream) {
|
||||
stream.listener(result)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -192,16 +201,28 @@ export function connect(
|
||||
|
||||
subscribe(method: string, params: unknown, onData: StreamingListener): () => void {
|
||||
const id = nextId()
|
||||
streamListeners.set(id, onData)
|
||||
sendRaw({ id, deviceToken, method, params })
|
||||
streamListeners.set(id, { method, params, listener: onData })
|
||||
|
||||
if (state === 'connected') {
|
||||
sendRaw({ id, deviceToken, method, params })
|
||||
}
|
||||
|
||||
return () => {
|
||||
const stream = streamListeners.get(id)
|
||||
streamListeners.delete(id)
|
||||
sendRaw({
|
||||
id: nextId(),
|
||||
deviceToken,
|
||||
method: 'terminal.unsubscribe',
|
||||
params: { subscriptionId: id }
|
||||
// Why: the runtime keys terminal subscription cleanup by terminal
|
||||
// handle, not by the RPC request id used to open the stream.
|
||||
params:
|
||||
stream?.method === 'terminal.subscribe' &&
|
||||
stream.params &&
|
||||
typeof stream.params === 'object' &&
|
||||
typeof (stream.params as { terminal?: unknown }).terminal === 'string'
|
||||
? { subscriptionId: (stream.params as { terminal: string }).terminal }
|
||||
: { subscriptionId: id }
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
# Mobile Terminal Output Streaming Findings
|
||||
|
||||
Date: 2026-04-28
|
||||
|
||||
## Scope
|
||||
|
||||
The investigation was limited to `mobile/`. Server-side files under `src/main/` were read for context only and were not modified.
|
||||
|
||||
## Verified Findings
|
||||
|
||||
- The phone can connect to `ws://192.168.0.179:6768`, list worktrees, list terminals, and call `terminal.send`.
|
||||
- The phone's saved host token is valid; a direct WebSocket probe from the desktop using the same token can call `worktree.ps`, `terminal.list`, `terminal.subscribe`, `terminal.send`, and `terminal.read`.
|
||||
- `TerminalWebView` can render text when the React Native side writes to it after xterm initializes. A temporary marker written after `init()` appeared visibly in the WebView.
|
||||
- Messages posted to the WebView before its page installs message handlers can be dropped. The mobile fix queues `init`, `write`, and `clear` until the WebView reports `web-ready`.
|
||||
- The WebView also has an internal queue for writes that arrive after `web-ready` but before xterm finishes `init()`.
|
||||
- The selected physical-phone test worktree was `refs/heads/tasks-improvements` at `/Users/jinwoohong/orca/workspaces/orca/pr-1172-review`.
|
||||
- For that test worktree, `terminal.subscribe` produced an initial `scrollback` event with an empty `lines` array and no serialized buffer.
|
||||
- Sending commands to that test terminal returned `ok:true`, but a direct WebSocket `terminal.read` for the same handle still returned an empty tail and no live `data` chunks were observed.
|
||||
- Creating a fresh terminal with `terminal.create` in that same worktree also returned a writable handle, but `terminal.send` followed by delayed `terminal.read` still returned an empty tail.
|
||||
- Adding a mobile-side `terminal.read` fallback did not make the current physical-phone test terminal display output, because the direct WebSocket `terminal.read` response for `mobile-output-test` remained `tail: []` and `lastOutputAt: null` after `echo hi`.
|
||||
- Calling `terminal.show` before `terminal.send` on `mobile-output-test` returned a connected/writable terminal with a `ptyId`, but a delayed `terminal.read` still returned an empty tail.
|
||||
- Root cause found after building a no-phone repro: daemon-backed PTYs were forwarding provider data to the desktop renderer, but not into `runtime.onPtyData`. The runtime tail buffer and `terminal.subscribe` listeners are fed by `runtime.onPtyData`, so the phone saw accepted sends with no output.
|
||||
- Fix: `src/main/ipc/pty.ts` now forwards `provider.onData` into `runtime.onPtyData` for non-`LocalPtyProvider` providers. Local PTYs already use the LocalPtyProvider configure hook, so the guard avoids duplicate local output.
|
||||
- After restarting Electron, `pnpm exec tsx mobile/scripts/test-subscribe.ts <deviceToken>` passed: both `streamSawMarker` and `readSawMarker` were `true`.
|
||||
- After the backend fix, the physical phone rendered the repro marker in `TerminalWebView`.
|
||||
- Because of the empty tail/no live stream on that specific desktop terminal, the physical `ls` test did not prove the WebView display path after real PTY output.
|
||||
|
||||
## Changes Made
|
||||
|
||||
- `mobile/src/terminal/TerminalWebView.tsx`
|
||||
- Added a native-side queue so WebView messages are not sent until the HTML reports `web-ready`.
|
||||
- Added a WebView-side queue so writes wait until xterm finishes `init()`.
|
||||
- `mobile/src/transport/rpc-client.ts`
|
||||
- Stored full stream request metadata so active streams can be replayed after reconnect.
|
||||
- Avoided sending a subscription before the socket reaches `connected`.
|
||||
- Fixed `terminal.unsubscribe` to use the terminal handle as `subscriptionId`, matching runtime cleanup behavior.
|
||||
- `mobile/app/h/[hostId]/session/[worktreeId].tsx`
|
||||
- Clears and resubscribes when the worktree changes.
|
||||
- Tracks the active terminal handle in a ref so stale handles are replaced when `terminal.list` changes.
|
||||
- Routes scrollback and live data into `TerminalWebView`.
|
||||
|
||||
## Useful Commands
|
||||
|
||||
```bash
|
||||
agent-device devices --json
|
||||
agent-device snapshot --platform android --serial R3CX105QXRH --json
|
||||
agent-device fill @e33 "ls" --platform android --serial R3CX105QXRH --json
|
||||
agent-device click @e34 --platform android --serial R3CX105QXRH --json
|
||||
agent-device screenshot /tmp/mobile-terminal.png --platform android --serial R3CX105QXRH --json
|
||||
orca terminal send --terminal term_926b8898-f843-461a-acd2-482f741327ad --text r --json
|
||||
orca terminal read --terminal term_926b8898-f843-461a-acd2-482f741327ad --json
|
||||
```
|
||||
|
||||
## Next Debug Boundary
|
||||
|
||||
Do not repeat WebView readiness tests unless the WebView changes again; the WebView can render post-init writes. For future regressions, run the no-phone repro first. If it fails, debug the WebSocket/runtime/PTY bridge before touching the mobile UI. If it passes but the phone is blank, debug `TerminalWebView` or session-screen routing.
|
||||
@@ -403,6 +403,11 @@ export function registerPtyHandlers(
|
||||
const isLocalProvider = localProvider instanceof LocalPtyProvider
|
||||
|
||||
localDataUnsub = localProvider.onData((payload) => {
|
||||
if (!(localProvider instanceof LocalPtyProvider)) {
|
||||
// Why: daemon-backed PTYs emit through provider.onData, not the
|
||||
// LocalPtyProvider configure hook. Runtime tails power mobile read/stream.
|
||||
runtime?.onPtyData(payload.id, payload.data, Date.now())
|
||||
}
|
||||
if (mainWindow.isDestroyed()) {
|
||||
// Why: clear the pending flush timer so it doesn't fire after the window
|
||||
// is gone. Without this, macOS app re-activation leaks orphaned timers
|
||||
|
||||
Reference in New Issue
Block a user