mirror of
https://github.com/stablyai/orca.git
synced 2026-11-01 17:47:40 +00:00
Resume dialog: show a finished resume as a result, not a picker (#27732)
* fix(native-chat): show a finished resume as a result, not a picker Once a resume starts and no chat is left to choose, the dialog drops the Select all row and the checkbox column, so each chat's check reads as its status instead of a ticked box. The Don't-ask-again box only shows while choosing; a run started by the launch's auto-resume says so and links to the setting. The finished description is past tense. * fix(i18n): keep Agent untranslated in the ja resume copy * fix(native-chat): keep resume results actionable and keyboard accessible * Fix hidden resume preference replay after run starts * Consume resume picker preference after applying it * fix(native-chat): keep Don't ask again after Resume while auto-resume is off The box now shows on the result view too whenever the setting is off and the user started the resume, and saves only while it is visible, so a hidden tick still never replays. Automatic launches keep their 'Resumed automatically' note instead.
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useAppStore } from '../store'
|
||||
import { Button } from './ui/button'
|
||||
import { NATIVE_CHAT_RESUME_SETTING_SECTION_ID } from './settings/native-chat-resume-setting-target'
|
||||
|
||||
/**
|
||||
* Says a run started on its own, in the place the "Don't ask again" box takes in the picker, and
|
||||
* where to change that. Only a run this launch's preference started gets it.
|
||||
*/
|
||||
export function NativeChatResumeAutoNote({
|
||||
inFlight,
|
||||
onOpenSettings
|
||||
}: {
|
||||
inFlight: boolean
|
||||
/** Closes the dialog so the settings page is not left under it. */
|
||||
onOpenSettings: () => void
|
||||
}): React.JSX.Element {
|
||||
const openSettingsPage = useAppStore((state) => state.openSettingsPage)
|
||||
const openSettingsTarget = useAppStore((state) => state.openSettingsTarget)
|
||||
return (
|
||||
<div className="order-last flex min-w-0 flex-wrap items-center text-xs text-muted-foreground sm:order-none sm:mr-auto">
|
||||
<span>
|
||||
{inFlight
|
||||
? translate(
|
||||
'auto.components.NativeChatResumeAutoNote.resuming',
|
||||
'Resuming automatically after the restart.'
|
||||
)
|
||||
: translate(
|
||||
'auto.components.NativeChatResumeAutoNote.resumed',
|
||||
'Resumed automatically after the restart.'
|
||||
)}
|
||||
</span>
|
||||
<Button
|
||||
variant="link"
|
||||
size="xs"
|
||||
onClick={() => {
|
||||
onOpenSettings()
|
||||
openSettingsPage()
|
||||
openSettingsTarget({
|
||||
pane: 'experimental',
|
||||
repoId: null,
|
||||
sectionId: NATIVE_CHAT_RESUME_SETTING_SECTION_ID
|
||||
})
|
||||
}}
|
||||
>
|
||||
{translate(
|
||||
'auto.components.NativeChatResumeAutoNote.changeInSettings',
|
||||
'Change in Settings'
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMemo } from 'react'
|
||||
import { useContext, useMemo } from 'react'
|
||||
import { Folder, FolderTree, GitBranch, Monitor } from 'lucide-react'
|
||||
import { RepoIconGlyph } from '@/components/repo/repo-icon'
|
||||
import { WorktreeHostContextBadge } from '@/components/sidebar/WorktreeHostContextBadge'
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
chatState,
|
||||
coveredKeys,
|
||||
ResumeTreeDepthContext,
|
||||
ResumeTreeSelectingContext,
|
||||
useResumeTreeExpansion,
|
||||
type FailureProps,
|
||||
type TreeProps
|
||||
@@ -82,6 +83,7 @@ function GroupNode({
|
||||
}): React.JSX.Element {
|
||||
const selection = resumeSelectionState(covered, tree.selected)
|
||||
const expanded = tree.isExpanded(nodeKey)
|
||||
const selecting = useContext(ResumeTreeSelectingContext)
|
||||
return (
|
||||
<>
|
||||
<ResumeTreeRow
|
||||
@@ -95,7 +97,9 @@ function GroupNode({
|
||||
checkboxLabel={checkboxLabel}
|
||||
>
|
||||
{label}
|
||||
<ResumeTreeCount selectedCount={selection.selectedCount} total={selection.total} />
|
||||
{selecting && (
|
||||
<ResumeTreeCount selectedCount={selection.selectedCount} total={selection.total} />
|
||||
)}
|
||||
</ResumeTreeRow>
|
||||
{/* Collapsing unmounts the rows only; their selection lives with the dialog. */}
|
||||
{expanded && children}
|
||||
@@ -346,13 +350,16 @@ export function ResumeOnRestartGroups({
|
||||
failureFor,
|
||||
onFailureAction,
|
||||
renderStatus,
|
||||
selectableIds
|
||||
selectableIds,
|
||||
selecting = true
|
||||
}: {
|
||||
candidates: readonly ResumeCandidate[]
|
||||
listedAt: number
|
||||
busy: boolean
|
||||
selected: ReadonlySet<string>
|
||||
onToggle: (sessionId: string, checked: boolean) => void
|
||||
/** False once every chat shows a run status, so no checkbox is left to act on. */
|
||||
selecting?: boolean
|
||||
} & FailureProps): React.JSX.Element {
|
||||
const machines = useMemo(
|
||||
() =>
|
||||
@@ -388,27 +395,29 @@ export function ResumeOnRestartGroups({
|
||||
const showMachines =
|
||||
machines.length > 1 || machines.some((machine) => machine.hostId !== LOCAL_EXECUTION_HOST_ID)
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
{machines.map((machine) =>
|
||||
showMachines ? (
|
||||
<MachineNode
|
||||
key={machine.hostId}
|
||||
hostId={machine.hostId}
|
||||
candidates={machine.candidates}
|
||||
workspaces={machine.workspaces}
|
||||
hostLabelById={hostLabelById}
|
||||
tree={tree}
|
||||
/>
|
||||
) : (
|
||||
<MachineContents
|
||||
key={machine.hostId}
|
||||
hostId={machine.hostId}
|
||||
workspaces={machine.workspaces}
|
||||
depth={0}
|
||||
tree={tree}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
<ResumeTreeSelectingContext.Provider value={selecting}>
|
||||
<div className="flex flex-col">
|
||||
{machines.map((machine) =>
|
||||
showMachines ? (
|
||||
<MachineNode
|
||||
key={machine.hostId}
|
||||
hostId={machine.hostId}
|
||||
candidates={machine.candidates}
|
||||
workspaces={machine.workspaces}
|
||||
hostLabelById={hostLabelById}
|
||||
tree={tree}
|
||||
/>
|
||||
) : (
|
||||
<MachineContents
|
||||
key={machine.hostId}
|
||||
hostId={machine.hostId}
|
||||
workspaces={machine.workspaces}
|
||||
depth={0}
|
||||
tree={tree}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</ResumeTreeSelectingContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -200,7 +200,7 @@ it('leaves a failure a retry cannot fix out of Select all', async () => {
|
||||
expect(button('Resume 1 chat').disabled).toBe(false)
|
||||
})
|
||||
|
||||
it('disables Select all and the workspace checkbox while a resume runs', async () => {
|
||||
it('drops Select all and the checkbox column while every listed chat is resuming', async () => {
|
||||
const continued = Promise.withResolvers<unknown>()
|
||||
rpc.mockImplementation((_target, method) =>
|
||||
method === 'agentSession.restartResumable'
|
||||
@@ -215,12 +215,11 @@ it('disables Select all and the workspace checkbox while a resume runs', async (
|
||||
)
|
||||
await act(async () => button('Resume 2 chats').click())
|
||||
await act(async () => button('Resuming chats 0/2').click())
|
||||
const list = document.querySelector('[aria-label="Chats that would be resumed"]')!
|
||||
const boxes = [...list.querySelectorAll('[role="checkbox"]')]
|
||||
expect(boxes).toHaveLength(2)
|
||||
for (const box of boxes) {
|
||||
expect(box.hasAttribute('disabled')).toBe(true)
|
||||
}
|
||||
const list = document.querySelector('[aria-label="Chats in this resume"]')!
|
||||
expect(list.querySelectorAll('[role="checkbox"]')).toHaveLength(0)
|
||||
expect(list.textContent).not.toContain('Select all')
|
||||
expect(list.textContent).not.toContain('of 2')
|
||||
expect(list.querySelectorAll('[role="img"][aria-label*="Waiting to start"]')).toHaveLength(2)
|
||||
await act(async () => continued.resolve({ sessions: [], failed: [], resumed: [], continued: [] }))
|
||||
})
|
||||
|
||||
|
||||
@@ -223,13 +223,13 @@ it('closes on Resume and shows the resume in the status bar until the host answe
|
||||
expect(rpc.mock.calls.length).toBe(reads)
|
||||
expect(document.querySelector('[role="dialog"]')?.textContent).toContain('Resuming 2 chats')
|
||||
expect(button('Resuming…').disabled).toBe(true)
|
||||
// Each chat being resumed shows a spinner where its checkbox was; tree nodes stay disabled.
|
||||
// With nothing left to choose there is no checkbox column; each spinner sits beside its chat.
|
||||
expect(runStatus('Prompt a')).toMatch(/^Prompt a: Waiting to start · \d+s$/)
|
||||
expect(runStatus('Prompt b')).toMatch(/^Prompt b: Waiting to start · \d+s$/)
|
||||
expect(chatBoxes()).toHaveLength(0)
|
||||
const status = document.querySelector('[role="img"][aria-label^="Prompt a: "]')
|
||||
expect(status?.parentElement?.classList.contains('w-7')).toBe(true)
|
||||
expect(status?.parentElement?.parentElement?.firstElementChild?.contains(status)).toBe(true)
|
||||
expect(document.querySelector('[role="tree"] .w-7')).toBeNull()
|
||||
expect(status?.parentElement?.classList.contains('size-4.5')).toBe(true)
|
||||
expect(status?.closest('[role="treeitem"]')?.getAttribute('aria-level')).toBe('2')
|
||||
expect(status?.closest('label')).toBeNull()
|
||||
|
||||
@@ -569,7 +569,9 @@ it('lists a chat the resume could not carry on when the dialog reopens, with wha
|
||||
const dialog = document.querySelector('[role="dialog"]')
|
||||
expect(dialog).not.toBeNull()
|
||||
expect(dialog?.textContent).toContain('Resumed 1 of 2 chats')
|
||||
expect(dialog?.textContent).toContain("Don't ask again (resume automatically)")
|
||||
// The setting is still off, so the box stays after Resume.
|
||||
expect(dialog?.textContent).toContain("Don't ask again")
|
||||
expect(dialog?.textContent).not.toContain('Resumed automatically')
|
||||
expect(dialog?.textContent).not.toContain('Dismiss failed')
|
||||
expect(runStatus('Prompt a')).toBe('Prompt a: Resumed')
|
||||
expect(chatBoxes()).toHaveLength(1)
|
||||
|
||||
@@ -42,6 +42,8 @@ import {
|
||||
useNativeChatRestartRun
|
||||
} from './native-chat-resume-on-restart-store'
|
||||
import { useResumeRunPanel } from './NativeChatResumeRunPanel'
|
||||
import { NativeChatResumeAutoNote } from './NativeChatResumeAutoNote'
|
||||
import { moveInTree } from './native-chat-resume-tree-keys'
|
||||
|
||||
/**
|
||||
* What would be resumed, shown before anything runs.
|
||||
@@ -56,7 +58,9 @@ import { useResumeRunPanel } from './NativeChatResumeRunPanel'
|
||||
* Resume closes the dialog at once and the status-bar entry carries the run, then any chat it could
|
||||
* not carry on. The run lives in the store, as a skill update's does, so the dialog is one view of it:
|
||||
* reopened mid-run, each chat in it shows where it stands in its checkbox's place, and the dialog
|
||||
* stays on that run until it is closed after the run has finished.
|
||||
* stays on that run until it is closed after the run has finished. Once no chat is left to choose,
|
||||
* it is a result: no Select all and no checkbox column. The "don't ask again" box stays while
|
||||
* the setting is off.
|
||||
*
|
||||
* A chat an earlier resume could not carry on is listed too, as the same row plus what went wrong
|
||||
* and what to do; selecting it and resuming is a retry, unless the host says a retry cannot run.
|
||||
@@ -76,56 +80,6 @@ function selectedByDefault(failure: ResumeFailure | undefined): boolean {
|
||||
return guidance.primary === 'retry' || guidance.secondary === 'retry'
|
||||
}
|
||||
|
||||
/**
|
||||
* Tree keys. On a row's checkbox: Up/Down step between the enabled checkboxes, Home/End jump to the
|
||||
* first/last, Left collapses and Right expands the node through its own disclosure; Space toggles
|
||||
* natively. On the tree itself (its Tab stop): Down/Home enter at the first checkbox, Up/End at
|
||||
* the last.
|
||||
*/
|
||||
function moveInTree(event: React.KeyboardEvent<HTMLElement>): void {
|
||||
const target = event.target
|
||||
if (!(target instanceof HTMLElement)) {
|
||||
return
|
||||
}
|
||||
const boxes = [
|
||||
...event.currentTarget.querySelectorAll<HTMLElement>('[role="checkbox"]:not(:disabled)')
|
||||
]
|
||||
const onTree = target === event.currentTarget
|
||||
if (!onTree && target.getAttribute('role') !== 'checkbox') {
|
||||
return
|
||||
}
|
||||
const jump =
|
||||
event.key === 'Home' || (onTree && event.key === 'ArrowDown')
|
||||
? boxes[0]
|
||||
: event.key === 'End' || (onTree && event.key === 'ArrowUp')
|
||||
? boxes.at(-1)
|
||||
: undefined
|
||||
if (jump) {
|
||||
event.preventDefault()
|
||||
jump.focus()
|
||||
return
|
||||
}
|
||||
if (onTree) {
|
||||
return
|
||||
}
|
||||
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
||||
event.preventDefault()
|
||||
boxes[boxes.indexOf(target) + (event.key === 'ArrowDown' ? 1 : -1)]?.focus()
|
||||
return
|
||||
}
|
||||
if (event.key === 'ArrowLeft' || event.key === 'ArrowRight') {
|
||||
event.preventDefault()
|
||||
// Rows are flat treeitems, so the nearest one is this checkbox's own node.
|
||||
const disclosure = target
|
||||
.closest('[role="treeitem"]')
|
||||
?.querySelector<HTMLButtonElement>('button[aria-expanded]')
|
||||
const open = disclosure?.getAttribute('aria-expanded') === 'true'
|
||||
if (disclosure && open === (event.key === 'ArrowLeft')) {
|
||||
disclosure.click()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function NativeChatResumeOnRestartModal(): React.JSX.Element | null {
|
||||
const offerEnabled = useNativeChatRestartOfferEnabled()
|
||||
const { candidates, failed, listedAt } = useNativeChatRestartOffer(offerEnabled)
|
||||
@@ -143,6 +97,9 @@ export function NativeChatResumeOnRestartModal(): React.JSX.Element | null {
|
||||
)
|
||||
const updateSettings = useAppStore((store) => store.updateSettings)
|
||||
const [dontAskAgain, setDontAskAgain] = useState(false)
|
||||
const autoResumeOn = useAppStore(
|
||||
(store) => store.settings?.nativeChatResumeWorkOnRestart === true
|
||||
)
|
||||
const resumeButtonRef = useRef<HTMLButtonElement>(null)
|
||||
// The store's: the resume outlives this dialog, which can close or reopen mid-run.
|
||||
const resuming = useNativeChatRestartResuming()
|
||||
@@ -194,6 +151,7 @@ export function NativeChatResumeOnRestartModal(): React.JSX.Element | null {
|
||||
run,
|
||||
rows,
|
||||
failureFor,
|
||||
selectableIds,
|
||||
open
|
||||
})
|
||||
useEffect(() => {
|
||||
@@ -202,17 +160,21 @@ export function NativeChatResumeOnRestartModal(): React.JSX.Element | null {
|
||||
}
|
||||
}, [offerEnabled, open, run])
|
||||
const allSelection = resumeSelectionState(selectable, ticked)
|
||||
const choosing = runPanel?.choosing ?? true
|
||||
// While the setting is off the box stays, before and after Resume; an automatic run explains itself.
|
||||
const showDontAskAgain = !autoResumeOn && runPanel?.startedBy !== 'launch'
|
||||
|
||||
const toggleSelected = useCallback((sessionId: string, checked: boolean) => {
|
||||
setOverrides((current) => new Map(current).set(sessionId, checked))
|
||||
}, [])
|
||||
|
||||
/** Applied on whichever action the user takes, so the box means the same thing every way out. */
|
||||
/** Consume the box's choice once; a hidden box must never replay. */
|
||||
const persistPreference = useCallback(async (): Promise<void> => {
|
||||
if (dontAskAgain) {
|
||||
if (dontAskAgain && showDontAskAgain) {
|
||||
setDontAskAgain(false)
|
||||
await updateSettings({ nativeChatResumeWorkOnRestart: true }).catch(() => undefined)
|
||||
}
|
||||
}, [dontAskAgain, updateSettings])
|
||||
}, [dontAskAgain, showDontAskAgain, updateSettings])
|
||||
|
||||
// Never closes the dialog: only the user's own ways out do, and the store once nothing is left.
|
||||
const resume = useCallback(
|
||||
@@ -327,42 +289,51 @@ export function NativeChatResumeOnRestartModal(): React.JSX.Element | null {
|
||||
<div
|
||||
role="tree"
|
||||
tabIndex={0}
|
||||
aria-label={translate(
|
||||
'auto.components.NativeChatResumeOnRestartModal.listLabel',
|
||||
'Chats that would be resumed'
|
||||
)}
|
||||
aria-label={
|
||||
choosing
|
||||
? translate(
|
||||
'auto.components.NativeChatResumeOnRestartModal.listLabel',
|
||||
'Chats that would be resumed'
|
||||
)
|
||||
: translate(
|
||||
'auto.components.NativeChatResumeOnRestartModal.resultListLabel',
|
||||
'Chats in this resume'
|
||||
)
|
||||
}
|
||||
// The sidebar's own surface, so its workspaces read here as they do there.
|
||||
className="min-h-0 overflow-y-auto scrollbar-sleek rounded-md bg-worktree-sidebar p-1.5 pb-2"
|
||||
onKeyDown={moveInTree}
|
||||
>
|
||||
{/* Here, not in the groups: a list may hold one set of groups per machine. The tree's one
|
||||
divider sets it apart from the nodes. */}
|
||||
<div className="mb-1 border-b border-worktree-sidebar-border pb-0.5">
|
||||
<ResumeTreeRow
|
||||
depth={0}
|
||||
name={selectAllLabel}
|
||||
checked={allSelection.checked}
|
||||
disabled={busy || allSelection.total === 0}
|
||||
onCheckedChange={() =>
|
||||
toggleResumeSelection(selectable, allSelection, toggleSelected)
|
||||
}
|
||||
checkboxLabel={translate(
|
||||
'auto.components.NativeChatResumeOnRestartModal.selectAll',
|
||||
'Select all chats'
|
||||
)}
|
||||
>
|
||||
<span className="min-w-0 truncate text-xs font-semibold text-muted-foreground">
|
||||
{selectAllLabel}
|
||||
</span>
|
||||
<span className="ml-auto shrink-0 pl-2 text-[11px] tabular-nums text-muted-foreground">
|
||||
{translate(
|
||||
'auto.components.NativeChatResumeOnRestartModal.selectedCount',
|
||||
'{{value0}} of {{value1}} selected',
|
||||
{ value0: allSelection.selectedCount, value1: allSelection.total }
|
||||
{choosing && (
|
||||
<div className="mb-1 border-b border-worktree-sidebar-border pb-0.5">
|
||||
<ResumeTreeRow
|
||||
depth={0}
|
||||
name={selectAllLabel}
|
||||
checked={allSelection.checked}
|
||||
disabled={busy || allSelection.total === 0}
|
||||
onCheckedChange={() =>
|
||||
toggleResumeSelection(selectable, allSelection, toggleSelected)
|
||||
}
|
||||
checkboxLabel={translate(
|
||||
'auto.components.NativeChatResumeOnRestartModal.selectAll',
|
||||
'Select all chats'
|
||||
)}
|
||||
</span>
|
||||
</ResumeTreeRow>
|
||||
</div>
|
||||
>
|
||||
<span className="min-w-0 truncate text-xs font-semibold text-muted-foreground">
|
||||
{selectAllLabel}
|
||||
</span>
|
||||
<span className="ml-auto shrink-0 pl-2 text-[11px] tabular-nums text-muted-foreground">
|
||||
{translate(
|
||||
'auto.components.NativeChatResumeOnRestartModal.selectedCount',
|
||||
'{{value0}} of {{value1}} selected',
|
||||
{ value0: allSelection.selectedCount, value1: allSelection.total }
|
||||
)}
|
||||
</span>
|
||||
</ResumeTreeRow>
|
||||
</div>
|
||||
)}
|
||||
<ResumeOnRestartGroups
|
||||
candidates={runPanel?.rows ?? rows}
|
||||
listedAt={listedAt}
|
||||
@@ -373,36 +344,42 @@ export function NativeChatResumeOnRestartModal(): React.JSX.Element | null {
|
||||
onFailureAction={(action, sessionId) => void actOnFailure(action, sessionId)}
|
||||
renderStatus={runPanel?.renderStatus}
|
||||
selectableIds={selectableIds}
|
||||
selecting={choosing}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Two controls: one deletes the offer, one acts on it. Closing snoozes, so it needs none. */}
|
||||
<DialogFooter className="sm:items-center">
|
||||
{/* Why order-last: the narrow footer stacks bottom-up, so this keeps the option above the actions. */}
|
||||
<label className="order-last flex min-w-0 items-start gap-2.5 sm:order-none sm:mr-auto">
|
||||
<Checkbox
|
||||
checked={dontAskAgain}
|
||||
disabled={busy}
|
||||
onCheckedChange={(next) => setDontAskAgain(next === true)}
|
||||
className="mt-0.5"
|
||||
/>
|
||||
<span className="min-w-0 space-y-0.5">
|
||||
<span className="block text-sm">
|
||||
{translate(
|
||||
'auto.components.NativeChatResumeOnRestartModal.dontAskAgain',
|
||||
"Don't ask again (resume automatically)"
|
||||
)}
|
||||
</span>
|
||||
{/* Where to undo it; what it does is the body copy's job. */}
|
||||
<span className="block text-xs text-muted-foreground">
|
||||
{translate(
|
||||
'auto.components.NativeChatResumeOnRestartModal.dontAskAgainHint',
|
||||
'You can turn this off in Settings → Experimental → Chat UI.'
|
||||
)}
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
{rows.length === 0 ? (
|
||||
{runPanel?.startedBy === 'launch' ? (
|
||||
<NativeChatResumeAutoNote inFlight={busy} onOpenSettings={snooze} />
|
||||
) : (
|
||||
showDontAskAgain && (
|
||||
// Why order-last: the narrow footer stacks bottom-up, so this keeps the option above the actions.
|
||||
<label className="order-last flex min-w-0 items-start gap-2.5 sm:order-none sm:mr-auto">
|
||||
<Checkbox
|
||||
checked={dontAskAgain}
|
||||
onCheckedChange={(next) => setDontAskAgain(next === true)}
|
||||
className="mt-0.5"
|
||||
/>
|
||||
<span className="min-w-0 space-y-0.5">
|
||||
<span className="block text-sm">
|
||||
{translate(
|
||||
'auto.components.NativeChatResumeOnRestartModal.dontAskAgain',
|
||||
"Don't ask again (resume automatically)"
|
||||
)}
|
||||
</span>
|
||||
{/* Where to undo it; what it does is the body copy's job. */}
|
||||
<span className="block text-xs text-muted-foreground">
|
||||
{translate(
|
||||
'auto.components.NativeChatResumeOnRestartModal.dontAskAgainHint',
|
||||
'You can turn this off in Settings → Experimental → Chat UI.'
|
||||
)}
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
)
|
||||
)}
|
||||
{rows.length === 0 || (run?.inFlight === false && !choosing) ? (
|
||||
<Button ref={resumeButtonRef} variant="default" size="sm" onClick={snooze}>
|
||||
{translate('auto.components.NativeChatResumeOnRestartModal.done', 'Done')}
|
||||
</Button>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Loader2 } from 'lucide-react'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { useNow } from '@/hooks/use-now'
|
||||
import type { ResumeCandidate, ResumeFailure } from './native-chat-resume-on-restart-grouping'
|
||||
import { resumeRunInFlight, type ResumeRun } from './native-chat-resume-run'
|
||||
import { resumeRunInFlight, type ResumeRun, type ResumeRunStarter } from './native-chat-resume-run'
|
||||
import { resumeRunView, type ResumeRunFilter } from './native-chat-resume-run-view'
|
||||
import { ResumeRunStatusIcon } from './NativeChatResumeRunStatusIcon'
|
||||
import { ResumeRunSummary } from './NativeChatResumeRunSummary'
|
||||
@@ -16,11 +16,37 @@ import { ResumeRunSummary } from './NativeChatResumeRunSummary'
|
||||
|
||||
export type ResumeRunPanel = {
|
||||
title: React.ReactNode
|
||||
/** Null once the run is over: the offer's own copy then describes what is left. */
|
||||
description: string | null
|
||||
description: string
|
||||
summary: React.ReactNode
|
||||
rows: ResumeCandidate[]
|
||||
renderStatus: (sessionId: string, title: string) => React.ReactNode
|
||||
/** Whether any listed chat still shows a checkbox; when none does, the dialog is a result. */
|
||||
choosing: boolean
|
||||
startedBy: ResumeRunStarter
|
||||
}
|
||||
|
||||
/** What the run did, in the past tense: the offer's copy describes an action not yet taken. */
|
||||
function finishedDescription(everyChatResumed: boolean, byUpdate: boolean): string {
|
||||
if (everyChatResumed) {
|
||||
return byUpdate
|
||||
? translate(
|
||||
'auto.components.NativeChatResumeRunPanel.finishedUpdate',
|
||||
'These chats were working when Orca installed an update. Orca restored each one where it stopped, with its full context, and asked the agent to check what it was doing before carrying on. Your own prompts weren’t re-sent.'
|
||||
)
|
||||
: translate(
|
||||
'auto.components.NativeChatResumeRunPanel.finished',
|
||||
'These chats were working when Orca closed. Orca restored each one where it stopped, with its full context, and asked the agent to check what it was doing before carrying on. Your own prompts weren’t re-sent.'
|
||||
)
|
||||
}
|
||||
return byUpdate
|
||||
? translate(
|
||||
'auto.components.NativeChatResumeRunPanel.finishedPartialUpdate',
|
||||
'These chats were working when Orca installed an update. The ones Orca resumed were restored where they stopped, with their full context, and asked to check what they were doing before carrying on. Your own prompts weren’t re-sent. Check the list for chats that still need attention or can be resumed.'
|
||||
)
|
||||
: translate(
|
||||
'auto.components.NativeChatResumeRunPanel.finishedPartial',
|
||||
'These chats were working when Orca closed. The ones Orca resumed were restored where they stopped, with their full context, and asked to check what they were doing before carrying on. Your own prompts weren’t re-sent. Check the list for chats that still need attention or can be resumed.'
|
||||
)
|
||||
}
|
||||
|
||||
function runTitle(run: ResumeRun, total: number, resumed: number): React.ReactNode {
|
||||
@@ -56,11 +82,13 @@ export function useResumeRunPanel({
|
||||
run,
|
||||
rows,
|
||||
failureFor,
|
||||
selectableIds,
|
||||
open
|
||||
}: {
|
||||
run: ResumeRun | null
|
||||
rows: readonly ResumeCandidate[]
|
||||
failureFor: (sessionId: string) => ResumeFailure | undefined
|
||||
selectableIds: ReadonlySet<string>
|
||||
open: boolean
|
||||
}): ResumeRunPanel | null {
|
||||
// Each opening starts on All, as the dialog's own ticks start from their defaults.
|
||||
@@ -94,7 +122,10 @@ export function useResumeRunPanel({
|
||||
'auto.components.NativeChatResumeRunPanel.description',
|
||||
'Each chat is restored with its full context and asked to check where it stopped. You can close this; the status bar keeps track.'
|
||||
)
|
||||
: null,
|
||||
: finishedDescription(
|
||||
view.counts.resumed === view.counts.all,
|
||||
run.entries.some((entry) => entry.candidate.trigger === 'update')
|
||||
),
|
||||
summary: (
|
||||
<ResumeRunSummary
|
||||
counts={view.counts}
|
||||
@@ -106,6 +137,9 @@ export function useResumeRunPanel({
|
||||
/>
|
||||
),
|
||||
rows: view.rows,
|
||||
// Current selection ignores the filter, so changing tabs never moves the layout.
|
||||
choosing: [...selectableIds].some((sessionId) => !view.statusBySession.has(sessionId)),
|
||||
startedBy: run.startedBy,
|
||||
renderStatus: (sessionId, title) => {
|
||||
const status = view.statusBySession.get(sessionId)
|
||||
return status ? (
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { useContext } from 'react'
|
||||
import { ChevronRight } from 'lucide-react'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { Checkbox } from './ui/checkbox'
|
||||
import { ResumeTreeSelectingContext } from './native-chat-resume-tree-state'
|
||||
|
||||
/** Indent per tree level, right of the shared checkbox column. */
|
||||
const RESUME_TREE_INDENT_PX = 18
|
||||
@@ -8,12 +10,13 @@ const RESUME_TREE_INDENT_PX = 18
|
||||
/** Width of the checkbox column (w-7). */
|
||||
const RESUME_TREE_CHECKBOX_COLUMN_PX = 28
|
||||
|
||||
/** Where a row's body starts: the checkbox column, the arrow slot (size-4.5) and the body's pl-1. */
|
||||
const RESUME_TREE_BODY_OFFSET_PX = RESUME_TREE_CHECKBOX_COLUMN_PX + 18 + 4
|
||||
/** Where a row's body starts after the checkbox column: the arrow slot (size-4.5) and the body's pl-1. */
|
||||
const RESUME_TREE_BODY_OFFSET_PX = 18 + 4
|
||||
|
||||
/**
|
||||
* One row of the resume tree: the checkbox in the list's single left column, the indent for its
|
||||
* depth, a disclosure arrow (an empty slot on a leaf, so icons line up), then the caller's body.
|
||||
* With nothing left to select there is no checkbox column, and a leaf's status takes its arrow slot.
|
||||
*
|
||||
* The row is a flat `treeitem` carrying its own `aria-level`, so the tree's keyboard handling can
|
||||
* find a row's disclosure without walking nested groups.
|
||||
@@ -53,37 +56,46 @@ export function ResumeTreeRow({
|
||||
below?: React.ReactNode
|
||||
children: React.ReactNode
|
||||
}): React.JSX.Element {
|
||||
const selecting = useContext(ResumeTreeSelectingContext)
|
||||
const indent = depth * RESUME_TREE_INDENT_PX
|
||||
const Content = checkboxSlot ? 'div' : 'label'
|
||||
const column = selecting ? RESUME_TREE_CHECKBOX_COLUMN_PX : 0
|
||||
const Content = checkboxSlot || !selecting ? 'div' : 'label'
|
||||
return (
|
||||
<div role="treeitem" aria-level={depth + 1} aria-expanded={expanded} className="flex flex-col">
|
||||
<div className="group/row relative flex min-w-0 items-center gap-1">
|
||||
<Content
|
||||
data-compact={compact}
|
||||
data-status={Boolean(checkboxSlot)}
|
||||
data-status={Boolean(checkboxSlot) || !selecting}
|
||||
className="flex h-7 min-w-0 flex-1 cursor-pointer items-center rounded-md pr-2.5 group-hover/row:bg-worktree-sidebar-accent has-[:disabled]:cursor-default data-[compact=true]:h-6.5 data-[status=true]:cursor-default"
|
||||
>
|
||||
<span className="flex w-7 shrink-0 justify-center">
|
||||
{checkboxSlot ?? (
|
||||
<Checkbox
|
||||
checked={checked}
|
||||
disabled={disabled}
|
||||
onCheckedChange={(next) => onCheckedChange(next === true)}
|
||||
aria-label={checkboxLabel}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
{selecting && (
|
||||
<span className="flex w-7 shrink-0 justify-center">
|
||||
{checkboxSlot ?? (
|
||||
<Checkbox
|
||||
checked={checked}
|
||||
disabled={disabled}
|
||||
onCheckedChange={(next) => onCheckedChange(next === true)}
|
||||
aria-label={checkboxLabel}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
<span aria-hidden="true" className="shrink-0" style={{ width: indent }} />
|
||||
{/* The arrow's slot, kept on a leaf too so icons line up. */}
|
||||
<span aria-hidden="true" className="size-4.5 shrink-0" />
|
||||
{!selecting && checkboxSlot ? (
|
||||
<span className="flex size-4.5 shrink-0 items-center justify-center">
|
||||
{checkboxSlot}
|
||||
</span>
|
||||
) : (
|
||||
<span aria-hidden="true" className="size-4.5 shrink-0" />
|
||||
)}
|
||||
<span className="flex min-w-0 flex-1 items-center gap-1.5 pl-1">{children}</span>
|
||||
</Content>
|
||||
{expanded !== undefined && (
|
||||
// Laid over its slot rather than inside the label, so pressing it never toggles the
|
||||
// checkbox. Off the tab order: Left/Right on the row's checkbox do the same.
|
||||
// Picker keys use the checkbox; result groups expose the native disclosure button.
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={-1}
|
||||
tabIndex={selecting ? -1 : 0}
|
||||
aria-expanded={expanded}
|
||||
aria-label={
|
||||
expanded
|
||||
@@ -100,6 +112,10 @@ export function ResumeTreeRow({
|
||||
}
|
||||
onClick={(event) => {
|
||||
onExpandedChange?.(!expanded)
|
||||
if (!selecting) {
|
||||
event.currentTarget.focus()
|
||||
return
|
||||
}
|
||||
// A pointer press focuses the arrow, where no tree key works; hand focus to the
|
||||
// row's checkbox, or to the tree when that is disabled.
|
||||
const row = event.currentTarget.closest('[role="treeitem"]')
|
||||
@@ -107,8 +123,8 @@ export function ResumeTreeRow({
|
||||
const next = box ?? row?.closest<HTMLElement>('[role="tree"]')
|
||||
next?.focus()
|
||||
}}
|
||||
className="group/disclosure absolute top-1/2 flex size-4.5 -translate-y-1/2 items-center justify-center rounded-sm text-muted-foreground hover:bg-foreground/8"
|
||||
style={{ left: RESUME_TREE_CHECKBOX_COLUMN_PX + indent }}
|
||||
className="group/disclosure absolute top-1/2 flex size-4.5 -translate-y-1/2 items-center justify-center rounded-sm text-muted-foreground hover:bg-foreground/8 outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
style={{ left: column + indent }}
|
||||
>
|
||||
<ChevronRight className="size-3 transition-transform group-aria-expanded/disclosure:rotate-90 motion-reduce:transition-none" />
|
||||
</button>
|
||||
@@ -116,7 +132,10 @@ export function ResumeTreeRow({
|
||||
{trailing}
|
||||
</div>
|
||||
{below && (
|
||||
<div className="pr-2.5" style={{ paddingLeft: RESUME_TREE_BODY_OFFSET_PX + indent }}>
|
||||
<div
|
||||
className="pr-2.5"
|
||||
style={{ paddingLeft: column + RESUME_TREE_BODY_OFFSET_PX + indent }}
|
||||
>
|
||||
{below}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -17,7 +17,8 @@ import {
|
||||
beginResumeRun,
|
||||
resumeRunInFlight,
|
||||
resumeRunPendingIds,
|
||||
type ResumeRun
|
||||
type ResumeRun,
|
||||
type ResumeRunStarter
|
||||
} from './native-chat-resume-run'
|
||||
import {
|
||||
forgetUnsentResumes,
|
||||
@@ -211,7 +212,10 @@ export async function reopenNativeChatRestartOffer(): Promise<void> {
|
||||
}
|
||||
|
||||
/** One host action owns the selection; its status feed reports each chat before the reply. */
|
||||
export async function continueNativeChatRestartOffer(sessionIds: readonly string[]): Promise<void> {
|
||||
export async function continueNativeChatRestartOffer(
|
||||
sessionIds: readonly string[],
|
||||
startedBy: ResumeRunStarter = 'user'
|
||||
): Promise<void> {
|
||||
const requested = [...new Set(sessionIds)]
|
||||
if (requested.length === 0) {
|
||||
return
|
||||
@@ -221,7 +225,8 @@ export async function continueNativeChatRestartOffer(sessionIds: readonly string
|
||||
const rows = new Map([...offer.candidates, ...offer.failed].map((row) => [row.sessionId, row]))
|
||||
let actionRun = beginResumeRun(
|
||||
requested.flatMap((sessionId) => rows.get(sessionId) ?? []),
|
||||
Date.now()
|
||||
Date.now(),
|
||||
startedBy
|
||||
)
|
||||
setRun(actionRun)
|
||||
releaseRunStatus?.()
|
||||
@@ -347,7 +352,7 @@ async function loadLaunchOffer(): Promise<void> {
|
||||
requestNativeChatResumeOnRestartDialog()
|
||||
return
|
||||
}
|
||||
await continueNativeChatRestartOffer(allResumeSessionIds(offered))
|
||||
await continueNativeChatRestartOffer(allResumeSessionIds(offered), 'launch')
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
// @vitest-environment happy-dom
|
||||
import { act } from 'react'
|
||||
import { expect, it, vi, type Mock } from 'vitest'
|
||||
import { NativeChatResumeOnRestartModal } from './NativeChatResumeOnRestartModal'
|
||||
import { NativeChatResumeStatusSegment } from './status-bar/NativeChatResumeStatusSegment'
|
||||
import {
|
||||
createResumeModalFixture,
|
||||
failure,
|
||||
offered,
|
||||
type RestartRpc,
|
||||
type ResumeStatusStream
|
||||
} from './native-chat-resume-modal.test-support'
|
||||
import { lastToastShow } from './native-chat-resume-toast.test-support'
|
||||
import { chatBox, dontAskAgain } from './native-chat-resume-on-restart-modal.test-support'
|
||||
import { useAppStore } from '../store'
|
||||
|
||||
const rpc: Mock<RestartRpc> = vi.hoisted(() => vi.fn<RestartRpc>())
|
||||
const statusStream: ResumeStatusStream = vi.hoisted(() => ({
|
||||
emit: (_event: Parameters<ResumeStatusStream['emit']>[0]) => {},
|
||||
snapshot: new Map()
|
||||
}))
|
||||
vi.mock('@/runtime/structured-agent-session-client', () => ({
|
||||
callStructuredAgentSession: rpc,
|
||||
subscribeStructuredAgentSessionStatus: async (
|
||||
_target: unknown,
|
||||
emit: ResumeStatusStream['emit']
|
||||
) => {
|
||||
statusStream.emit = emit
|
||||
emit({ type: 'snapshot', sessions: [...statusStream.snapshot.values()] })
|
||||
return { unsubscribe: () => {} }
|
||||
}
|
||||
}))
|
||||
vi.mock('sonner', () => ({ toast: vi.fn() }))
|
||||
|
||||
const { mount, button, fakeHost } = createResumeModalFixture(rpc, statusStream)
|
||||
const dialog = () => document.querySelector('[role="dialog"]')
|
||||
const tree = () => document.querySelector<HTMLElement>('[role="tree"]')
|
||||
|
||||
async function mountSurfaces(): Promise<void> {
|
||||
await mount(
|
||||
<>
|
||||
<NativeChatResumeOnRestartModal />
|
||||
<NativeChatResumeStatusSegment iconOnly={false} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
it('shows a finished resume as a result, in the past tense, once nothing is left to choose', async () => {
|
||||
fakeHost()
|
||||
await mountSurfaces()
|
||||
await act(async () => button('Resume 2 chats').click())
|
||||
await act(async () => lastToastShow()?.())
|
||||
expect(dialog()?.textContent).toContain('Resumed 2 of 2 chats')
|
||||
expect(dialog()?.textContent).toContain(
|
||||
'Orca restored each one where it stopped, with its full context'
|
||||
)
|
||||
expect(dialog()?.textContent).not.toContain('Resuming restores')
|
||||
expect(tree()?.querySelectorAll('[role="checkbox"]')).toHaveLength(0)
|
||||
expect(tree()?.textContent).not.toContain('Select all')
|
||||
// Auto-resume is still off, so the box stays after Resume.
|
||||
expect(dialog()?.textContent).toContain("Don't ask again")
|
||||
// The user pressed Resume, so nothing says it happened on its own.
|
||||
expect(dialog()?.textContent).not.toContain('Resumed automatically')
|
||||
})
|
||||
|
||||
it('saves "Don\'t ask again" ticked on a finished resume the user started', async () => {
|
||||
fakeHost()
|
||||
await mountSurfaces()
|
||||
await act(async () => button('Resume 2 chats').click())
|
||||
await act(async () => lastToastShow()?.())
|
||||
expect(dialog()?.textContent).toContain('Resumed 2 of 2 chats')
|
||||
await act(async () => dontAskAgain().click())
|
||||
await act(async () => button('Done').click())
|
||||
expect(useAppStore.getState().settings?.nativeChatResumeWorkOnRestart).toBe(true)
|
||||
})
|
||||
|
||||
it('does not restore a hidden picker preference when closing a run', async () => {
|
||||
const host = fakeHost()
|
||||
host.hold('a', 'b')
|
||||
await mountSurfaces()
|
||||
await act(async () => dontAskAgain().click())
|
||||
await act(async () => button('Resume 2 chats').click())
|
||||
expect(useAppStore.getState().settings?.nativeChatResumeWorkOnRestart).toBe(true)
|
||||
|
||||
await act(async () => button('Resuming chats 0/2').click())
|
||||
expect(dialog()?.textContent).not.toContain("Don't ask again")
|
||||
await act(async () =>
|
||||
useAppStore.getState().updateSettings({ nativeChatResumeWorkOnRestart: false })
|
||||
)
|
||||
await act(async () => button('Close').click())
|
||||
const afterInFlightClose = useAppStore.getState().settings?.nativeChatResumeWorkOnRestart
|
||||
|
||||
await host.release('a', 'b')
|
||||
await act(async () => lastToastShow()?.())
|
||||
expect(dialog()?.textContent).toContain('Resumed 2 of 2 chats')
|
||||
// Turned off elsewhere, the box is back, unticked: the earlier tick was consumed.
|
||||
expect(dontAskAgain().getAttribute('data-state')).toBe('unchecked')
|
||||
await act(async () =>
|
||||
useAppStore.getState().updateSettings({ nativeChatResumeWorkOnRestart: false })
|
||||
)
|
||||
await act(async () => button('Done').click())
|
||||
expect([
|
||||
afterInFlightClose,
|
||||
useAppStore.getState().settings?.nativeChatResumeWorkOnRestart
|
||||
]).toEqual([false, false])
|
||||
})
|
||||
|
||||
it('starts a remaining-offer picker without the preference consumed by the previous resume', async () => {
|
||||
fakeHost()
|
||||
await mountSurfaces()
|
||||
await act(async () => chatBox('b').click())
|
||||
await act(async () => dontAskAgain().click())
|
||||
await act(async () => button('Resume 1 chat').click())
|
||||
expect(useAppStore.getState().settings?.nativeChatResumeWorkOnRestart).toBe(true)
|
||||
|
||||
await act(async () => button('1 chat to resume').click())
|
||||
expect(dialog()?.textContent).toContain('Resumed 1 of 1 chat')
|
||||
await act(async () => button('Close').click())
|
||||
await act(async () =>
|
||||
useAppStore.getState().updateSettings({ nativeChatResumeWorkOnRestart: false })
|
||||
)
|
||||
|
||||
await act(async () => button('1 chat to resume').click())
|
||||
const pickerCheckbox = dontAskAgain().getAttribute('data-state')
|
||||
await act(async () => button('Close').click())
|
||||
expect([pickerCheckbox, useAppStore.getState().settings?.nativeChatResumeWorkOnRestart]).toEqual([
|
||||
'unchecked',
|
||||
false
|
||||
])
|
||||
})
|
||||
|
||||
it('reopens a collapsed result group with tree keys', async () => {
|
||||
fakeHost()
|
||||
await mountSurfaces()
|
||||
expect(button('Collapse workspace').tabIndex).toBe(-1)
|
||||
await act(async () => button('Resume 2 chats').click())
|
||||
await act(async () => lastToastShow()?.())
|
||||
const disclosure = button('Collapse workspace')
|
||||
const press = async (key: string) =>
|
||||
act(async () => {
|
||||
document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true }))
|
||||
})
|
||||
expect(disclosure.tabIndex).toBe(0)
|
||||
tree()?.focus()
|
||||
await press('ArrowDown')
|
||||
expect(document.activeElement).toBe(disclosure)
|
||||
await press('ArrowLeft')
|
||||
expect(disclosure.getAttribute('aria-expanded')).toBe('false')
|
||||
expect(document.activeElement).toBe(disclosure)
|
||||
await press('ArrowRight')
|
||||
expect(disclosure.getAttribute('aria-expanded')).toBe('true')
|
||||
expect(document.activeElement).toBe(disclosure)
|
||||
})
|
||||
|
||||
it('shows a settled nonretryable failure as a result with a Done action', async () => {
|
||||
const failed = { ...failure('a'), outcome: 'unconfirmed' as const, retryable: false }
|
||||
let acted = false
|
||||
rpc.mockImplementation(async (_target, method) => {
|
||||
if (method === 'agentSession.restartResumable') {
|
||||
return { sessions: acted ? [] : [offered[0]], failed: acted ? [failed] : [] }
|
||||
}
|
||||
acted = true
|
||||
return {
|
||||
sessions: [],
|
||||
failed: [failed],
|
||||
continued: [{ sessionId: 'a', outcome: 'unknown' }]
|
||||
}
|
||||
})
|
||||
await mountSurfaces()
|
||||
await act(async () => button('Resume 1 chat').click())
|
||||
await act(async () => lastToastShow()?.())
|
||||
expect(dialog()?.textContent).toContain('Resumed 0 of 1 chat')
|
||||
expect(tree()?.textContent).not.toContain('Select all')
|
||||
expect(tree()?.querySelectorAll('[role="checkbox"]')).toHaveLength(0)
|
||||
expect(button('Open chat')).toBeTruthy()
|
||||
expect(button('Dismiss "Prompt a" in workspace')).toBeTruthy()
|
||||
expect(button('Done').disabled).toBe(false)
|
||||
expect(dialog()?.textContent).not.toContain('Resume 0 chats')
|
||||
})
|
||||
|
||||
it('does not place an untouched offer in the Need you filter', async () => {
|
||||
fakeHost()
|
||||
await mountSurfaces()
|
||||
const secondChat = document.querySelector<HTMLElement>(
|
||||
'[role="checkbox"][aria-label*="Prompt b"]'
|
||||
)
|
||||
await act(async () => secondChat?.click())
|
||||
await act(async () => button('Resume 1 chat').click())
|
||||
await act(async () => lastToastShow()?.())
|
||||
expect(dialog()?.textContent).toContain('Check the list for chats that still need attention')
|
||||
expect(dialog()?.textContent).not.toContain('Chats that need you are at the top')
|
||||
expect(button('Need you0')).toBeTruthy()
|
||||
expect(button('Resume 1 chat')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('says a launch resumed on its own and links to the setting', async () => {
|
||||
const openSettingsPage = vi.fn()
|
||||
const openSettingsTarget = vi.fn()
|
||||
useAppStore.setState((state) => ({
|
||||
settings: { ...state.settings!, nativeChatResumeWorkOnRestart: true },
|
||||
openSettingsPage,
|
||||
openSettingsTarget
|
||||
}))
|
||||
fakeHost()
|
||||
await mountSurfaces()
|
||||
expect(dialog()).toBeNull()
|
||||
await act(async () => lastToastShow()?.())
|
||||
expect(dialog()?.textContent).toContain('Resumed automatically after the restart.')
|
||||
expect(dialog()?.textContent).not.toContain("Don't ask again")
|
||||
|
||||
await act(async () => button('Change in Settings').click())
|
||||
expect(openSettingsPage).toHaveBeenCalledOnce()
|
||||
expect(openSettingsTarget).toHaveBeenCalledWith({
|
||||
pane: 'experimental',
|
||||
repoId: null,
|
||||
sectionId: 'native-chat-resume-on-restart'
|
||||
})
|
||||
// The dialog gets out of the way of the page it opened.
|
||||
expect(dialog()).toBeNull()
|
||||
})
|
||||
|
||||
it('keeps the checkboxes while a chat can still be chosen, whichever tab is shown', async () => {
|
||||
fakeHost({}, (sessionId) => (sessionId === 'b' ? 'refused' : 'continued'))
|
||||
await mountSurfaces()
|
||||
await act(async () => button('Resume 2 chats').click())
|
||||
await act(async () => button('1 chat failed to resume').click())
|
||||
expect(dialog()?.textContent).toContain('The ones Orca resumed were restored')
|
||||
expect(tree()?.textContent).toContain('Select all')
|
||||
await act(async () => button('Resumed1').click())
|
||||
// Filtering to rows that all show a status must not drop the column under the user.
|
||||
expect(tree()?.textContent).toContain('Select all')
|
||||
})
|
||||
@@ -18,7 +18,7 @@ const row = (sessionId: string): ResumeCandidate => ({
|
||||
recordedAt: 1
|
||||
})
|
||||
const failure = { ...row('a'), failedAt: 2, outcome: 'unconfirmed' as const, reason: 'pending' }
|
||||
const run = beginResumeRun([row('a'), row('b')], 10)
|
||||
const run = beginResumeRun([row('a'), row('b')], 10, 'user')
|
||||
|
||||
describe('the dialog over a bulk action', () => {
|
||||
it('never regresses a seen phase or verdict when live progress clears', () => {
|
||||
|
||||
@@ -7,9 +7,13 @@ export type ResumeRunHostStatus = Pick<
|
||||
'restartResume' | 'hostExecutionPhase'
|
||||
>
|
||||
|
||||
/** Who started a run: this launch's "resume automatically" preference, or the user's own click. */
|
||||
export type ResumeRunStarter = 'launch' | 'user'
|
||||
|
||||
/** Selection and history belong to this window; live progress and failures belong to the host. */
|
||||
export type ResumeRun = Readonly<{
|
||||
startedAt: number
|
||||
startedBy: ResumeRunStarter
|
||||
entries: readonly Readonly<{ candidate: ResumeCandidate; observedStatus?: ResumeRunHostStatus }>[]
|
||||
inFlight: boolean
|
||||
continued?: readonly RestartContinuationOutcome[]
|
||||
@@ -60,8 +64,17 @@ export function resumeRunInFlight(run: ResumeRun | null): boolean {
|
||||
return run?.inFlight ?? false
|
||||
}
|
||||
|
||||
export function beginResumeRun(candidates: readonly ResumeCandidate[], now: number): ResumeRun {
|
||||
return { startedAt: now, entries: candidates.map((candidate) => ({ candidate })), inFlight: true }
|
||||
export function beginResumeRun(
|
||||
candidates: readonly ResumeCandidate[],
|
||||
now: number,
|
||||
startedBy: ResumeRunStarter
|
||||
): ResumeRun {
|
||||
return {
|
||||
startedAt: now,
|
||||
startedBy,
|
||||
entries: candidates.map((candidate) => ({ candidate })),
|
||||
inFlight: true
|
||||
}
|
||||
}
|
||||
|
||||
const NONE: readonly string[] = []
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Tree keys. Picker checkboxes and result disclosures share arrow navigation; native Space/Enter
|
||||
* still acts on the focused control. The picker keeps its disclosures outside the Tab order.
|
||||
*/
|
||||
export function moveInTree(event: React.KeyboardEvent<HTMLElement>): void {
|
||||
const target = event.target
|
||||
if (!(target instanceof HTMLElement)) {
|
||||
return
|
||||
}
|
||||
const controls = [
|
||||
...event.currentTarget.querySelectorAll<HTMLElement>(
|
||||
'[role="checkbox"]:not(:disabled), button[aria-expanded][tabindex="0"]'
|
||||
)
|
||||
]
|
||||
const onTree = target === event.currentTarget
|
||||
if (!onTree && !controls.includes(target)) {
|
||||
return
|
||||
}
|
||||
const jump =
|
||||
event.key === 'Home' || (onTree && event.key === 'ArrowDown')
|
||||
? controls[0]
|
||||
: event.key === 'End' || (onTree && event.key === 'ArrowUp')
|
||||
? controls.at(-1)
|
||||
: undefined
|
||||
if (jump) {
|
||||
event.preventDefault()
|
||||
jump.focus()
|
||||
return
|
||||
}
|
||||
if (onTree) {
|
||||
return
|
||||
}
|
||||
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
||||
event.preventDefault()
|
||||
controls[controls.indexOf(target) + (event.key === 'ArrowDown' ? 1 : -1)]?.focus()
|
||||
return
|
||||
}
|
||||
if (event.key === 'ArrowLeft' || event.key === 'ArrowRight') {
|
||||
event.preventDefault()
|
||||
// Rows are flat treeitems, so the nearest one owns this control's disclosure.
|
||||
const disclosure = target
|
||||
.closest('[role="treeitem"]')
|
||||
?.querySelector<HTMLButtonElement>('button[aria-expanded]')
|
||||
const open = disclosure?.getAttribute('aria-expanded') === 'true'
|
||||
if (disclosure && open === (event.key === 'ArrowLeft')) {
|
||||
disclosure.click()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,9 @@ export function useResumeTreeExpansion(): {
|
||||
/** The tree depth a chat row renders at; the enclosing workspace node provides it. */
|
||||
export const ResumeTreeDepthContext = createContext(0)
|
||||
|
||||
/** False once no chat is selectable: the tree drops its checkbox column. */
|
||||
export const ResumeTreeSelectingContext = createContext(true)
|
||||
|
||||
/** Lets a row that an earlier resume could not carry on show what went wrong and what to do. */
|
||||
export type FailureProps = {
|
||||
failureFor?: (sessionId: string) => ResumeFailure | undefined
|
||||
|
||||
@@ -8,6 +8,7 @@ import { NativeChatSupportedAgents } from './NativeChatSupportedAgents'
|
||||
import { SearchableSetting } from './SearchableSetting'
|
||||
import { SettingsSwitch } from './SettingsFormControls'
|
||||
import { getExperimentalSearchEntry } from './experimental-search'
|
||||
import { NATIVE_CHAT_RESUME_SETTING_SECTION_ID } from './native-chat-resume-setting-target'
|
||||
|
||||
type NativeChatExperimentalSettingProps = {
|
||||
settings: GlobalSettings
|
||||
@@ -64,7 +65,10 @@ export function NativeChatExperimentalSetting({
|
||||
<div className="ml-4 space-y-4 border-l border-border pl-4">
|
||||
{/* Only structured sessions have a resume cursor to continue from. */}
|
||||
{structuredChatActive ? (
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div
|
||||
id={NATIVE_CHAT_RESUME_SETTING_SECTION_ID}
|
||||
className="flex items-start justify-between gap-4"
|
||||
>
|
||||
<div className="min-w-0 shrink space-y-0.5">
|
||||
<Label>
|
||||
{translate(
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
/** The auto-resume row in Settings → Experimental → Chat UI, so the resume dialog can link to it. */
|
||||
export const NATIVE_CHAT_RESUME_SETTING_SECTION_ID = 'native-chat-resume-on-restart'
|
||||
@@ -2154,7 +2154,8 @@
|
||||
"expandNode": "Expand {{value0}}",
|
||||
"selectMachine": "Select all chats on {{value0}}",
|
||||
"sshHost": "SSH",
|
||||
"done": "Done"
|
||||
"done": "Done",
|
||||
"resultListLabel": "Chats in this resume"
|
||||
},
|
||||
"StarNagCard": {
|
||||
"92b0f9d921": "is authenticated and try again.",
|
||||
@@ -17904,7 +17905,11 @@
|
||||
"resumingTitleOne": "Resuming 1 chat",
|
||||
"resumingTitle": "Resuming {{value0}} chats",
|
||||
"description": "Each chat is restored with its full context and asked to check where it stopped. You can close this; the status bar keeps track.",
|
||||
"resumedTitleOne": "Resumed {{value0}} of 1 chat"
|
||||
"resumedTitleOne": "Resumed {{value0}} of 1 chat",
|
||||
"finished": "These chats were working when Orca closed. Orca restored each one where it stopped, with its full context, and asked the agent to check what it was doing before carrying on. Your own prompts weren’t re-sent.",
|
||||
"finishedUpdate": "These chats were working when Orca installed an update. Orca restored each one where it stopped, with its full context, and asked the agent to check what it was doing before carrying on. Your own prompts weren’t re-sent.",
|
||||
"finishedPartial": "These chats were working when Orca closed. The ones Orca resumed were restored where they stopped, with their full context, and asked to check what they were doing before carrying on. Your own prompts weren’t re-sent. Check the list for chats that still need attention or can be resumed.",
|
||||
"finishedPartialUpdate": "These chats were working when Orca installed an update. The ones Orca resumed were restored where they stopped, with their full context, and asked to check what they were doing before carrying on. Your own prompts weren’t re-sent. Check the list for chats that still need attention or can be resumed."
|
||||
},
|
||||
"NativeChatResumeRunStatusIcon": {
|
||||
"waitingForReply": "Waiting for a reply",
|
||||
@@ -17924,6 +17929,11 @@
|
||||
"attention": "Need you",
|
||||
"running": "Running {{value0}}",
|
||||
"filter": "Show chats"
|
||||
},
|
||||
"NativeChatResumeAutoNote": {
|
||||
"resuming": "Resuming automatically after the restart.",
|
||||
"resumed": "Resumed automatically after the restart.",
|
||||
"changeInSettings": "Change in Settings"
|
||||
}
|
||||
},
|
||||
"i18n": {
|
||||
|
||||
@@ -17540,7 +17540,8 @@
|
||||
"expandNode": "Expandir {{value0}}",
|
||||
"selectMachine": "Seleccionar todos los chats en {{value0}}",
|
||||
"sshHost": "SSH",
|
||||
"done": "Listo"
|
||||
"done": "Listo",
|
||||
"resultListLabel": "Chats de esta reanudación"
|
||||
},
|
||||
"NativeChatResumeOutcomeRow": {
|
||||
"openChat": "Abrir chat",
|
||||
@@ -17646,7 +17647,11 @@
|
||||
"resumedTitleOne": "{{value0}} de 1 chat reanudado",
|
||||
"resumingTitleOne": "Reanudando 1 chat",
|
||||
"resumingTitle": "Reanudando {{value0}} chats",
|
||||
"description": "Cada chat se restaura con todo su contexto y se le pide que revise dónde se detuvo. Puedes cerrar esto; la barra de estado sigue el progreso."
|
||||
"description": "Cada chat se restaura con todo su contexto y se le pide que revise dónde se detuvo. Puedes cerrar esto; la barra de estado sigue el progreso.",
|
||||
"finished": "Estos chats estaban trabajando cuando Orca se cerró. Orca restauró cada uno donde se detuvo, con todo su contexto, y pidió al agente que revisara lo que estaba haciendo antes de continuar. Tus propios mensajes no se reenviaron.",
|
||||
"finishedUpdate": "Estos chats estaban trabajando cuando Orca instaló una actualización. Orca restauró cada uno donde se detuvo, con todo su contexto, y pidió al agente que revisara lo que estaba haciendo antes de continuar. Tus propios mensajes no se reenviaron.",
|
||||
"finishedPartial": "Estos chats estaban trabajando cuando Orca se cerró. Los que Orca reanudó se restauraron donde se detuvieron, con todo su contexto, y se les pidió revisar lo que estaban haciendo antes de continuar. Tus propios mensajes no se reenviaron. Consulta la lista para ver los chats que aún necesitan atención o que se pueden reanudar.",
|
||||
"finishedPartialUpdate": "Estos chats estaban trabajando cuando Orca instaló una actualización. Los que Orca reanudó se restauraron donde se detuvieron, con todo su contexto, y se les pidió revisar lo que estaban haciendo antes de continuar. Tus propios mensajes no se reenviaron. Consulta la lista para ver los chats que aún necesitan atención o que se pueden reanudar."
|
||||
},
|
||||
"NativeChatResumeRunStatusIcon": {
|
||||
"waitingForReply": "Esperando respuesta",
|
||||
@@ -17666,6 +17671,11 @@
|
||||
"attention": "Requieren tu atención",
|
||||
"running": "En curso desde hace {{value0}}",
|
||||
"filter": "Mostrar chats"
|
||||
},
|
||||
"NativeChatResumeAutoNote": {
|
||||
"resuming": "Reanudando automáticamente tras el reinicio.",
|
||||
"resumed": "Reanudados automáticamente tras el reinicio.",
|
||||
"changeInSettings": "Cambiar en Ajustes"
|
||||
}
|
||||
},
|
||||
"i18n": {
|
||||
|
||||
@@ -17661,7 +17661,8 @@
|
||||
"expandNode": "Développer {{value0}}",
|
||||
"selectMachine": "Sélectionner toutes les discussions sur {{value0}}",
|
||||
"sshHost": "SSH",
|
||||
"done": "Terminé"
|
||||
"done": "Terminé",
|
||||
"resultListLabel": "Discussions de cette reprise"
|
||||
},
|
||||
"linear-issue-attribute-filter-coverage-notice": {
|
||||
"statusPartialTeamCoverage": "Filtrage des statuts des équipes {{value0}} sur {{value1}} : les problèmes des équipes restantes ne sont pas inclus.",
|
||||
@@ -17748,7 +17749,11 @@
|
||||
"resumedTitleOne": "{{value0}} discussion sur 1 reprise",
|
||||
"resumingTitleOne": "Reprise d’une discussion",
|
||||
"resumingTitle": "Reprise de {{value0}} discussions",
|
||||
"description": "Chaque discussion est restaurée avec tout son contexte, et l’agent vérifie où il s’était arrêté. Vous pouvez fermer cette fenêtre ; la barre d’état suit la progression."
|
||||
"description": "Chaque discussion est restaurée avec tout son contexte, et l’agent vérifie où il s’était arrêté. Vous pouvez fermer cette fenêtre ; la barre d’état suit la progression.",
|
||||
"finished": "Ces discussions étaient en cours quand Orca s’est fermé. Orca a restauré chacune là où elle s’était arrêtée, avec tout son contexte, et a demandé à l’agent de vérifier ce qu’il faisait avant de continuer. Vos propres messages n’ont pas été renvoyés.",
|
||||
"finishedUpdate": "Ces discussions étaient en cours quand Orca a installé une mise à jour. Orca a restauré chacune là où elle s’était arrêtée, avec tout son contexte, et a demandé à l’agent de vérifier ce qu’il faisait avant de continuer. Vos propres messages n’ont pas été renvoyés.",
|
||||
"finishedPartial": "Ces discussions étaient en cours quand Orca s’est fermé. Celles qu’Orca a reprises ont été restaurées là où elles s’étaient arrêtées, avec tout leur contexte, et invitées à vérifier ce qu’elles faisaient avant de continuer. Vos propres messages n’ont pas été renvoyés. Consultez la liste pour voir les discussions qui nécessitent encore votre attention ou qui peuvent être reprises.",
|
||||
"finishedPartialUpdate": "Ces discussions étaient en cours quand Orca a installé une mise à jour. Celles qu’Orca a reprises ont été restaurées là où elles s’étaient arrêtées, avec tout leur contexte, et invitées à vérifier ce qu’elles faisaient avant de continuer. Vos propres messages n’ont pas été renvoyés. Consultez la liste pour voir les discussions qui nécessitent encore votre attention ou qui peuvent être reprises."
|
||||
},
|
||||
"NativeChatResumeRunStatusIcon": {
|
||||
"waitingForReply": "En attente de réponse",
|
||||
@@ -17768,6 +17773,11 @@
|
||||
"attention": "À traiter",
|
||||
"running": "En cours depuis {{value0}}",
|
||||
"filter": "Afficher les discussions"
|
||||
},
|
||||
"NativeChatResumeAutoNote": {
|
||||
"resuming": "Reprise automatique après le redémarrage.",
|
||||
"resumed": "Reprises automatiquement après le redémarrage.",
|
||||
"changeInSettings": "Modifier dans les paramètres"
|
||||
}
|
||||
},
|
||||
"i18n": {
|
||||
|
||||
@@ -17544,7 +17544,8 @@
|
||||
"expandNode": "{{value0}} を展開します",
|
||||
"selectMachine": "{{value0}} のチャットをすべて選択",
|
||||
"sshHost": "SSH",
|
||||
"done": "完了"
|
||||
"done": "完了",
|
||||
"resultListLabel": "今回再開したチャット"
|
||||
},
|
||||
"linear-issue-attribute-filter-coverage-notice": {
|
||||
"statusPartialTeamCoverage": "{{value0}}/{{value1}} チームステータスをフィルタリングしています。残りのチームからの問題は含まれません。",
|
||||
@@ -17646,7 +17647,11 @@
|
||||
"resumedTitleOne": "1 件中 {{value0}} 件のチャットを再開しました",
|
||||
"resumingTitleOne": "1 件のチャットを再開中",
|
||||
"resumingTitle": "{{value0}} 件のチャットを再開中",
|
||||
"description": "各チャットは全コンテキストとともに復元され、中断した箇所を確認するよう依頼されます。このダイアログは閉じても構いません。進行状況はステータスバーで確認できます。"
|
||||
"description": "各チャットは全コンテキストとともに復元され、中断した箇所を確認するよう依頼されます。このダイアログは閉じても構いません。進行状況はステータスバーで確認できます。",
|
||||
"finished": "これらのチャットは Orca が終了したときに作業中でした。Orca は各チャットを停止した時点から、すべてのコンテキストとともに復元し、続行する前に何をしていたかを確認するよう Agent に依頼しました。あなた自身のプロンプトは再送信されていません。",
|
||||
"finishedUpdate": "これらのチャットは Orca がアップデートをインストールしたときに作業中でした。Orca は各チャットを停止した時点から、すべてのコンテキストとともに復元し、続行する前に何をしていたかを確認するよう Agent に依頼しました。あなた自身のプロンプトは再送信されていません。",
|
||||
"finishedPartial": "これらのチャットは Orca が終了したときに作業中でした。Orca が再開したチャットは停止した時点から、すべてのコンテキストとともに復元され、続行する前に何をしていたかを確認するよう依頼されました。あなた自身のプロンプトは再送信されていません。対応が必要なチャットや再開できるチャットは、この一覧で確認してください。",
|
||||
"finishedPartialUpdate": "これらのチャットは Orca がアップデートをインストールしたときに作業中でした。Orca が再開したチャットは停止した時点から、すべてのコンテキストとともに復元され、続行する前に何をしていたかを確認するよう依頼されました。あなた自身のプロンプトは再送信されていません。対応が必要なチャットや再開できるチャットは、この一覧で確認してください。"
|
||||
},
|
||||
"NativeChatResumeRunStatusIcon": {
|
||||
"waitingForReply": "返信を待っています",
|
||||
@@ -17666,6 +17671,11 @@
|
||||
"attention": "要対応",
|
||||
"running": "経過 {{value0}}",
|
||||
"filter": "表示するチャット"
|
||||
},
|
||||
"NativeChatResumeAutoNote": {
|
||||
"resuming": "再起動後に自動で再開しています。",
|
||||
"resumed": "再起動後に自動で再開しました。",
|
||||
"changeInSettings": "設定で変更"
|
||||
}
|
||||
},
|
||||
"i18n": {
|
||||
|
||||
@@ -17544,7 +17544,8 @@
|
||||
"expandNode": "{{value0}} 확장",
|
||||
"selectMachine": "{{value0}}의 채팅 모두 선택",
|
||||
"sshHost": "SSH",
|
||||
"done": "완료"
|
||||
"done": "완료",
|
||||
"resultListLabel": "이번에 재개한 채팅"
|
||||
},
|
||||
"linear-issue-attribute-filter-coverage-notice": {
|
||||
"statusPartialTeamCoverage": "{{value1}} 팀 상태 중 {{value0}} 필터링 중 - 나머지 팀의 문제는 포함되지 않습니다.",
|
||||
@@ -17646,7 +17647,11 @@
|
||||
"resumedTitleOne": "채팅 1개 중 {{value0}}개를 재개했습니다",
|
||||
"resumingTitleOne": "채팅 1개 재개 중",
|
||||
"resumingTitle": "채팅 {{value0}}개 재개 중",
|
||||
"description": "각 채팅은 전체 컨텍스트와 함께 복원되며, 멈춘 지점을 확인하도록 요청됩니다. 이 창을 닫아도 상태 표시줄에서 진행 상황을 계속 볼 수 있습니다."
|
||||
"description": "각 채팅은 전체 컨텍스트와 함께 복원되며, 멈춘 지점을 확인하도록 요청됩니다. 이 창을 닫아도 상태 표시줄에서 진행 상황을 계속 볼 수 있습니다.",
|
||||
"finished": "이 채팅들은 Orca가 종료될 때 작업 중이었습니다. Orca는 각 채팅을 멈춘 지점에서 전체 컨텍스트와 함께 복원하고, 계속하기 전에 하던 작업을 확인하도록 에이전트에게 요청했습니다. 사용자의 프롬프트는 다시 전송되지 않았습니다.",
|
||||
"finishedUpdate": "이 채팅들은 Orca가 업데이트를 설치할 때 작업 중이었습니다. Orca는 각 채팅을 멈춘 지점에서 전체 컨텍스트와 함께 복원하고, 계속하기 전에 하던 작업을 확인하도록 에이전트에게 요청했습니다. 사용자의 프롬프트는 다시 전송되지 않았습니다.",
|
||||
"finishedPartial": "이 채팅들은 Orca가 종료될 때 작업 중이었습니다. Orca가 재개한 채팅은 멈춘 지점에서 전체 컨텍스트와 함께 복원되었고, 계속하기 전에 하던 작업을 확인하도록 요청받았습니다. 사용자의 프롬프트는 다시 전송되지 않았습니다. 아직 확인이 필요하거나 재개할 수 있는 채팅은 목록에서 확인하세요.",
|
||||
"finishedPartialUpdate": "이 채팅들은 Orca가 업데이트를 설치할 때 작업 중이었습니다. Orca가 재개한 채팅은 멈춘 지점에서 전체 컨텍스트와 함께 복원되었고, 계속하기 전에 하던 작업을 확인하도록 요청받았습니다. 사용자의 프롬프트는 다시 전송되지 않았습니다. 아직 확인이 필요하거나 재개할 수 있는 채팅은 목록에서 확인하세요."
|
||||
},
|
||||
"NativeChatResumeRunStatusIcon": {
|
||||
"waitingForReply": "응답 대기 중",
|
||||
@@ -17666,6 +17671,11 @@
|
||||
"attention": "확인 필요",
|
||||
"running": "경과 {{value0}}",
|
||||
"filter": "표시할 채팅"
|
||||
},
|
||||
"NativeChatResumeAutoNote": {
|
||||
"resuming": "다시 시작한 후 자동으로 재개하는 중입니다.",
|
||||
"resumed": "다시 시작한 후 자동으로 재개했습니다.",
|
||||
"changeInSettings": "설정에서 변경"
|
||||
}
|
||||
},
|
||||
"i18n": {
|
||||
|
||||
@@ -17558,7 +17558,8 @@
|
||||
"expandNode": "展开 {{value0}}",
|
||||
"selectMachine": "选择 {{value0}} 上的全部聊天",
|
||||
"sshHost": "SSH",
|
||||
"done": "完成"
|
||||
"done": "完成",
|
||||
"resultListLabel": "本次恢复的聊天"
|
||||
},
|
||||
"linear-issue-attribute-filter-coverage-notice": {
|
||||
"statusPartialTeamCoverage": "过滤 {{value1}} 团队状态中的 {{value0}} — 不包括其余团队的问题。",
|
||||
@@ -17660,7 +17661,11 @@
|
||||
"resumedTitleOne": "已恢复 {{value0}}/1 个聊天",
|
||||
"resumingTitleOne": "正在恢复 1 个聊天",
|
||||
"resumingTitle": "正在恢复 {{value0}} 个聊天",
|
||||
"description": "每个聊天都会带着完整上下文恢复,并被要求检查它停在了哪里。你可以关闭此窗口,状态栏会继续显示进度。"
|
||||
"description": "每个聊天都会带着完整上下文恢复,并被要求检查它停在了哪里。你可以关闭此窗口,状态栏会继续显示进度。",
|
||||
"finished": "Orca 关闭时这些聊天正在工作。Orca 已将每个聊天恢复到停止的位置并保留完整上下文,并让智能体在继续之前先检查它之前在做什么。你自己的提示没有被重新发送。",
|
||||
"finishedUpdate": "Orca 安装更新时这些聊天正在工作。Orca 已将每个聊天恢复到停止的位置并保留完整上下文,并让智能体在继续之前先检查它之前在做什么。你自己的提示没有被重新发送。",
|
||||
"finishedPartial": "Orca 关闭时这些聊天正在工作。Orca 恢复的聊天已回到停止的位置并保留完整上下文,并被要求在继续之前先检查之前在做什么。你自己的提示没有被重新发送。请在列表中查看仍需处理或可以恢复的聊天。",
|
||||
"finishedPartialUpdate": "Orca 安装更新时这些聊天正在工作。Orca 恢复的聊天已回到停止的位置并保留完整上下文,并被要求在继续之前先检查之前在做什么。你自己的提示没有被重新发送。请在列表中查看仍需处理或可以恢复的聊天。"
|
||||
},
|
||||
"NativeChatResumeRunStatusIcon": {
|
||||
"waitingForReply": "等待回复",
|
||||
@@ -17680,6 +17685,11 @@
|
||||
"attention": "需要处理",
|
||||
"running": "已运行 {{value0}}",
|
||||
"filter": "显示聊天"
|
||||
},
|
||||
"NativeChatResumeAutoNote": {
|
||||
"resuming": "重启后正在自动恢复。",
|
||||
"resumed": "重启后已自动恢复。",
|
||||
"changeInSettings": "在设置中更改"
|
||||
}
|
||||
},
|
||||
"i18n": {
|
||||
|
||||
Reference in New Issue
Block a user