fix(cloud): sign-in row state sticks while Agent accounts say not set up, expired or revoked

design-squad P1-P3 on #310 from the m4air run:
- P1: after a restart leatherback fell back to the title-derived row ("Claude Code - Idle":
  prompt = title label, lastAssistantMessage = "Idle"), whose message made "no turn finished"
  false. While Agent accounts say not set up / expired / revoked, the sign-in state now wins over
  every done/idle/unverifiable row shape; only a sign-in made on the sandbox (cliLogin) clears it,
  not the Orca-account placeholder the sandbox reports as an API key. Covered accounts with a
  signed-out sandbox clear once a turn ends after the account became active (first covered read).
- P2: sign-in rows use the catalog name (Claude/Codex), never the title or a stale reply.
- P2: the tooltip opens below, aligned to the start, so the workspace name stays visible.
- P3: the full row reads "Needs sign-in - Claude" / "Sign-in expired - Claude" and drops the
  stale message line.
This commit is contained in:
Neil
2026-11-10 17:51:48 -08:00
parent e4ab41d386
commit 67bdb68ed6
8 changed files with 324 additions and 182 deletions
@@ -423,7 +423,7 @@ const DashboardAgentRow = React.memo(function DashboardAgentRow({
expanded={expanded}
isInterrupted={isInterrupted}
stoppedByUser={agentMainAgentVerdict(agent.entry) === 'cancellation'}
lastAssistantMessage={lastAssistantMessage}
lastAssistantMessage={cloudStatus ? '' : lastAssistantMessage}
/>
</div>
)
@@ -4,14 +4,12 @@ import React, { useCallback } from 'react'
import { useAppStore } from '@/store'
import { cn } from '@/lib/utils'
import { AgentStateDot } from '@/components/AgentStateDot'
import {
StateIndicatorTooltip,
type StateIndicatorTooltipSide
} from '@/components/StateIndicatorTooltip'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { STATE_INDICATOR_TOOLTIP_DELAY_MS } from '@/components/StateIndicatorTooltip'
import { openOrcaCloudAgentAccounts } from '@/components/new-workspace/OrcaCloudAgentSetupRow'
/** What the status shows: its label, and the tooltip naming the fix. */
export type CloudAgentRowStatusText = { label: string; tooltip: string | null }
/** What the status shows: its label, the agent's catalog name, and the tooltip naming the fix. */
export type CloudAgentRowStatusText = { label: string; agentName: string; tooltip: string | null }
/** The composer's "Set up once" target, plus opening Settings, which the sidebar has closed. */
export function openCloudAgentAccountsSettings(): void {
@@ -19,17 +17,18 @@ export function openCloudAgentAccountsSettings(): void {
useAppStore.getState().openSettingsPage()
}
/** A focusable status button: the app tooltip names the fix on hover and on keyboard focus. */
/**
* A focusable status button: the app tooltip names the fix on hover and on keyboard focus. It
* opens below, from the row's start, so the workspace name above the row stays visible.
*/
export function CloudAgentRowSignInTarget({
tooltip,
tooltipSide = 'top',
ariaLabel,
className,
children
}: {
/** Null shows no tooltip (StateIndicatorTooltip passes the button through). */
/** Null shows no tooltip. */
tooltip: string | null
tooltipSide?: StateIndicatorTooltipSide
/** For the dot, whose button has no text of its own. */
ariaLabel?: string
className?: string
@@ -39,49 +38,58 @@ export function CloudAgentRowSignInTarget({
e.stopPropagation()
openCloudAgentAccountsSettings()
}, [])
const button = (
<button
type="button"
className={cn(
'inline-flex min-w-0 shrink-0 cursor-pointer items-center rounded-sm text-left',
'focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring',
className
)}
aria-label={ariaLabel}
data-cloud-sign-in-status=""
onClick={handleClick}
onMouseDown={(e) => e.stopPropagation()}
onPointerDown={(e) => e.stopPropagation()}
>
{children}
</button>
)
if (tooltip === null) {
return button
}
return (
<StateIndicatorTooltip label={tooltip} side={tooltipSide}>
<button
type="button"
className={cn(
'inline-flex min-w-0 shrink-0 cursor-pointer items-center rounded-sm text-left',
'focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring',
className
)}
aria-label={ariaLabel}
data-cloud-sign-in-status=""
onClick={handleClick}
onMouseDown={(e) => e.stopPropagation()}
onPointerDown={(e) => e.stopPropagation()}
>
{children}
</button>
</StateIndicatorTooltip>
<Tooltip delayDuration={STATE_INDICATOR_TOOLTIP_DELAY_MS}>
<TooltipTrigger asChild>{button}</TooltipTrigger>
<TooltipContent side="bottom" align="start" sideOffset={6}>
{tooltip}
</TooltipContent>
</Tooltip>
)
}
/** The status dot: the attention dot, opening Agent accounts. Its tooltip is the button's. */
export function CloudAgentRowSignInDot({
status,
tooltipSide,
size
}: {
status: CloudAgentRowStatusText
tooltipSide?: StateIndicatorTooltipSide
size: 'sm' | 'md'
}): React.JSX.Element {
return (
<CloudAgentRowSignInTarget
tooltip={status.tooltip}
tooltipSide={tooltipSide}
ariaLabel={status.label}
ariaLabel={`${status.label} - ${status.agentName}`}
>
<AgentStateDot state="permission" size={size} title={null} />
</CloudAgentRowSignInTarget>
)
}
/** The full row's label slot: the text links to Agent accounts; the rest of the slot opens the pane. */
/**
* The full row's label slot: "Needs sign-in - Claude", the status linking to Agent accounts and the
* agent's catalog name after it; the rest of the slot opens the pane.
*/
export function CloudAgentRowSignInLabel({
status,
emphasis
@@ -105,6 +113,12 @@ export function CloudAgentRowSignInLabel({
>
{status.label}
</CloudAgentRowSignInTarget>
<span
className={emphasis === 'highlighted' ? 'text-foreground/70' : 'text-muted-foreground/65'}
>
{' - '}
{status.agentName}
</span>
</span>
)
}
@@ -25,6 +25,8 @@ import {
readHostAgentSignInReport,
subscribeHostAgentSignInReports
} from '@/runtime/provider-accounts/sign-in/host-agent-sign-in-status-client'
import { cloudAgentAccountActiveSince } from '@/lib/orca-cloud/cloud-agent-account-active-since'
import { HOST_AGENT_SIGN_IN_AGENT_NAME } from '../../../../shared/host-agent-sign-in-status'
import { isOrcaCloudRuntimeEnvironment } from '../../../../shared/remote-runtime/host-catalog/orca-cloud-environment-name'
const subscribeNothing = (): (() => void) => () => {}
@@ -42,8 +44,8 @@ function orcaCloudEnvironmentIdForWorktree(state: AppState, worktreeId: string):
}
/**
* Why a cloud agent row that never ran a turn is not "Done": its agent isn't signed in.
* Null for local, SSH and paired hosts, and for any row that has finished a turn.
* Why a cloud agent row at rest is not "Done" or "Idle": its agent isn't signed in.
* Null for local, SSH and paired hosts, and for working or waiting rows.
*/
export function useCloudAgentRowSignIn(
agent: Pick<DashboardAgentRow, 'agentType' | 'entry' | 'tab' | 'childRow'>,
@@ -52,7 +54,6 @@ export function useCloudAgentRowSignIn(
const candidate = isCloudRowSignInCandidate({
agentType: agent.agentType,
dotState,
entry: agent.entry,
isChildRow: agent.childRow !== undefined
})
const worktreeId = agent.tab.worktreeId
@@ -81,12 +82,17 @@ export function useCloudAgentRowSignIn(
if (!environmentId || !signInAgent) {
return null
}
return cloudAgentRowSignIn(signInAgent, accounts, hostAgentSignInEntry(report, signInAgent))
return cloudAgentRowSignIn(signInAgent, accounts, hostAgentSignInEntry(report, signInAgent), {
turnEndedAt: agent.entry.turnEndedAt,
activeSince: cloudAgentAccountActiveSince(signInAgent)
})
}
export type CloudAgentRowStatus = {
dotState: AgentDotState
label: string
/** The catalog name ("Claude", "Codex"), never a terminal title such as "Claude Code". */
agentName: string
/** Null in send-target mode, which owns row clicks; otherwise the status links to Settings. */
tooltip: string | null
}
@@ -100,12 +106,14 @@ export function useCloudAgentRowStatus(
): CloudAgentRowStatus | null {
const t = useTranslate()
const signIn = useCloudAgentRowSignIn(agent, dotState)
if (!signIn) {
const signInAgent = cloudRowSignInAgent(agent.agentType)
if (!signIn || !signInAgent) {
return null
}
return {
dotState: 'permission',
label: cloudAgentRowSignInLabel(signIn, t),
agentName: HOST_AGENT_SIGN_IN_AGENT_NAME[signInAgent],
tooltip: sendTargetStatus ? null : cloudAgentRowSignInTooltip(signIn, t)
}
}
@@ -190,3 +190,33 @@ export const fullClaudeSignInExpiredTooltip = defineFixture({
),
interact: focusStatusText
})
/** After a restart the pane can fall back to its title-derived row ("Claude Code - Idle"). */
function titleDerivedRow(): DashboardAgentRow {
const base = row('claude', null)
return {
...base,
state: 'idle',
startedAt: 0,
entry: {
...base.entry,
state: 'working',
prompt: 'Claude Code',
lastAssistantMessage: 'Idle',
stateStartedAt: now,
updatedAt: now
}
}
}
/** Revoked account, title-derived row after a restart: still "Sign-in expired - Claude". */
export const claudeRevokedAfterRestart = cloudRowFixture(
accounts({ state: 'revoked' }, { state: 'none' }),
titleDerivedRow()
)
export const fullClaudeRevokedAfterRestart = cloudRowFixture(
accounts({ state: 'revoked' }, { state: 'none' }),
titleDerivedRow(),
'full'
)
@@ -18,6 +18,7 @@ import type * as HostAgentSignInClient from '@/runtime/provider-accounts/sign-in
import type { PublicKnownRuntimeEnvironment } from '../../../../../../shared/remote-runtime/host-catalog/runtime-environments'
import { makeTabWithIds } from '@/components/activity/ActivityPrototypePage-test-fixtures'
import DashboardAgentRow from '@/components/dashboard/DashboardAgentRow'
import { _resetCloudAgentAccountActiveSinceForTest } from '@/lib/orca-cloud/cloud-agent-account-active-since'
import { CompactAgentRow } from './worktree-card-compact-agent-row'
Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true })
@@ -118,6 +119,41 @@ function makeMirroredAgent(worktreeId: string): DashboardAgentRowData {
}
}
/**
* The row leatherback showed 30 s after a restart ("Claude Code - Idle", grey dot, "now"): the
* title-derived fallback row (worktree-title-derived-agent-rows), built when no hook entry is
* live. Its prompt is the title label and its message the translated "Idle".
*/
function makeTitleDerivedAgent(worktreeId: string): DashboardAgentRowData {
return {
paneKey: 'tab-1:leaf-1',
tab: makeTabWithIds('tab-1', worktreeId),
agentType: 'claude',
rowSource: 'live',
state: 'idle',
startedAt: 0,
entry: {
paneKey: 'tab-1:leaf-1',
state: 'working',
prompt: 'Claude Code',
updatedAt: 2000,
stateStartedAt: 2000,
stateHistory: [],
agentType: 'claude',
terminalTitle: '✳ Claude Code',
lastAssistantMessage: 'Idle',
observation: {
origin: 'title',
authorityId: 'title-derived',
incarnation: 0,
revision: 2000,
observedAt: 2000,
kind: 'snapshot'
}
}
}
}
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the row reads only id, name and source.
const cloudEnvironment = {
id: CLOUD_ENV,
@@ -136,12 +172,18 @@ afterEach(() => {
act(() => root?.unmount())
document.body.replaceChildren()
_resetHostAgentSignInReportsForTest()
_resetCloudAgentAccountActiveSinceForTest()
})
function renderRow(agent: DashboardAgentRowData, onActivate = () => {}): HTMLElement {
const container = document.createElement('div')
document.body.appendChild(container)
root = createRoot(container)
rerenderRow(agent, onActivate)
return container
}
function rerenderRow(agent: DashboardAgentRowData, onActivate = () => {}): void {
act(() => {
root!.render(
<TooltipProvider>
@@ -149,99 +191,106 @@ function renderRow(agent: DashboardAgentRowData, onActivate = () => {}): HTMLEle
</TooltipProvider>
)
})
return container
}
describe('CompactAgentRow on an Orca Cloud workspace whose agent is not signed in', () => {
function signedIn(method: 'apiKey' | 'cliLogin' | 'orcaAccount'): void {
rememberHostAgentSignInReport(CLOUD_ENV, {
checkedAt: 1,
agents: [{ agent: 'claude', state: 'signedIn', method }]
})
}
function signedOut(): void {
rememberHostAgentSignInReport(CLOUD_ENV, {
checkedAt: 1,
agents: [{ agent: 'claude', state: 'signedOut' }]
})
}
describe('CompactAgentRow on an Orca Cloud workspace: Agent accounts decide', () => {
it('shows Needs sign-in instead of Done when Claude is not set up', () => {
const container = renderRow(makeAgent(CLOUD_WORKTREE, true))
expect(container.textContent).toContain('Needs sign-in - Claude')
expect(container.textContent).not.toContain('Done')
})
it('shows Sign-in expired when the Orca account sign-in expired', () => {
it('shows Sign-in expired for an expired or revoked Orca account sign-in', () => {
setOrcaCloudAgentAccountsStateForTest(accounts({ state: 'expired' }))
const container = renderRow(makeAgent(CLOUD_WORKTREE, true))
expect(container.textContent).toContain('Sign-in expired - Claude')
act(() => setOrcaCloudAgentAccountsStateForTest(accounts({ state: 'revoked' })))
expect(container.textContent).toContain('Sign-in expired - Claude')
})
it('wins over the title-derived idle row seen after a restart, and never says "Claude Code"', () => {
setOrcaCloudAgentAccountsStateForTest(accounts({ state: 'revoked' }))
const container = renderRow(makeTitleDerivedAgent(CLOUD_WORKTREE))
expect(container.textContent).toContain('Sign-in expired - Claude')
expect(container.textContent).not.toContain('Claude Code')
expect(container.textContent).not.toContain('Idle')
})
it('sticks across the row shapes that follow a restart', () => {
setOrcaCloudAgentAccountsStateForTest(accounts({ state: 'revoked' }))
const container = renderRow(makeMirroredAgent(CLOUD_WORKTREE))
expect(container.textContent).toContain('Sign-in expired - Claude')
rerenderRow(makeTitleDerivedAgent(CLOUD_WORKTREE))
expect(container.textContent).toContain('Sign-in expired - Claude')
rerenderRow(makeAgent(CLOUD_WORKTREE, false))
expect(container.textContent).toContain('Sign-in expired - Claude')
rerenderRow({ ...makeMirroredAgent(CLOUD_WORKTREE), state: 'unverifiable' })
expect(container.textContent).toContain('Sign-in expired - Claude')
})
it('wins over turn evidence while the account is not set up', () => {
const container = renderRow(makeAgent(CLOUD_WORKTREE, false))
expect(container.textContent).toContain('Needs sign-in - Claude')
})
it('is not cleared by the sandbox reporting the Orca-account placeholder as signed in', () => {
setOrcaCloudAgentAccountsStateForTest(accounts({ state: 'revoked' }))
signedIn('apiKey')
const container = renderRow(makeAgent(CLOUD_WORKTREE, true))
expect(container.textContent).toContain('Sign-in expired - Claude')
})
it('trusts the sandbox report: signedOut shows Needs sign-in even when the account covers it', () => {
setOrcaCloudAgentAccountsStateForTest(accounts({ state: 'active' }))
rememberHostAgentSignInReport(CLOUD_ENV, {
checkedAt: 1,
agents: [{ agent: 'claude', state: 'signedOut' }]
})
const container = renderRow(makeAgent(CLOUD_WORKTREE, true))
expect(container.textContent).toContain('Needs sign-in - Claude')
it('is cleared by a sign-in made on the sandbox itself (/login there)', () => {
signedIn('cliLogin')
expect(renderRow(makeAgent(CLOUD_WORKTREE, true)).textContent).toContain('Done - Claude')
})
it('keeps Done when the sandbox reports Claude signed in (a /login there)', () => {
rememberHostAgentSignInReport(CLOUD_ENV, {
checkedAt: 1,
agents: [{ agent: 'claude', state: 'signedIn', method: 'cliLogin' }]
})
const container = renderRow(makeAgent(CLOUD_WORKTREE, true))
expect(container.textContent).toContain('Done - Claude')
it('clears when the account becomes active', () => {
setOrcaCloudAgentAccountsStateForTest(accounts({ state: 'revoked' }))
const container = renderRow(makeTitleDerivedAgent(CLOUD_WORKTREE))
expect(container.textContent).toContain('Sign-in expired - Claude')
act(() => setOrcaCloudAgentAccountsStateForTest(accounts({ state: 'active' })))
expect(container.textContent).toContain('Claude Code - Idle')
})
it('keeps Done for a covered agent idle after a turn', () => {
setOrcaCloudAgentAccountsStateForTest(accounts({ state: 'active' }))
expect(renderRow(makeAgent(CLOUD_WORKTREE, false)).textContent).toContain('Done - Claude')
})
it('covered but the sandbox is signed out: needs sign-in until a turn ends after the account became active', () => {
setOrcaCloudAgentAccountsStateForTest(accounts({ state: 'active' }))
signedOut()
const container = renderRow(makeAgent(CLOUD_WORKTREE, false))
expect(container.textContent).toContain('Done - Claude')
})
it('keeps Done after a finished turn even when the account is not set up', () => {
const container = renderRow(makeAgent(CLOUD_WORKTREE, false))
expect(container.textContent).toContain('Done - Claude')
})
it('leaves a local workspace unchanged', () => {
const container = renderRow(makeAgent(LOCAL_WORKTREE, true))
expect(container.textContent).toContain('Done - Claude')
expect(container.textContent).not.toContain('Needs sign-in')
})
it('shows Needs sign-in for the row mirrored from the cloud host (no sessionBoundary)', () => {
const container = renderRow(makeMirroredAgent(CLOUD_WORKTREE))
expect(container.textContent).toContain('Needs sign-in - Claude')
expect(container.textContent).not.toContain('Done')
})
it('keeps Done for a mirrored row with a finished turn', () => {
const mirrored = makeMirroredAgent(CLOUD_WORKTREE)
const container = renderRow({
...mirrored,
entry: {
...mirrored.entry,
stateHistory: [{ state: 'working', prompt: '', startedAt: 600 }],
turnEndedAt: 1000
}
})
expect(container.textContent).toContain('Done - Claude')
})
it('keeps Done for a mirrored row with an assistant reply', () => {
const mirrored = makeMirroredAgent(CLOUD_WORKTREE)
const container = renderRow({
...mirrored,
entry: { ...mirrored.entry, lastAssistantMessage: 'Fixed the redirect.' }
})
expect(container.textContent).not.toContain('Needs sign-in')
})
it('ignores sessionBoundary: turn evidence alone decides', () => {
const finished = makeAgent(CLOUD_WORKTREE, false)
const container = renderRow({
...finished,
entry: { ...finished.entry, sessionBoundary: true }
})
rerenderRow({ ...finished, entry: { ...finished.entry, turnEndedAt: Date.now() + 60_000 } })
expect(container.textContent).toContain('Done - Claude')
})
it('shows Sign-in expired for a revoked Orca account sign-in', () => {
it('leaves working rows alone', () => {
setOrcaCloudAgentAccountsStateForTest(accounts({ state: 'revoked' }))
const container = renderRow(makeMirroredAgent(CLOUD_WORKTREE))
expect(container.textContent).toContain('Sign-in expired - Claude')
const working = makeAgent(CLOUD_WORKTREE, true)
const container = renderRow({
...working,
state: 'working',
entry: { ...working.entry, state: 'working', prompt: 'Fix the login redirect' }
})
expect(container.textContent).not.toContain('Sign-in expired')
})
it('shows nothing extra while the Orca account path is paused (providerState disabled)', () => {
@@ -251,13 +300,15 @@ describe('CompactAgentRow on an Orca Cloud workspace whose agent is not signed i
paused.load.status.credentials.providerState = { claude: 'disabled', codex: 'on' }
}
setOrcaCloudAgentAccountsStateForTest(paused)
const container = renderRow(makeMirroredAgent(CLOUD_WORKTREE))
expect(container.textContent).toContain('Done - Claude')
expect(renderRow(makeMirroredAgent(CLOUD_WORKTREE)).textContent).toContain('Done - Claude')
})
it('leaves the same mirrored shape on a local workspace unchanged', () => {
const container = renderRow(makeMirroredAgent(LOCAL_WORKTREE))
it('leaves a local workspace unchanged, whatever the row shape', () => {
setOrcaCloudAgentAccountsStateForTest(accounts({ state: 'revoked' }))
const container = renderRow(makeAgent(LOCAL_WORKTREE, true))
expect(container.textContent).toContain('Done - Claude')
rerenderRow(makeTitleDerivedAgent(LOCAL_WORKTREE))
expect(container.textContent).toContain('Claude Code - Idle')
})
})
@@ -280,15 +331,18 @@ function fullRowLabel(container: HTMLElement): string | null {
}
describe('DashboardAgentRow (full row) on an Orca Cloud workspace', () => {
it('shows Needs sign-in instead of Done when Claude is not set up', () => {
it('reads "Needs sign-in - Claude" when Claude is not set up', () => {
const container = renderFullRow(makeAgent(CLOUD_WORKTREE, true))
expect(fullRowLabel(container)).toBe('Needs sign-in')
expect(fullRowLabel(container)).toBe('Needs sign-in - Claude')
expect(container.querySelector('[aria-label="Needs attention"]')).not.toBeNull()
})
it('shows Sign-in expired when the Orca account sign-in expired', () => {
setOrcaCloudAgentAccountsStateForTest(accounts({ state: 'expired' }))
expect(fullRowLabel(renderFullRow(makeAgent(CLOUD_WORKTREE, true)))).toBe('Sign-in expired')
it('reads "Sign-in expired - Claude" over the title-derived idle row', () => {
setOrcaCloudAgentAccountsStateForTest(accounts({ state: 'revoked' }))
const container = renderFullRow(makeTitleDerivedAgent(CLOUD_WORKTREE))
expect(fullRowLabel(container)).toBe('Sign-in expired - Claude')
expect(container.textContent).not.toContain('Claude Code')
expect(container.textContent).not.toContain('Idle')
})
it('keeps Done for a covered agent idle after a turn', () => {
@@ -296,28 +350,23 @@ describe('DashboardAgentRow (full row) on an Orca Cloud workspace', () => {
expect(fullRowLabel(renderFullRow(makeAgent(CLOUD_WORKTREE, false)))).toBe('Done')
})
it('shows Needs sign-in for the row mirrored from the cloud host', () => {
expect(fullRowLabel(renderFullRow(makeMirroredAgent(CLOUD_WORKTREE)))).toBe('Needs sign-in')
})
it('leaves a local workspace unchanged', () => {
const container = renderFullRow(makeAgent(LOCAL_WORKTREE, true))
expect(fullRowLabel(container)).toBe('Done')
expect(container.textContent).not.toContain('Needs sign-in')
expect(container.querySelector('[data-cloud-sign-in-status]')).toBeNull()
})
})
describe('a row that renders before the Agent accounts read lands', () => {
it('picks up Needs sign-in once the accounts load (restored row)', () => {
it('picks up the sign-in state once the accounts load (restored row)', () => {
setOrcaCloudAgentAccountsStateForTest({
...accounts({ state: 'none' }),
load: { kind: 'loading' }
})
const container = renderRow(makeMirroredAgent(CLOUD_WORKTREE))
expect(container.textContent).toContain('Done - Claude')
act(() => setOrcaCloudAgentAccountsStateForTest(accounts({ state: 'none' })))
expect(container.textContent).toContain('Needs sign-in - Claude')
const container = renderRow(makeTitleDerivedAgent(CLOUD_WORKTREE))
expect(container.textContent).toContain('Claude Code - Idle')
act(() => setOrcaCloudAgentAccountsStateForTest(accounts({ state: 'revoked' })))
expect(container.textContent).toContain('Sign-in expired - Claude')
})
it('picks it up once the cloud environment is known', () => {
@@ -325,14 +374,7 @@ describe('a row that renders before the Agent accounts read lands', () => {
const container = renderFullRow(makeMirroredAgent(CLOUD_WORKTREE))
expect(fullRowLabel(container)).toBe('Done')
act(() => useAppStore.setState({ runtimeEnvironments: [cloudEnvironment] }))
expect(fullRowLabel(container)).toBe('Needs sign-in')
})
it('goes back to Done when Claude is set up', () => {
const container = renderRow(makeMirroredAgent(CLOUD_WORKTREE))
expect(container.textContent).toContain('Needs sign-in - Claude')
act(() => setOrcaCloudAgentAccountsStateForTest(accounts({ state: 'active' })))
expect(container.textContent).toContain('Done - Claude')
expect(fullRowLabel(container)).toBe('Needs sign-in - Claude')
})
})
@@ -347,9 +389,9 @@ function statusButtons(container: HTMLElement): HTMLButtonElement[] {
}
/** Keyboard focus opens the app tooltip; read its content, not a title attribute. */
function focusTooltip(button: HTMLButtonElement): string | null {
function focusTooltip(button: HTMLButtonElement): Element | null {
act(() => button.focus())
return document.body.querySelector('[role="tooltip"]')?.textContent ?? null
return document.body.querySelector('[role="tooltip"]')
}
describe.each([
@@ -362,25 +404,28 @@ describe.each([
it('has a focusable dot and text, with no native title', () => {
const [dot, text] = statusButtons(render(makeAgent(CLOUD_WORKTREE, true)))
expect(dot.getAttribute('aria-label')).toBe('Needs sign-in')
expect(dot.getAttribute('aria-label')).toBe('Needs sign-in - Claude')
expect(text.textContent).toBe('Needs sign-in')
expect(dot.hasAttribute('title') || text.hasAttribute('title')).toBe(false)
})
it('shows the app tooltip on keyboard focus of the text', () => {
it('shows the app tooltip below and from the start on keyboard focus, clear of the name above', () => {
const [, text] = statusButtons(render(makeAgent(CLOUD_WORKTREE, true)))
expect(focusTooltip(text)).toBe('Set up once in Settings → Agent accounts')
expect(focusTooltip(text)?.textContent).toBe('Set up once in Settings → Agent accounts')
const content = document.body.querySelector('[data-side]')
expect(content?.getAttribute('data-side')).toBe('bottom')
expect(content?.getAttribute('data-align')).toBe('start')
})
it('shows the app tooltip on keyboard focus of the dot', () => {
const [dot] = statusButtons(render(makeAgent(CLOUD_WORKTREE, true)))
expect(focusTooltip(dot)).toBe('Set up once in Settings → Agent accounts')
expect(focusTooltip(dot)?.textContent).toBe('Set up once in Settings → Agent accounts')
})
it('tells an expired sign-in to renew', () => {
setOrcaCloudAgentAccountsStateForTest(accounts({ state: 'expired' }))
const [, text] = statusButtons(render(makeAgent(CLOUD_WORKTREE, true)))
expect(focusTooltip(text)).toBe('Renew in Settings → Agent accounts')
expect(focusTooltip(text)?.textContent).toBe('Renew in Settings → Agent accounts')
})
it('clicking the text opens Agent accounts, not the pane', () => {
@@ -168,7 +168,9 @@ export const CompactAgentRow = React.memo(function CompactAgentRow({
const held = heldMessageRef.current
const stableMessage =
turnHoldable && !currentMessage && held?.turn === turn ? held.message : undefined
const secondary = getCompactAgentSecondary(agent, now, stableMessage, presentation, t)
// A sign-in row names its agent by catalog name, never a stale reply or a title like "Claude Code".
const secondary =
cloudStatus?.agentName ?? getCompactAgentSecondary(agent, now, stableMessage, presentation, t)
const { leadingText, trailingText } = getCompactAgentLineOrder(
agent,
dotState,
@@ -245,7 +247,7 @@ export const CompactAgentRow = React.memo(function CompactAgentRow({
<>
{/* Show the actionable disabled reason over every hit area. */}
{signIn ? (
<CloudAgentRowSignInDot status={signIn} tooltipSide="right" size="sm" />
<CloudAgentRowSignInDot status={signIn} size="sm" />
) : (
<AgentStateDot
state={dotState}
@@ -0,0 +1,47 @@
// When this renderer first saw each agent covered (account or key) in the Orca account's Agent
// accounts, for "a turn ended after the account became active". The server sends no such time, so
// the first covered read stands in; any uncovered read resets it.
import {
HOST_AGENT_SIGN_IN_AGENTS,
type HostAgentSignInAgent
} from '../../../../shared/host-agent-sign-in-status'
import { orcaCloudAgentCoverageFromAccounts } from './orca-cloud-agent-accounts-model'
import {
readOrcaCloudAgentAccounts,
subscribeOrcaCloudAgentAccounts
} from './orca-cloud-agent-accounts-store'
const activeSince = new Map<HostAgentSignInAgent, number>()
let watching = false
function observe(): void {
const accounts = readOrcaCloudAgentAccounts()
if (accounts.load.kind !== 'ready') {
return
}
for (const agent of HOST_AGENT_SIGN_IN_AGENTS) {
const kind = orcaCloudAgentCoverageFromAccounts(agent, accounts)?.kind
if (kind === 'account' || kind === 'key') {
if (!activeSince.has(agent)) {
activeSince.set(agent, Date.now())
}
} else {
activeSince.delete(agent)
}
}
}
/** Null until this renderer has seen the agent covered. */
export function cloudAgentAccountActiveSince(agent: HostAgentSignInAgent): number | null {
if (!watching) {
watching = true
subscribeOrcaCloudAgentAccounts(observe)
}
observe()
return activeSince.get(agent) ?? null
}
/** Tests only. */
export function _resetCloudAgentAccountActiveSinceForTest(): void {
activeSince.clear()
}
@@ -1,11 +1,10 @@
// Private layer, Orca Cloud only: a cloud Claude/Codex pane whose agent isn't signed in sits at its
// own "Not logged in" screen, yet its row would read "Done". The row says why instead. The source of
// truth is the Orca account's Agent accounts (credential state and providerState, via the shared
// coverage selector), combined with "no turn finished"; the sandbox's own sign-in report can
// clear it (signedIn) or add to it (signedOut).
// own "Not logged in" screen, yet its row would read "Done" or "Idle". The row says why instead.
// The source of truth is the Orca account's Agent accounts (credential state and providerState, via
// the shared coverage selector). While they say not set up, expired or revoked, the sign-in state
// wins over every idle/done row shape (hook, mirrored or title-derived).
import type { AgentDotState } from '@/components/AgentStateDot'
import type { Translate } from '@/i18n/use-translate'
import type { AgentStatusEntry } from '../../../../shared/agent-status/contracts/agent-status-types'
import {
HOST_AGENT_SIGN_IN_AGENTS,
type HostAgentSignInAgent,
@@ -22,64 +21,61 @@ export function cloudRowSignInAgent(agentType: string): HostAgentSignInAgent | n
return HOST_AGENT_SIGN_IN_AGENTS.find((agent) => agent === agentType) ?? null
}
/** The turn evidence on a row: what only a turn that ran and ended leaves behind. */
export type CloudRowTurnEvidence = Pick<
AgentStatusEntry,
'lastAssistantMessage' | 'stateHistory' | 'turnEndedAt'
>
/** Row states that claim nothing live: a sign-in state replaces them. Working and waiting keep theirs. */
const SIGN_IN_OVERRIDABLE_DOTS: ReadonlySet<AgentDotState> = new Set([
'done',
'idle',
'unverifiable',
'unconfirmed'
])
/**
* True when no turn has finished: no state history, no turn end and no assistant reply. Hook
* markers such as sessionBoundary are not used: a row mirrored from a cloud host can lack them.
*/
export function hasNoFinishedTurn(entry: CloudRowTurnEvidence): boolean {
return (
(entry.stateHistory?.length ?? 0) === 0 &&
entry.turnEndedAt === undefined &&
!entry.lastAssistantMessage?.trim()
)
}
/**
* Only a row that has not finished a turn can be waiting on sign-in: a finished turn proves the
* agent reached its provider, so its Done stays.
*/
/** A main Claude/Codex row at rest; whether it is on Orca Cloud is checked after. */
export function isCloudRowSignInCandidate(row: {
agentType: string
dotState: AgentDotState
entry: CloudRowTurnEvidence
isChildRow: boolean
}): boolean {
return (
!row.isChildRow &&
(row.dotState === 'done' || row.dotState === 'idle') &&
cloudRowSignInAgent(row.agentType) !== null &&
hasNoFinishedTurn(row.entry)
SIGN_IN_OVERRIDABLE_DOTS.has(row.dotState) &&
cloudRowSignInAgent(row.agentType) !== null
)
}
/**
* Agent accounts decide: not set up -> needsSignIn, expired or revoked -> expired. A sandbox that
* reports the agent signedIn (an account placeholder, a key, or a /login there) clears the row;
* one that reports signedOut needs sign-in too. A paused or never-offered provider, or a read
* not ready yet, shows nothing; the row updates when the read lands.
* Agent accounts decide.
* - Not set up -> needsSignIn; expired or revoked -> expired. These win over the row's turn
* evidence and over the sandbox reporting an Orca-account placeholder or key as signed in; only
* a sign-in made on the sandbox itself (cliLogin) clears them.
* - Covered (account or key): normal, unless the sandbox reports signedOut, and then only until a
* turn ends after the account became active (`activeSince`).
* - Paused or never offered: only a sandbox signedOut shows needsSignIn. A read not ready yet
* shows nothing; the row updates when it lands.
*/
export function cloudAgentRowSignIn(
agent: HostAgentSignInAgent,
accounts: OrcaCloudAgentAccountsRead,
hostEntry: HostAgentSignInEntry | null
hostEntry: HostAgentSignInEntry | null,
turn: { turnEndedAt?: number; activeSince: number | null }
): CloudAgentRowSignIn | null {
if (hostEntry?.state === 'signedIn' || hostEntry?.state === 'notInstalled') {
if (hostEntry?.state === 'notInstalled') {
return null
}
const coverage = orcaCloudAgentCoverageFromAccounts(agent, accounts)?.kind
if (coverage === 'expired') {
return 'expired'
if (coverage === 'expired' || coverage === 'notSetUp') {
if (hostEntry?.state === 'signedIn' && hostEntry.method === 'cliLogin') {
return null
}
return coverage === 'expired' ? 'expired' : 'needsSignIn'
}
if (coverage === 'notSetUp' || hostEntry?.state === 'signedOut') {
return 'needsSignIn'
if (hostEntry?.state !== 'signedOut') {
return null
}
return null
const turnEndedSinceActive =
turn.activeSince !== null &&
turn.turnEndedAt !== undefined &&
turn.turnEndedAt > turn.activeSince
return turnEndedSinceActive ? null : 'needsSignIn'
}
export function cloudAgentRowSignInLabel(signIn: CloudAgentRowSignIn, t: Translate): string {