diff --git a/src/renderer/src/components/agent-sign-in-strip/CloudAgentSignInStrip.tsx b/src/renderer/src/components/agent-sign-in-strip/CloudAgentSignInStrip.tsx
index 3132c871845..5583bcb9e7c 100644
--- a/src/renderer/src/components/agent-sign-in-strip/CloudAgentSignInStrip.tsx
+++ b/src/renderer/src/components/agent-sign-in-strip/CloudAgentSignInStrip.tsx
@@ -18,6 +18,7 @@ import { Button } from '@/components/ui/button'
import type { ExternalLinkSource } from '@/components/open-external/copy-external-link'
import { CloudStripPasteForm } from './CloudStripPasteForm'
import { StripPrimaryButton, stripPrimaryLink } from './CloudStripPrimaryButton'
+import { CloudStripSettingsLink } from './CloudStripSettingsLink'
import {
DropdownMenu,
DropdownMenuContent,
@@ -103,6 +104,8 @@ export type CloudStripActions = {
* setup's sign-in page, known only to main). Actions carrying a `url` copy it without this.
*/
linkFor?: (action: CloudStripPrimary) => ExternalLinkSource | null
+ /** CHAT-RENEW-INPLACE: the model's `settingsLink` ("Renew in Settings"). */
+ openSettings?: () => void
}
/** GA-P3t: the strip's severity look; anything that isn't a warning or an error reads as info. */
@@ -336,6 +339,9 @@ export function CloudAgentSignInStrip({
) : null}
+ {model.settingsLink && !stacked ? (
+
+ ) : null}
{/* Renew and Restart are the whole action and the strip leaves on its own; Escape still cancels. */}
{model.noClose ||
(!model.dismiss &&
@@ -378,6 +384,13 @@ export function CloudAgentSignInStrip({
{model.primary.label}
) : null}
+ {model.settingsLink ? (
+
+ ) : null}
) : null}
diff --git a/src/renderer/src/components/agent-sign-in-strip/CloudRenewInPlace.fixture.tsx b/src/renderer/src/components/agent-sign-in-strip/CloudRenewInPlace.fixture.tsx
new file mode 100644
index 00000000000..387efbaaf71
--- /dev/null
+++ b/src/renderer/src/components/agent-sign-in-strip/CloudRenewInPlace.fixture.tsx
@@ -0,0 +1,136 @@
+// CHAT-RENEW-INPLACE: the in-place renewal, step by step, on all three surfaces (a launch held on the
+// expired sign-in, a running terminal, a structured chat). design-squad's per-step copy.
+import React from 'react'
+import { defineFixture } from '../../component-fixtures/component-fixture'
+import { translate } from '@/i18n/i18n'
+import type { OrcaCloudAgentSetup } from '../../../../shared/orca-cloud-agents/orca-cloud-agents-wire'
+import {
+ CloudAgentSignInStrip,
+ CloudStripLayer,
+ cloudStripWidth,
+ type CloudStripActions
+} from './CloudAgentSignInStrip'
+import type { CloudStripModel } from './cloud-agent-strip-model'
+import { cloudAgentStripModel } from './cloud-agent-strip-model'
+import { cloudAgentAwaitingStartStripModel } from './cloud-agent-setup-strip-model'
+import { cloudChatRunningStripModel, cloudRunningStripModel } from './cloud-running-strip-model'
+import { cloudRenewInPlaceModel } from './cloud-renew-in-place'
+
+const noop = (): void => {}
+const ACTIONS: CloudStripActions = {
+ primary: noop,
+ submitCode: noop,
+ pasteCode: noop,
+ useCode: noop,
+ useApiKey: noop,
+ cancel: noop,
+ openSettings: noop
+}
+
+type Surface = 'held' | 'terminal' | 'chat'
+type Step = 'expired' | 'browser' | 'checking' | 'rejected' | 'renewed'
+
+const SETUP: Record, OrcaCloudAgentSetup> = {
+ expired: { phase: 'idle' },
+ browser: { phase: 'browser', startedAt: 0, canReopen: true, acceptsCode: true },
+ checking: {
+ phase: 'browser',
+ startedAt: 0,
+ canReopen: true,
+ acceptsCode: true,
+ code: 'checking'
+ },
+ rejected: { phase: 'browser', startedAt: 0, canReopen: true, acceptsCode: true, code: 'rejected' }
+}
+
+function expiredModel(surface: Surface): CloudStripModel {
+ const strip = { phase: 'expired' as const, agent: 'claude' as const }
+ return surface === 'held'
+ ? cloudAgentStripModel({
+ agent: 'claude',
+ reason: 'accountExpired',
+ view: null,
+ hostLabel: '',
+ t: translate
+ })
+ : surface === 'chat'
+ ? cloudChatRunningStripModel(strip, translate)
+ : cloudRunningStripModel(strip, translate)
+}
+
+function stepModel(surface: Surface, step: Step): CloudStripModel {
+ if (step !== 'renewed') {
+ return cloudRenewInPlaceModel({
+ agent: 'claude',
+ expired: expiredModel(surface),
+ setup: SETUP[step],
+ t: translate
+ })
+ }
+ const renewed = { phase: 'renewed' as const, agent: 'claude' as const, resumable: true }
+ // Step 5 is each surface's own: held waits for Start (team-lead B), the terminal offers Restart
+ // (RA-11), the chat's next message uses the renewed sign-in.
+ return surface === 'held'
+ ? cloudAgentAwaitingStartStripModel({
+ agent: 'claude',
+ awaiting: 'renewed',
+ queuedPrompt: true,
+ t: translate
+ })
+ : surface === 'chat'
+ ? cloudChatRunningStripModel(renewed, translate)
+ : cloudRunningStripModel(renewed, translate)
+}
+
+function Frame({
+ surface,
+ step,
+ width
+}: {
+ surface: Surface
+ step: Step
+ width: number
+}): React.JSX.Element {
+ return (
+
+ )
+}
+
+const fixture = (surface: Surface, step: Step, width = 900) =>
+ defineFixture({
+ width: width + 40,
+ render: () =>
+ })
+
+export const heldExpired = fixture('held', 'expired')
+export const heldBrowser = fixture('held', 'browser')
+export const heldChecking = fixture('held', 'checking')
+export const heldRejected = fixture('held', 'rejected')
+export const heldRenewed = fixture('held', 'renewed')
+export const terminalExpired = fixture('terminal', 'expired')
+export const terminalBrowser = fixture('terminal', 'browser')
+export const terminalChecking = fixture('terminal', 'checking')
+export const terminalRejected = fixture('terminal', 'rejected')
+export const terminalRenewed = fixture('terminal', 'renewed')
+export const chatExpired = fixture('chat', 'expired')
+export const chatBrowser = fixture('chat', 'browser')
+export const chatChecking = fixture('chat', 'checking')
+export const chatRejected = fixture('chat', 'rejected')
+export const chatRenewed = fixture('chat', 'renewed')
+/** Narrow: the code field stacks under the text, and "Renew in Settings" gets its own line. */
+export const chatBrowserNarrow = fixture('chat', 'browser', 440)
+export const terminalRejectedNarrow = fixture('terminal', 'rejected', 440)
diff --git a/src/renderer/src/components/agent-sign-in-strip/CloudRunningStripPortal.test.tsx b/src/renderer/src/components/agent-sign-in-strip/CloudRunningStripPortal.test.tsx
index 33e5548aa71..33af6feba9f 100644
--- a/src/renderer/src/components/agent-sign-in-strip/CloudRunningStripPortal.test.tsx
+++ b/src/renderer/src/components/agent-sign-in-strip/CloudRunningStripPortal.test.tsx
@@ -49,7 +49,14 @@ vi.mock('@/lib/orca-cloud/orca-cloud-environment-id', () => ({
}))
vi.mock('@/lib/orca-cloud/orca-cloud-agent-accounts-store', () => ({
orcaCloudAgentAccountsLoadedAt: () => 0,
- useOrcaCloudAgentAccounts: () => ({ load: { kind: 'loading' }, keyKinds: {} })
+ useOrcaCloudAgentAccounts: () => ({
+ load: { kind: 'loading' },
+ keyKinds: {},
+ setups: { claude: { phase: 'idle' }, codex: { phase: 'idle' } }
+ }),
+ runOrcaCloudAgentSetups: () => undefined,
+ stopOrcaCloudAgentSetups: () => undefined,
+ reloadOrcaCloudAgentAccounts: async () => undefined
}))
vi.mock('@/lib/orca-cloud/cloud-running-agent-signout', () => ({
clearCloudRunningSignout: () => undefined,
diff --git a/src/renderer/src/components/agent-sign-in-strip/CloudRunningStripPortal.tsx b/src/renderer/src/components/agent-sign-in-strip/CloudRunningStripPortal.tsx
index bdfde2a50c4..690d5b6aa60 100644
--- a/src/renderer/src/components/agent-sign-in-strip/CloudRunningStripPortal.tsx
+++ b/src/renderer/src/components/agent-sign-in-strip/CloudRunningStripPortal.tsx
@@ -42,6 +42,7 @@ import {
cloudRestartFreshModel,
cloudRunningStripModel
} from './cloud-running-strip-model'
+import { openOrcaCloudAccountsSettings, withRenewInPlace } from './cloud-renew-in-place'
const NO_ACTIONS: CloudStripActions = {
primary: () => undefined,
@@ -52,12 +53,6 @@ const NO_ACTIONS: CloudStripActions = {
cancel: () => undefined
}
-function openAccounts(): void {
- const store = useAppStore.getState()
- store.openSettingsTarget({ pane: 'accounts', repoId: null, sectionId: 'orca-cloud' })
- store.openSettingsPage()
-}
-
export function CloudRunningStripPortal({
pane,
tabId,
@@ -192,7 +187,7 @@ export function CloudRunningStripPortal({
const actions: CloudStripActions = {
primary: (action) => {
if (action.kind === 'renew') {
- openAccounts()
+ openOrcaCloudAccountsSettings()
} else if (action.kind === 'restart' && strip.phase === 'renewed') {
restart({ agent: strip.agent, resumable: strip.resumable })
}
@@ -204,6 +199,19 @@ export function CloudRunningStripPortal({
// State 2's ×; state 1 has none (Renew is the action) and Escape leaves it showing.
cancel: () => (strip.phase === 'renewed' ? clearCloudRunningSignout(paneKey) : undefined)
}
+ // CHAT-RENEW-INPLACE: "sign-in expired · Renew" renews right here; renewed keeps RA-11's Restart.
+ const model = cloudRunningStripModel(strip, t)
+ const shown =
+ strip.phase === 'expired'
+ ? withRenewInPlace({
+ agent: strip.agent,
+ setup: accounts.setups[strip.agent],
+ model,
+ actions,
+ key: paneKey,
+ t
+ })
+ : { model, actions }
return (
)
diff --git a/src/renderer/src/components/agent-sign-in-strip/CloudStripSettingsLink.tsx b/src/renderer/src/components/agent-sign-in-strip/CloudStripSettingsLink.tsx
new file mode 100644
index 00000000000..e8bc3692d04
--- /dev/null
+++ b/src/renderer/src/components/agent-sign-in-strip/CloudStripSettingsLink.tsx
@@ -0,0 +1,34 @@
+import React from 'react'
+import { cn } from '@/lib/class-names'
+
+/**
+ * CHAT-RENEW-INPLACE: the strip's quiet "Renew in Settings" link, at the end of the actions row, or on
+ * its own line under the buttons in the narrow stack (design-squad).
+ */
+export function CloudStripSettingsLink({
+ label,
+ onOpen,
+ ownLine = false
+}: {
+ label: string
+ onOpen?: () => void
+ ownLine?: boolean
+}): React.JSX.Element | null {
+ if (!onOpen) {
+ return null
+ }
+ return (
+
+ )
+}
diff --git a/src/renderer/src/components/agent-sign-in-strip/cloud-agent-strip-model.test.ts b/src/renderer/src/components/agent-sign-in-strip/cloud-agent-strip-model.test.ts
index 547f12c3306..388752f0dd0 100644
--- a/src/renderer/src/components/agent-sign-in-strip/cloud-agent-strip-model.test.ts
+++ b/src/renderer/src/components/agent-sign-in-strip/cloud-agent-strip-model.test.ts
@@ -5,11 +5,11 @@ import { cloudAgentStripModel } from './cloud-agent-strip-model'
const base = { agent: 'claude' as const, hostLabel: 'the sandbox', t: translate }
describe('cloudAgentStripModel', () => {
- it('account recovery (S4-c): Renew, account-level wording, no per-workspace words', () => {
+ it('account recovery (S4-c), held: Renew, "Renew to start Claude." (CHAT-RENEW-INPLACE copy)', () => {
const model = cloudAgentStripModel({ ...base, reason: 'accountExpired', view: null })
expect(model).toMatchObject({
title: 'Your Claude sign-in expired',
- detail: 'Sign in once to fix every workspace.',
+ detail: 'Renew to start Claude.',
primary: { label: 'Renew', action: { kind: 'renew' } }
})
})
diff --git a/src/renderer/src/components/agent-sign-in-strip/cloud-agent-strip-model.ts b/src/renderer/src/components/agent-sign-in-strip/cloud-agent-strip-model.ts
index 3f7b7a6baa6..e0f7827e35e 100644
--- a/src/renderer/src/components/agent-sign-in-strip/cloud-agent-strip-model.ts
+++ b/src/renderer/src/components/agent-sign-in-strip/cloud-agent-strip-model.ts
@@ -57,6 +57,11 @@ export type CloudStripModel = {
otherWays: boolean
/** Already on a code: ⋯ offers the paste fallback and Cancel instead of "Use a code". */
codeStep?: boolean
+ /**
+ * CHAT-RENEW-INPLACE: a quiet text link at the end of the actions row ("Renew in Settings"); in
+ * the narrow stack it gets its own line under the buttons.
+ */
+ settingsLink?: string
/** RA-11 state 2: a dismiss × with this label (the strip otherwise drops its × on Renew). */
dismiss?: string
/** RA-11 "Restarting…": nothing to close; the strip leaves with the result. */
@@ -121,15 +126,15 @@ export function cloudAgentStripModel(args: {
return {
status: 'warning',
title: t('agentSignInStrip.expired', 'Your {{agent}} sign-in expired', { agent }),
- detail: t('agentSignInStrip.expiredDetail', 'Sign in once to fix every workspace.'),
+ detail: t('agentSignInStrip.expiredHeldDetail', 'Renew to start {{agent}}.', { agent }),
description: null,
primary: { label: t('agentSignInStrip.renew', 'Renew'), action: { kind: 'renew' } },
code: null,
paste: null,
otherWays: false,
announcement: t(
- 'agentSignInStrip.expiredAnnounce',
- 'Your {{agent}} sign-in expired. Renew it once to fix every workspace.',
+ 'agentSignInStrip.expiredHeldAnnounce',
+ 'Your {{agent}} sign-in expired. Renew to start {{agent}}.',
{ agent }
)
}
diff --git a/src/renderer/src/components/agent-sign-in-strip/cloud-chat-running-strip.test.tsx b/src/renderer/src/components/agent-sign-in-strip/cloud-chat-running-strip.test.tsx
index 45147d775d6..aac72b6f24e 100644
--- a/src/renderer/src/components/agent-sign-in-strip/cloud-chat-running-strip.test.tsx
+++ b/src/renderer/src/components/agent-sign-in-strip/cloud-chat-running-strip.test.tsx
@@ -21,7 +21,8 @@ const stub = vi.hoisted(() => {
cleared: string[]
calls: { paneKey: string; turnEndedFallback?: boolean }[]
openedSettings: number
- } = { strip: null, held: false, cleared: [], calls: [], openedSettings: 0 }
+ renewsRun: string[][]
+ } = { strip: null, held: false, cleared: [], calls: [], openedSettings: 0, renewsRun: [] }
return state
})
@@ -47,6 +48,9 @@ vi.mock('@/lib/orca-cloud/orca-cloud-environment-id', () => ({
}))
vi.mock('@/lib/orca-cloud/orca-cloud-agent-accounts-store', () => ({
orcaCloudAgentAccountsLoadedAt: () => 1,
+ runOrcaCloudAgentSetups: (agents: string[]) => stub.renewsRun.push(agents),
+ stopOrcaCloudAgentSetups: () => undefined,
+ reloadOrcaCloudAgentAccounts: async () => undefined,
useOrcaCloudAgentAccounts: () => ({
load: { kind: 'loading' },
keyKinds: {},
@@ -118,7 +122,9 @@ describe('the running strip on a structured chat (B2)', () => {
mount()
expect(screen.getAllByText(/Claude sign-in expired/).length).toBeGreaterThan(0)
fireEvent.click(screen.getByRole('button', { name: /Renew/ }))
- expect(stub.openedSettings).toBe(1)
+ // CHAT-RENEW-INPLACE: Renew runs the renewal right here, never Settings.
+ expect(stub.renewsRun).toEqual([['claude']])
+ expect(stub.openedSettings).toBe(0)
expect(stub.calls.at(-1)).toEqual({ paneKey: STATUS_KEY, turnEndedFallback: true })
})
diff --git a/src/renderer/src/components/agent-sign-in-strip/cloud-renew-in-place.test.ts b/src/renderer/src/components/agent-sign-in-strip/cloud-renew-in-place.test.ts
new file mode 100644
index 00000000000..8620ee7337e
--- /dev/null
+++ b/src/renderer/src/components/agent-sign-in-strip/cloud-renew-in-place.test.ts
@@ -0,0 +1,153 @@
+// CHAT-RENEW-INPLACE: every expired strip renews in place, with design-squad's per-step copy.
+import { beforeEach, describe, expect, it, vi } from 'vitest'
+import type { OrcaCloudAgentSetup } from '../../../../shared/orca-cloud-agents/orca-cloud-agents-wire'
+import type { CloudStripActions } from './CloudAgentSignInStrip'
+import type { CloudStripModel } from './cloud-agent-strip-model'
+import { translate as t } from '@/i18n/i18n'
+
+const store = vi.hoisted(() => ({ run: vi.fn(), stop: vi.fn() }))
+vi.mock('@/lib/orca-cloud/orca-cloud-agent-accounts-store', () => ({
+ runOrcaCloudAgentSetups: store.run,
+ stopOrcaCloudAgentSetups: store.stop,
+ reloadOrcaCloudAgentAccounts: async () => undefined
+}))
+vi.mock('@/lib/orca-cloud/cloud-agent-launch-hold', () => ({
+ releaseCloudAgentHold: vi.fn(),
+ startCloudAgentHold: vi.fn()
+}))
+vi.mock('@/store', () => ({ useAppStore: { getState: () => ({}) } }))
+vi.mock('../agent-sign-in/AgentSignInPanePortals', () => ({
+ agentSignInCardActions: () => ({ copyText: vi.fn(), openLink: vi.fn() })
+}))
+const api = vi.hoisted(() => ({ reopen: vi.fn(async () => undefined), submitClaudeCode: vi.fn() }))
+vi.stubGlobal('window', { api: { orcaCloudAgents: api } })
+
+const { cloudRenewInPlaceActions, cloudRenewInPlaceModel } = await import('./cloud-renew-in-place')
+
+const EXPIRED: CloudStripModel = {
+ status: 'warning',
+ title: 'Your Claude sign-in expired',
+ detail: 'Renew to keep working. Your conversation is kept.',
+ description: null,
+ primary: { label: 'Renew', action: { kind: 'renew' } },
+ code: null,
+ paste: null,
+ otherWays: false,
+ announcement: 'Your Claude sign-in expired.'
+}
+
+const model = (setup: OrcaCloudAgentSetup) =>
+ cloudRenewInPlaceModel({ agent: 'claude', expired: EXPIRED, setup, t })
+
+describe('cloudRenewInPlaceModel (design-squad per-step copy)', () => {
+ it("1 expired: the surface's own strip, [Renew], no Settings link", () => {
+ expect(model({ phase: 'idle' })).toBe(EXPIRED)
+ expect(model({ phase: 'saved' })).toBe(EXPIRED)
+ })
+
+ it('2 browser: same title, paste code, [Finish renewing] [Open again], "Renew in Settings"', () => {
+ const shown = model({ phase: 'browser', startedAt: 0, canReopen: true, acceptsCode: true })
+ expect(shown.title).toBe('Your Claude sign-in expired')
+ expect(shown.detail).toBe('Finish in your browser, then paste the code here.')
+ expect(shown.paste?.setupCode).toEqual({
+ placeholder: 'Paste code',
+ submitLabel: 'Finish renewing'
+ })
+ expect(shown.primary).toMatchObject({ label: 'Open again', action: { kind: 'reopenSetup' } })
+ expect(shown.settingsLink).toBe('Renew in Settings')
+ expect(JSON.stringify(shown)).not.toContain('Finish setup')
+ })
+
+ it('3 checking: the field and its button disabled', () => {
+ const shown = model({
+ phase: 'browser',
+ startedAt: 0,
+ canReopen: true,
+ acceptsCode: true,
+ code: 'checking'
+ })
+ expect(shown.paste).toMatchObject({ submitting: true })
+ expect(shown.paste?.setupCode?.submitLabel).toBe('Checking the code…')
+ })
+
+ it('4 rejected: "Code didn\'t work", the newest-code detail, [Open again] and the Settings link', () => {
+ const shown = model({
+ phase: 'browser',
+ startedAt: 0,
+ canReopen: true,
+ acceptsCode: true,
+ code: 'rejected'
+ })
+ expect(shown.title).toBe('Code didn’t work')
+ expect(shown.detail).toBe('Paste the newest code from the sign-in page, or open it again.')
+ expect(shown.paste?.refusal).toBeTruthy()
+ expect(shown.primary?.action.kind).toBe('reopenSetup')
+ expect(shown.settingsLink).toBe('Renew in Settings')
+ })
+
+ it('a setup that failed outright starts again from Renew', () => {
+ const shown = model({ phase: 'failed', error: 'denied' })
+ expect(shown.status).toBe('error')
+ expect(shown.primary?.action.kind).toBe('renew')
+ })
+})
+
+describe('cloudRenewInPlaceActions', () => {
+ const surface = (): CloudStripActions => ({
+ primary: vi.fn(),
+ submitCode: vi.fn(),
+ pasteCode: vi.fn(),
+ useCode: vi.fn(),
+ useApiKey: vi.fn(),
+ cancel: vi.fn()
+ })
+ const actions = (setup: OrcaCloudAgentSetup, own = surface(), openSettings = vi.fn()) => ({
+ own,
+ openSettings,
+ built: cloudRenewInPlaceActions({
+ agent: 'claude',
+ setup,
+ surface: own,
+ openSettings,
+ copyText: vi.fn(),
+ openLink: vi.fn()
+ })
+ })
+ beforeEach(() => {
+ store.run.mockClear()
+ store.stop.mockClear()
+ api.reopen.mockClear()
+ })
+
+ it('Renew runs the setup here (never the Settings page or the sign-in sheet)', () => {
+ const { built, own, openSettings } = actions({ phase: 'idle' })
+ built.primary({ kind: 'renew' })
+ expect(store.run).toHaveBeenCalledExactlyOnceWith(['claude'])
+ expect(own.primary).not.toHaveBeenCalled()
+ expect(openSettings).not.toHaveBeenCalled()
+ })
+
+ it('Open again reopens the setup page; "Renew in Settings" opens Settings', () => {
+ const { built, openSettings } = actions({ phase: 'browser', startedAt: 0, canReopen: true })
+ built.primary({ kind: 'reopenSetup' })
+ expect(api.reopen).toHaveBeenCalledExactlyOnceWith({ agent: 'claude' })
+ built.openSettings?.()
+ expect(openSettings).toHaveBeenCalledOnce()
+ })
+
+ it('6: × mid-flow stops the setup (back to step 1) and never dismisses the strip', () => {
+ const { built, own } = actions({ phase: 'browser', startedAt: 0, canReopen: true })
+ built.cancel()
+ expect(store.stop).toHaveBeenCalledExactlyOnceWith('claude')
+ expect(own.cancel).not.toHaveBeenCalled()
+ })
+
+ it('outside a renewal, the surface keeps its own × and Restart', () => {
+ const { built, own } = actions({ phase: 'idle' })
+ built.cancel()
+ built.primary({ kind: 'restart' })
+ expect(store.stop).not.toHaveBeenCalled()
+ expect(own.cancel).toHaveBeenCalledOnce()
+ expect(own.primary).toHaveBeenCalledExactlyOnceWith({ kind: 'restart' })
+ })
+})
diff --git a/src/renderer/src/components/agent-sign-in-strip/cloud-renew-in-place.ts b/src/renderer/src/components/agent-sign-in-strip/cloud-renew-in-place.ts
new file mode 100644
index 00000000000..f6514d36286
--- /dev/null
+++ b/src/renderer/src/components/agent-sign-in-strip/cloud-renew-in-place.ts
@@ -0,0 +1,159 @@
+// CHAT-RENEW-INPLACE (Option A, design-squad per-step copy): every "sign-in expired" strip — a held
+// launch, a running terminal, a structured chat — renews in place. Renew runs the same Orca Cloud
+// setup as "Set up once" right in the strip (re-running it replaces the account's expired sign-in),
+// instead of sending the person to Settings. One model and one set of actions for every surface, so
+// the terminal and the chat can't drift apart.
+// 1 expired: [Renew] 2 browser: paste code, [Finish renewing] [Open again] ×
+// 3 checking: field and buttons disabled 4 rejected: "Code didn't work", [Open again]
+// 5 renewed: the surface's own renewed state (held: Start; terminal: Restart; chat: next message)
+// × mid-flow stops the setup and goes back to step 1; it never dismisses the strip.
+import type { Translate } from '@/i18n/use-translate'
+import { useAppStore } from '@/store'
+import {
+ runOrcaCloudAgentSetups,
+ stopOrcaCloudAgentSetups
+} from '@/lib/orca-cloud/orca-cloud-agent-accounts-store'
+import type { OrcaCloudAgentSetup } from '../../../../shared/orca-cloud-agents/orca-cloud-agents-wire'
+import type { HostAgentSignInAgent } from '../../../../shared/host-agent-sign-in-status'
+import type { CloudStripActions } from './CloudAgentSignInStrip'
+import type { CloudStripModel } from './cloud-agent-strip-model'
+import { cloudAgentSetupStripModel } from './cloud-agent-setup-strip-model'
+import { cloudSetupStripActions } from './cloud-setup-strip-actions'
+import { agentSignInCardActions } from '../agent-sign-in/AgentSignInPanePortals'
+
+/** The renewal's own setup is under way (steps 2-4): the strip shows it instead of step 1. */
+export function renewInPlaceRunning(setup: OrcaCloudAgentSetup): boolean {
+ return setup.phase !== 'idle' && setup.phase !== 'saved'
+}
+
+/**
+ * Steps 1-4 over a surface's "expired" model (step 1 keeps the surface's own detail: running "Renew
+ * to keep working…", held "Renew to start {{agent}}."). Step 5 is each surface's renewed state.
+ */
+export function cloudRenewInPlaceModel(args: {
+ agent: HostAgentSignInAgent
+ expired: CloudStripModel
+ setup: OrcaCloudAgentSetup
+ t: Translate
+}): CloudStripModel {
+ const { expired, setup, t } = args
+ if (!renewInPlaceRunning(setup)) {
+ return expired
+ }
+ const flow = cloudAgentSetupStripModel({ agent: args.agent, setup, ready: false, t })
+ const settingsLink = t('agentSignInStrip.renewInSettings', 'Renew in Settings')
+ // A failed setup keeps its own words; a refused code says "Code didn't work" (as #484).
+ const keepsTitle =
+ setup.phase === 'failed' || (setup.phase === 'browser' && setup.code === 'rejected')
+ const finishRenewing = t('agentSignInStrip.finishRenewing', 'Finish renewing')
+ const paste = flow.paste
+ ? {
+ ...flow.paste,
+ // Never the setup's "Finish setup" (design-squad).
+ ...(flow.paste.setupCode
+ ? {
+ setupCode: {
+ ...flow.paste.setupCode,
+ submitLabel:
+ setup.phase === 'browser' && setup.code === 'checking'
+ ? flow.paste.setupCode.submitLabel
+ : finishRenewing
+ }
+ }
+ : {}),
+ ...(flow.paste.submitLabel ? { submitLabel: finishRenewing } : {})
+ }
+ : null
+ return {
+ ...flow,
+ title: keepsTitle ? flow.title : expired.title,
+ paste,
+ // A setup that failed outright starts again from Renew.
+ primary:
+ setup.phase === 'failed' && flow.primary?.action.kind === 'setUpOnce'
+ ? { ...flow.primary, action: { kind: 'renew' } }
+ : flow.primary,
+ ...(setup.phase === 'browser' || setup.phase === 'paste' ? { settingsLink } : {}),
+ announcement: keepsTitle ? flow.announcement : `${expired.title}. ${flow.announcement}`
+ }
+}
+
+/**
+ * Renew starts the setup here; Open again, the code and the paste go to it; × stops it (back to
+ * step 1). With no renewal running, everything else is the surface's own (its × or Restart).
+ */
+export function cloudRenewInPlaceActions(args: {
+ agent: HostAgentSignInAgent
+ setup: OrcaCloudAgentSetup
+ /** The surface's own actions, used outside the renewal (Restart, a renewed strip's ×). */
+ surface: CloudStripActions
+ /** "Renew in Settings": the same renewal in Settings › Accounts › Orca Cloud. */
+ openSettings: () => void
+ copyText: (text: string) => void
+ openLink: (url: string) => void
+}): CloudStripActions {
+ const running = renewInPlaceRunning(args.setup)
+ const flow = cloudSetupStripActions({
+ agent: args.agent,
+ // Renewing holds nothing itself: Start (step 5) belongs to the surface.
+ holdKey: '',
+ setupRunning: running,
+ copyText: args.copyText,
+ openLink: args.openLink,
+ useApiKey: args.surface.useApiKey,
+ keepHoldOnCancel: true
+ })
+ return {
+ ...args.surface,
+ primary: (action) => {
+ if (action.kind === 'renew') {
+ runOrcaCloudAgentSetups([args.agent])
+ } else if (action.kind === 'reopenSetup' || action.kind === 'copyAndOpen') {
+ flow.primary(action)
+ } else {
+ args.surface.primary(action)
+ }
+ },
+ submitCode: running ? flow.submitCode : args.surface.submitCode,
+ submitSetupCode: flow.submitSetupCode,
+ pasteCode: running ? flow.pasteCode : args.surface.pasteCode,
+ openSettings: args.openSettings,
+ cancel: () => (running ? stopOrcaCloudAgentSetups(args.agent) : args.surface.cancel())
+ }
+}
+
+/** "Renew in Settings": Settings › Accounts › Orca Cloud, where the same renewal runs. */
+export function openOrcaCloudAccountsSettings(): void {
+ const store = useAppStore.getState()
+ store.openSettingsTarget({ pane: 'accounts', repoId: null, sectionId: 'orca-cloud' })
+ store.openSettingsPage()
+}
+
+/** A surface's expired strip, renewing in place: its model (steps 1-4) and actions together. */
+export function withRenewInPlace(args: {
+ agent: HostAgentSignInAgent
+ setup: OrcaCloudAgentSetup
+ model: CloudStripModel
+ actions: CloudStripActions
+ /** The pane or chat key, for the sign-in page's copy and open. */
+ key: string
+ t: Translate
+}): { model: CloudStripModel; actions: CloudStripActions } {
+ const page = agentSignInCardActions(args.key)
+ return {
+ model: cloudRenewInPlaceModel({
+ agent: args.agent,
+ expired: args.model,
+ setup: args.setup,
+ t: args.t
+ }),
+ actions: cloudRenewInPlaceActions({
+ agent: args.agent,
+ setup: args.setup,
+ surface: args.actions,
+ openSettings: openOrcaCloudAccountsSettings,
+ copyText: page.copyText,
+ openLink: page.openLink
+ })
+ }
+}
diff --git a/src/renderer/src/components/agent-sign-in-strip/cloud-setup-strip-actions.ts b/src/renderer/src/components/agent-sign-in-strip/cloud-setup-strip-actions.ts
index ab90eac24da..7325eea6371 100644
--- a/src/renderer/src/components/agent-sign-in-strip/cloud-setup-strip-actions.ts
+++ b/src/renderer/src/components/agent-sign-in-strip/cloud-setup-strip-actions.ts
@@ -31,7 +31,7 @@ export function cloudSetupStripActions(args: {
keepHoldOnCancel?: boolean
}): CloudStripActions {
const { agent, holdKey } = args
- const api = window.api.orcaCloudAgents
+ const api = window.api?.orcaCloudAgents
return {
primary: (action) => {
if (action.kind === 'setUpOnce') {
diff --git a/src/renderer/src/components/agent-sign-in-strip/use-cloud-chat-running-strip.ts b/src/renderer/src/components/agent-sign-in-strip/use-cloud-chat-running-strip.ts
index 9a5ab27a572..92330c53794 100644
--- a/src/renderer/src/components/agent-sign-in-strip/use-cloud-chat-running-strip.ts
+++ b/src/renderer/src/components/agent-sign-in-strip/use-cloud-chat-running-strip.ts
@@ -26,12 +26,7 @@ import {
import type { CloudStripActions } from './CloudAgentSignInStrip'
import type { CloudHeldStrip } from './use-cloud-held-strip'
import { cloudChatRunningStripModel } from './cloud-running-strip-model'
-
-function openAccounts(): void {
- const store = useAppStore.getState()
- store.openSettingsTarget({ pane: 'accounts', repoId: null, sectionId: 'orca-cloud' })
- store.openSettingsPage()
-}
+import { openOrcaCloudAccountsSettings, withRenewInPlace } from './cloud-renew-in-place'
/** The Orca Cloud environment a structured chat's tab runs on, or null. */
function useChatEnvironmentId(tabId: string): string | null {
@@ -94,7 +89,8 @@ export function useCloudChatAccountStrip(args: ChatAccountArgs): CloudRunningStr
}
return cloudRunningStrip({
paneKey: args.statusKey,
- entry: status ?? { agentType: agent, state: 'idle' },
+ // An idle chat (no message yet) has no status entry: decided from its own agent.
+ entry: status ?? { agentType: agent, state: 'waiting' },
accounts,
held,
now: orcaCloudAgentAccountsLoadedAt(),
@@ -108,11 +104,12 @@ export function useCloudChatAccountStrip(args: ChatAccountArgs): CloudRunningStr
export function useCloudChatRunningStrip(args: ChatAccountArgs): CloudHeldStrip | null {
const t = useTranslate()
const strip = useCloudChatAccountStrip(args)
+ const accounts = useOrcaCloudAgentAccounts(strip !== null)
if (!strip) {
return null
}
const actions: CloudStripActions = {
- primary: (action) => (action.kind === 'renew' ? openAccounts() : undefined),
+ primary: (action) => (action.kind === 'renew' ? openOrcaCloudAccountsSettings() : undefined),
submitCode: () => undefined,
pasteCode: () => undefined,
useCode: () => undefined,
@@ -120,7 +117,20 @@ export function useCloudChatRunningStrip(args: ChatAccountArgs): CloudHeldStrip
// Expired has no ×; renewed's × dismisses.
cancel: () => (strip.phase === 'renewed' ? clearCloudRunningSignout(args.statusKey) : undefined)
}
- return { model: cloudChatRunningStripModel(strip, t), prompt: null, actions }
+ const model = cloudChatRunningStripModel(strip, t)
+ // CHAT-RENEW-INPLACE: the chat's "sign-in expired · Renew" renews right here.
+ const shown =
+ strip.phase === 'expired'
+ ? withRenewInPlace({
+ agent: strip.agent,
+ setup: accounts.setups[strip.agent],
+ model,
+ actions,
+ key: args.statusKey,
+ t
+ })
+ : { model, actions }
+ return { ...shown, prompt: null }
}
/**
diff --git a/src/renderer/src/components/agent-sign-in-strip/use-cloud-held-strip.ts b/src/renderer/src/components/agent-sign-in-strip/use-cloud-held-strip.ts
index fa9989f86ae..5711413b7db 100644
--- a/src/renderer/src/components/agent-sign-in-strip/use-cloud-held-strip.ts
+++ b/src/renderer/src/components/agent-sign-in-strip/use-cloud-held-strip.ts
@@ -21,6 +21,11 @@ import {
cloudAgentSetupStripModel
} from './cloud-agent-setup-strip-model'
import { cloudSetupStripActions } from './cloud-setup-strip-actions'
+import {
+ cloudRenewInPlaceActions,
+ cloudRenewInPlaceModel,
+ openOrcaCloudAccountsSettings
+} from './cloud-renew-in-place'
export type CloudHeldStrip = {
model: CloudStripModel
@@ -70,7 +75,7 @@ export function useCloudHeldStrip(
const setupHold = hold?.reason === 'notSetUp' || awaiting !== null
const setup = accounts.setups[agent]
// HELD-STALE-COVERED: the setup just finished here: "is set up · Start" (the account, read fresh).
- const model =
+ const shownBeforeRenew =
awaiting === 'setUp'
? cloudAgentSetupStripModel({ agent, setup: accounts.setups[agent], ready: true, t })
: awaiting
@@ -95,6 +100,11 @@ export function useCloudHeldStrip(
hostLabel: cloudGate?.hostLabel ?? t('agentSignInStrip.sandbox', 'the sandbox'),
t
})
+ // CHAT-RENEW-INPLACE: a launch held on an expired sign-in renews right here (steps 1-4).
+ const renewing = hold?.reason === 'accountExpired'
+ const model = renewing
+ ? cloudRenewInPlaceModel({ agent, expired: shownBeforeRenew, setup, t })
+ : shownBeforeRenew
const setupRunning = setup.phase !== 'idle' && setup.phase !== 'saved' && setup.phase !== 'failed'
const holdActions: CloudStripActions | null = setupHold
? cloudSetupStripActions({
@@ -107,7 +117,7 @@ export function useCloudHeldStrip(
keepHoldOnCancel: chat
})
: null
- const actions: CloudStripActions = holdActions ?? {
+ const gateOrRecovery: CloudStripActions = {
primary: (action) => {
// RA-11's restart belongs to the running-pane strip; a held launch never offers it.
if (
@@ -152,9 +162,21 @@ export function useCloudHeldStrip(
}
}
}
+ const actions: CloudStripActions =
+ holdActions ??
+ (renewing
+ ? cloudRenewInPlaceActions({
+ agent,
+ setup,
+ surface: gateOrRecovery,
+ openSettings: openOrcaCloudAccountsSettings,
+ copyText: gateActions.copyText,
+ openLink: gateActions.openLink
+ })
+ : gateOrRecovery)
// A held chat's strip has nothing to cancel unless a setup is running: no × (Escape is a no-op).
const shownModel =
- chat && hold && !(setupHold && setupRunning) ? { ...model, noClose: true } : model
+ chat && hold && !((setupHold || renewing) && setupRunning) ? { ...model, noClose: true } : model
return {
key,
active,
diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json
index dc213e5b036..7caf5e1b393 100644
--- a/src/renderer/src/i18n/locales/en.json
+++ b/src/renderer/src/i18n/locales/en.json
@@ -29017,6 +29017,10 @@
"nothingNeedsAttention": "Nothing needs attention."
},
"agentSignInStrip": {
+ "renewInSettings": "Renew in Settings",
+ "finishRenewing": "Finish renewing",
+ "expiredHeldDetail": "Renew to start {{agent}}.",
+ "expiredHeldAnnounce": "Your {{agent}} sign-in expired. Renew to start {{agent}}.",
"rowNeedsSignIn": "Needs sign-in",
"rowNeedsSignInTooltip": "Set up once for Orca Cloud",
"rowExpired": "Sign-in expired",