fix(native-chat): put Continue in the card that says the reply stopped (#27097)

* fix(native-chat): put Continue in the card that says Orca stopped the reply

Continue sat in its own row above the message box, away from the
reply it continues and often outside the chat column. It now sits
inside the transcript's notice card that says why the reply stopped,
as a filled primary button beside that sentence.

* fix(native-chat): one Continue that the stop card never repeats

- A paired server with no name is called "the remote server", so the card
  words the stop from its cause instead of the host's stored "You can
  continue in this conversation." beside the Continue button.
- Continue is an xs primary button that fits the text row without a margin
  workaround.
- continueNow is a plain useCallback: its inputs (target, session id, the
  composer error reporter) are already stable.
- Test the real chain: a cut journal through the structured session shows
  exactly one Continue, inside the stop card.
This commit is contained in:
Brennan Benson
2026-10-11 13:01:52 -07:00
committed by GitHub
parent 28d07a6b63
commit 37c2b43876
13 changed files with 249 additions and 67 deletions
@@ -42,7 +42,7 @@ import type {
} from '../../../../shared/agent-session-journal-types'
import type { RuntimeClientTarget } from '@/runtime/runtime-client-target'
import {
NativeChatInterruptedContinue,
NativeChatInterruptedContinueButton,
useNativeChatInterruptedContinuation
} from './NativeChatInterruptedContinue'
@@ -78,7 +78,7 @@ function Harness(props: Props): React.JSX.Element {
// The chat's own composer error, as NativeChatStructuredSession holds it.
const [composerError, setComposerError] = useState<string | null>(null)
const continuation = useNativeChatInterruptedContinuation({
composer: { clearError: () => setComposerError(null) },
reportComposerError: setComposerError,
target: props.target ?? PAIRED,
sessionId: 'session-1',
journalItems: props.journalItems ?? cutChat,
@@ -87,14 +87,17 @@ function Harness(props: Props): React.JSX.Element {
})
return (
<TooltipProvider delayDuration={0}>
<span data-testid="offered">{continuation.offeredTurnItemId ?? 'none'}</span>
<span data-testid="offered">{continuation.view.offeredTurnItemId ?? 'none'}</span>
<span data-testid="available">{String(continuation.view.continueAvailable)}</span>
<span data-testid="remote">{String(continuation.view.remoteHost)}</span>
<span data-testid="error">{composerError ?? continuation.continueError?.text ?? 'none'}</span>
<button type="button" onClick={() => setComposerError(ATTACHMENTS)}>
compose
</button>
<NativeChatInterruptedContinue continuation={continuation} />
{/* As the cut's own row shows it. */}
{continuation.view.offeredTurnItemId === null ? null : (
<NativeChatInterruptedContinueButton onContinue={continuation.view.continueNow} />
)}
</TooltipProvider>
)
}
@@ -1,4 +1,4 @@
import { useId, useMemo, useState } from 'react'
import { useCallback, useId, useMemo, useState } from 'react'
import { Play } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
@@ -21,14 +21,12 @@ import type { NativeChatComposerNoticeContent } from './native-chat-composer-not
type ContinueAnswer = { outcome?: string }
export type NativeChatInterruptedContinuation = {
/** Told to the chat's rows: the machine's name, whether it is remote, and if it can continue. */
/** Told to the chat's rows: the machine's name, whether it is remote, if it can continue, and the
* cut row that carries Continue. */
view: NativeChatOrcaStopView
/** The cut turn Continue is offered on right now, if any. */
offeredTurnItemId: string | null
/** A Continue that did not go through, for the composer's notice card: shown while the chat
* still sits on that cut and gone once it is continued from anywhere. */
continueError: (NativeChatComposerNoticeContent & { onDismiss: () => void }) | null
continueNow: () => void
}
/**
@@ -42,10 +40,10 @@ export function useNativeChatInterruptedContinuation(input: {
journalItems: readonly AgentJournalRenderItem[]
submissions: readonly Pick<AgentJournalSubmission, 'dispatchState'>[]
isWorking: boolean
/** The composer's own error; a Continue click is the user's newer action, so it clears it. */
composer: { clearError: () => void }
/** Stable; a Continue click is the user's newer action, so it clears the composer's error. */
reportComposerError: (text: null) => void
}): NativeChatInterruptedContinuation {
const { target, sessionId } = input
const { target, sessionId, reportComposerError } = input
const hostLabel = useStructuredAgentSessionHostLabel(target)
const capability = useStructuredAgentSessionHostCapabilityState(
target,
@@ -66,7 +64,7 @@ export function useNativeChatInterruptedContinuation(input: {
capability === 'supported' && cut && !input.isWorking && !resuming && asked !== cut.turnItemId
? cut.turnItemId
: null
const continueNow = (): void => {
const continueNow = useCallback((): void => {
if (offered === null) {
return
}
@@ -78,19 +76,25 @@ export function useNativeChatInterruptedContinuation(input: {
}
setAsked(turnItemId)
setFailedOn(null)
input.composer.clearError()
reportComposerError(null)
void callStructuredAgentSession<ContinueAnswer>(target, 'agentSession.continueInterrupted', {
sessionId,
turnItemId
}).then((answer) => (answer.outcome === 'refused' ? failed() : undefined), failed)
}
}, [offered, target, sessionId, reportComposerError])
// Unknown counts as able: a host that writes cause rows has Continue, and the words stay put.
const continueAvailable = capability !== 'unsupported'
// One object per change, so the chat's rows re-render only when what they show changes.
const remoteHost = target.kind === 'environment'
const view = useMemo(
() => ({ hostLabel, remoteHost, continueAvailable }),
[hostLabel, remoteHost, continueAvailable]
() => ({
hostLabel,
remoteHost,
continueAvailable,
offeredTurnItemId: offered,
continueNow
}),
[hostLabel, remoteHost, continueAvailable, offered, continueNow]
)
const failedHere =
failedOn !== null && cut?.turnItemId === failedOn
@@ -101,37 +105,27 @@ export function useNativeChatInterruptedContinuation(input: {
: null
return {
view,
offeredTurnItemId: offered,
continueError: failedHere ? { text: failedHere, onDismiss: () => setFailedOn(null) } : null,
continueNow
continueError: failedHere ? { text: failedHere, onDismiss: () => setFailedOn(null) } : null
}
}
export function NativeChatInterruptedContinue({
continuation
/** Continue, on the row that says Orca stopped this reply: the one action that picks it back up. */
export function NativeChatInterruptedContinueButton({
onContinue
}: {
continuation: NativeChatInterruptedContinuation
}): React.JSX.Element | null {
onContinue: () => void
}): React.JSX.Element {
const explanationId = useId()
if (continuation.offeredTurnItemId === null) {
return null
}
const explanation = translate(
'components.native-chat.interruptedContinue.explanation',
'Continue, and the agent first checks whether its last step finished.'
)
return (
<div className="mx-auto flex w-full max-w-4xl items-center justify-end px-4 py-1">
<>
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"
variant="ghost"
size="xs"
aria-describedby={explanationId}
onClick={continuation.continueNow}
>
<Play className="size-3" />
<Button type="button" size="xs" aria-describedby={explanationId} onClick={onContinue}>
<Play />
{translate('components.native-chat.interruptedContinue.continue', 'Continue')}
</Button>
</TooltipTrigger>
@@ -142,6 +136,6 @@ export function NativeChatInterruptedContinue({
<span id={explanationId} className="sr-only">
{explanation}
</span>
</div>
</>
)
}
@@ -1,9 +1,10 @@
// @vitest-environment happy-dom
import '@testing-library/jest-dom/vitest'
import { act, cleanup, render, screen } from '@testing-library/react'
import { act, cleanup, fireEvent, render, screen } from '@testing-library/react'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { projectStructuredItemsToNativeChat } from '../../../../shared/structured-agent-session-projection'
import type { AgentJournalStatusItem } from '../../../../shared/agent-session-journal-types'
import { TooltipProvider } from '@/components/ui/tooltip'
import { MessageRow } from './NativeChatMessageRow'
import { NativeChatNoticeRow } from './NativeChatNoticeRow'
import { i18n } from '@/i18n/i18n'
@@ -32,17 +33,21 @@ it('keeps a skipped compaction warning truthful instead of showing the success s
function orcaStopView(
hostLabel: string | null,
continueAvailable: boolean,
remoteHost: boolean
remoteHost: boolean,
offer: Pick<NativeChatOrcaStopView, 'offeredTurnItemId' | 'continueNow'>
): NativeChatOrcaStopView {
return { hostLabel, remoteHost, continueAvailable }
return { hostLabel, remoteHost, continueAvailable, ...offer }
}
const NO_OFFER = { offeredTurnItemId: null, continueNow: () => {} }
function renderStatus(
body: AgentJournalStatusItem,
hostLabel: string | null = null,
continueAvailable = false,
remoteHost = false,
agentName?: string
agentName?: string,
offer: Pick<NativeChatOrcaStopView, 'offeredTurnItemId' | 'continueNow'> = NO_OFFER
) {
const [message] = projectStructuredItemsToNativeChat([
{
@@ -54,16 +59,18 @@ function renderStatus(
turnScope: { kind: 'turn', turnItemId: 'cut-turn' }
}
])
const view = orcaStopView(hostLabel, continueAvailable, remoteHost)
const view = orcaStopView(hostLabel, continueAvailable, remoteHost, offer)
return render(
<NativeChatOrcaStopContext.Provider value={view}>
<MessageRow
message={message!}
agentName={agentName}
expandSignal={false}
onScrollMessageToTop={vi.fn()}
/>
</NativeChatOrcaStopContext.Provider>
<TooltipProvider>
<NativeChatOrcaStopContext.Provider value={view}>
<MessageRow
message={message!}
agentName={agentName}
expandSignal={false}
onScrollMessageToTop={vi.fn()}
/>
</NativeChatOrcaStopContext.Provider>
</TooltipProvider>
)
}
@@ -101,6 +108,29 @@ describe('the row an Orca stop leaves', () => {
).toBeInTheDocument()
})
it('carries Continue on the cut it is offered for, and only there', () => {
const continueNow = vi.fn()
renderStatus(orcaStopRow('quit'), 'studio-mac', true, false, undefined, {
offeredTurnItemId: 'cut-turn',
continueNow
})
const sentence = screen.getByText(
'Orca on studio-mac was closed while this response was in progress.'
)
const button = screen.getByRole('button', { name: 'Continue' })
// Beside the words that say why, in the same card.
expect(sentence.parentElement).toContainElement(button)
fireEvent.click(button)
expect(continueNow).toHaveBeenCalledOnce()
cleanup()
renderStatus(orcaStopRow('quit'), 'studio-mac', true, false, undefined, {
offeredTurnItemId: 'another-turn',
continueNow
})
expect(screen.queryByRole('button', { name: 'Continue' })).toBeNull()
})
it("says a remote host's Orca stopped, not that it was closed", () => {
renderStatus(orcaStopRow('quit'), 'QA SSH', true, true)
expect(
@@ -161,6 +191,16 @@ describe('the row an Orca stop leaves', () => {
)
})
it('calls a paired server with no name the remote server, so its words match the button', () => {
renderStatus(orcaStopRow('update'), null, true, true)
expect(
screen.getByText(
'Orca on the remote server restarted for an update while this response was in progress.'
)
).toBeInTheDocument()
expect(screen.queryByText(LEGACY_TEXT)).toBeNull()
})
it('keeps the host words for a stop Orca did not cause', () => {
const { orcaStop: _orcaStop, presentation: _presentation, ...agentExit } = orcaStopRow('update')
renderStatus(agentExit, 'studio-mac')
@@ -13,6 +13,7 @@ import {
} from '../../../../shared/agent-session-host-status-rows'
import type { NativeChatTextBlock } from '../../../../shared/native-chat-types'
import { useNativeChatOrcaStopView } from './native-chat-orca-stop-context'
import { NativeChatInterruptedContinueButton } from './NativeChatInterruptedContinue'
import { nativeChatOrcaStopRowText } from './native-chat-orca-stop-words'
import { AGENT_SESSION_ORCA_STOP_PRESENTATION } from '../../../../shared/agent-session-orca-stop'
import { ProviderFrameRow } from './NativeChatTranscriptChrome'
@@ -109,10 +110,18 @@ export function NativeChatNoticeRow({
)
}
// The host's row about an Orca stop names the cause and the machine, muted: Orca stopped, not the
// agent. With no machine to name it keeps the host's own words.
// agent. Outside a chat that knows its machine it keeps the host's own words.
const { orcaStop } = block
const { hostLabel, remoteHost, continueAvailable } = orcaStopView
const named = orcaStop !== undefined && hostLabel !== null
// A paired server with no name to show is still a remote server; its id is not a name.
const machine =
hostLabel ??
(remoteHost
? translate('components.native-chat.notices.orcaStopUnnamedServer', 'the remote server')
: null)
const named = orcaStop !== undefined && machine !== null
const continueOffered =
orcaStop?.turnItemId !== undefined && orcaStop.turnItemId === orcaStopView.offeredTurnItemId
const failure = readWholeAgentSessionFailureFact(block.failure)
// Only reword auth text fully described by its fact; host text may also carry command advice.
const authSurface =
@@ -122,7 +131,7 @@ export function NativeChatNoticeRow({
)
: undefined
const text = named
? nativeChatOrcaStopRowText(orcaStop.cause, hostLabel, { continueAvailable, remoteHost })
? nativeChatOrcaStopRowText(orcaStop.cause, machine, { continueAvailable, remoteHost })
: failure && authSurface
? agentSessionFailureSentence(
failure,
@@ -153,7 +162,10 @@ export function NativeChatNoticeRow({
>
<div className="flex items-start gap-2">
{Icon ? <Icon aria-hidden="true" className="mt-0.5 size-4 shrink-0" /> : null}
<p className="min-w-0 whitespace-pre-wrap break-words">{text}</p>
<p className="min-w-0 flex-1 whitespace-pre-wrap break-words">{text}</p>
{continueOffered ? (
<NativeChatInterruptedContinueButton onContinue={orcaStopView.continueNow} />
) : null}
</div>
{claudeSignIn && isClaudeSignInFailureKind(block.failure?.kind) ? (
<Button
@@ -0,0 +1,126 @@
// @vitest-environment happy-dom
import '@testing-library/jest-dom/vitest'
import { cleanup, render, screen } from '@testing-library/react'
import { useImperativeHandle } from 'react'
import { afterEach, expect, it, vi } from 'vitest'
import type * as HostCapabilityModule from '@/runtime/structured-agent-session-host-capability'
const { mocks, moduleFactories, resetStructuredSessionMocks } = await vi.hoisted(async () =>
(await import('./NativeChatStructuredSession.test-harness')).createStructuredSessionMocks()
)
vi.mock('@/lib/structured-agent-session-launch', () =>
moduleFactories.structuredAgentSessionLaunch()
)
vi.mock('@/runtime/structured-agent-session-client', () =>
moduleFactories.structuredAgentSessionClient()
)
vi.mock('./use-structured-agent-session', () => moduleFactories.useStructuredAgentSession())
vi.mock('./use-native-chat-font-size', () => moduleFactories.useNativeChatFontSize())
vi.mock('./use-native-chat-file-link-context', () => moduleFactories.useNativeChatFileLinkContext())
vi.mock('./use-native-chat-file-link-click', () => moduleFactories.useNativeChatFileLinkClick())
vi.mock('./NativeChatComposer', () => moduleFactories.nativeChatComposer())
vi.mock('./NativeChatApprovalCard', () => moduleFactories.nativeChatApprovalCard())
vi.mock('./NativeChatQuestionCard', () => moduleFactories.nativeChatQuestionCard())
vi.mock('@/runtime/structured-agent-session-host-capability', async (importOriginal) => {
const original = await importOriginal<typeof HostCapabilityModule>()
const { AGENT_SESSION_CONTINUE_INTERRUPTED_RUNTIME_CAPABILITY } =
await import('../../../../shared/agent-session-continue-interrupted-capability')
return {
...original,
useStructuredAgentSessionHostCapabilityState: (
...args: Parameters<typeof original.useStructuredAgentSessionHostCapabilityState>
) =>
args[1] === AGENT_SESSION_CONTINUE_INTERRUPTED_RUNTIME_CAPABILITY
? 'supported'
: original.useStructuredAgentSessionHostCapabilityState(...args)
}
})
// The transcript's real rows, projected from the journal; only the list's windowing is left out.
vi.mock('./NativeChatMessageList', async () => {
const { MessageRow } = await import('./NativeChatMessageRow')
const { projectStructuredItemsToNativeChat } =
await import('../../../../shared/structured-agent-session-projection')
return {
NativeChatMessageList: (props: {
ref?: React.Ref<unknown>
journalItems?: Parameters<typeof projectStructuredItemsToNativeChat>[0]
}) => {
useImperativeHandle(props.ref, () => ({ revealLatest: () => {}, revealFindMatch: () => {} }))
return (
<div data-testid="transcript">
{projectStructuredItemsToNativeChat(props.journalItems ?? []).map((message) => (
<MessageRow
key={message.id}
message={message}
expandSignal={false}
onScrollMessageToTop={() => {}}
/>
))}
</div>
)
}
}
})
import { TooltipProvider } from '@/components/ui/tooltip'
import { NativeChatStructuredSession } from './NativeChatStructuredSession'
import { agentJournalItemKey } from '../../../../shared/agent-session-journal-item-key'
import type { AgentJournalRenderItem } from '../../../../shared/agent-session-journal-types'
const TURN = agentJournalItemKey({ provider: 'codex', threadId: 't', turnId: 'cut', ordinal: 1 })
const cutChat: AgentJournalRenderItem[] = [
{
itemId: TURN,
revision: 2,
sequence: 1,
observedAt: 1,
body: { kind: 'turn', turnId: 'cut', state: 'interrupted', startedAt: 1, completedAt: 5 }
},
{
itemId: agentJournalItemKey({ provider: 'orca', clientMessageId: 'stale-session:s:death-1-5' }),
revision: 1,
sequence: 2,
observedAt: 6,
turnScope: { kind: 'turn', turnItemId: TURN },
body: {
kind: 'status',
text: 'Codex stopped while this response was in progress. You can continue in this conversation.',
tone: 'error',
presentation: 'orca-stop',
orcaStop: { cause: 'crash' }
}
}
]
afterEach(() => {
cleanup()
resetStructuredSessionMocks()
})
it('offers one Continue, in the card that says Orca stopped, and the card does not say it again', () => {
mocks.journalItems = cutChat
render(
<TooltipProvider>
<NativeChatStructuredSession
isVisible
isFocusedGroup
tabId="orca-stop-tab"
sessionId="orca-stop-session"
// A paired server with no name to show.
target={{ kind: 'environment', environmentId: 'env-unnamed' }}
agent="codex"
/>
</TooltipProvider>
)
// Throws on a second Continue anywhere in the pane, such as the old row above the composer.
const button = screen.getByRole('button', { name: 'Continue' })
const sentence = screen.getByText(
'Orca on the remote server stopped unexpectedly while this response was in progress.'
)
expect(sentence.parentElement).toContainElement(button)
expect(screen.getByTestId('transcript')).toContainElement(button)
expect(screen.queryByText(/You can continue in this conversation/)).toBeNull()
})
@@ -31,10 +31,7 @@ import { useStructuredNativeChatPaneCommands } from './use-structured-native-cha
import type { NativeChatStructuredViewProps } from './native-chat-view-types'
import { NativeChatStructuredSessionStatus } from './NativeChatStructuredSessionStatus'
import { useNativeChatLaunchDraftSignal } from './use-native-chat-launch-draft-adoption'
import {
NativeChatInterruptedContinue,
useNativeChatInterruptedContinuation
} from './NativeChatInterruptedContinue'
import { useNativeChatInterruptedContinuation } from './NativeChatInterruptedContinue'
import { NativeChatOrcaStopContext } from './native-chat-orca-stop-context'
import { structuredSessionNotices } from './native-chat-structured-session-notices'
import { NativeChatPromptSlotNotices } from './NativeChatComposerNotices'
@@ -142,7 +139,7 @@ export function NativeChatStructuredSession(
journalItems: controller.journalItems,
submissions: controller.submissions,
isWorking: controller.isWorking,
composer: { clearError: () => reportComposerError(null) }
reportComposerError
})
const needsFailureFacts =
submits.queuedMessages.cards.some((card) => card.state === 'returned') ||
@@ -319,7 +316,6 @@ export function NativeChatStructuredSession(
</div>
{readFailedFinally ? null : (
<>
<NativeChatInterruptedContinue continuation={continuation} />
{/* Host-held drafts, never transcript rows. Above the status area, so running shells and agents sit next to the composer. */}
<NativeChatQueuedMessageList
controller={submits.queuedMessages}
@@ -8,12 +8,17 @@ export type NativeChatOrcaStopView = {
hostLabel: string | null
remoteHost: boolean
continueAvailable: boolean
/** The cut turn whose row carries Continue right now, if any. */
offeredTurnItemId: string | null
continueNow: () => void
}
export const NativeChatOrcaStopContext = createContext<NativeChatOrcaStopView>({
hostLabel: null,
remoteHost: false,
continueAvailable: false
continueAvailable: false,
offeredTurnItemId: null,
continueNow: () => {}
})
export function useNativeChatOrcaStopView(): NativeChatOrcaStopView {
+2 -1
View File
@@ -18224,7 +18224,8 @@
"orcaStopQuit": "Orca on {{machine}} was closed while this response was in progress.",
"orcaStopCrash": "Orca on {{machine}} stopped unexpectedly while this response was in progress.",
"orcaStopRemote": "Orca on {{machine}} stopped while this response was in progress.",
"orcaStopCanContinue": "You can continue in this conversation."
"orcaStopCanContinue": "You can continue in this conversation.",
"orcaStopUnnamedServer": "the remote server"
},
"composer": {
"imageUnsupported": "Image paste is not supported for this agent.",
+2 -1
View File
@@ -18328,7 +18328,8 @@
"orcaStopQuit": "Orca en {{machine}} se cerró mientras esta respuesta estaba en curso.",
"orcaStopCrash": "Orca en {{machine}} se detuvo inesperadamente mientras esta respuesta estaba en curso.",
"orcaStopRemote": "Orca en {{machine}} se detuvo mientras esta respuesta estaba en curso.",
"orcaStopCanContinue": "Puedes continuar en esta conversación."
"orcaStopCanContinue": "Puedes continuar en esta conversación.",
"orcaStopUnnamedServer": "el servidor remoto"
},
"providerFrame": {
"byteLength": "{{value0}} bytes"
+2 -1
View File
@@ -18305,7 +18305,8 @@
"orcaStopQuit": "Orca sur {{machine}} a été fermé pendant que cette réponse était en cours.",
"orcaStopCrash": "Orca sur {{machine}} s'est arrêté de manière inattendue pendant que cette réponse était en cours.",
"orcaStopRemote": "Orca sur {{machine}} s'est arrêté pendant que cette réponse était en cours.",
"orcaStopCanContinue": "Vous pouvez continuer dans cette conversation."
"orcaStopCanContinue": "Vous pouvez continuer dans cette conversation.",
"orcaStopUnnamedServer": "le serveur distant"
},
"providerFrame": {
"byteLength": "{{value0}} octets"
+2 -1
View File
@@ -18238,7 +18238,8 @@
"orcaStopQuit": "この応答の途中で、{{machine}} の Orca が終了しました。",
"orcaStopCrash": "この応答の途中で、{{machine}} の Orca が予期せず停止しました。",
"orcaStopRemote": "この応答の途中で、{{machine}} の Orca が停止しました。",
"orcaStopCanContinue": "この会話で続行できます。"
"orcaStopCanContinue": "この会話で続行できます。",
"orcaStopUnnamedServer": "リモートサーバー"
},
"providerFrame": {
"byteLength": "{{value0}}バイト"
+2 -1
View File
@@ -18238,7 +18238,8 @@
"orcaStopQuit": "이 응답이 진행되는 동안 {{machine}}의 Orca가 종료되었습니다.",
"orcaStopCrash": "이 응답이 진행되는 동안 {{machine}}의 Orca가 예기치 않게 중지되었습니다.",
"orcaStopRemote": "이 응답이 진행되는 동안 {{machine}}의 Orca가 중지되었습니다.",
"orcaStopCanContinue": "이 대화에서 계속할 수 있습니다."
"orcaStopCanContinue": "이 대화에서 계속할 수 있습니다.",
"orcaStopUnnamedServer": "원격 서버"
},
"providerFrame": {
"byteLength": "{{value0}}바이트"
+2 -1
View File
@@ -18217,7 +18217,8 @@
"orcaStopQuit": "此回复进行期间,{{machine}} 上的 Orca 已关闭。",
"orcaStopCrash": "此回复进行期间,{{machine}} 上的 Orca 意外停止。",
"orcaStopRemote": "此回复进行期间,{{machine}} 上的 Orca 已停止。",
"orcaStopCanContinue": "你可以在此对话中继续。"
"orcaStopCanContinue": "你可以在此对话中继续。",
"orcaStopUnnamedServer": "远程服务器"
},
"providerFrame": {
"byteLength": "{{value0}} 字节"