mirror of
https://github.com/stablyai/orca.git
synced 2026-09-28 08:02:43 +00:00
refactor(mobile): delete the seam surface with no production reader
Each was confirmed to have zero non-test readers before and after:
- HostSessionTerminalOperations.subscribe, its adapter body and its test. The
live stream still calls subscribeMobileTerminalSafely directly, as on main,
so the method existed only to be tested.
- pasteImages and releaseImages, declared on the native-chat interface and
never implemented or called.
- explicitQuickCommandScope, exported with no callers.
- The quick-command snapshot's totalCount and repoId, which nothing read, and
with them the workspaceId argument that existed only to compute repoId. The
hook takes { operations, enabled } again, as main took { client, enabled }.
- The web-artifact result variant, line and column on the resolve request,
rename's third parameter and the directory variant's truncated. The
legacy-list variant's truncated stays: the panel reads it.
Deleting the terminal subscribe also brings the parity oracle's identity
fields to 14, matching main exactly.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
This commit is contained in:
@@ -5,7 +5,6 @@ export type HostFileExplorerDirectoryResult =
|
||||
| {
|
||||
kind: 'directory'
|
||||
entries: MobileDirEntry[]
|
||||
truncated: boolean
|
||||
}
|
||||
| {
|
||||
kind: 'legacy-list'
|
||||
|
||||
@@ -105,7 +105,6 @@ export function MobileSessionSheets({ controller }: { controller: MobileSessionC
|
||||
visible={showQuickCommands && quickCommandsSupported === true}
|
||||
onClose={() => setShowQuickCommands(false)}
|
||||
operations={sessionOperations?.quickCommand ?? null}
|
||||
workspaceId={worktreeId}
|
||||
repoId={
|
||||
isFolderWorkspaceRoute || isFloatingWorkspaceRoute
|
||||
? null
|
||||
|
||||
@@ -24,7 +24,6 @@ type Props = {
|
||||
visible: boolean
|
||||
onClose: () => void
|
||||
operations: HostSessionQuickCommandOperations | null
|
||||
workspaceId: string
|
||||
repoId: string | null
|
||||
repoName: string | null
|
||||
onLaunch: (command: TerminalQuickCommand) => boolean
|
||||
@@ -36,14 +35,12 @@ export function QuickCommandsSheet({
|
||||
visible,
|
||||
onClose,
|
||||
operations,
|
||||
workspaceId,
|
||||
repoId,
|
||||
repoName,
|
||||
onLaunch
|
||||
}: Props) {
|
||||
const { commands, loading, ready, error, persist } = useQuickCommands({
|
||||
operations,
|
||||
workspaceId,
|
||||
enabled: visible
|
||||
})
|
||||
const [view, setView] = useState<SheetView>('list')
|
||||
|
||||
@@ -44,13 +44,6 @@ export type HostSessionNativeChatOperations = {
|
||||
beforeOffset?: number
|
||||
): Promise<HostSessionNativeChatReadResult>
|
||||
stop(target: HostSessionNativeChatTarget, deadline?: number): Promise<MobileNativeChatSendOutcome>
|
||||
pasteImages?(
|
||||
target: HostSessionNativeChatTarget,
|
||||
references: readonly string[],
|
||||
deadline?: number,
|
||||
followedByText?: boolean
|
||||
): Promise<boolean>
|
||||
releaseImages?(target: HostSessionNativeChatTarget, references: readonly string[]): Promise<void>
|
||||
/** Drops the legacy full-inventory fallback's cached listing for a workspace. Without it a
|
||||
* second visit inside one connection serves the first read's inventory, so a file created in
|
||||
* between is missing from `@` autocomplete. */
|
||||
|
||||
@@ -1,25 +1,11 @@
|
||||
import type {
|
||||
TerminalQuickCommand,
|
||||
TerminalQuickCommandScope
|
||||
} from '../../../src/shared/terminal-quick-command-types'
|
||||
import type { TerminalQuickCommand } from '../../../src/shared/terminal-quick-command-types'
|
||||
import type { TerminalQuickCommandMutation } from '../terminal/quick-commands'
|
||||
|
||||
export type HostSessionQuickCommandSnapshot = {
|
||||
commands: TerminalQuickCommand[]
|
||||
totalCount: number
|
||||
repoId: string | null
|
||||
}
|
||||
|
||||
export type HostSessionQuickCommandOperations = {
|
||||
snapshot(workspaceId: string, signal?: AbortSignal): Promise<HostSessionQuickCommandSnapshot>
|
||||
mutate(
|
||||
workspaceId: string,
|
||||
mutation: TerminalQuickCommandMutation
|
||||
): Promise<HostSessionQuickCommandSnapshot>
|
||||
}
|
||||
|
||||
export function explicitQuickCommandScope(
|
||||
scope: TerminalQuickCommandScope | undefined
|
||||
): TerminalQuickCommandScope {
|
||||
return scope?.type === 'repo' ? { type: 'repo', repoId: scope.repoId } : { type: 'global' }
|
||||
snapshot(signal?: AbortSignal): Promise<HostSessionQuickCommandSnapshot>
|
||||
mutate(mutation: TerminalQuickCommandMutation): Promise<HostSessionQuickCommandSnapshot>
|
||||
}
|
||||
|
||||
@@ -13,14 +13,6 @@ export type HostSessionTerminalFileTarget =
|
||||
grantId: string
|
||||
workspaceId?: string
|
||||
}
|
||||
| {
|
||||
kind: 'web-artifact'
|
||||
/** The terminal text the desktop re-resolves for every chunk read. */
|
||||
pathText: string
|
||||
displayName: string
|
||||
previewKind: 'text' | 'raster'
|
||||
workspaceId?: string
|
||||
}
|
||||
|
||||
export type HostSessionTerminalFileResolveRequest = {
|
||||
workspaceId: string
|
||||
@@ -28,8 +20,6 @@ export type HostSessionTerminalFileResolveRequest = {
|
||||
pathText: string
|
||||
cwd: string | null
|
||||
nativeChatContext: RuntimeNativeChatFileContext | null
|
||||
line: number | null
|
||||
column: number | null
|
||||
}
|
||||
|
||||
export type HostSessionTerminalFileOperations = {
|
||||
|
||||
@@ -52,11 +52,6 @@ export type HostSessionTerminalSubscribeArgs = {
|
||||
}
|
||||
|
||||
export type HostSessionTerminalOperations = {
|
||||
subscribe(
|
||||
args: HostSessionTerminalSubscribeArgs,
|
||||
onEvent: (event: HostSessionTerminalStreamEvent) => void,
|
||||
onError: () => void
|
||||
): () => void
|
||||
sendInput(
|
||||
terminalId: string,
|
||||
text: string,
|
||||
@@ -70,5 +65,5 @@ export type HostSessionTerminalOperations = {
|
||||
clientId: string | null
|
||||
): Promise<boolean>
|
||||
clear(terminalId: string): Promise<boolean>
|
||||
rename(terminalId: string, title: string, workspaceId: string): Promise<boolean>
|
||||
rename(terminalId: string, title: string): Promise<boolean>
|
||||
}
|
||||
|
||||
@@ -77,9 +77,7 @@ async function openMobileFileTapAsync<T extends FileTapSessionTab>(
|
||||
terminalHandle,
|
||||
pathText: options.pathText,
|
||||
cwd,
|
||||
nativeChatContext: options.nativeChatContext ?? null,
|
||||
line: options.line,
|
||||
column: options.column
|
||||
nativeChatContext: options.nativeChatContext ?? null
|
||||
})
|
||||
if (!resolved) {
|
||||
reportOpenFailure(options)
|
||||
|
||||
@@ -70,7 +70,7 @@ const HEAD_CALLBACK_BODY_SHA256 = 'e66e6436cdb9a66e870c06fdfc140106502fbeddd8db4
|
||||
const HEAD_EFFECT_SHA256 = 'd9ebfaabc1e79773cdada7ab370b20459ed972f1f8edce1652199f4d0391cd13'
|
||||
const HEAD_CONTENT_HOOK_SHA256 = '9c3b612fef3f370d66873aefdbe1d701f20cb64ded31fef5cc45fde6f8189581'
|
||||
const HEAD_NESTED_FUNCTION_SHA256 =
|
||||
'3b75d854a92b086907571f4c877961bd429f76e3826979cee5b0e94ed2007c8e'
|
||||
'ad78ecb8bd61cd0cc3a7037bbd7dcc06b0fbe0de62b23090b8350c2947d65c3d'
|
||||
const HEAD_NATIVE_REGISTRATION_SHA256 =
|
||||
'cab85e4e4a3f43289ba93ddea9ccce57aea83e0bf14fd1620a965aad0c1cb49e'
|
||||
const HEAD_NATIVE_REMOVAL_SHA256 =
|
||||
@@ -79,13 +79,13 @@ const HEAD_TIMER_CREATION_SHA256 =
|
||||
'1a31b625e2174c3db77272249843196d2b6b06ab1e654a96d8f7858e3082e66b'
|
||||
const HEAD_TIMER_CLEANUP_SHA256 = 'c73f1d1c2cc89642f3d727d6f3b6b81860a9d6f34234541a2065ec3d1a8cd116'
|
||||
const HEAD_RUNTIME_STRING_SHA256 =
|
||||
'2f78f3e8f835b3a786c8940dabc127d0d131e909c0e736a4a341c5d50ce35059'
|
||||
'58ee753e4fc849c693aa6ebb71b2ff7c38dbbba50977cacc737b03c4bb2a7224'
|
||||
const HEAD_HOST_JSX_SHA256 = '390405926b1695fa3a33686f0bc192b432f5468d8576499d7cafbb4922defbb5'
|
||||
const HEAD_LEAF_JSX_SHA256 = 'd5f1ef0db57c63eb3e4ee7c98e8483bc21882a151ce0ca24e42c7d1234e1dace'
|
||||
const HEAD_LEAF_JSX_SHA256 = '15ea1af8ae1a28a4a5f2ea31c2aac908fc24a0fe68923733889f1d40d0c8e3a3'
|
||||
const HEAD_STYLE_REFERENCE_SHA256 =
|
||||
'295a3501c2c6d7bea7c8bbf38b3f3534f01344cd7e1b91bb8e07c040821d596a'
|
||||
const HEAD_IDENTITY_FIELD_SHA256 =
|
||||
'2084e23285fc128e02ffbf751092ffc834b2d257c5c4bd782769f0bf97d2f161'
|
||||
'89bdd544369e41829581e80d320112019f3127fd3529c810d9651e5ece37338b'
|
||||
const HEAD_NAVIGATION_SHA256 = '9d96f5dad7de555d6553eac39c0fab00efad507470fd562cb9beaa32db16f512'
|
||||
const HEAD_CAPABILITY_SHA256 = 'ca219f7909a091717110b823d5b94a20770ad3ae51894e0fa765e8628309392d'
|
||||
|
||||
@@ -529,9 +529,7 @@ describe('mobile session route extraction parity', () => {
|
||||
)
|
||||
expect(hash(native.cleanups)).toBe(HEAD_TIMER_CLEANUP_SHA256)
|
||||
const compatibility = readCompatibilityFacts(definitions)
|
||||
// 15, not main's 14: the seam moved two hook-side `deviceToken` fields into three
|
||||
// adapter-side `client: { id }` builders. Every binding main pinned is still pinned.
|
||||
expect(compatibility.identityFields).toHaveLength(15)
|
||||
expect(compatibility.identityFields).toHaveLength(14)
|
||||
expect(hash(compatibility.identityFields)).toBe(HEAD_IDENTITY_FIELD_SHA256)
|
||||
expect(compatibility.navigation).toHaveLength(6)
|
||||
expect(hash(compatibility.navigation)).toBe(HEAD_NAVIGATION_SHA256)
|
||||
@@ -541,7 +539,7 @@ describe('mobile session route extraction parity', () => {
|
||||
|
||||
it('preserves runtime strings, styles, and the expanded JSX tree', () => {
|
||||
const strings = readRuntimeStrings()
|
||||
expect(strings).toHaveLength(593)
|
||||
expect(strings).toHaveLength(591)
|
||||
expect(hash(strings)).toBe(HEAD_RUNTIME_STRING_SHA256)
|
||||
const jsx = readJsxFacts(readDefinitions())
|
||||
expect(jsx.host).toHaveLength(124)
|
||||
|
||||
@@ -5,8 +5,6 @@ import type {
|
||||
HostSessionQuickCommandOperations,
|
||||
HostSessionQuickCommandSnapshot
|
||||
} from './host-session-quick-command-operations'
|
||||
import { isFloatingWorkspaceWorktreeId } from './floating-workspace'
|
||||
import { getRepoIdFromMobileWorktreeId } from './mobile-session-route-helpers'
|
||||
|
||||
const LOAD_CUTOVER_MAX_RETRIES = 5
|
||||
|
||||
@@ -14,16 +12,16 @@ export function nativeHostSessionQuickCommandOperations(
|
||||
client: RpcClient
|
||||
): HostSessionQuickCommandOperations {
|
||||
return {
|
||||
async snapshot(workspaceId, signal) {
|
||||
async snapshot(signal) {
|
||||
// The ok check sits outside the retry, as it did before this seam existed: a refusal
|
||||
// envelope is an answer, and replaying it would depend on the host's error text.
|
||||
const response = await loadWithCutoverRetry(client, signal)
|
||||
if (!response.ok) {
|
||||
throw new Error(response.error.message || 'Failed to load quick commands')
|
||||
}
|
||||
return quickCommandSnapshot(response.result, workspaceId, 'Failed to load quick commands')
|
||||
return quickCommandSnapshot(response.result, 'Failed to load quick commands')
|
||||
},
|
||||
async mutate(workspaceId, mutation) {
|
||||
async mutate(mutation) {
|
||||
// Why no cutover retry here: a quick-command mutation is not idempotent, so a replay
|
||||
// after a logical cutover could apply the same edit twice.
|
||||
const response = await client.sendRequest('settings.updateTerminalQuickCommands', {
|
||||
@@ -32,7 +30,7 @@ export function nativeHostSessionQuickCommandOperations(
|
||||
if (!response.ok) {
|
||||
throw new Error(response.error.message || 'Failed to save quick command')
|
||||
}
|
||||
return quickCommandSnapshot(response.result, workspaceId, 'Failed to save quick command')
|
||||
return quickCommandSnapshot(response.result, 'Failed to save quick command')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -55,7 +53,6 @@ async function loadWithCutoverRetry(client: RpcClient, signal?: AbortSignal) {
|
||||
|
||||
function quickCommandSnapshot(
|
||||
result: unknown,
|
||||
workspaceId: string,
|
||||
invalidResultMessage: string
|
||||
): HostSessionQuickCommandSnapshot {
|
||||
const commands = parseNormalizedTerminalQuickCommands(
|
||||
@@ -64,12 +61,5 @@ function quickCommandSnapshot(
|
||||
if (!commands) {
|
||||
throw new Error(invalidResultMessage)
|
||||
}
|
||||
return {
|
||||
commands,
|
||||
totalCount: commands.length,
|
||||
repoId:
|
||||
workspaceId.startsWith('folder:') || isFloatingWorkspaceWorktreeId(workspaceId)
|
||||
? null
|
||||
: getRepoIdFromMobileWorktreeId(workspaceId)
|
||||
}
|
||||
return { commands }
|
||||
}
|
||||
|
||||
@@ -3,47 +3,14 @@ import type { RpcClient } from '../transport/rpc-client'
|
||||
import { nativeHostSessionTerminalOperations } from './native-host-session-terminal-operations'
|
||||
|
||||
describe('native host session terminal operations', () => {
|
||||
it('preserves the existing mobile subscription and input RPC semantics', async () => {
|
||||
const unsubscribe = vi.fn()
|
||||
let onData: ((event: unknown) => void) | null = null
|
||||
const subscribe = vi.fn((_method, _params, listener) => {
|
||||
onData = listener
|
||||
return unsubscribe
|
||||
})
|
||||
it('preserves the existing mobile terminal input RPC semantics', async () => {
|
||||
const sendRequest = vi
|
||||
.fn()
|
||||
.mockResolvedValue({ ok: true, result: { send: { accepted: true } } })
|
||||
const operations = nativeHostSessionTerminalOperations({
|
||||
subscribe,
|
||||
sendRequest
|
||||
} as unknown as RpcClient)
|
||||
const onEvent = vi.fn()
|
||||
|
||||
const cleanup = operations.subscribe(
|
||||
{
|
||||
workspaceId: 'workspace-1',
|
||||
terminalId: 'terminal-native-1',
|
||||
clientId: 'device-1',
|
||||
viewport: { cols: 90, rows: 30 },
|
||||
visible: true,
|
||||
capabilities: { terminalBinaryStream: 1 }
|
||||
},
|
||||
onEvent,
|
||||
vi.fn()
|
||||
)
|
||||
onData?.({ type: 'data', chunk: 'hello' })
|
||||
|
||||
expect(subscribe).toHaveBeenCalledWith(
|
||||
'terminal.subscribe',
|
||||
{
|
||||
terminal: 'terminal-native-1',
|
||||
client: { id: 'device-1', type: 'mobile' },
|
||||
viewport: { cols: 90, rows: 30 },
|
||||
capabilities: { terminalBinaryStream: 1 }
|
||||
},
|
||||
expect.any(Function)
|
||||
)
|
||||
expect(onEvent).toHaveBeenCalledWith({ type: 'data', chunk: 'hello' })
|
||||
await expect(operations.sendInput('terminal-native-1', 'ls', true, 'device-1')).resolves.toBe(
|
||||
true
|
||||
)
|
||||
@@ -60,9 +27,7 @@ describe('native host session terminal operations', () => {
|
||||
await expect(
|
||||
operations.setDisplayMode('terminal-native-1', 'auto', { cols: 90, rows: 30 }, 'device-1')
|
||||
).resolves.toBe(true)
|
||||
await expect(
|
||||
operations.rename('terminal-native-1', 'Build', 'workspace-native-1')
|
||||
).resolves.toBe(true)
|
||||
await expect(operations.rename('terminal-native-1', 'Build')).resolves.toBe(true)
|
||||
await expect(operations.clear('terminal-native-1')).resolves.toBe(true)
|
||||
expect(sendRequest).toHaveBeenCalledWith('terminal.setDisplayMode', {
|
||||
terminal: 'terminal-native-1',
|
||||
@@ -77,8 +42,5 @@ describe('native host session terminal operations', () => {
|
||||
expect(sendRequest).toHaveBeenCalledWith('terminal.clearBuffer', {
|
||||
terminal: 'terminal-native-1'
|
||||
})
|
||||
|
||||
cleanup()
|
||||
expect(unsubscribe).toHaveBeenCalledOnce()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,26 +2,11 @@ import { isTerminalSendRpcAccepted } from '../terminal/terminal-send-rpc-respons
|
||||
import { TERMINAL_INPUT_SEND_OPTIONS } from '../terminal/terminal-send-request'
|
||||
import type { RpcClient } from '../transport/rpc-client'
|
||||
import type { HostSessionTerminalOperations } from './host-session-terminal-operations'
|
||||
import { subscribeMobileTerminalSafely } from './mobile-terminal-stream-subscribe'
|
||||
|
||||
export function nativeHostSessionTerminalOperations(
|
||||
client: RpcClient
|
||||
): HostSessionTerminalOperations {
|
||||
return {
|
||||
subscribe(args, onEvent, onError) {
|
||||
const unsubscribe = subscribeMobileTerminalSafely(
|
||||
client,
|
||||
{
|
||||
terminal: args.terminalId,
|
||||
...(args.clientId ? { client: { id: args.clientId, type: 'mobile' as const } } : {}),
|
||||
viewport: args.viewport,
|
||||
capabilities: args.capabilities
|
||||
},
|
||||
(event) => onEvent(event as Parameters<typeof onEvent>[0]),
|
||||
onError
|
||||
)
|
||||
return unsubscribe
|
||||
},
|
||||
async sendInput(terminalId, text, enter, clientId) {
|
||||
return client
|
||||
.sendRequest(
|
||||
|
||||
@@ -40,7 +40,7 @@ export function useMobileSessionCloseActions(scope: MobileSessionContentCreateAc
|
||||
|
||||
try {
|
||||
const title = value.trim()
|
||||
if (await sessionOperations.terminal.rename(target.handle, title, worktreeId)) {
|
||||
if (await sessionOperations.terminal.rename(target.handle, title)) {
|
||||
setTerminals((prev) => {
|
||||
const next = prev.map((terminal) =>
|
||||
terminal.handle === target.handle
|
||||
|
||||
@@ -8,7 +8,6 @@ import type { HostSessionQuickCommandOperations } from './host-session-quick-com
|
||||
|
||||
type Args = {
|
||||
operations: HostSessionQuickCommandOperations | null
|
||||
workspaceId: string
|
||||
// Fetch only while the sheet is open — quick commands are settings data we
|
||||
// don't need to keep hydrated for every session screen.
|
||||
enabled: boolean
|
||||
@@ -31,14 +30,13 @@ type PendingMutation = {
|
||||
|
||||
type MutationContext = {
|
||||
operations: HostSessionQuickCommandOperations
|
||||
workspaceId: string
|
||||
confirmed: TerminalQuickCommand[]
|
||||
pending: PendingMutation[]
|
||||
queue: Promise<void>
|
||||
nextMutationId: number
|
||||
}
|
||||
|
||||
export function useQuickCommands({ operations, workspaceId, enabled }: Args): QuickCommandsState {
|
||||
export function useQuickCommands({ operations, enabled }: Args): QuickCommandsState {
|
||||
const [commands, setCommands] = useState<TerminalQuickCommand[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [ready, setReady] = useState(false)
|
||||
@@ -53,11 +51,10 @@ export function useQuickCommands({ operations, workspaceId, enabled }: Args): Qu
|
||||
return
|
||||
}
|
||||
let mutationContext = mutationContextRef.current
|
||||
if (mutationContext?.operations !== operations || mutationContext.workspaceId !== workspaceId) {
|
||||
if (mutationContext?.operations !== operations) {
|
||||
// A request for an old host must not delay or update mutations on a new one.
|
||||
mutationContext = {
|
||||
operations,
|
||||
workspaceId,
|
||||
confirmed: [],
|
||||
pending: [],
|
||||
queue: Promise.resolve(),
|
||||
@@ -89,7 +86,7 @@ export function useQuickCommands({ operations, workspaceId, enabled }: Args): Qu
|
||||
) {
|
||||
return
|
||||
}
|
||||
const snapshot = await operations.snapshot(workspaceId, abortController.signal)
|
||||
const snapshot = await operations.snapshot(abortController.signal)
|
||||
if (
|
||||
stale ||
|
||||
operationId !== operationIdRef.current ||
|
||||
@@ -124,20 +121,14 @@ export function useQuickCommands({ operations, workspaceId, enabled }: Args): Qu
|
||||
stale = true
|
||||
abortController.abort()
|
||||
}
|
||||
}, [enabled, operations, workspaceId])
|
||||
}, [enabled, operations])
|
||||
|
||||
const persist = useCallback(
|
||||
async (commandMutation: TerminalQuickCommandMutation) => {
|
||||
// Why: the loaded list is the optimistic/rollback baseline; mutating
|
||||
// before it arrives would make failure recovery show invented state.
|
||||
const mutationContext = mutationContextRef.current
|
||||
if (
|
||||
!operations ||
|
||||
loading ||
|
||||
!ready ||
|
||||
mutationContext?.operations !== operations ||
|
||||
mutationContext.workspaceId !== workspaceId
|
||||
) {
|
||||
if (!operations || loading || !ready || mutationContext?.operations !== operations) {
|
||||
return false
|
||||
}
|
||||
const mutation: PendingMutation = {
|
||||
@@ -157,7 +148,7 @@ export function useQuickCommands({ operations, workspaceId, enabled }: Args): Qu
|
||||
try {
|
||||
// Why: an invalid success payload throws inside the adapter rather than
|
||||
// confirming [] — a later full-list mutation would erase live commands.
|
||||
const snapshot = await operations.mutate(workspaceId, commandMutation)
|
||||
const snapshot = await operations.mutate(commandMutation)
|
||||
mutationContext.confirmed = snapshot.commands
|
||||
succeeded = true
|
||||
return true
|
||||
@@ -191,7 +182,7 @@ export function useQuickCommands({ operations, workspaceId, enabled }: Args): Qu
|
||||
)
|
||||
return await request
|
||||
},
|
||||
[loading, operations, ready, workspaceId]
|
||||
[loading, operations, ready]
|
||||
)
|
||||
|
||||
return { commands, loading, ready, error, persist }
|
||||
|
||||
Reference in New Issue
Block a user