refactor(native-chat): drop the reconnect vocabulary the resume action left behind

Resuming became one action — reattach and ask the agent to carry on — so the
notification helpers no longer need to be told which action they are reporting.
Every caller passed `continue`; the `reconnect` branch, its helper and its
catalog keys are gone.

The dialog and the launch path had grown two copies of the same call: same RPC,
same response shape, same announce-and-settle. That now lives once in the store
module that owns the offer, which also takes the dismiss call, leaving the modal
presentational. The two copies had drifted — only the dialog's caught a
malformed payload — and the unified one keeps the defensive reading.

No behaviour change. `agentSession.restartResume` stays: it is a published wire
method even though nothing in the app calls it.
This commit is contained in:
Brennan Benson
2026-09-20 23:34:11 -07:00
parent db148c2f58
commit ec2dce4f24
11 changed files with 128 additions and 193 deletions
@@ -1,11 +1,9 @@
// Asking an interrupted agent to carry on, on the user's opt-in.
//
// Reattaching and continuing are still SEPARATE operations: `resume` reattaches and sends nothing;
// this adds one message on top of it. What changed is WHO may ask. Resuming from the restart prompt
// comes here, and so does an opted-in launch, so this module is no longer unreachable from a
// setting — do not restate that old guarantee. It is acceptable because the work is the user's own,
// the message asks the agent to verify its last action before repeating it, and the launch toast
// reports what happened.
// Reattaching and continuing are SEPARATE operations: `resume` reattaches and sends nothing; this
// adds one message on top of it. Both the restart prompt and an opted-in launch come here, so a
// SETTING can reach this send — acceptable because the work is the user's own, the message asks the
// agent to verify its last action before repeating it, and the launch toast reports what happened.
import type { AgentJournalMessageItem } from '../../../shared/agent-session-journal-types'
import type { AgentSessionMutationEnvelope } from '../../../shared/agent-session-wire'
@@ -40,11 +40,9 @@ export type StructuredAgentSessionRestartResumeSurfaces = {
/** The resume-capable hold; see the runner for why a hold and not a send. */
hold: (sessionId: string, holderId: string) => Promise<void>
release: (sessionId: string, holderId: string) => void
/** The host's own send. Reached ONLY from `continueAfterRestart`; `resume` still never calls it,
* so reattaching on its own sends nothing. That is no longer a guarantee about SETTINGS, though:
* a launch the user opted into calls `continueAfterRestart` directly, which is acceptable
* because the work is the user's own, the message asks the agent to verify its last action
* before repeating it, and the launch toast reports what happened.
/** The host's own send. Reached ONLY from `continueAfterRestart`, so reattaching on its own sends
* nothing — but an opted-in launch calls `continueAfterRestart` too, so this is not a guarantee
* about settings; see the continuation module for why that is acceptable.
*
* Typed against the wire result rather than a hand-written subset: a narrower local shape hid
* `value.submission` here once, and the continuation reads it. */
@@ -1,8 +1,9 @@
// `agentSession.restartResumable` / `agentSession.restartResume` — the restart-resume offer.
// The restart-resume offer: list it, act on it, or turn it down.
//
// Both reach for records on disk this process may not have opened yet, so they build the host the
// way hold and reveal do. Listing is read-only and takes nothing live; resuming goes through the
// host's single resume path, which re-derives eligibility rather than trusting the ids it is given.
// Each method reaches for records on disk this process may not have opened yet, so each builds the
// host the way hold and reveal do. Listing is read-only and takes nothing live; acting goes through
// the host's single resume path, which re-derives eligibility rather than trusting the ids it is
// given.
import { defineMethod } from '../core'
import {
@@ -32,9 +33,9 @@ export const STRUCTURED_AGENT_SESSION_RESTART_RESUME_METHODS = [
}
}),
defineMethod({
// Reattach AND ask each reattached agent to carry on — what the desktop prompt now calls
// resuming, and what an opted-in launch runs without asking. Still a separate method from
// `restartResume`, which sends nothing, but no longer one that only a button can reach.
// Reattach AND ask each reattached agent to carry on — what the desktop prompt calls resuming,
// and what an opted-in launch runs without asking. Separate from `restartResume`, which sends
// nothing, but reachable from a setting rather than only from a button.
name: 'agentSession.restartContinue',
params: RestartResumeParams,
handler: async (params, ctx) => {
@@ -47,9 +48,9 @@ export const STRUCTURED_AGENT_SESSION_RESTART_RESUME_METHODS = [
}
}),
defineMethod({
// Reattach only, no send. The desktop prompt stopped calling this once its single action became
// resume-and-continue, but it stays: it is a published wire method, and its absence is what an
// older or non-desktop client would be met with.
// Reattach only, no send. No Orca surface calls it now — the desktop prompt's single action is
// resume-and-continue but it is a PUBLISHED wire method, so dropping it is a wire removal an
// older or non-desktop client would meet as an unknown method.
name: 'agentSession.restartResume',
params: RestartResumeParams,
handler: async (params, ctx) => {
@@ -111,8 +111,7 @@ it('keeps next-launch preference out of the current resume action', async () =>
expect(rpc).toHaveBeenCalledTimes(2)
})
// One primary action and one way out of it. The vacuous plain-reconnect button, the Not now button
// and the "what gets sent" popover are gone; the body copy carries the transparency now.
// One primary action and one way out of it; the body copy carries the transparency.
it('offers exactly Dismiss all and the resume action', async () => {
rpc.mockImplementation(async (_target, method) =>
method === 'agentSession.restartResumable' ? { sessions: offered } : { results: [] }
@@ -127,9 +126,8 @@ it('offers exactly Dismiss all and the resume action', async () => {
])
})
// Snoozing saves the preference like every other way out of the dialog, and calls NOTHING: the
// offer is the host's and stays exactly where it was. Closing the dialog is the only snooze left
// now that Not now is gone, so it has to keep doing all of that.
// Closing is the only snooze, so it carries the whole of one: saves the preference like every
// other way out, and calls NOTHING — the offer is the host's and stays exactly where it was.
it('snoozes to the status-bar offer when the dialog is closed', async () => {
rpc.mockImplementation(async (_target, method) =>
method === 'agentSession.restartResumable' ? { sessions: offered } : { results: [] }
@@ -11,15 +11,8 @@ import {
DialogTitle
} from './ui/dialog'
import { useAppStore } from '../store'
import { callStructuredAgentSession } from '@/runtime/structured-agent-session-client'
import { translate } from '@/i18n/i18n'
import { ResumeOnRestartGroups } from './NativeChatResumeOnRestartGroups'
import {
announceRestartDismissUnconfirmed,
announceRestartResults,
announceRestartUnconfirmed,
type RestartActionOutcome
} from './native-chat-restart-action-notifications'
import { selectedResumeSessionIds } from './native-chat-resume-on-restart-grouping'
import {
consumeNativeChatResumeOnRestartDialogRequest,
@@ -27,8 +20,8 @@ import {
subscribeNativeChatResumeOnRestartDialog
} from './native-chat-resume-on-restart-dialog'
import {
clearNativeChatRestartOffer,
settleNativeChatRestartOffer,
continueNativeChatRestartOffer,
dismissNativeChatRestartOffer,
useNativeChatRestartOffer
} from './native-chat-resume-on-restart-store'
@@ -37,24 +30,17 @@ import {
*
* The list is the point. Resuming a chat that was not working starts a provider the user never
* asked for and puts a misleading row in front of them, so they see exactly which chats the last
* teardown recorded as mid-turn and decide. The checkbox is the opt-in to skipping this prompt in
* future — it removes the PROMPT, never a safety check: automatic mode calls the same RPC, which
* re-derives the same predicate and staggers the same way.
* teardown recorded as mid-turn and decide. The checkbox removes the PROMPT, never a safety check:
* automatic mode calls the same RPC, which re-derives the same predicate and staggers the same way.
*
* Resuming reattaches each session where it stopped AND asks that agent to carry on, which is the
* only reason the prompt is worth showing: reattaching alone is what simply opening the chat does.
* The user's own prompt is never re-sent, and every string here has to keep saying so.
*
* Closing is a SNOOZE: the host keeps the offer and the status bar keeps a way back to it, so
* looking around before deciding cannot cost the recovery. Dismiss all is the only path that spends
* it, and even that loses nothing — opening a chat takes a resume-capable hold, which re-acquires
* the provider at the same cursor and retires the offer for it.
* Closing is a SNOOZE, so looking around before deciding cannot cost the recovery. Dismiss all is
* the only path that spends the offer.
*/
// Structured sessions run on the machine hosting the runtime; both launch resolvers refuse anything
// else, so there is no remote target to aim this at.
const LOCAL = { kind: 'local' } as const
export function NativeChatResumeOnRestartModal(): React.JSX.Element | null {
const structuredEnabled = useAppStore(
(store) => store.settings?.experimentalStructuredNativeChat === true
@@ -70,14 +56,9 @@ export function NativeChatResumeOnRestartModal(): React.JSX.Element | null {
const updateSettings = useAppStore((store) => store.updateSettings)
const [dontAskAgain, setDontAskAgain] = useState(false)
const [busy, setBusy] = useState(false)
/**
* Which of the OFFERED chats to leave out. Tracked as EXCLUSIONS rather than a selection because
* the list is the host's and arrives — and shrinks — under an open dialog; a stored selection
* would need seeding from an effect every time it changed.
*
* This changes which eligible chats are acted on, never what is eligible: the ids below are
* intersected back against the host's own list, and the host re-derives the predicate regardless.
*/
/** Which of the OFFERED chats to leave out. Tracked as EXCLUSIONS rather than a selection because
* the list is the host's and arrives — and shrinks — under an open dialog; a stored selection
* would need seeding from an effect every time it changed. */
const [excluded, setExcluded] = useState<ReadonlySet<string>>(() => new Set())
const selected = useMemo(
() =>
@@ -108,30 +89,12 @@ export function NativeChatResumeOnRestartModal(): React.JSX.Element | null {
}
}, [dontAskAgain, updateSettings])
/**
* The one action: reattach, then ask each agent to carry on.
*
* `restartContinue` is the host method behind it — reattaching without a send is a separate RPC
* this surface no longer calls, because opening the chat already does exactly that.
*/
const resume = useCallback(
async (sessionIds: string[]): Promise<void> => {
setBusy(true)
try {
void persistPreference()
const result = await callStructuredAgentSession<{
/** Which chats the host actually reattached, and so which claims it spent. Optional
* because the payload is unvalidated: a shape this side did not expect must not turn a
* delivered continuation into a failure report. */
resumed?: RestartActionOutcome[]
continued: RestartActionOutcome[]
}>(LOCAL, 'agentSession.restartContinue', { sessionIds })
announceRestartResults(sessionIds, result.continued, 'continue')
// Resuming spends the claims, so the offer has to shrink with it — otherwise the status bar
// keeps counting chats the host has already handed back.
settleNativeChatRestartOffer((result.resumed ?? []).map((entry) => entry.sessionId))
} catch {
announceRestartUnconfirmed(sessionIds.length, 'continue')
await continueNativeChatRestartOffer(sessionIds)
} finally {
setBusy(false)
consumeNativeChatResumeOnRestartDialogRequest()
@@ -146,18 +109,14 @@ export function NativeChatResumeOnRestartModal(): React.JSX.Element | null {
void persistPreference()
}, [persistPreference])
/** The only path that spends the markers. */
const dismissAll = useCallback(async (): Promise<void> => {
setBusy(true)
void persistPreference()
// The dismissal is the user's and lands here, whatever the host answers. A write Orca cannot
// confirm is reported rather than allowed to trap the dialog open behind a rejected promise.
// Bookkeeping never gates the user's own action: the dialog closes here whatever the host
// answers, rather than being trapped open behind a rejected promise.
consumeNativeChatResumeOnRestartDialogRequest()
try {
await callStructuredAgentSession(LOCAL, 'agentSession.restartResumableDismiss', {})
clearNativeChatRestartOffer()
} catch {
announceRestartDismissUnconfirmed()
await dismissNativeChatRestartOffer()
} finally {
setBusy(false)
}
@@ -180,8 +139,8 @@ export function NativeChatResumeOnRestartModal(): React.JSX.Element | null {
}
}}
>
{/* Height is capped, never the data: seeing WHICH chats would be resumed is the whole
point, so the list scrolls inside the dialog while the header and primary action stay. */}
{/* Height is capped, never the data: the list scrolls inside the dialog so the header and
the primary action stay put however many chats were interrupted. */}
<DialogContent className="grid-rows-[auto_minmax(0,1fr)_auto_auto] sm:max-w-xl max-h-[85vh]">
<DialogHeader>
<DialogTitle>
@@ -194,8 +153,8 @@ export function NativeChatResumeOnRestartModal(): React.JSX.Element | null {
)}
</span>
</DialogTitle>
{/* Carries the transparency an explainer popover used to hide behind an icon: what the
agent is told, and what is NOT re-sent. */}
{/* The transparency, in the copy rather than behind a disclosure: what the agent is
told, and what is NOT re-sent. */}
<DialogDescription>
{interruptedByUpdate
? translate(
@@ -250,8 +209,8 @@ export function NativeChatResumeOnRestartModal(): React.JSX.Element | null {
</span>
</label>
{/* Two controls, and they are opposites: one spends the offer, one acts on it. Closing the
dialog is neither — it snoozes, so it needs no button of its own. */}
{/* Two controls, and they are opposites: one spends the offer, one acts on it. Closing is
neither — it snoozes, so it needs no button of its own. */}
<DialogFooter className="sm:justify-between">
{/* Quiet, not destructive: this spends an offer, and opening a chat still reattaches it. */}
<Button variant="ghost" size="sm" disabled={busy} onClick={() => void dismissAll()}>
@@ -4,31 +4,15 @@ import { translate } from '@/i18n/i18n'
/**
* What Orca tells the user after acting on a restart offer.
*
* `continue` is the only action a surface takes now — resuming always reattaches AND sends. The
* `reconnect` wording belongs to the host's plain-reattach RPC, which is still published on the
* wire, so its vocabulary stays here rather than being reinvented if anything calls it again.
* Resuming reattaches AND asks each agent to carry on, so every message here has to say a message
* went out. An opted-in launch runs with no dialog in front of it, and these toasts are the only
* place that user learns it happened.
*/
/** One `continued` row as the host reports it. */
export type RestartActionOutcome = {
sessionId: string
outcome: 'resumed' | 'continued' | 'pending' | 'unknown' | 'refused'
}
function announceResumed(count: number): void {
if (count <= 0) {
return
}
toast(
count === 1
? translate('auto.components.NativeChatResumeOnRestartModal.resumedOne', 'Resumed 1 chat')
: translate(
'auto.components.NativeChatResumeOnRestartModal.resumedMany',
'Resumed {{value0}} chats',
{
value0: count
}
)
)
outcome: 'continued' | 'pending' | 'unknown' | 'refused'
}
function announceContinued(count: number): void {
@@ -49,22 +33,17 @@ function announceContinued(count: number): void {
)
}
export function announceRestartUnconfirmed(count: number, action: 'reconnect' | 'continue'): void {
/** Delivery the host never confirmed. Reported, never retried — a second send is the user's call. */
export function announceRestartUnconfirmed(count: number): void {
if (count <= 0) {
return
}
toast(
action === 'continue'
? translate(
'auto.components.NativeChatResumeOnRestartModal.continueUnconfirmed',
'Continuation delivery is unconfirmed for {{value0}} chats. Open them to check before sending another message.',
{ value0: count, count }
)
: translate(
'auto.components.NativeChatResumeOnRestartModal.reconnectUnconfirmed',
'Resuming is unconfirmed for {{value0}} chats. You can still open them normally.',
{ value0: count, count }
)
translate(
'auto.components.NativeChatResumeOnRestartModal.continueUnconfirmed',
'Continuation delivery is unconfirmed for {{value0}} chats. Open them to check before sending another message.',
{ value0: count, count }
)
)
}
@@ -80,8 +59,7 @@ export function announceRestartDismissUnconfirmed(): void {
export function announceRestartResults(
requested: readonly string[],
results: readonly RestartActionOutcome[],
action: 'reconnect' | 'continue'
results: readonly RestartActionOutcome[]
): void {
const bySession = new Map(results.map((result) => [result.sessionId, result.outcome]))
let succeeded = 0
@@ -89,7 +67,7 @@ export function announceRestartResults(
let refused = 0
for (const sessionId of new Set(requested)) {
const outcome = bySession.get(sessionId)
if (outcome === (action === 'continue' ? 'continued' : 'resumed')) {
if (outcome === 'continued') {
succeeded += 1
} else if (outcome === 'pending' || outcome === 'unknown') {
unconfirmed += 1
@@ -98,25 +76,15 @@ export function announceRestartResults(
refused += 1
}
}
if (action === 'continue') {
announceContinued(succeeded)
} else {
announceResumed(succeeded)
}
announceContinued(succeeded)
if (refused > 0) {
toast(
action === 'continue'
? translate(
'auto.components.NativeChatResumeOnRestartModal.continueRefused',
'{{value0}} chats could not be continued. Open them to continue manually.',
{ value0: refused, count: refused }
)
: translate(
'auto.components.NativeChatResumeOnRestartModal.reconnectRefused',
'{{value0}} chats could not be resumed. You can still open them normally.',
{ value0: refused, count: refused }
)
translate(
'auto.components.NativeChatResumeOnRestartModal.continueRefused',
'{{value0}} chats could not be continued. Open them to continue manually.',
{ value0: refused, count: refused }
)
)
}
announceRestartUnconfirmed(unconfirmed, action)
announceRestartUnconfirmed(unconfirmed)
}
@@ -1,13 +1,17 @@
let pendingOpen = false
const listeners = new Set<() => void>()
function notify(): void {
for (const listener of listeners) {
listener()
}
}
// Why: the launch load and the status-bar entry both open this dialog, and either can fire before
// it subscribes. Keeping the request as an external snapshot prevents mount ordering from losing it.
export function requestNativeChatResumeOnRestartDialog(): void {
pendingOpen = true
for (const listener of listeners) {
listener()
}
notify()
}
export function consumeNativeChatResumeOnRestartDialogRequest(): void {
@@ -15,9 +19,7 @@ export function consumeNativeChatResumeOnRestartDialogRequest(): void {
return
}
pendingOpen = false
for (const listener of listeners) {
listener()
}
notify()
}
export function getNativeChatResumeOnRestartDialogRequest(): boolean {
@@ -2,6 +2,7 @@ import { useEffect, useSyncExternalStore } from 'react'
import { callStructuredAgentSession } from '@/runtime/structured-agent-session-client'
import { useAppStore } from '../store'
import {
announceRestartDismissUnconfirmed,
announceRestartResults,
announceRestartUnconfirmed,
type RestartActionOutcome
@@ -10,13 +11,12 @@ import { allResumeSessionIds, type ResumeCandidate } from './native-chat-resume-
import { requestNativeChatResumeOnRestartDialog } from './native-chat-resume-on-restart-dialog'
/**
* Which interrupted chats the host is still offering to resume.
* Which interrupted chats the host is still offering to resume, and every action that moves that.
*
* The offer is the HOST's answer, not a list whichever surface rendered first happens to be
* holding. It has to be, because the host retires an offer for reasons no renderer can see —
* simply reopening a chat re-acquires its provider at the same cursor, which is the reattach half
* of a resume. So this fetches the list and both surfaces read it, and anything about to ACT on
* the offer asks the host again first.
* holding. It has to be, because the host retires an offer for reasons no renderer can see — simply
* reopening a chat re-acquires its provider at the same cursor, which is the reattach half of a
* resume. So both surfaces read this one answer, and every action asks the host again first.
*
* What stays on this side is the user's own facts: the snooze, and the preference that decides
* whether the launch asks at all.
@@ -81,7 +81,7 @@ export async function refreshNativeChatRestartOffer(): Promise<readonly ResumeCa
}
/** Drops the chats the host reports it has settled, leaving the ones it did not. */
export function settleNativeChatRestartOffer(sessionIds: readonly string[]): void {
function settle(sessionIds: readonly string[]): void {
const settled = new Set(sessionIds)
const remaining = offer.candidates.filter((candidate) => !settled.has(candidate.sessionId))
if (remaining.length === offer.candidates.length) {
@@ -91,20 +91,64 @@ export function settleNativeChatRestartOffer(sessionIds: readonly string[]): voi
}
/** The offer was abandoned outright; the host has already spent the markers. */
export function clearNativeChatRestartOffer(): void {
function clear(): void {
if (offer.candidates.length === 0) {
return
}
publish({ ...offer, candidates: [] })
}
/**
* Reattach the offered chats, ask each agent to carry on, then shrink the offer by what the host
* says it spent — otherwise the status bar keeps counting chats already handed back.
*
* `sessionIds` is the dialog's selection. An opted-in launch names nothing, so the host acts on
* whatever it still offers rather than on a list this side captured a moment earlier, and passes
* `reported` instead: the chats the user was shown, which is what the toasts count.
*
* Never rejects. The payload is unvalidated, and a shape this side did not expect is reported as
* an unconfirmed delivery — the message may well have gone out.
*/
export async function continueNativeChatRestartOffer(
sessionIds: readonly string[] | undefined,
reported: readonly string[] = sessionIds ?? []
): Promise<void> {
try {
const result = await callStructuredAgentSession<{
/** Which chats the host reattached, and so which claims it spent. */
resumed?: { sessionId: string }[]
continued: RestartActionOutcome[]
}>(LOCAL, 'agentSession.restartContinue', sessionIds ? { sessionIds } : {})
announceRestartResults(reported, result.continued)
settle((result.resumed ?? []).map((entry) => entry.sessionId))
} catch {
announceRestartUnconfirmed(reported.length)
}
}
/**
* Turning the offer down for good, which is the only path that spends the markers.
*
* Nothing is lost: opening a chat takes a resume-capable hold, which re-acquires the provider at
* the same cursor. A write the host never confirmed leaves the offer standing and says so.
*/
export async function dismissNativeChatRestartOffer(): Promise<void> {
try {
await callStructuredAgentSession(LOCAL, 'agentSession.restartResumableDismiss', {})
clear()
} catch {
announceRestartDismissUnconfirmed()
}
}
/**
* This launch's single read of the offer, and the one decision the preference makes: ask, or
* resume without asking.
*
* "Resume automatically" has to mean the same thing the button means, or the preference is a lie:
* the identical call, reattaching AND asking each agent to carry on. Reattaching on its own is
* what opening the chat already does, so a silent version of that would recover nothing.
* what opening the chat already does, so a silent version of that would recover nothing. It is
* never silent either — the toasts are where an opted-in user learns a message went out.
*
* Runs once however many surfaces mount, so the count and the dialog describe the same answer and
* an opted-in launch cannot dispatch twice.
@@ -120,21 +164,7 @@ async function loadLaunchOffer(): Promise<void> {
requestNativeChatResumeOnRestartDialog()
return
}
// Identical call to the dialog's own button; the host re-derives eligibility either way.
const result = await callStructuredAgentSession<{
/** Which chats the host reattached, and so which claims it spent. Optional because the payload
* is unvalidated, exactly as the dialog reads it. */
resumed?: RestartActionOutcome[]
continued: RestartActionOutcome[]
}>(LOCAL, 'agentSession.restartContinue', {}).catch(() => null)
if (!result) {
announceRestartUnconfirmed(offered.length, 'continue')
return
}
// Automatic must never be silent: someone who ticked the box months ago still sees this, and
// this is the only place they learn a message went out on their behalf.
announceRestartResults(allResumeSessionIds(offered), result.continued, 'continue')
settleNativeChatRestartOffer((result.resumed ?? []).map((entry) => entry.sessionId))
await continueNativeChatRestartOffer(undefined, allResumeSessionIds(offered))
}
/**
+1 -8
View File
@@ -76,14 +76,7 @@
"continueRefused_one": "{{value0}} chat could not be continued. Open it to continue manually.",
"continueRefused_other": "{{value0}} chats could not be continued. Open them to continue manually.",
"continueUnconfirmed_one": "Continuation delivery is unconfirmed for {{value0}} chat. Open it to check before sending another message.",
"continueUnconfirmed_other": "Continuation delivery is unconfirmed for {{value0}} chats. Open them to check before sending another message.",
"manyAgents": "{{value0}} agents",
"oneAgent": "1 agent",
"projects": "Folder workspaces",
"reconnectRefused_one": "{{value0}} chat could not be resumed. You can still open it normally.",
"reconnectRefused_other": "{{value0}} chats could not be resumed. You can still open them normally.",
"reconnectUnconfirmed_one": "Resuming is unconfirmed for {{value0}} chat. You can still open it normally.",
"reconnectUnconfirmed_other": "Resuming is unconfirmed for {{value0}} chats. You can still open them normally."
"continueUnconfirmed_other": "Continuation delivery is unconfirmed for {{value0}} chats. Open them to check before sending another message."
},
"NewWorkspaceComposerCard": {
"0e587e31fb": "yaml",
-11
View File
@@ -1954,28 +1954,17 @@
"listLabel": "Chats that would be resumed",
"dontAskAgain": "Don't ask again (resume automatically)",
"dontAskAgainHint": "You can turn this off in Settings → Experimental → Chat UI.",
"resumedOne": "Resumed 1 chat",
"resumedMany": "Resumed {{value0}} chats",
"oneAgent": "1 agent",
"manyAgents": "{{value0}} agents",
"continuedOne": "Resumed 1 chat and asked it to continue",
"continuedMany": "Resumed {{value0}} chats and asked them to continue",
"selectAgent": "Resume {{value0}} chat \"{{value1}}\" in {{value2}}",
"projects": "Folder workspaces",
"resumeSelected": "Resume {{value0}} chats",
"resumeSelectedOne": "Resume 1 chat",
"continueUnconfirmed": "Continuation delivery is unconfirmed for {{value0}} chats. Open them to check before sending another message.",
"continueUnconfirmed_one": "Continuation delivery is unconfirmed for {{value0}} chat. Open it to check before sending another message.",
"continueUnconfirmed_other": "Continuation delivery is unconfirmed for {{value0}} chats. Open them to check before sending another message.",
"reconnectUnconfirmed": "Resuming is unconfirmed for {{value0}} chats. You can still open them normally.",
"reconnectUnconfirmed_one": "Resuming is unconfirmed for {{value0}} chat. You can still open it normally.",
"reconnectUnconfirmed_other": "Resuming is unconfirmed for {{value0}} chats. You can still open them normally.",
"continueRefused": "{{value0}} chats could not be continued. Open them to continue manually.",
"continueRefused_one": "{{value0}} chat could not be continued. Open it to continue manually.",
"continueRefused_other": "{{value0}} chats could not be continued. Open them to continue manually.",
"reconnectRefused": "{{value0}} chats could not be resumed. You can still open them normally.",
"reconnectRefused_one": "{{value0}} chat could not be resumed. You can still open it normally.",
"reconnectRefused_other": "{{value0}} chats could not be resumed. You can still open them normally.",
"dismissAll": "Dismiss all",
"dismissUnconfirmed": "Dismissing the resume offer was not confirmed — it may still be in the status bar."
},
@@ -5,12 +5,11 @@
// would make the two lanes behave differently — and the wording is the part that tells an agent to
// VERIFY its last action before repeating it. Both reasons point the same way.
//
// Sending this needs the user's OPT-IN, not their presence. Resuming from the restart prompt sends
// it, and so does the launch itself once the user ticked "resume automatically" — an earlier
// comment here promised no setting could ever reach this module, and that is no longer true. It is
// acceptable because the work being continued is the user's own, the wording above tells the agent
// to VERIFY its last action before repeating it, and the launch reports what it did. Reattaching
// without a send remains a separate operation that never comes here.
// Sending this needs the user's OPT-IN, not their presence: the restart prompt's resume sends it,
// and so does a launch the user ticked "resume automatically" for. That is acceptable because the
// work being continued is the user's own, the wording above tells the agent to VERIFY its last
// action before repeating it, and the launch reports what it did. Reattaching without a send
// remains a separate operation that never comes here.
export const AGENT_SESSION_RESTART_CONTINUATION_MESSAGE =
"Orca restarted, so your previous reply was cut off partway through. Before continuing, check whether your most recent action completed — don't repeat it if it did. Then carry on."