mirror of
https://github.com/stablyai/orca.git
synced 2026-11-15 18:37:24 +00:00
wip chat-renew-inplace: idle strip + send block
This commit is contained in:
@@ -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 =
|
||||
|
||||
@@ -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."
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user