mirror of
https://github.com/stablyai/orca.git
synced 2026-10-01 00:02:10 +00:00
fix(mobile): ask to update Orca only when the host answered without agent launch
An unread or failed status read settles with no capabilities, which the AI buttons read as an old host and showed "Update Orca on your computer". The update copy now needs a status the host actually returned; an unread one keeps the buttons disabled without blaming the desktop's version.
This commit is contained in:
@@ -90,13 +90,14 @@ export function MobilePRSidebar({
|
||||
prRepo,
|
||||
refetch
|
||||
})
|
||||
const { hostCapabilities, statusPending } = useHostProtocolGates()
|
||||
const { hostCapabilities, statusPending, statusReadable } = useHostProtocolGates()
|
||||
const triage = useMobilePrAiTriage({
|
||||
client,
|
||||
connState,
|
||||
worktreeId,
|
||||
hostCapabilities,
|
||||
hostStatusPending: statusPending
|
||||
hostStatusPending: statusPending,
|
||||
hostStatusReadable: statusReadable
|
||||
})
|
||||
// Keyed on the PR payload identity so overrides re-fetch with each PR refetch
|
||||
// instead of staying a stale one-shot snapshot for the whole session.
|
||||
|
||||
@@ -52,12 +52,13 @@ export function MobileDiffReviewRouteScreen() {
|
||||
)
|
||||
}, [hostId, name, router, worktreeId])
|
||||
|
||||
const { hostCapabilities, statusPending } = useHostProtocolGates()
|
||||
const { hostCapabilities, statusPending, statusReadable } = useHostProtocolGates()
|
||||
const controller = useMobileDiffReviewController({
|
||||
client,
|
||||
connState,
|
||||
hostCapabilities,
|
||||
hostStatusPending: statusPending,
|
||||
hostStatusReadable: statusReadable,
|
||||
hostId,
|
||||
worktreeId,
|
||||
name,
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { resolveMobileAgentLaunchAvailability } from './mobile-agent-launch-availability'
|
||||
|
||||
const LAUNCH = ['agent.launch.v2', 'agent.launch.replay.v1', 'agent.launch.replay-required.v1']
|
||||
|
||||
describe('resolveMobileAgentLaunchAvailability', () => {
|
||||
it('is available when the host advertises the replay-required launch', () => {
|
||||
expect(
|
||||
resolveMobileAgentLaunchAvailability({
|
||||
hostCapabilities: LAUNCH,
|
||||
statusPending: false,
|
||||
statusReadable: true
|
||||
})
|
||||
).toBe('available')
|
||||
})
|
||||
|
||||
it('asks for an update only when the host answered without the capability', () => {
|
||||
expect(
|
||||
resolveMobileAgentLaunchAvailability({
|
||||
hostCapabilities: ['agent.launch.v2'],
|
||||
statusPending: false,
|
||||
statusReadable: true
|
||||
})
|
||||
).toBe('update-required')
|
||||
})
|
||||
|
||||
it('does not blame the host version when its status could not be read', () => {
|
||||
expect(
|
||||
resolveMobileAgentLaunchAvailability({
|
||||
hostCapabilities: [],
|
||||
statusPending: false,
|
||||
statusReadable: false
|
||||
})
|
||||
).toBe('unverified')
|
||||
})
|
||||
|
||||
it('waits while the status is being read', () => {
|
||||
expect(
|
||||
resolveMobileAgentLaunchAvailability({
|
||||
hostCapabilities: [],
|
||||
statusPending: true,
|
||||
statusReadable: false
|
||||
})
|
||||
).toBe('checking')
|
||||
})
|
||||
})
|
||||
@@ -3,15 +3,30 @@ import { supportsMobileExistingAgentLaunch } from './mobile-existing-agent-launc
|
||||
/**
|
||||
* Whether an AI button that starts an agent with a prompt can be used on this host. There is no
|
||||
* older-host fallback: the path before `agent.launch` typed the prompt into a bare shell.
|
||||
* `unverified`: the host's status could not be read, so whether it is too old is unknown.
|
||||
*/
|
||||
export type MobileAgentLaunchAvailability = 'checking' | 'available' | 'update-required'
|
||||
export type MobileAgentLaunchAvailability =
|
||||
| 'checking'
|
||||
| 'available'
|
||||
| 'update-required'
|
||||
| 'unverified'
|
||||
|
||||
/** The host status gates this reads (`useHostProtocolGates`). */
|
||||
export type MobileAgentLaunchHostStatus = {
|
||||
hostCapabilities: readonly string[]
|
||||
statusPending: boolean
|
||||
statusReadable: boolean
|
||||
}
|
||||
|
||||
export function resolveMobileAgentLaunchAvailability(
|
||||
hostCapabilities: readonly string[],
|
||||
hostStatusPending: boolean
|
||||
host: MobileAgentLaunchHostStatus
|
||||
): MobileAgentLaunchAvailability {
|
||||
if (supportsMobileExistingAgentLaunch(hostCapabilities)) {
|
||||
if (supportsMobileExistingAgentLaunch(host.hostCapabilities)) {
|
||||
return 'available'
|
||||
}
|
||||
return hostStatusPending ? 'checking' : 'update-required'
|
||||
if (host.statusPending) {
|
||||
return 'checking'
|
||||
}
|
||||
// Only a host that answered without the capability is too old to update.
|
||||
return host.statusReadable ? 'update-required' : 'unverified'
|
||||
}
|
||||
|
||||
@@ -57,6 +57,7 @@ function ReviewWhileUnreachable({
|
||||
connState,
|
||||
hostCapabilities: [],
|
||||
hostStatusPending: false,
|
||||
hostStatusReadable: true,
|
||||
hostId: 'host-1',
|
||||
worktreeId: 'wt-1',
|
||||
name: 'review',
|
||||
|
||||
@@ -45,6 +45,7 @@ describe('useMobileDiffReviewController', () => {
|
||||
connState,
|
||||
hostCapabilities: [],
|
||||
hostStatusPending: false,
|
||||
hostStatusReadable: true,
|
||||
hostId: 'host-1',
|
||||
worktreeId: 'wt-1',
|
||||
name: 'review',
|
||||
|
||||
@@ -35,6 +35,7 @@ type ControllerInput = {
|
||||
connState: ConnectionState
|
||||
hostCapabilities: readonly string[]
|
||||
hostStatusPending: boolean
|
||||
hostStatusReadable: boolean
|
||||
hostId: string
|
||||
worktreeId: string
|
||||
name: string
|
||||
@@ -51,6 +52,7 @@ export function useMobileDiffReviewController(input: ControllerInput) {
|
||||
connState,
|
||||
hostCapabilities,
|
||||
hostStatusPending,
|
||||
hostStatusReadable,
|
||||
hostId,
|
||||
worktreeId,
|
||||
name,
|
||||
@@ -264,10 +266,11 @@ export function useMobileDiffReviewController(input: ControllerInput) {
|
||||
return {
|
||||
...interactions,
|
||||
...prSidebar,
|
||||
agentLaunchAvailability: resolveMobileAgentLaunchAvailability(
|
||||
agentLaunchAvailability: resolveMobileAgentLaunchAvailability({
|
||||
hostCapabilities,
|
||||
hostStatusPending
|
||||
),
|
||||
statusPending: hostStatusPending,
|
||||
statusReadable: hostStatusReadable
|
||||
}),
|
||||
// Exposed so the screen can thread the RPC client + worktree into the PR
|
||||
// sidebar's lazy check-detail fetches (U5) and mutation actions (U6).
|
||||
client,
|
||||
|
||||
@@ -25,10 +25,12 @@ type Input = {
|
||||
worktreeId: string
|
||||
hostCapabilities: readonly string[]
|
||||
hostStatusPending: boolean
|
||||
hostStatusReadable: boolean
|
||||
}
|
||||
|
||||
export function useMobilePrAiTriage(input: Input) {
|
||||
const { client, connState, worktreeId, hostCapabilities, hostStatusPending } = input
|
||||
const { client, connState, worktreeId, hostCapabilities, hostStatusPending, hostStatusReadable } =
|
||||
input
|
||||
const [busyKey, setBusyKey] = useState<PrAiTriageKey | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
// The agent started without its prompt; kept so the user can paste it in themselves.
|
||||
@@ -36,10 +38,11 @@ export function useMobilePrAiTriage(input: Input) {
|
||||
// Synchronous lock: setBusyKey commits async, so a fast double-tap could pass the
|
||||
// busyKey check twice before either render. The ref flips immediately and dedupes.
|
||||
const inFlightRef = useRef(false)
|
||||
const availability: MobileAgentLaunchAvailability = resolveMobileAgentLaunchAvailability(
|
||||
const availability: MobileAgentLaunchAvailability = resolveMobileAgentLaunchAvailability({
|
||||
hostCapabilities,
|
||||
hostStatusPending
|
||||
)
|
||||
statusPending: hostStatusPending,
|
||||
statusReadable: hostStatusReadable
|
||||
})
|
||||
|
||||
const launch = useCallback(
|
||||
async (key: PrAiTriageKey, buildPrompt: () => string): Promise<boolean> => {
|
||||
|
||||
@@ -20,7 +20,8 @@ type Params = {
|
||||
}
|
||||
|
||||
export function useMobileCommitFailureRecovery({ client, connState, worktreeId, failure }: Params) {
|
||||
const { hostCapabilities, statusPending } = useHostProtocolGates()
|
||||
const hostStatus = useHostProtocolGates()
|
||||
const { hostCapabilities } = hostStatus
|
||||
const [launching, setLaunching] = useState(false)
|
||||
const [launchError, setLaunchError] = useState<string | null>(null)
|
||||
// The agent started without its prompt; kept so the user can paste it in themselves. Keyed by the
|
||||
@@ -31,7 +32,7 @@ export function useMobileCommitFailureRecovery({ client, connState, worktreeId,
|
||||
} | null>(null)
|
||||
const undeliveredPrompt = undelivered?.failure === failure ? undelivered.prompt : null
|
||||
const summary = useMemo(() => (failure ? summarizeCommitFailure(failure.error) : null), [failure])
|
||||
const availability = resolveMobileAgentLaunchAvailability(hostCapabilities, statusPending)
|
||||
const availability = resolveMobileAgentLaunchAvailability(hostStatus)
|
||||
|
||||
useEffect(() => {
|
||||
setLaunchError(null)
|
||||
|
||||
Reference in New Issue
Block a user