diff --git a/src/renderer/src/components/dashboard/DashboardAgentRow.tsx b/src/renderer/src/components/dashboard/DashboardAgentRow.tsx
index 10a436f21b3..1e867dcc7d1 100644
--- a/src/renderer/src/components/dashboard/DashboardAgentRow.tsx
+++ b/src/renderer/src/components/dashboard/DashboardAgentRow.tsx
@@ -423,7 +423,7 @@ const DashboardAgentRow = React.memo(function DashboardAgentRow({
expanded={expanded}
isInterrupted={isInterrupted}
stoppedByUser={agentMainAgentVerdict(agent.entry) === 'cancellation'}
- lastAssistantMessage={lastAssistantMessage}
+ lastAssistantMessage={cloudStatus ? '' : lastAssistantMessage}
/>
)
diff --git a/src/renderer/src/components/orca-cloud/CloudAgentRowSignInTarget.tsx b/src/renderer/src/components/orca-cloud/CloudAgentRowSignInTarget.tsx
index 3a666db9a8b..daba491c714 100644
--- a/src/renderer/src/components/orca-cloud/CloudAgentRowSignInTarget.tsx
+++ b/src/renderer/src/components/orca-cloud/CloudAgentRowSignInTarget.tsx
@@ -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 = (
+
+ )
+ if (tooltip === null) {
+ return button
+ }
return (
-
-
-
+
+ {button}
+
+ {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 (
)
}
-/** 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}
+
+ {' - '}
+ {status.agentName}
+
)
}
diff --git a/src/renderer/src/components/orca-cloud/use-cloud-agent-row-sign-in.ts b/src/renderer/src/components/orca-cloud/use-cloud-agent-row-sign-in.ts
index e2c7ce22b73..98464fe0292 100644
--- a/src/renderer/src/components/orca-cloud/use-cloud-agent-row-sign-in.ts
+++ b/src/renderer/src/components/orca-cloud/use-cloud-agent-row-sign-in.ts
@@ -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,
@@ -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)
}
}
diff --git a/src/renderer/src/components/sidebar/workspace-card/activity/CompactAgentRowCloudSignIn.fixture.tsx b/src/renderer/src/components/sidebar/workspace-card/activity/CompactAgentRowCloudSignIn.fixture.tsx
index 090ba4b61de..d19e8522a1a 100644
--- a/src/renderer/src/components/sidebar/workspace-card/activity/CompactAgentRowCloudSignIn.fixture.tsx
+++ b/src/renderer/src/components/sidebar/workspace-card/activity/CompactAgentRowCloudSignIn.fixture.tsx
@@ -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'
+)
diff --git a/src/renderer/src/components/sidebar/workspace-card/activity/worktree-card-compact-agent-row.cloud-sign-in.test.tsx b/src/renderer/src/components/sidebar/workspace-card/activity/worktree-card-compact-agent-row.cloud-sign-in.test.tsx
index 12ef1abd242..9d5bee296d6 100644
--- a/src/renderer/src/components/sidebar/workspace-card/activity/worktree-card-compact-agent-row.cloud-sign-in.test.tsx
+++ b/src/renderer/src/components/sidebar/workspace-card/activity/worktree-card-compact-agent-row.cloud-sign-in.test.tsx
@@ -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(
@@ -149,99 +191,106 @@ function renderRow(agent: DashboardAgentRowData, onActivate = () => {}): HTMLEle
)
})
- 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', () => {
diff --git a/src/renderer/src/components/sidebar/workspace-card/activity/worktree-card-compact-agent-row.tsx b/src/renderer/src/components/sidebar/workspace-card/activity/worktree-card-compact-agent-row.tsx
index 96b080d7b46..fc337f0c942 100644
--- a/src/renderer/src/components/sidebar/workspace-card/activity/worktree-card-compact-agent-row.tsx
+++ b/src/renderer/src/components/sidebar/workspace-card/activity/worktree-card-compact-agent-row.tsx
@@ -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 ? (
-
+
) : (
()
+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()
+}
diff --git a/src/renderer/src/lib/orca-cloud/cloud-agent-row-sign-in.ts b/src/renderer/src/lib/orca-cloud/cloud-agent-row-sign-in.ts
index 447b9dd4f94..e9308be89c3 100644
--- a/src/renderer/src/lib/orca-cloud/cloud-agent-row-sign-in.ts
+++ b/src/renderer/src/lib/orca-cloud/cloud-agent-row-sign-in.ts
@@ -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 = 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 {