mirror of
https://github.com/stablyai/orca.git
synced 2026-10-22 08:03:37 +00:00
* Access the Floating Workspace from mobile (#8405) Surface the desktop Floating Workspace (the global, repo-less scratchpad of terminal tabs under the synthetic `global-floating-terminal` id) on the mobile app so a Claude session left running there is reachable from a phone. Adds a terminal-icon button to the mobile host header (phone + tablet sidebar) that opens the existing Session screen for the floating id. The sentinel already had host-side RPC support (#5946: local runtime, homedir cwd, explicit-id fast paths in session.tabs.*); this wires up the mobile surface and gates it on a new `floatingWorkspaceEnabled` status flag so the entry hides on hosts that predate it or where the feature is disabled. The Session screen learns an `isFloatingWorkspaceRoute` flag (mirroring the existing `folder:` route pattern) that hides repo-backed surfaces — Files, Source Control, PR/checks, agent history — skips the diff-comment and GitHub probes, routes terminal URL taps to the phone browser, and limits the New Tab drawer to terminals + agents (browser/markdown creation resolves a real worktree host-side and stays desktop-only). useLiveWorktreeName short-circuits for the sentinel so it no longer polls worktree.show forever. Extracted the host status.get gating into a useHostStatusGates hook to keep the host screen under the max-lines ratchet. * Harden mobile Floating Workspace routing * Fix mobile host gate reuse race * Harden floating mobile session polling * fix(mobile): harden floating workspace route reuse * fix(mobile): skip floating workspace repo lookup * fix(mobile): clarify floating workspace header action
121 lines
3.7 KiB
TypeScript
121 lines
3.7 KiB
TypeScript
import { createElement } from 'react'
|
|
import { act, create, type ReactTestRenderer } from 'react-test-renderer'
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import type { RpcClient } from '../transport/rpc-client'
|
|
import { FLOATING_WORKSPACE_WORKTREE_ID } from './floating-workspace'
|
|
import { useMobileNativeChatReadability } from './use-mobile-native-chat-readability'
|
|
|
|
describe('useMobileNativeChatReadability', () => {
|
|
let renderer: ReactTestRenderer | null = null
|
|
let readable = false
|
|
|
|
beforeEach(() => {
|
|
globalThis.IS_REACT_ACT_ENVIRONMENT = true
|
|
readable = false
|
|
})
|
|
|
|
afterEach(() => {
|
|
act(() => renderer?.unmount())
|
|
renderer = null
|
|
})
|
|
|
|
async function mount(
|
|
connectionId: string | null,
|
|
worktreeId = 'repo::/worktree'
|
|
): Promise<ReturnType<typeof vi.fn>> {
|
|
const sendRequest = vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
result: { repos: [{ id: 'repo', connectionId }] }
|
|
})
|
|
const client = {
|
|
sendRequest
|
|
} as unknown as RpcClient
|
|
function Harness(): null {
|
|
readable = useMobileNativeChatReadability(client, worktreeId)
|
|
return null
|
|
}
|
|
const original = console.error
|
|
const consoleSpy = vi.spyOn(console, 'error').mockImplementation((...args) => {
|
|
if (typeof args[0] === 'string' && args[0].includes('react-test-renderer is deprecated')) {
|
|
return
|
|
}
|
|
original(...args)
|
|
})
|
|
try {
|
|
await act(async () => {
|
|
renderer = create(createElement(Harness))
|
|
await Promise.resolve()
|
|
})
|
|
} finally {
|
|
consoleSpy.mockRestore()
|
|
}
|
|
return sendRequest
|
|
}
|
|
|
|
it('admits local and runtime-owned transcript hosts', async () => {
|
|
await mount(null)
|
|
expect(readable).toBe(true)
|
|
act(() => renderer?.unmount())
|
|
renderer = null
|
|
|
|
await mount('runtime-ssh-environment')
|
|
expect(readable).toBe(true)
|
|
})
|
|
|
|
it('fails closed for Model-A SSH transcript hosts', async () => {
|
|
await mount('model-a-ssh')
|
|
expect(readable).toBe(false)
|
|
})
|
|
|
|
it('treats the host-local floating workspace as readable without listing repos', async () => {
|
|
const sendRequest = await mount(null, FLOATING_WORKSPACE_WORKTREE_ID)
|
|
|
|
expect(readable).toBe(true)
|
|
expect(sendRequest).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('fails closed immediately while a reused route resolves its new worktree', async () => {
|
|
let resolveNext: (response: unknown) => void = () => {}
|
|
const client = {
|
|
sendRequest: vi
|
|
.fn()
|
|
.mockResolvedValueOnce({
|
|
ok: true,
|
|
result: { repos: [{ id: 'local-repo', connectionId: null }] }
|
|
})
|
|
.mockImplementationOnce(() => new Promise((resolve) => (resolveNext = resolve)))
|
|
} as unknown as RpcClient
|
|
function Harness({ worktreeId }: { worktreeId: string }): null {
|
|
readable = useMobileNativeChatReadability(client, worktreeId)
|
|
return null
|
|
}
|
|
const original = console.error
|
|
const consoleSpy = vi.spyOn(console, 'error').mockImplementation((...args) => {
|
|
if (typeof args[0] === 'string' && args[0].includes('react-test-renderer is deprecated')) {
|
|
return
|
|
}
|
|
original(...args)
|
|
})
|
|
try {
|
|
await act(async () => {
|
|
renderer = create(createElement(Harness, { worktreeId: 'local-repo::/one' }))
|
|
await Promise.resolve()
|
|
})
|
|
expect(readable).toBe(true)
|
|
|
|
act(() => renderer?.update(createElement(Harness, { worktreeId: 'ssh-repo::/two' })))
|
|
expect(readable).toBe(false)
|
|
await act(async () => {
|
|
resolveNext({
|
|
ok: true,
|
|
result: { repos: [{ id: 'ssh-repo', connectionId: 'model-a-ssh' }] }
|
|
})
|
|
await Promise.resolve()
|
|
})
|
|
expect(readable).toBe(false)
|
|
} finally {
|
|
consoleSpy.mockRestore()
|
|
}
|
|
})
|
|
})
|