feat(cloud): RA-11 strip over a running Orca Cloud agent whose sign-in expires; strip reserves its height

- When the Orca account goes from active/expiring to expired/revoked, every running Orca Cloud
  Claude/Codex pane shows the strip (same component, place, height as #315): 'Your Claude sign-in
  expired' / 'Renew to keep working. Your conversation is kept.' / Renew (compact: 'Claude sign-in
  expired' + Renew). The process keeps running; nothing is parsed from terminal output.
- One Renew clears state 1 everywhere; the panes that showed it move to state 2: 'Claude sign-in
  renewed' / 'Restart Claude to pick up where you left off.' (or 'Starts a new conversation.') /
  Restart Claude / Dismiss. Restart reuses the account-restart path (queuePaneAccountRestarts), which
  resumes the pane's own session. Never automatic; a turn that ends cleanly after the renewal clears it.
- Kill switch / never offered: nothing new on a running pane. A held launch keeps #315's strip.
- Account reads while a running cloud agent pane is mounted: on window focus and at most once a
  minute (one timer per window).
- #315 P2: the strip is a pane-top banner (useReservePaneTopSpace), so the terminal refits below it
  and no row is covered; a spacer keeps it under a chromeless pane's title-row buttons.
- Spec: terminal starts below the strip and its last row stays visible (held and running); state 2's
  Dismiss sits a full title row below the pane's ×. Removes the debug screenshot left in #315's spec
  and polls the resize assertion (it raced the layout).
This commit is contained in:
Neil
2026-11-10 18:25:46 -08:00
parent defa46442a
commit 047c96855e
12 changed files with 573 additions and 29 deletions
@@ -11,6 +11,8 @@ import {
type CloudStripActions
} from './CloudAgentSignInStrip'
import { cloudAgentStripModel } from './cloud-agent-strip-model'
import { cloudRunningStripModel } from './cloud-running-strip-model'
import type { CloudRunningStrip } from '@/lib/orca-cloud/cloud-running-agent-signout'
const noop = (): void => {}
const ACTIONS: CloudStripActions = {
@@ -24,29 +26,34 @@ const ACTIONS: CloudStripActions = {
const PROMPT = 'Fix the flaky auth test and open a PR'
function Pane({
reason,
reason = 'accountExpired',
view = null,
agent = 'claude',
width = 900
width = 900,
running
}: {
reason: CloudAgentStripReason
reason?: CloudAgentStripReason
view?: AgentSignInGateView | null
agent?: HostAgentSignInAgent
width?: number
/** RA-11: the strip over a running agent instead of a held launch. */
running?: CloudRunningStrip
}): React.JSX.Element {
const model = cloudAgentStripModel({
agent,
reason,
view,
hostLabel: translate('agentSignInStrip.sandbox', 'the sandbox'),
t: translate
})
const model = running
? cloudRunningStripModel(running, translate)
: cloudAgentStripModel({
agent,
reason,
view,
hostLabel: translate('agentSignInStrip.sandbox', 'the sandbox'),
t: translate
})
return (
<div className="bg-background p-4">
{/* Stands in for a terminal pane: its title row (split, close) stays above the strip. */}
<div
data-strip-pane
className="relative h-[220px] overflow-hidden rounded-md border border-border bg-background"
className="pane relative h-[220px] overflow-hidden rounded-md border border-border bg-background"
style={{ width }}
>
<div className="pane-title-overlay-layer">
@@ -64,14 +71,21 @@ function Pane({
<CloudStripLayer>
<CloudAgentSignInStrip
model={model}
prompt={PROMPT}
prompt={running ? null : PROMPT}
width={cloudStripWidth(width)}
actions={ACTIONS}
/>
</CloudStripLayer>
<pre className="absolute inset-x-0 bottom-0 top-20 p-3 font-mono text-xs text-muted-foreground">
~/acme/web $
</pre>
{/* The terminal: it starts below the strip (reserved, never covered) and keeps its rows. */}
<div className="xterm-container overflow-hidden font-mono text-xs leading-4 text-muted-foreground">
{running ? (
<>
<div>⏺ Reading src/auth/session.ts…</div>
<div>⏺ API Error: 401 · Not logged in · Run /login</div>
</>
) : null}
<div data-terminal-last-row>~/acme/web $</div>
</div>
</div>
</div>
)
@@ -128,3 +142,22 @@ export const waitingNarrow = fixture({
export const needExpiredNarrow = fixture({ reason: 'accountExpired', width: 520 })
/** Below 360 px: one line and Renew; the detail stays the region's description. */
export const needExpiredCompact = fixture({ reason: 'accountExpired', width: 320 })
/** RA-11 state 1: Claude was running when its Orca-account sign-in expired. */
export const runningExpired = fixture({ running: { phase: 'expired', agent: 'claude' } })
export const runningExpiredCompact = fixture({
running: { phase: 'expired', agent: 'claude' },
width: 320
})
/** RA-11 state 2: renewed; this pane's agent still needs a restart (resumes its conversation). */
export const runningRenewed = fixture({
running: { phase: 'renewed', agent: 'claude', resumable: true }
})
/** RA-11 state 2 when the session can't be resumed. */
export const runningRenewedFresh = fixture({
running: { phase: 'renewed', agent: 'claude', resumable: false }
})
export const runningRenewedNarrow = fixture({
running: { phase: 'renewed', agent: 'claude', resumable: true },
width: 520
})
@@ -23,6 +23,7 @@ import {
} from '@/components/ui/dropdown-menu'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { cn } from '@/lib/utils'
import { useReservePaneTopSpace } from '../terminal-pane/appearance/pane-top-banner/PaneTopWarningBanner'
import type {
CloudStripModel,
CloudStripPrimary,
@@ -69,11 +70,15 @@ function useLiveStripWidth(
* buttons stay visible and clickable while it shows (RA-1). The pane and its fixture share it.
*/
export function CloudStripLayer({ children }: { children: React.ReactNode }): React.JSX.Element {
// A pane-top banner (RA-11): the pane reserves its height and the terminal refits below it, so
// no row sits under the strip. The spacer keeps it below a chromeless pane's title-row buttons.
const ref = useReservePaneTopSpace()
return (
<div
data-cloud-strip-layer
className="absolute inset-x-0 top-[var(--orca-pane-title-height,24px)] z-20"
>
<div ref={ref} data-cloud-strip-layer className="pane-top-banner">
<div
aria-hidden
className="pointer-events-none h-[var(--orca-pane-title-height,24px)] [.pane[data-has-title]_&]:hidden"
/>
{children}
</div>
)
@@ -282,12 +287,12 @@ export function CloudAgentSignInStrip({
</DropdownMenu>
) : null}
{/* Renew is the whole action and the strip leaves on its own; Escape still cancels. */}
{model.status === 'done' || primary?.action.kind === 'renew' ? null : (
{!model.dismiss && (model.status === 'done' || primary?.action.kind === 'renew') ? null : (
<Button
type="button"
variant="ghost"
size="icon-xs"
aria-label={t('agentSignInStrip.cancel', 'Cancel sign-in (Esc)')}
aria-label={model.dismiss ?? t('agentSignInStrip.cancel', 'Cancel sign-in (Esc)')}
onClick={actions.cancel}
>
<X aria-hidden />
@@ -32,6 +32,7 @@ import {
type CloudStripActions
} from './CloudAgentSignInStrip'
import { cloudAgentStripModel } from './cloud-agent-strip-model'
import { CloudRunningStripPortal } from './CloudRunningStripPortal'
const ABANDON_CHECK_MS = 1_000
@@ -117,6 +118,10 @@ function CloudStripPortal({
const gateActions = agentSignInCardActions(paneKey)
const actions: CloudStripActions = {
primary: (action) => {
// RA-11's restart belongs to the running-pane strip; a held launch never offers it.
if (action.kind === 'restart') {
return
}
switch (action.kind) {
case 'renew':
openSettings('accounts', 'orca-cloud')
@@ -167,7 +172,14 @@ export function CloudAgentSignInStripPortals({
return (
<>
{controller.managedPanes.map((pane, index) => (
<CloudStripPortal key={pane.id} pane={pane} tabId={controller.tabId} first={index === 0} />
<React.Fragment key={pane.id}>
<CloudStripPortal pane={pane} tabId={controller.tabId} first={index === 0} />
<CloudRunningStripPortal
pane={pane}
tabId={controller.tabId}
worktreeId={controller.worktreeId}
/>
</React.Fragment>
))}
</>
)
@@ -0,0 +1,112 @@
// RA-11: the strip over an Orca Cloud pane whose agent is already running when its Orca-account
// sign-in expires. Same component, place and height as #315's pre-launch strip; the terminal keeps
// running and refits below it, so its scrollback stays readable and selectable.
import type React from 'react'
import { createPortal } from 'react-dom'
import { useAppStore } from '@/store'
import { useTranslate } from '@/i18n/use-translate'
import { makePaneKey } from '../../../../shared/workspace-layout/stable-pane-id'
import { parseRemoteRuntimePtyId } from '../../../../shared/remote-runtime/remote-runtime-pty-id'
import type { ManagedPane } from '@/lib/pane-manager/pane-manager-types'
import { useAgentSignInGate } from '@/lib/agent-sign-in/agent-sign-in-gate-store'
import { useCloudAgentHold } from '@/lib/orca-cloud/cloud-agent-launch-hold'
import {
orcaCloudAgentAccountsLoadedAt,
useOrcaCloudAgentAccounts
} from '@/lib/orca-cloud/orca-cloud-agent-accounts-store'
import { isOrcaCloudEnvironmentId } from '@/lib/orca-cloud/orca-cloud-environment-id'
import { runtimeEnvironmentIdOrLocalUnlessTwin } from '@/lib/workspace/ownership/worktree-runtime-owner'
import {
clearCloudRunningSignout,
cloudRunningStrip,
useCloudRunningAccountReads,
useCloudRunningSignoutVersion
} from '@/lib/orca-cloud/cloud-running-agent-signout'
import {
CloudAgentSignInStrip,
CloudStripLayer,
cloudStripWidth,
type CloudStripActions
} from './CloudAgentSignInStrip'
import { cloudRunningStripModel } from './cloud-running-strip-model'
function openAccounts(): void {
const store = useAppStore.getState()
store.openSettingsTarget({ pane: 'accounts', repoId: null, sectionId: 'orca-cloud' })
store.openSettingsPage()
}
export function CloudRunningStripPortal({
pane,
tabId,
worktreeId
}: {
pane: ManagedPane
tabId: string
worktreeId: string
}): React.JSX.Element | null {
const t = useTranslate()
const paneKey = makePaneKey(tabId, pane.leafId)
useCloudRunningSignoutVersion()
const ptyId = useAppStore(
(state) => state.terminalLayoutsByTabId[tabId]?.ptyIdsByLeafId?.[pane.leafId] ?? null
)
const environmentId = useAppStore(
(state) =>
(ptyId ? parseRemoteRuntimePtyId(ptyId)?.environmentId : null) ??
runtimeEnvironmentIdOrLocalUnlessTwin(state, worktreeId)
)
const cloud = environmentId !== null && isOrcaCloudEnvironmentId(environmentId)
const entry = useAppStore((state) =>
cloud ? (state.agentStatusByPaneKey[paneKey] ?? null) : null
)
const running = cloud && (entry?.agentType === 'claude' || entry?.agentType === 'codex')
useCloudRunningAccountReads(running)
const accounts = useOrcaCloudAgentAccounts(running)
const hold = useCloudAgentHold(paneKey)
const gate = useAgentSignInGate(paneKey)
const held = hold !== null || gate !== null
if (!running) {
return null
}
// The read that showed the renewal dates it (a pure read, never the render clock).
const strip = cloudRunningStrip({
paneKey,
entry,
accounts,
held,
now: orcaCloudAgentAccountsLoadedAt()
})
if (!strip) {
return null
}
const actions: CloudStripActions = {
primary: (action) => {
if (action.kind === 'renew') {
openAccounts()
} else if (action.kind === 'restart' && ptyId) {
// Never automatic: the person asked. The pane's own session resumes when it has one.
useAppStore.getState().queuePaneAccountRestarts([ptyId], strip.agent)
clearCloudRunningSignout(paneKey)
}
},
submitCode: () => undefined,
pasteCode: () => undefined,
useCode: () => undefined,
useApiKey: () => undefined,
// State 2's ×; state 1 has none (Renew is the action) and Escape leaves it showing.
cancel: () => (strip.phase === 'renewed' ? clearCloudRunningSignout(paneKey) : undefined)
}
return createPortal(
<CloudStripLayer>
<CloudAgentSignInStrip
model={cloudRunningStripModel(strip, t)}
prompt={null}
width={cloudStripWidth(pane.container.clientWidth)}
actions={actions}
/>
</CloudStripLayer>,
pane.container,
`cloud-running-strip-${pane.id}`
)
}
@@ -17,6 +17,8 @@ export type CloudStripPrimary =
| { kind: 'submitCode' }
| { kind: 'retry' }
| { kind: 'useDeviceCode' }
/** RA-11: restart the running agent in its pane, resuming its conversation when it can. */
| { kind: 'restart' }
export type CloudStripModel = {
status: CloudStripStatus
@@ -35,6 +37,8 @@ export type CloudStripModel = {
otherWays: boolean
/** Already on a code: ⋯ offers the paste fallback and Cancel instead of "Use a code". */
codeStep?: boolean
/** RA-11 state 2: a dismiss × with this label (the strip otherwise drops its × on Renew). */
dismiss?: string
/** One sentence for the polite region. */
announcement: string
}
@@ -0,0 +1,52 @@
// RA-11 copy (design-squad): the strip over an Orca Cloud pane whose agent was already running when
// its Orca-account sign-in expired. Same words as #315's pre-launch strip, then the per-pane restart.
import type { Translate } from '@/i18n/use-translate'
import type { CloudRunningStrip } from '@/lib/orca-cloud/cloud-running-agent-signout'
import { HOST_AGENT_SIGN_IN_AGENT_NAME } from '../../../../shared/host-agent-sign-in-status'
import type { CloudStripModel } from './cloud-agent-strip-model'
export function cloudRunningStripModel(strip: CloudRunningStrip, t: Translate): CloudStripModel {
const agent = HOST_AGENT_SIGN_IN_AGENT_NAME[strip.agent]
const base = { code: null, paste: null, otherWays: false, description: null }
if (strip.phase === 'expired') {
return {
...base,
status: 'warning',
title: t('agentSignInStrip.expired', 'Your {{agent}} sign-in expired', { agent }),
compactTitle: t('agentSignInStrip.expiredCompact', '{{agent}} sign-in expired', { agent }),
detail: t(
'agentSignInStrip.runningExpiredDetail',
'Renew to keep working. Your conversation is kept.'
),
primary: { label: t('agentSignInStrip.renew', 'Renew'), action: { kind: 'renew' } },
announcement: t(
'agentSignInStrip.runningExpiredAnnounce',
'Your {{agent}} sign-in expired. Renew to keep working. Your conversation is kept.',
{ agent }
)
}
}
const restart = t('agentSignInStrip.restartAgent', 'Restart {{agent}}', { agent })
return {
...base,
status: 'done',
title: t('agentSignInStrip.renewed', '{{agent}} sign-in renewed', { agent }),
compactTitle: t('agentSignInStrip.renewed', '{{agent}} sign-in renewed', { agent }),
detail: strip.resumable
? t('agentSignInStrip.renewedResume', 'Restart {{agent}} to pick up where you left off.', {
agent
})
: t('agentSignInStrip.renewedFresh', 'Starts a new conversation.'),
primary: {
label: restart,
narrowLabel: t('agentSignInStrip.restart', 'Restart'),
action: { kind: 'restart' }
},
dismiss: t('agentSignInStrip.dismiss', 'Dismiss'),
announcement: t(
'agentSignInStrip.renewedAnnounce',
'{{agent}} sign-in renewed. Restart {{agent}} to use it.',
{ agent }
)
}
}
@@ -25,7 +25,7 @@ function layoutPaneTopBanners(pane: HTMLElement, leaving?: HTMLElement): void {
}
/** Reserves the banner's height at the top of its pane so the terminal refits below it. */
function useReservePaneTopSpace(): React.RefObject<HTMLDivElement | null> {
export function useReservePaneTopSpace(): React.RefObject<HTMLDivElement | null> {
const ref = useRef<HTMLDivElement>(null)
useLayoutEffect(() => {
const banner = ref.current
+10 -1
View File
@@ -28048,7 +28048,16 @@
"cancelSignIn": "Cancel sign-in",
"copyAndOpen": "Copy code and open",
"cancelledToast": "The agent didn’t start. The shell is still open.",
"expiredCompact": "{{agent}} sign-in expired"
"expiredCompact": "{{agent}} sign-in expired",
"runningExpiredDetail": "Renew to keep working. Your conversation is kept.",
"runningExpiredAnnounce": "Your {{agent}} sign-in expired. Renew to keep working. Your conversation is kept.",
"restartAgent": "Restart {{agent}}",
"renewed": "{{agent}} sign-in renewed",
"renewedResume": "Restart {{agent}} to pick up where you left off.",
"renewedFresh": "Starts a new conversation.",
"restart": "Restart",
"dismiss": "Dismiss",
"renewedAnnounce": "{{agent}} sign-in renewed. Restart {{agent}} to use it."
},
"cloudAgentCheck": {
"keyAgentSignedOut": "{{agent}} isn’t signed in.",
@@ -0,0 +1,113 @@
import { beforeEach, describe, expect, it } from 'vitest'
import type { OrcaCloudAgentsStatus } from '../../../../shared/orca-cloud-agents/orca-cloud-agents-wire'
import type { OrcaCloudAgentAccountsState } from './orca-cloud-agent-accounts-store'
import {
clearCloudRunningSignout,
cloudRunningStrip,
resetCloudRunningSignoutForTest
} from './cloud-running-agent-signout'
function accounts(
claude: OrcaCloudAgentsStatus['credentials']['claude'],
extra: Partial<OrcaCloudAgentsStatus['credentials']> = {}
): Pick<OrcaCloudAgentAccountsState, 'load' | 'keyKinds'> {
return {
load: {
kind: 'ready',
status: {
credentials: {
claude,
codex: { state: 'none' },
providerEnabled: { claude: true, codex: true },
...extra
},
sources: { defaults: {}, coveredByKey: { claude: false, codex: false } }
}
},
keyKinds: { claude: null, codex: null }
}
}
const RUNNING = { agentType: 'claude', state: 'working' as const, providerSession: { id: 's1' } }
const strip = (
account: ReturnType<typeof accounts>,
entry: Parameters<typeof cloudRunningStrip>[0]['entry'] = RUNNING,
now = 1_000,
paneKey = 'tab:a'
) => cloudRunningStrip({ paneKey, entry, accounts: account, held: false, now })
describe('cloudRunningStrip (RA-11)', () => {
beforeEach(resetCloudRunningSignoutForTest)
it('shows nothing while the account works, and state 1 once it expires or is revoked', () => {
expect(strip(accounts({ state: 'active' }))).toBeNull()
expect(strip(accounts({ state: 'revoked' }))).toEqual({ phase: 'expired', agent: 'claude' })
expect(strip(accounts({ state: 'expired' }), RUNNING, 1_000, 'tab:b')).toEqual({
phase: 'expired',
agent: 'claude'
})
})
it('after Renew the panes that showed state 1 move to state 2, resumable when the pane has a session', () => {
strip(accounts({ state: 'revoked' }))
expect(strip(accounts({ state: 'active' }))).toEqual({
phase: 'renewed',
agent: 'claude',
resumable: true
})
// A pane that never showed state 1 gets nothing.
expect(strip(accounts({ state: 'active' }), RUNNING, 1_000, 'tab:other')).toBeNull()
strip(
accounts({ state: 'revoked' }),
{ ...RUNNING, providerSession: undefined },
1_000,
'tab:c'
)
expect(
strip(
accounts({ state: 'active' }),
{ ...RUNNING, providerSession: undefined },
1_000,
'tab:c'
)
).toMatchObject({ phase: 'renewed', resumable: false })
})
it('never auto-restarts: state 2 stays until Restart or dismiss; a clean turn after renewal clears it', () => {
strip(accounts({ state: 'revoked' }))
expect(strip(accounts({ state: 'active' }), RUNNING, 2_000)?.phase).toBe('renewed')
expect(strip(accounts({ state: 'active' }), RUNNING, 9_000)?.phase).toBe('renewed')
// The agent's next call succeeded through egress: its turn ended cleanly after the renewal.
const recovered = { ...RUNNING, state: 'done' as const, turnEndedAt: 3_000 }
expect(strip(accounts({ state: 'active' }), recovered, 9_000)).toBeNull()
strip(accounts({ state: 'revoked' }), RUNNING, 1_000, 'tab:d')
strip(accounts({ state: 'active' }), RUNNING, 2_000, 'tab:d')
clearCloudRunningSignout('tab:d')
expect(strip(accounts({ state: 'active' }), RUNNING, 3_000, 'tab:d')).toBeNull()
})
it('shows nothing new under the kill switch, for a held launch, or without a running account agent', () => {
expect(
strip(
accounts(
{ state: 'revoked' },
{
providerEnabled: { claude: false, codex: true },
providerState: { claude: 'disabled', codex: 'on' }
}
)
)
).toBeNull()
expect(
cloudRunningStrip({
paneKey: 'tab:e',
entry: RUNNING,
accounts: accounts({ state: 'revoked' }),
held: true,
now: 1
})
).toBeNull()
expect(strip(accounts({ state: 'revoked' }), { ...RUNNING, agentType: 'gemini' })).toBeNull()
expect(strip(accounts({ state: 'revoked' }), null)).toBeNull()
})
})
@@ -0,0 +1,157 @@
// RA-11: an Orca Cloud pane whose agent is already running when its Orca-account sign-in expires or
// is revoked. State 1 ("expired") shows on every such pane; one Renew (S0) clears it everywhere and
// the panes that showed it move to state 2 ("renewed": Restart, never automatic) until restarted,
// dismissed, or the agent's next turn ends cleanly on its own. Decided from the Orca account (the
// same read the build sheet uses), never from terminal output.
import { useEffect, useSyncExternalStore } from 'react'
import type { HostAgentSignInAgent } from '../../../../shared/host-agent-sign-in-status'
import { agentTurnEndedUncleanly } from '../../../../shared/agent-status/lifecycle/agent-main-agent-verdict'
import type { AgentStatusEntry } from '../../../../shared/agent-status/contracts/agent-status-types'
import { orcaCloudAgentCoverage } from './orca-cloud-agent-accounts-model'
import {
ensureFreshOrcaCloudAgentAccounts,
type OrcaCloudAgentAccountsState
} from './orca-cloud-agent-accounts-store'
export type CloudRunningStrip =
| { phase: 'expired'; agent: HostAgentSignInAgent }
| { phase: 'renewed'; agent: HostAgentSignInAgent; resumable: boolean }
/** Panes that showed state 1, and when their account came back (state 2). */
const signedOut = new Map<string, { agent: HostAgentSignInAgent; renewedAt: number | null }>()
const listeners = new Set<() => void>()
let version = 0
function notify(): void {
version += 1
for (const listener of listeners) {
listener()
}
}
function subscribe(listener: () => void): () => void {
listeners.add(listener)
return () => listeners.delete(listener)
}
/** Re-renders when a pane's remembered state changes (dismiss, restart). */
export function useCloudRunningSignoutVersion(): number {
return useSyncExternalStore(
subscribe,
() => version,
() => version
)
}
/** Forget a pane: dismissed, restarted, or its agent went away. */
export function clearCloudRunningSignout(paneKey: string): void {
if (signedOut.delete(paneKey)) {
notify()
}
}
function isAccountAgent(agent: unknown): agent is HostAgentSignInAgent {
return agent === 'claude' || agent === 'codex'
}
/**
* What a running Orca Cloud pane shows, or null. `entry` is the pane's agent status (the agent is
* running); `held` is true while #315's launch hold or the per-workspace gate owns the pane.
*/
export function cloudRunningStrip(args: {
paneKey: string
entry:
| (Pick<
AgentStatusEntry,
'agentType' | 'state' | 'turnEndedAt' | 'mainAgent' | 'interrupted'
> & {
providerSession?: unknown
})
| null
accounts: Pick<OrcaCloudAgentAccountsState, 'load' | 'keyKinds'>
held: boolean
now: number
}): CloudRunningStrip | null {
const { paneKey, entry, accounts } = args
const remembered = signedOut.get(paneKey)
const agent = entry?.agentType
if (args.held || !isAccountAgent(agent) || accounts.load.kind !== 'ready') {
return null
}
const coverage = orcaCloudAgentCoverage(agent, accounts.load.status, accounts.keyKinds[agent])
switch (coverage.kind) {
case 'expired':
if (!remembered || remembered.renewedAt !== null) {
signedOut.set(paneKey, { agent, renewedAt: null })
}
return { phase: 'expired', agent }
case 'account':
case 'key': {
if (!remembered) {
return null
}
const renewedAt = remembered.renewedAt ?? args.now
if (remembered.renewedAt === null) {
signedOut.set(paneKey, { agent, renewedAt })
}
// Recovered on its own: a turn that ended cleanly after the renewal needs no restart.
if (
entry &&
entry.state === 'done' &&
(entry.turnEndedAt ?? 0) > renewedAt &&
!agentTurnEndedUncleanly(entry)
) {
signedOut.delete(paneKey)
return null
}
return { phase: 'renewed', agent, resumable: Boolean(entry?.providerSession) }
}
// Kill switch, never offered, not set up: nothing new on a running pane.
case 'killSwitch':
case 'unavailable':
case 'notSetUp':
return null
}
}
let liveReaders = 0
let timer: ReturnType<typeof setInterval> | null = null
const LIVE_READ_MS = 60_000
function readNow(): void {
void ensureFreshOrcaCloudAgentAccounts().catch(() => undefined)
}
/**
* While any running Orca Cloud agent pane is mounted, keep the account read fresh: on window focus
* and at most once a minute (the read itself is cached for 30 s). One timer for the whole window.
*/
export function useCloudRunningAccountReads(active: boolean): void {
useEffect(() => {
if (!active) {
return
}
liveReaders += 1
if (liveReaders === 1) {
window.addEventListener('focus', readNow)
timer = setInterval(readNow, LIVE_READ_MS)
readNow()
}
return () => {
liveReaders -= 1
if (liveReaders === 0) {
window.removeEventListener('focus', readNow)
if (timer) {
clearInterval(timer)
timer = null
}
}
}
}, [active])
}
/** Tests only. */
export function resetCloudRunningSignoutForTest(): void {
signedOut.clear()
notify()
}
@@ -180,6 +180,11 @@ export function subscribeOrcaCloudAgentAccounts(listener: () => void): () => voi
}
/** Reads again when the last read is stale or missing, for a launch about to decide. */
/** When the current account view was read (ms); 0 before the first read. */
export function orcaCloudAgentAccountsLoadedAt(): number {
return loadedAt
}
/** A ready read from within the last 30 s; older reads may miss a revoke (RA-1). */
export function isOrcaCloudAgentAccountsFresh(): boolean {
return state.load.kind === 'ready' && Date.now() - loadedAt <= FRESH_MS
@@ -33,7 +33,7 @@ async function boxes(page: Page, id: string) {
strip: await box('[data-cloud-sign-in-strip]'),
text: await box('[data-strip-text]'),
actions: await box('[data-strip-actions]'),
terminalTop: (await box('pre')).y
terminalTop: (await box('.xterm-container')).y
}
}
@@ -69,10 +69,12 @@ test('narrowing the pane after mount re-lays the strip out; the title stays read
}
}, width)
await expect(strip).toHaveAttribute('data-strip-width', layout)
const titleBox = await strip.getByText(title, { exact: true }).boundingBox()
// Readable: at least ~10 characters wide, never collapsed to one letter.
await page.screenshot({ path: `/tmp/vdrv/strip-${width}.png` })
expect(titleBox?.width ?? 0, `${width}px`).toBeGreaterThan(70)
const titleLine = strip.locator('[data-strip-text] > span').first()
await expect(titleLine).toHaveText(title)
// Readable: at least ~10 characters wide, never collapsed to one letter (once laid out).
await expect
.poll(async () => (await titleLine.boundingBox())?.width ?? 0, { message: `${width}px` })
.toBeGreaterThan(70)
expect((await strip.boundingBox())?.height).toBe(56)
await expect(strip.getByRole('button', { name: 'Renew' })).toBeVisible()
}
@@ -97,3 +99,43 @@ test('the pane’s split and close buttons stay hit-testable above the strip', a
}
}
})
// RA-11 + #315 P2: the strip reserves its height; the terminal refits below it, so no row (and not
// the last one) ever sits under the strip, held launch or running agent alike.
test('the terminal starts below the strip and its last row stays visible', async ({ page }) => {
for (const id of ['needExpired', 'runningExpired', 'runningRenewed', 'runningExpiredCompact']) {
await page.goto(`/fixtures.html?id=agent-sign-in-strip/CloudAgentSignInStrip:${id}`)
await expect(page.locator('html')).toHaveAttribute('data-fixture-state', 'ready')
const strip = await page.locator('[data-cloud-sign-in-strip]').boundingBox()
const terminal = await page.locator('.xterm-container').boundingBox()
const lastRow = await page.locator('[data-terminal-last-row]').boundingBox()
const pane = await page.locator('[data-strip-pane]').boundingBox()
if (!strip || !terminal || !lastRow || !pane) {
throw new Error(`missing bounds in ${id}`)
}
expect(terminal.y, id).toBeGreaterThanOrEqual(strip.y + strip.height)
expect(lastRow.y, id).toBeGreaterThanOrEqual(strip.y + strip.height)
expect(lastRow.y + lastRow.height, id).toBeLessThanOrEqual(pane.y + pane.height)
}
})
test('RA-11: state 1 offers Renew only; state 2 offers Restart and a dismiss clear of the pane buttons', async ({
page
}) => {
await page.goto('/fixtures.html?id=agent-sign-in-strip/CloudAgentSignInStrip:runningExpired')
await expect(page.locator('html')).toHaveAttribute('data-fixture-state', 'ready')
const strip = page.locator('[data-cloud-sign-in-strip]')
await expect(strip).toContainText('Renew to keep working. Your conversation is kept.')
await expect(strip.getByRole('button')).toHaveCount(1)
await page.goto('/fixtures.html?id=agent-sign-in-strip/CloudAgentSignInStrip:runningRenewed')
await expect(page.locator('html')).toHaveAttribute('data-fixture-state', 'ready')
await expect(strip).toContainText('Restart Claude to pick up where you left off.')
await expect(strip.getByRole('button', { name: 'Restart Claude' })).toBeVisible()
const dismiss = await strip.getByRole('button', { name: 'Dismiss' }).boundingBox()
const close = await page.getByRole('button', { name: 'Close pane', exact: true }).boundingBox()
if (!dismiss || !close) {
throw new Error('missing bounds')
}
// Not stacked next to the pane's ×: the strip sits a whole title row lower.
expect(dismiss.y).toBeGreaterThanOrEqual(close.y + close.height)
})