mirror of
https://github.com/stablyai/orca.git
synced 2026-11-13 09:27:29 +00:00
@@ -1,4 +1,4 @@
|
||||
import { TERMINAL_SHORTCUT_STORAGE_KEYS } from '../page-storage-keys'
|
||||
import { TERMINAL_SETTINGS_STORAGE_KEYS } from '../page-storage-keys'
|
||||
import { readShellSession, type BridgeShellSession } from './bridge-client-session'
|
||||
import { bridgeRouteMoved } from './bridge-route-update'
|
||||
import {
|
||||
@@ -110,20 +110,20 @@ export function createBridgeClientShellSession(args: {
|
||||
if (held !== null && update === null) {
|
||||
args.onReplaced()
|
||||
}
|
||||
const refreshShortcuts = update !== null && message.refreshTerminalShortcuts === true
|
||||
const refreshSettings = update !== null && message.refreshTerminalShortcuts === true
|
||||
const sentStorage = message.storage ?? {}
|
||||
const sentOversize = message.storageOversize ?? []
|
||||
const storage = refreshShortcuts ? { ...update.storage } : undefined
|
||||
const storageOversize = refreshShortcuts
|
||||
const storage = refreshSettings ? { ...update.storage } : undefined
|
||||
const storageOversize = refreshSettings
|
||||
? [
|
||||
...update.storageOversize.filter(
|
||||
(key) => !TERMINAL_SHORTCUT_STORAGE_KEYS.some((item) => item === key)
|
||||
(key) => !TERMINAL_SETTINGS_STORAGE_KEYS.some((item) => item === key)
|
||||
),
|
||||
...TERMINAL_SHORTCUT_STORAGE_KEYS.filter((key) => sentOversize.includes(key))
|
||||
...TERMINAL_SETTINGS_STORAGE_KEYS.filter((key) => sentOversize.includes(key))
|
||||
]
|
||||
: update?.storageOversize
|
||||
if (refreshShortcuts && storage) {
|
||||
for (const key of TERMINAL_SHORTCUT_STORAGE_KEYS) {
|
||||
if (refreshSettings && storage) {
|
||||
for (const key of TERMINAL_SETTINGS_STORAGE_KEYS) {
|
||||
if (sentStorage[key] === undefined) {
|
||||
delete storage[key]
|
||||
} else {
|
||||
@@ -182,7 +182,7 @@ export function createBridgeClientShellSession(args: {
|
||||
listener()
|
||||
}
|
||||
readyListeners.clear()
|
||||
if (refreshShortcuts) {
|
||||
if (refreshSettings) {
|
||||
for (const listener of storageListeners) {
|
||||
listener()
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {
|
||||
TERMINAL_SHORTCUT_STORAGE_KEYS,
|
||||
TERMINAL_SETTINGS_STORAGE_KEYS,
|
||||
isHostWorkspaceStorageKey,
|
||||
isPageStorageKeyForRoute,
|
||||
PAGE_STORAGE_MAX_VALUE_CHARS
|
||||
@@ -59,13 +59,13 @@ export function publishPageStorage(
|
||||
readWorkspaceKey = forWorkspaceKeyReader
|
||||
}
|
||||
|
||||
/** Apply native shortcut settings without replacing page-owned drafts or pending journals. */
|
||||
export function refreshPageTerminalShortcuts(
|
||||
/** Apply native terminal settings without replacing page-owned drafts or pending journals. */
|
||||
export function refreshPageTerminalSettings(
|
||||
entries: Readonly<Record<string, string>>,
|
||||
oversize: readonly string[]
|
||||
): void {
|
||||
const nextOversize = new Set(oversizeKeys)
|
||||
for (const key of TERMINAL_SHORTCUT_STORAGE_KEYS) {
|
||||
for (const key of TERMINAL_SETTINGS_STORAGE_KEYS) {
|
||||
const value = entries[key]
|
||||
if (value === undefined) {
|
||||
values.delete(key)
|
||||
|
||||
@@ -21,6 +21,13 @@ export const TERMINAL_SHORTCUT_STORAGE_KEYS = [
|
||||
'orca:custom-accessory-keys'
|
||||
] as const
|
||||
|
||||
export const TERMINAL_SETTINGS_STORAGE_KEYS = [
|
||||
...TERMINAL_SHORTCUT_STORAGE_KEYS,
|
||||
'orca:terminalTextScale',
|
||||
'orca:terminalAutocompleteEnabled',
|
||||
'orca:terminalLinkOpenMode'
|
||||
] as const
|
||||
|
||||
export const PAGE_STORAGE_EXACT_KEYS = [
|
||||
'orca:last-visited-worktree',
|
||||
'orca:hostSidebarWidth',
|
||||
|
||||
@@ -10,7 +10,9 @@ import {
|
||||
retainMirroredStorage,
|
||||
writeMirroredStorage
|
||||
} from '../storage/mirrored-storage-keys'
|
||||
import { saveTerminalLinkOpenMode } from '../storage/preferences'
|
||||
import { TERMINAL_SHORTCUT_STORAGE_KEYS } from './page-storage-keys'
|
||||
import { createFakeBridgePortPair } from './bridge/bridge-port-pair-test-harness'
|
||||
|
||||
const store = vi.hoisted(() => ({
|
||||
values: new Map<string, string>(),
|
||||
@@ -154,3 +156,97 @@ it.each(TERMINAL_SHORTCUT_STORAGE_KEYS)(
|
||||
releaseKeys()
|
||||
}
|
||||
)
|
||||
|
||||
it('publishes the last queued browser-link save after native return hydrated the earlier value', async () => {
|
||||
const key = 'orca:terminalLinkOpenMode'
|
||||
focus.active = false
|
||||
store.values.set(key, 'orca-browser')
|
||||
const releaseKeys = retainMirroredStorage([key])
|
||||
const refresh = () => hydrateMirroredStorage([key])
|
||||
let page = 'orca-browser'
|
||||
const publish = () => {
|
||||
page = readMirroredStorage([key])[key]!
|
||||
}
|
||||
function Harness() {
|
||||
useShellPageStorageRefresh(refresh, publish)
|
||||
return null
|
||||
}
|
||||
let renderer!: ReturnType<typeof create>
|
||||
await act(async () => {
|
||||
renderer = create(createElement(Harness))
|
||||
})
|
||||
const first = saveTerminalLinkOpenMode('phone-browser')
|
||||
const last = saveTerminalLinkOpenMode('orca-browser')
|
||||
await act(async () => {
|
||||
await Promise.resolve()
|
||||
store.pending.shift()!()
|
||||
await first
|
||||
focus.active = true
|
||||
renderer.update(createElement(Harness))
|
||||
})
|
||||
expect(page).toBe('phone-browser')
|
||||
await act(async () => {
|
||||
store.pending.shift()!()
|
||||
await last
|
||||
})
|
||||
expect(store.values.get(key)).toBe('orca-browser')
|
||||
expect(page).toBe('orca-browser')
|
||||
act(() => renderer.unmount())
|
||||
releaseKeys()
|
||||
})
|
||||
|
||||
it('republishes an accepted page zoom after an older native snapshot crosses the pinch', async () => {
|
||||
const key = 'orca:terminalTextScale'
|
||||
store.values.set(key, '1')
|
||||
const releaseKeys = retainMirroredStorage([key])
|
||||
const storage = { [key]: '1' }
|
||||
let hold = false
|
||||
const held: string[] = []
|
||||
const pair = createFakeBridgePortPair({
|
||||
route: { pathname: '/h/host-a/session/wt-1' },
|
||||
storage,
|
||||
holdToPage: (json) => {
|
||||
if (hold) {
|
||||
held.push(json)
|
||||
}
|
||||
return hold
|
||||
},
|
||||
onStorageWrite: writeMirroredStorage
|
||||
})
|
||||
await pair.flush()
|
||||
let displayedScale = 1
|
||||
pair.client.onStorageUpdate(() => {
|
||||
displayedScale = Number(pair.client.getShellSession()!.storage[key])
|
||||
})
|
||||
const refresh = () => hydrateMirroredStorage([key])
|
||||
const publish = () => {
|
||||
Object.assign(storage, readMirroredStorage([key]))
|
||||
pair.host.publishStorage()
|
||||
}
|
||||
function Harness() {
|
||||
useShellPageStorageRefresh(refresh, publish)
|
||||
return null
|
||||
}
|
||||
let renderer!: ReturnType<typeof create>
|
||||
await act(async () => {
|
||||
renderer = create(createElement(Harness))
|
||||
await pair.flush()
|
||||
})
|
||||
hold = true
|
||||
pair.host.publishStorage()
|
||||
displayedScale = 1.5
|
||||
expect(pair.client.notifyStorageWrite(key, '1.5')).toBe(true)
|
||||
await act(async () => pair.flush())
|
||||
pair.deliverToPage(held.shift()!)
|
||||
expect(displayedScale).toBe(1)
|
||||
for (const frame of held) {
|
||||
pair.deliverToPage(frame)
|
||||
}
|
||||
await act(async () => store.pending.shift()!())
|
||||
expect(store.values.get(key)).toBe('1.5')
|
||||
expect(displayedScale).toBe(1.5)
|
||||
act(() => renderer.unmount())
|
||||
pair.client.close()
|
||||
pair.host.dispose()
|
||||
releaseKeys()
|
||||
})
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useCallback } from 'react'
|
||||
import { AppState } from 'react-native'
|
||||
import { useFocusEffect } from 'expo-router'
|
||||
import { subscribeMirroredKey } from '../storage/mirrored-storage-keys'
|
||||
import { TERMINAL_SHORTCUT_STORAGE_KEYS } from './page-storage-keys'
|
||||
import { TERMINAL_SETTINGS_STORAGE_KEYS } from './page-storage-keys'
|
||||
|
||||
export function useShellPageStorageRefresh(refresh: () => Promise<unknown>, publish: () => void) {
|
||||
useFocusEffect(
|
||||
@@ -18,9 +18,9 @@ export function useShellPageStorageRefresh(refresh: () => Promise<unknown>, publ
|
||||
(error: unknown) => console.warn('[web-shell] could not refresh page settings', error)
|
||||
)
|
||||
}
|
||||
const stopWatching = TERMINAL_SHORTCUT_STORAGE_KEYS.map((key) =>
|
||||
const stopWatching = TERMINAL_SETTINGS_STORAGE_KEYS.map((key) =>
|
||||
subscribeMirroredKey(key, (_value, source) => {
|
||||
if (focused && source === 'persisted') {
|
||||
if (focused && (source === 'persisted' || key === 'orca:terminalTextScale')) {
|
||||
publish()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -3,7 +3,7 @@ import { createElement, useEffect } from 'react'
|
||||
import { createFakeBridgePortPair } from '../mobile-web-shell/bridge/bridge-port-pair-test-harness'
|
||||
import pageAsyncStorage, {
|
||||
publishPageStorage,
|
||||
refreshPageTerminalShortcuts
|
||||
refreshPageTerminalSettings
|
||||
} from '../mobile-web-shell/bridge/page-async-storage'
|
||||
import type { BridgeRpcClient } from '../mobile-web-shell/bridge/bridge-rpc-client'
|
||||
import { act, create, type ReactTestRenderer } from 'react-test-renderer'
|
||||
@@ -421,7 +421,7 @@ it('finishes custom-key hydration when side-rail geometry arrives during the rea
|
||||
expect(scope.setVisibleBuiltInIds.mock.calls.at(-1)![0]).not.toContain('arrowUp')
|
||||
})
|
||||
|
||||
it('refreshes a mounted terminal from native shortcut settings without router focus, remount or draft writes', async () => {
|
||||
it('refreshes mounted terminal settings without router focus, remount or draft writes', async () => {
|
||||
const layoutKey = 'orca:terminal-accessory-layout'
|
||||
const customKey = 'orca:custom-accessory-keys'
|
||||
const draftKey = 'orca:terminalDrafts:host-a:wt-1'
|
||||
@@ -436,6 +436,9 @@ it('refreshes a mounted terminal from native shortcut settings without router fo
|
||||
const storage: Record<string, string> = {
|
||||
[layoutKey]: initial,
|
||||
[customKey]: '[]',
|
||||
'orca:terminalTextScale': '1',
|
||||
'orca:terminalAutocompleteEnabled': 'false',
|
||||
'orca:terminalLinkOpenMode': 'orca-browser',
|
||||
[draftKey]: 'old draft'
|
||||
}
|
||||
const pair = createFakeBridgePortPair({ route: { pathname: '/h/host-a/session/wt-1' }, storage })
|
||||
@@ -449,15 +452,17 @@ it('refreshes a mounted terminal from native shortcut settings without router fo
|
||||
)
|
||||
pair.client.onStorageUpdate(() => {
|
||||
const session = pair.client.getShellSession()!
|
||||
refreshPageTerminalShortcuts(session.storage, session.storageOversize)
|
||||
refreshPageTerminalSettings(session.storage, session.storageOversize)
|
||||
})
|
||||
vi.mocked(AsyncStorage.getItem).mockImplementation(pageAsyncStorage.getItem)
|
||||
const scope = sessionScope()
|
||||
let ready = false
|
||||
let mounts = 0
|
||||
function Harness() {
|
||||
useEffect(() => {
|
||||
mounts += 1
|
||||
}, [])
|
||||
ready = useMobileSessionPreferenceFocus(scope).terminalPreferencesReady
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the lifecycle reads only the supplied session members.
|
||||
useMobileSessionLifecycle(scope as never)
|
||||
return null
|
||||
@@ -469,12 +474,19 @@ it('refreshes a mounted terminal from native shortcut settings without router fo
|
||||
await pageAsyncStorage.setItem(draftKey, 'unsent page draft')
|
||||
await pair.flush()
|
||||
const writes = pair.storageWrites.length
|
||||
storage['orca:terminalTextScale'] = '1.5'
|
||||
storage['orca:terminalAutocompleteEnabled'] = 'true'
|
||||
storage['orca:terminalLinkOpenMode'] = 'phone-browser'
|
||||
storage[layoutKey] = layout(['enter', 'arrowUp'], false)
|
||||
storage[customKey] = JSON.stringify([{ id: 'two', label: 'Two', bytes: '2', enter: false }])
|
||||
await act(async () => {
|
||||
pair.host.publishStorage()
|
||||
await pair.flush()
|
||||
})
|
||||
expect(scope.setTerminalTextScale).toHaveBeenLastCalledWith(1.5)
|
||||
expect(scope.setAutocompleteEnabled).toHaveBeenLastCalledWith(true)
|
||||
expect(scope.setTerminalLinkOpenMode).toHaveBeenLastCalledWith('phone-browser')
|
||||
expect(ready).toBe(true)
|
||||
expect(scope.setDirectionPadEnabled).toHaveBeenLastCalledWith(false)
|
||||
expect(scope.setVisibleBuiltInIds).toHaveBeenLastCalledWith(['enter', 'arrowUp'])
|
||||
expect(scope.setCustomKeys).toHaveBeenLastCalledWith([
|
||||
@@ -502,11 +514,17 @@ it('refreshes a mounted terminal from native shortcut settings without router fo
|
||||
|
||||
delete storage[layoutKey]
|
||||
delete storage[customKey]
|
||||
delete storage['orca:terminalTextScale']
|
||||
delete storage['orca:terminalAutocompleteEnabled']
|
||||
delete storage['orca:terminalLinkOpenMode']
|
||||
const afterLocalWrite = pair.storageWrites.length
|
||||
await act(async () => {
|
||||
pair.host.publishStorage()
|
||||
await pair.flush()
|
||||
})
|
||||
expect(scope.setTerminalTextScale).toHaveBeenLastCalledWith(1)
|
||||
expect(scope.setAutocompleteEnabled).toHaveBeenLastCalledWith(false)
|
||||
expect(scope.setTerminalLinkOpenMode).toHaveBeenLastCalledWith('orca-browser')
|
||||
expect(await pageAsyncStorage.getItem(layoutKey)).toBeNull()
|
||||
expect(scope.setDirectionPadEnabled).toHaveBeenLastCalledWith(true)
|
||||
expect(scope.setCustomKeys).toHaveBeenLastCalledWith([])
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useShellStorage } from '../platform/shell-storage'
|
||||
import { settleSessionPreferenceRead } from './session-preference-read'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useFocusEffect } from 'expo-router'
|
||||
@@ -23,6 +24,7 @@ export function useMobileSessionPreferenceFocus(
|
||||
setAutocompleteEnabled,
|
||||
setTerminalLinkOpenMode
|
||||
} = scope
|
||||
const shellStorage = useShellStorage()
|
||||
const [terminalPreferencesReady, setTerminalPreferencesReady] = useState(false)
|
||||
// Reload on Settings return without dropping readiness or remounting terminal panes.
|
||||
useFocusEffect(
|
||||
@@ -51,7 +53,7 @@ export function useMobileSessionPreferenceFocus(
|
||||
return () => {
|
||||
active = false
|
||||
}
|
||||
}, [])
|
||||
}, [shellStorage])
|
||||
)
|
||||
return { terminalPreferencesReady }
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
import { storageReadResultSchema } from '../src/mobile-web-shell/bridge/bridge-native-verbs'
|
||||
import {
|
||||
publishPageStorage,
|
||||
refreshPageTerminalShortcuts
|
||||
refreshPageTerminalSettings
|
||||
} from '../src/mobile-web-shell/bridge/page-async-storage'
|
||||
import {
|
||||
RouteScreenPaintProvider,
|
||||
@@ -162,7 +162,7 @@ bootstrapShellPage({
|
||||
client.onStorageUpdate(() => {
|
||||
const current = client.getShellSession()
|
||||
if (current !== null) {
|
||||
refreshPageTerminalShortcuts(current.storage, current.storageOversize)
|
||||
refreshPageTerminalSettings(current.storage, current.storageOversize)
|
||||
}
|
||||
})
|
||||
// Activation settles to English on failure, so the page always mounts.
|
||||
|
||||
Reference in New Issue
Block a user