wip chat-renew-inplace: idle strip + send block

This commit is contained in:
Neil
2026-11-12 23:37:20 -08:00
parent 65b4b3b0d1
commit 7eccb72a9a
6 changed files with 126 additions and 23 deletions
@@ -22,16 +22,19 @@ function tabStillOpen(tabId: string): boolean {
export function CloudStructuredChatBanners({
tabId,
paneKey,
statusKey
statusKey,
agent
}: {
tabId: string
/** The launch-hold key (from the session id). */
paneKey: string
/** The agent-status key (tab and session): the running strip (B2) reads the agent through it. */
statusKey: string
/** The chat's agent, for an idle chat with no status yet. */
agent?: string
}): React.JSX.Element {
const { key, active, strip } = useCloudHeldStrip(paneKey, null, { chat: true })
const running = useCloudChatRunningStrip({ tabId, statusKey, holdKey: paneKey })
const running = useCloudChatRunningStrip({ tabId, statusKey, holdKey: paneKey, agent })
const shown = strip ?? running
useEffect(() => {
if (!active) {
@@ -2,7 +2,7 @@
// CHAT-BANNERS B2: RA-11's running strip on a structured chat. Expired shows Renew; renewed confirms
// with no Restart (the chat's next message uses the renewed sign-in) and its × dismisses; a held
// launch's strip wins over it.
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
import { cleanup, fireEvent, render, renderHook, screen } from '@testing-library/react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { TooltipProvider } from '@/components/ui/tooltip'
@@ -94,6 +94,7 @@ vi.stubGlobal(
)
const { CloudStructuredChatBanners } = await import('./CloudStructuredChatBanners')
const { useCloudChatSendBlock } = await import('./use-cloud-chat-running-strip')
const mount = () =>
render(
@@ -139,3 +140,54 @@ describe('the running strip on a structured chat (B2)', () => {
expect(screen.queryByRole('button', { name: /Renew/ })).toBeNull()
})
})
describe('an idle chat (no message yet, no status entry)', () => {
const IDLE_TAB = 'structured-agent-session-claude_idle'
const idle = () =>
render(
<TooltipProvider>
<CloudStructuredChatBanners
tabId={TAB_ID}
paneKey={HOLD_KEY}
statusKey={`${IDLE_TAB}:no-status`}
agent="claude"
/>
</TooltipProvider>
)
it("expired account: the strip shows, decided from the chat's own agent", () => {
stub.strip = { phase: 'expired', agent: 'claude' }
idle()
expect(screen.getAllByText(/Claude sign-in expired/).length).toBeGreaterThan(0)
expect(stub.calls.at(-1)?.paneKey).toBe(`${IDLE_TAB}:no-status`)
})
it('covered account: no strip', () => {
idle()
expect(screen.queryByRole('region')).toBeNull()
})
it('a held launch shows the held strip instead', () => {
stub.strip = { phase: 'expired', agent: 'claude' }
stub.held = true
idle()
expect(screen.getByRole('button', { name: 'Set up once' })).toBeTruthy()
})
})
describe('Send while the account is expired', () => {
const args = { tabId: TAB_ID, statusKey: STATUS_KEY, holdKey: HOLD_KEY, agent: 'claude' }
it('expired: Send is held with the Renew hint (typing is not touched)', () => {
stub.strip = { phase: 'expired', agent: 'claude' }
const { result } = renderHook(() => useCloudChatSendBlock(args))
expect(result.current).toBe('Renew Claude sign-in above to send')
})
it('renewed or covered: Send is free again', () => {
stub.strip = { phase: 'renewed', agent: 'claude', resumable: true }
expect(renderHook(() => useCloudChatSendBlock(args)).result.current).toBeUndefined()
stub.strip = null
expect(renderHook(() => useCloudChatSendBlock(args)).result.current).toBeUndefined()
})
})
@@ -4,7 +4,10 @@
// with the renewed sign-in.
import { useAppStore } from '@/store'
import { useTranslate } from '@/i18n/use-translate'
import type { HostAgentSignInAgent } from '../../../../shared/host-agent-sign-in-status'
import {
HOST_AGENT_SIGN_IN_AGENT_NAME,
type HostAgentSignInAgent
} from '../../../../shared/host-agent-sign-in-status'
import { parseExecutionHostId } from '../../../../shared/execution-host'
import { useCloudAgentHold } from '@/lib/orca-cloud/cloud-agent-launch-hold'
import {
@@ -15,6 +18,7 @@ import { isOrcaCloudEnvironmentId } from '@/lib/orca-cloud/orca-cloud-environmen
import {
clearCloudRunningSignout,
cloudRunningStrip,
type CloudRunningStrip,
useCloudRunningAccountReads,
useCloudRunningSignoutVersion,
useWorkspaceUsesAccount
@@ -43,27 +47,33 @@ function useChatEnvironmentId(tabId: string): string | null {
})
}
/**
* statusKey: the chat's agent-status key (its tab and session); holdKey: its launch-hold key. A held
* launch shows the held strip instead.
*/
export function useCloudChatRunningStrip(args: {
type ChatAccountArgs = {
tabId: string
statusKey: string
holdKey: string
}): CloudHeldStrip | null {
const t = useTranslate()
/** The chat's own agent: an idle chat (no message yet) has no status entry to name it. */
agent?: string
}
/**
* The RA-11 decision for a structured chat: expired / renewed / none. An idle chat with no status
* entry is decided from its own agent, so the strip shows as soon as the account expires, before a
* first message could fail. A held launch wins (null here).
*/
export function useCloudChatAccountStrip(args: ChatAccountArgs): CloudRunningStrip | null {
const revision = useCloudRunningSignoutVersion()
const environmentId = useChatEnvironmentId(args.tabId)
const cloud = environmentId !== null && isOrcaCloudEnvironmentId(environmentId)
const entry = useAppStore((state) =>
const status = useAppStore((state) =>
cloud ? (state.agentStatusByPaneKey[args.statusKey] ?? null) : null
)
const agentType = status?.agentType ?? args.agent
const agent: HostAgentSignInAgent | null =
entry?.agentType === 'claude' ? 'claude' : entry?.agentType === 'codex' ? 'codex' : null
const running = cloud && agent !== null
useCloudRunningAccountReads(running)
const accounts = useOrcaCloudAgentAccounts(running)
agentType === 'claude' ? 'claude' : agentType === 'codex' ? 'codex' : null
const watched = cloud && agent !== null
// Focus and the 30 s poll keep the account read fresh for any open cloud chat of an account agent.
useCloudRunningAccountReads(watched)
const accounts = useOrcaCloudAgentAccounts(watched)
// D13: only a workspace that runs the agent on the person's own account (never shared or keyed).
const credential =
agent && accounts.load.kind === 'ready' ? accounts.load.status.credentials[agent] : null
@@ -73,18 +83,18 @@ export function useCloudChatRunningStrip(args: {
credential.state !== 'expiring' &&
credential.state !== 'none'
const usesAccount = useWorkspaceUsesAccount(
running ? environmentId : null,
watched ? environmentId : null,
agent,
expired,
orcaCloudAgentAccountsLoadedAt()
)
const held = useCloudAgentHold(args.holdKey) !== null
if (!running) {
if (!watched || !agent) {
return null
}
const strip = cloudRunningStrip({
return cloudRunningStrip({
paneKey: args.statusKey,
entry,
entry: status ?? { agentType: agent, state: 'idle' },
accounts,
held,
now: orcaCloudAgentAccountsLoadedAt(),
@@ -92,6 +102,12 @@ export function useCloudChatRunningStrip(args: {
revision,
turnEndedFallback: true
})
}
/** The strip a structured chat shows for its account while its agent runs or waits idle. */
export function useCloudChatRunningStrip(args: ChatAccountArgs): CloudHeldStrip | null {
const t = useTranslate()
const strip = useCloudChatAccountStrip(args)
if (!strip) {
return null
}
@@ -106,3 +122,17 @@ export function useCloudChatRunningStrip(args: {
}
return { model: cloudChatRunningStripModel(strip, t), prompt: null, actions }
}
/**
* While the chat's account is expired, typing stays open and only Send is held, with this hint
* (the person's draft is never lost); Renew lifts it at once.
*/
export function useCloudChatSendBlock(args: ChatAccountArgs): string | undefined {
const t = useTranslate()
const strip = useCloudChatAccountStrip(args)
return strip?.phase === 'expired'
? t('orcaCloud.chatSend.renewAbove', 'Renew {{agent}} sign-in above to send', {
agent: HOST_AGENT_SIGN_IN_AGENT_NAME[strip.agent]
})
: undefined
}
@@ -1,6 +1,8 @@
import { translate } from '@/i18n/i18n'
import { useCloudChatHoldHint } from '../../agent-sign-in-strip/use-cloud-chat-hold-hint'
import { structuredCloudHoldKey } from '@/lib/orca-cloud/cloud-structured-launch-hold'
import { useCloudChatSendBlock } from '../../agent-sign-in-strip/use-cloud-chat-running-strip'
import { structuredAgentSessionPaneKey } from '../../../../../shared/agent-session/view/structured-agent-session-projection'
import { useTranslate, type Translate } from '@/i18n/use-translate'
import {
managedRuntimeInstallCard,
@@ -33,7 +35,10 @@ const RESERVED: NativeChatInstallSlotState = { kind: 'reserved' }
/** Reserve unknown readiness with an editable composer and held sends; missing runtimes show installation, while read failures leave it alone. */
export function useNativeChatInstallSlot(
props: Pick<NativeChatStructuredViewProps, 'target' | 'agent' | 'isVisible' | 'sessionId'>,
props: Pick<
NativeChatStructuredViewProps,
'target' | 'agent' | 'isVisible' | 'sessionId' | 'tabId'
>,
agentLabel: string,
layout: ManagedRuntimeInstallLayout
): {
@@ -49,7 +54,15 @@ export function useNativeChatInstallSlot(
hostProgram
)
// CHAT-BANNERS: an Orca Cloud launch held on the chat's strip has no agent to send to yet.
const cloudHoldHint = useCloudChatHoldHint(structuredCloudHoldKey(props.sessionId), '')
const cloudHoldKey = structuredCloudHoldKey(props.sessionId)
const cloudHoldHint = useCloudChatHoldHint(cloudHoldKey, '')
// While its Orca Cloud account is expired, the chat takes typing but holds Send (draft kept).
const cloudSendBlock = useCloudChatSendBlock({
tabId: props.tabId,
statusKey: structuredAgentSessionPaneKey(props.tabId, props.sessionId),
holdKey: cloudHoldKey,
agent: props.agent
})
const unavailableHint =
cloudHoldHint ??
(slot.kind === 'install'
@@ -57,7 +70,8 @@ export function useNativeChatInstallSlot(
: undefined)
return {
slot,
sendBlockedReason: unavailableHint ?? installSlotSendBlockedReason(slot, agentLabel, t),
sendBlockedReason:
unavailableHint ?? cloudSendBlock ?? installSlotSendBlockedReason(slot, agentLabel, t),
unavailableHint
}
}
@@ -152,6 +152,7 @@ export function nativeChatPaneHeader(
tabId={view.tabId}
paneKey={structuredCloudHoldKey(view.sessionId)}
statusKey={structuredAgentSessionPaneKey(view.tabId, view.sessionId)}
agent={view.agent}
/>
)
const header =
+3
View File
@@ -28413,6 +28413,9 @@
"setUpNotStarted": "{{agent}} is set up. Open the workspace to start it.",
"notStartedRow": "{{agent}} hasn’t started"
},
"chatSend": {
"renewAbove": "Renew {{agent}} sign-in above to send"
},
"chatSignIn": {
"renewAbove": "Your {{agent}} sign-in for Orca Cloud didn’t work. Renew it above, then send your message again."
},