From a54fd3830edecf3e5e587a2f5223a008fa4e577b Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Sun, 18 Oct 2026 00:28:19 -0700 Subject: [PATCH] Antigravity install: one clear message in the chat, with the message box staying in place (#30001) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(native-chat): a chat whose host has no program to run shows one clear install message, with the message box staying in place, disabled A new chat replaces its Start-a-chat copy with an install message (what it is, its size, one Install button, then steps, progress and Cancel). A chat with messages gets a slim strip above the composer instead. Settings' row is unchanged. * fix(native-chat): the install message drops its Settings hint until that Settings row exists * fix(native-chat): satisfy the not-found lint on the install card's phase lookup * fix(native-chat): the install message follows the design system: a still full bar once the download is done, and a plain ghost Cancel * fix(native-chat): the install card's size caption uses the download-size string main already has Main's Antigravity work added the same "downloadSize" key to the install strings, so each locale held it twice with slightly different text. The card now reads main's one string, so the hero's caption under its button ends in a full stop like the Settings row's. * refactor(native-chat): the install row keeps only its Settings layout A chat now shows its own install card, so nothing renders the row's chat layout any more: Settings > Chat > Providers and the Antigravity accounts section are its only users. The row and its view lose the surface choice, the chat-only heading string goes from every locale, and the view always says and offers what Settings did. * fix(native-chat): the install message points to Settings › Chat › Providers again, now that main has that row Only chats on this computer show it: that row installs here, not on an SSH or paired host. * fix(native-chat): a disabled chat box keeps its placeholder and dims, so it says why it can't take a message The editor drew its placeholder only while editable, so an install-blocked or held box looked empty and enabled. The plain text box already dims and keeps its placeholder. * fix(native-chat): keep setup reachable and honor composer input availability * fix(native-chat): update picker availability after commit --------- Co-authored-by: Orca Worker --- src/main/ipc/pty/runtime/spawn-preflight.ts | 5 +- src/main/ipc/pty/spawn-pane-env.ts | 5 +- .../agent-session-bounded-frames.test.ts | 16 +- .../agent-session-subscriber-catch-up.ts | 7 +- ...tructured-agent-session-client-delivery.ts | 3 +- .../structured-agent-session-subscribers.ts | 11 +- src/main/runtime/spawned-pty-env-fixtures.ts | 3 +- ...red-agent-session-codex-held-sends.test.ts | 4 +- .../ManagedRuntimeInstallRow.tsx | 54 +---- .../managed-runtime-install-card.test.ts | 88 +++++++ .../managed-runtime-install-card.ts | 223 ++++++++++++++++++ .../managed-runtime-install-view.test.ts | 50 ++-- .../managed-runtime-install-view.ts | 30 +-- .../use-managed-runtime-install-card.ts | 66 ++++++ .../native-chat/NativeChatComposer.tsx | 15 +- .../NativeChatComposerActions.test.tsx | 46 +++- .../native-chat/NativeChatComposerField.tsx | 4 +- .../native-chat/NativeChatEmptyPane.tsx | 19 ++ .../native-chat/NativeChatInstallHero.tsx | 106 +++++++++ .../native-chat/NativeChatInstallSlot.tsx | 69 ++++-- .../native-chat/NativeChatInstallStrip.tsx | 74 ++++++ .../native-chat/NativeChatInstallTile.tsx | 32 +++ .../native-chat/NativeChatPromptEditor.tsx | 13 +- ...tructuredSession.launch-lifecycle.test.tsx | 34 ++- ...tiveChatStructuredSession.test-harness.tsx | 1 + .../NativeChatStructuredSession.tsx | 19 +- .../native-chat/native-chat-composer-types.ts | 3 + .../native-chat/use-native-chat-dictation.ts | 35 ++- .../use-native-chat-picker-state.test.tsx | 46 ++-- .../use-native-chat-picker-state.ts | 23 +- .../settings/AntigravityAccountsSection.tsx | 1 - .../settings/NativeChatProvidersSetting.tsx | 1 - src/renderer/src/i18n/locales/en.json | 24 +- src/renderer/src/i18n/locales/es.json | 24 +- src/renderer/src/i18n/locales/fr.json | 24 +- src/renderer/src/i18n/locales/ja.json | 24 +- src/renderer/src/i18n/locales/ko.json | 24 +- src/renderer/src/i18n/locales/zh.json | 24 +- ...oating-editor-file-id-layout-owner.test.ts | 22 +- 39 files changed, 1059 insertions(+), 213 deletions(-) create mode 100644 src/renderer/src/components/managed-runtime/managed-runtime-install-card.test.ts create mode 100644 src/renderer/src/components/managed-runtime/managed-runtime-install-card.ts create mode 100644 src/renderer/src/components/managed-runtime/use-managed-runtime-install-card.ts create mode 100644 src/renderer/src/components/native-chat/NativeChatEmptyPane.tsx create mode 100644 src/renderer/src/components/native-chat/NativeChatInstallHero.tsx create mode 100644 src/renderer/src/components/native-chat/NativeChatInstallStrip.tsx create mode 100644 src/renderer/src/components/native-chat/NativeChatInstallTile.tsx diff --git a/src/main/ipc/pty/runtime/spawn-preflight.ts b/src/main/ipc/pty/runtime/spawn-preflight.ts index 0e68e3ac25a..39dc59e3afb 100644 --- a/src/main/ipc/pty/runtime/spawn-preflight.ts +++ b/src/main/ipc/pty/runtime/spawn-preflight.ts @@ -19,7 +19,10 @@ import { resolvePtySpawnStartupTarget } from '../spawn-startup-target' import { resolvePtySpawnHostEnv } from '../spawn-host-env' import { ptySpawnPaneFromIds, stampPtySpawnPaneEnv } from '../spawn-pane-env' import { getConnectionExecutionHostId } from '../../../../shared/execution-host' -import { ensureJcodeRuntimeDir, JCODE_RUNTIME_DIR_ENV_KEY } from '../../../../shared/jcode-runtime-dir' +import { + ensureJcodeRuntimeDir, + JCODE_RUNTIME_DIR_ENV_KEY +} from '../../../../shared/jcode-runtime-dir' export async function prepareRuntimePtySpawn( ctx: RuntimePtySpawnState diff --git a/src/main/ipc/pty/spawn-pane-env.ts b/src/main/ipc/pty/spawn-pane-env.ts index 4fef46e613e..a580da480d9 100644 --- a/src/main/ipc/pty/spawn-pane-env.ts +++ b/src/main/ipc/pty/spawn-pane-env.ts @@ -1,7 +1,10 @@ // The pane identity a new PTY's env carries, stamped from the spawn's own ids, for both spawn lanes. import { mkdirSync } from 'node:fs' -import { buildJcodeRuntimeDirEnv, JCODE_RUNTIME_DIR_ENV_KEY } from '../../../shared/jcode-runtime-dir' +import { + buildJcodeRuntimeDirEnv, + JCODE_RUNTIME_DIR_ENV_KEY +} from '../../../shared/jcode-runtime-dir' import { isTerminalLeafId, makePaneKey } from '../../../shared/stable-pane-id' import { isValidTerminalTabId } from '../../../shared/terminal-tab-id' import type { TuiAgent } from '../../../shared/tui-agent' diff --git a/src/main/native-chat/agent-session-wire/agent-session-bounded-frames.test.ts b/src/main/native-chat/agent-session-wire/agent-session-bounded-frames.test.ts index cfd6ef50488..236e36028ad 100644 --- a/src/main/native-chat/agent-session-wire/agent-session-bounded-frames.test.ts +++ b/src/main/native-chat/agent-session-wire/agent-session-bounded-frames.test.ts @@ -174,7 +174,15 @@ describe('frames to a bounded-frame subscriber', () => { let held: ReadonlySet = new Set() const subscribers = new AgentSessionSubscribers({ readCurrentWork: () => - new StructuredAgentSessionCurrentWork(journal, 1, 0, undefined, undefined, false, () => held) + new StructuredAgentSessionCurrentWork( + journal, + 1, + 0, + undefined, + undefined, + false, + () => held + ) }) const frames: AgentSessionSubscribeEvent[] = [] const emit = (event: AgentSessionSubscribeEvent) => frames.push(event) @@ -197,9 +205,9 @@ describe('frames to a bounded-frame subscriber', () => { } } expect([...delivered.keys()].sort()).toEqual([...ids].sort()) - expect([...delivered.values()].every((submission) => submission.dispatchState === state)).toBe( - true - ) + expect( + [...delivered.values()].every((submission) => submission.dispatchState === state) + ).toBe(true) } const close = subscribers.open({ id: 'phone', diff --git a/src/main/native-chat/agent-session-wire/agent-session-subscriber-catch-up.ts b/src/main/native-chat/agent-session-wire/agent-session-subscriber-catch-up.ts index 83fe1d52edd..52a3d422d02 100644 --- a/src/main/native-chat/agent-session-wire/agent-session-subscriber-catch-up.ts +++ b/src/main/native-chat/agent-session-wire/agent-session-subscriber-catch-up.ts @@ -186,12 +186,7 @@ function emitCaughtUp( refreshPublishedWork(port, subscriber, current.journal, work, shared) return } - if ( - emitCheckpoint || - shared.activity !== undefined || - commandsChanged || - queuedChanged - ) { + if (emitCheckpoint || shared.activity !== undefined || commandsChanged || queuedChanged) { port.emit( subscriber, { diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-client-delivery.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-client-delivery.ts index 9f827f0891b..e6e139c8471 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-client-delivery.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-client-delivery.ts @@ -125,8 +125,7 @@ export class StructuredAgentSessionClientDelivery { captureChildWorkStops = ( sessionId: string, handleIds: readonly string[] - ): CapturedChildWorkStops => - this.statusFeed.captureChildWorkStops(sessionId, handleIds) + ): CapturedChildWorkStops => this.statusFeed.captureChildWorkStops(sessionId, handleIds) childWorkIncomplete = (sessionId: string): boolean => this.statusFeed.childWorkIncomplete(sessionId) diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-subscribers.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-subscribers.ts index fb2344fb05d..d2522cb4202 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-subscribers.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-subscribers.ts @@ -297,10 +297,13 @@ export class AgentSessionSubscribers { emitCheckpoint = false, activity?: AgentSessionTurnActivity | null ): void { - deliverToSubscriber( - this.deliveryPort(), - { subscriber, journal, hostNow, emitCheckpoint, activity } - ) + deliverToSubscriber(this.deliveryPort(), { + subscriber, + journal, + hostNow, + emitCheckpoint, + activity + }) } private now = (): number => this.hooks.now?.() ?? Date.now() diff --git a/src/main/runtime/spawned-pty-env-fixtures.ts b/src/main/runtime/spawned-pty-env-fixtures.ts index 6f2cae951f9..0d84dd3d5e6 100644 --- a/src/main/runtime/spawned-pty-env-fixtures.ts +++ b/src/main/runtime/spawned-pty-env-fixtures.ts @@ -3,7 +3,8 @@ import { buildJcodeRuntimeDirEnv, JCODE_RUNTIME_DIR_ENV_KEY } from '../../shared /** Reconstructs identity formulas from fake spawn args; wiring and directory readiness need real-path tests. */ export function spawnedPtyEnv(call: unknown): Record { - const values: Record = typeof call === 'object' && call !== null ? { ...call } : {} + const values: Record = + typeof call === 'object' && call !== null ? { ...call } : {} const env: Record = {} if (typeof values.env === 'object' && values.env !== null) { for (const [key, value] of Object.entries(values.env)) { diff --git a/src/main/runtime/structured-agent-session-codex-held-sends.test.ts b/src/main/runtime/structured-agent-session-codex-held-sends.test.ts index f78e7818bca..2138b219eb2 100644 --- a/src/main/runtime/structured-agent-session-codex-held-sends.test.ts +++ b/src/main/runtime/structured-agent-session-codex-held-sends.test.ts @@ -198,7 +198,9 @@ describe('a Codex send whose turn completes without taking it', () => { .flatMap((event) => (event.type === 'batch' ? event.batch.submissions : [])) .filter((entry) => entry.clientMessageId === compact.clientMessageId) expect(states.length).toBeGreaterThan(0) - expect(states.every((entry) => entry.dispatchState !== 'unknown' && !entry.recovered)).toBe(true) + expect(states.every((entry) => entry.dispatchState !== 'unknown' && !entry.recovered)).toBe( + true + ) handlers?.onNotification?.('turn/started', { threadId: THREAD, diff --git a/src/renderer/src/components/managed-runtime/ManagedRuntimeInstallRow.tsx b/src/renderer/src/components/managed-runtime/ManagedRuntimeInstallRow.tsx index 7c8c0113296..0279add1b47 100644 --- a/src/renderer/src/components/managed-runtime/ManagedRuntimeInstallRow.tsx +++ b/src/renderer/src/components/managed-runtime/ManagedRuntimeInstallRow.tsx @@ -1,6 +1,5 @@ import { useState } from 'react' import { translate } from '@/i18n/i18n' -import { cn } from '@/lib/utils' import { cancelManagedRuntimeInstall, installManagedRuntime @@ -10,26 +9,19 @@ import { Button } from '../ui/button' import { Progress } from '../ui/progress' import { managedRuntimeInstallView, - type ManagedRuntimeInstallAction, - type ManagedRuntimeInstallSurface + type ManagedRuntimeInstallAction } from './managed-runtime-install-view' -/** Install, progress and Cancel for the runtime Orca provides an agent, on the host it runs on. - * Settings and a chat show the same host answer, so either one's action moves both. */ +/** Settings' Install, progress and Cancel for the runtime Orca provides an agent, on the host it + * runs on. A chat's install card shows the same host answer, so either one's action moves both. */ export function ManagedRuntimeInstallRow(props: { observation: Extract agentLabel: string - surface: ManagedRuntimeInstallSurface }): React.JSX.Element { - const { observation, agentLabel, surface } = props + const { observation, agentLabel } = props // A click holds its button until the host answers, so a double click starts nothing twice. const [pending, setPending] = useState(null) - const view = managedRuntimeInstallView( - observation.snapshot, - agentLabel, - surface, - pending === 'cancel' - ) + const view = managedRuntimeInstallView(observation.snapshot, agentLabel, pending === 'cancel') const [refusal, setRefusal] = useState(null) const act = (action: ManagedRuntimeInstallAction): void => { setPending(action.kind) @@ -54,41 +46,15 @@ export function ManagedRuntimeInstallRow(props: { ) .finally(() => setPending(null)) } - const chat = surface === 'chat' const alert = refusal ?? view.error const { action } = view return ( -
+
- {chat ? ( -

- {translate( - 'components.managed-runtime.chatTitle', - 'Install {{agent}} to start chatting.', - { - agent: agentLabel - } - )} -

- ) : null} - {/* Settings keeps the line while the first read is out, so nothing shifts when it lands. */} - {view.status !== null || !chat ? ( -

- {view.status} -

- ) : null} + {/* The line stays while the first read is out, so nothing shifts when it lands. */} +

+ {view.status} +

{view.progress === null ? null : ( ): ManagedRuntimeOperation => ({ + id: 'op-1', + kind: 'install', + phase: 'downloading', + downloadedBytes: 41_200_000, + totalBytes: 111_500_000, + ...patch +}) + +describe('managedRuntimeInstallCard', () => { + it('walks the hero and the strip through offer, run, failure and cancel', () => { + const offer = managedRuntimeInstallCard(idle, 'Fake', 'hero') + expect(offer).toMatchObject({ + title: 'Install Fake to start chatting', + detail: + "One-time 112 MB download. It's shared by Fake chats on the computer running this chat.", + hint: 'Also in Settings › Chat › Providers', + action: { kind: 'install', label: 'Install Fake' } + }) + expect(managedRuntimeInstallCard(idle, 'Fake', 'strip')).toMatchObject({ + title: 'Install Fake to continue', + detail: '112 MB download on the computer running this chat', + action: { label: 'Install' } + }) + + const running = managedRuntimeInstallCard({ ...idle, operation: op({}) }, 'Fake', 'hero') + expect(running).toMatchObject({ + title: 'Downloading Fake', + progress: { detail: '41.2 of 111.5 MB', percentText: '37%' }, + action: { kind: 'cancel', label: 'Cancel', operationId: 'op-1' } + }) + expect(running.steps?.map((step) => step.state)).toEqual(['current', 'todo', 'todo']) + const checking = managedRuntimeInstallCard( + { ...idle, operation: op({ phase: 'verifying' }) }, + 'Fake', + 'strip', + true + ) + expect(checking.title).toBe('Cancelling…') + expect(checking.progress).toMatchObject({ percent: null }) + expect(checking.steps?.map((step) => step.state)).toEqual(['done', 'done', 'current']) + + const failed = managedRuntimeInstallCard( + { + ...idle, + operation: op({ + phase: 'failed', + error: "Couldn't download Fake. Check your network connection or proxy, then try again." + }) + }, + 'Fake', + 'hero' + ) + expect(failed).toMatchObject({ + tone: 'failed', + title: "Couldn't install Fake", + detail: "Couldn't download Fake. Check your network connection or proxy, then try again.", + hint: '112 MB download.', + action: { label: 'Try again' } + }) + expect( + managedRuntimeInstallCard({ ...idle, operation: op({ phase: 'cancelled' }) }, 'Fake', 'hero') + ).toMatchObject({ + title: 'Installation cancelled', + detail: 'Nothing was installed.', + action: { label: 'Retry installation' } + }) + }) +}) diff --git a/src/renderer/src/components/managed-runtime/managed-runtime-install-card.ts b/src/renderer/src/components/managed-runtime/managed-runtime-install-card.ts new file mode 100644 index 00000000000..e14cfd3c1ee --- /dev/null +++ b/src/renderer/src/components/managed-runtime/managed-runtime-install-card.ts @@ -0,0 +1,223 @@ +import { translate } from '@/i18n/i18n' +import type { + ManagedRuntimeInstallState, + ManagedRuntimeOperation +} from '../../../../shared/managed-runtime-install' +import { megabytes, type ManagedRuntimeInstallAction } from './managed-runtime-install-view' + +/** `hero`: the whole-pane message of a chat with no messages; `strip`: the slim line above the + * composer outside that pane. Both read one card, so they never disagree. */ +export type ManagedRuntimeInstallLayout = 'hero' | 'strip' + +export type ManagedRuntimeInstallCardStep = { label: string; state: 'done' | 'current' | 'todo' } + +export type ManagedRuntimeInstallCard = { + tone: 'offer' | 'running' | 'failed' | 'cancelled' + title: string + /** The sentence under the title: what it is, why it failed, or what ended. */ + detail: string | null + /** `percent` is null while the size of what remains is unknown. */ + progress: { percent: number | null; detail: string; percentText: string | null } | null + steps: ManagedRuntimeInstallCardStep[] | null + hint: string | null + action: ManagedRuntimeInstallAction | null +} + +const STEP_PHASES: readonly ManagedRuntimeOperation['phase'][] = [ + 'downloading', + 'extracting', + 'verifying' +] + +function stepLabels(): string[] { + return [ + translate('components.managed-runtime.stepDownload', 'Download'), + translate('components.managed-runtime.stepUnpack', 'Unpack'), + translate('components.managed-runtime.stepCheck', 'Check') + ] +} + +function runningCard( + operation: ManagedRuntimeOperation, + agent: string, + cancelling: boolean +): ManagedRuntimeInstallCard | null { + const current = STEP_PHASES.indexOf(operation.phase) + if (current === -1) { + return null + } + const { downloadedBytes: done, totalBytes: total } = operation + const percent = current === 0 && total ? Math.min(100, (done / total) * 100) : null + const detail = + current === 1 + ? translate('components.managed-runtime.unpackingDetail', 'Unpacking the runtime') + : current === 2 + ? translate('components.managed-runtime.checkingDetail', 'Checking the downloaded runtime') + : total === null + ? translate('components.managed-runtime.downloading', 'Downloading {{done}} MB.', { + done: megabytes(done) + }) + : translate('components.managed-runtime.progressOf', '{{done}} of {{total}} MB', { + done: megabytes(done), + total: megabytes(total) + }) + const cancellingText = translate('components.managed-runtime.cancelling', 'Cancelling…') + const title = cancelling + ? cancellingText + : current === 0 + ? translate('components.managed-runtime.downloadingTitle', 'Downloading {{agent}}', { agent }) + : current === 1 + ? translate('components.managed-runtime.unpackingTitle', 'Unpacking {{agent}}', { agent }) + : translate('components.managed-runtime.checkingTitle', 'Checking the download') + return { + tone: 'running', + title, + detail: null, + progress: { + percent, + detail, + percentText: percent === null ? null : `${Math.round(percent)}%` + }, + steps: stepLabels().map((label, index) => ({ + label, + state: index < current ? 'done' : index === current ? 'current' : 'todo' + })), + hint: null, + action: { + kind: 'cancel', + label: cancelling + ? cancellingText + : translate('components.managed-runtime.cancelShort', 'Cancel'), + operationId: operation.id + } + } +} + +function offerDetail( + state: ManagedRuntimeInstallState, + agent: string, + hero: boolean, + size: string | null +): string | null { + if (state.damaged) { + return translate('components.managed-runtime.damaged', 'This copy of {{agent}} is damaged.', { + agent + }) + } + if (!hero) { + return size === null + ? null + : translate( + 'components.managed-runtime.stripSize', + '{{size}} MB download on the computer running this chat', + { size } + ) + } + return size === null + ? translate( + 'components.managed-runtime.heroBodyNoSize', + "One-time download. It's shared by {{agent}} chats on the computer running this chat.", + { agent } + ) + : translate( + 'components.managed-runtime.heroBody', + "One-time {{size}} MB download. It's shared by {{agent}} chats on the computer running this chat.", + { size, agent } + ) +} + +function offerCard( + state: ManagedRuntimeInstallState, + agent: string, + hero: boolean, + size: string | null +): ManagedRuntimeInstallCard { + const title = hero + ? translate('components.managed-runtime.installTitle', 'Install {{agent}} to start chatting', { + agent + }) + : translate('components.managed-runtime.continueTitle', 'Install {{agent}} to continue', { + agent + }) + const label = hero + ? translate('components.managed-runtime.install', 'Install {{agent}}', { agent }) + : translate('components.managed-runtime.installShort', 'Install') + const hint = translate( + 'components.managed-runtime.settingsHint', + 'Also in Settings › Chat › Providers' + ) + return { + tone: 'offer', + title, + detail: offerDetail(state, agent, hero, size), + progress: null, + steps: null, + hint: hero ? hint : null, + action: { kind: 'install', label } + } +} + +/** What an install card says and offers for the host's answer. `cancelling`: this surface's Cancel + * is still out, so it says so until the host's cancel settles. */ +export function managedRuntimeInstallCard( + state: ManagedRuntimeInstallState, + agent: string, + layout: ManagedRuntimeInstallLayout, + cancelling = false +): ManagedRuntimeInstallCard { + const { operation } = state + const running = operation ? runningCard(operation, agent, cancelling) : null + if (running) { + return running + } + const hero = layout === 'hero' + const size = state.downloadBytes ? String(Math.ceil(state.downloadBytes / 1_000_000)) : null + const sizeHint = + hero && size !== null + ? translate('components.managed-runtime.downloadSize', '{{size}} MB download.', { size }) + : null + const install = (label: string): ManagedRuntimeInstallAction => ({ kind: 'install', label }) + const retry = translate('components.managed-runtime.retry', 'Retry installation') + if (operation?.kind === 'install' && operation.phase === 'cancelled') { + return { + tone: 'cancelled', + title: translate('components.managed-runtime.cancelledTitle', 'Installation cancelled'), + detail: translate('components.managed-runtime.cancelledBody', 'Nothing was installed.'), + progress: null, + steps: null, + hint: sizeHint, + action: install(retry) + } + } + if (operation?.kind === 'install' && operation.phase === 'failed') { + return { + tone: 'failed', + title: translate('components.managed-runtime.failedTitle', "Couldn't install {{agent}}", { + agent + }), + detail: + operation.error ?? + translate('components.managed-runtime.failed', 'Installation failed. Try again.'), + progress: null, + steps: null, + hint: sizeHint, + action: install(translate('components.managed-runtime.tryAgain', 'Try again')) + } + } + if (!state.available) { + return { + tone: 'offer', + title: translate( + 'components.managed-runtime.unavailable', + '{{agent}} is not available on this host.', + { agent } + ), + detail: null, + progress: null, + steps: null, + hint: null, + action: null + } + } + return offerCard(state, agent, hero, size) +} diff --git a/src/renderer/src/components/managed-runtime/managed-runtime-install-view.test.ts b/src/renderer/src/components/managed-runtime/managed-runtime-install-view.test.ts index cd8eac3acad..1f881795b2f 100644 --- a/src/renderer/src/components/managed-runtime/managed-runtime-install-view.test.ts +++ b/src/renderer/src/components/managed-runtime/managed-runtime-install-view.test.ts @@ -25,35 +25,30 @@ const op = (patch: Partial): ManagedRuntimeOperation => ...patch }) -function shown(patch: Partial, surface: 'settings' | 'chat') { - const view = managedRuntimeInstallView( - { kind: 'known', state: { ...idle, ...patch } }, - 'Fake', - surface - ) +function shown(patch: Partial) { + const view = managedRuntimeInstallView({ kind: 'known', state: { ...idle, ...patch } }, 'Fake') return [view.status, view.action?.label ?? null, view.error] } describe('managedRuntimeInstallView', () => { - it('says and offers what the host state allows on each surface', () => { + it('says and offers what the host state allows', () => { const installed = { installedVersion: '1.3.0', installedCurrent: true, source: 'managed' as const } - expect(shown({}, 'settings')).toEqual(['Not installed.', 'Install Fake', null]) - expect(shown({}, 'chat')).toEqual([null, 'Install Fake', null]) - expect(shown({ downloadBytes: 111_456_962 }, 'chat')).toEqual([ + expect(shown({})).toEqual(['Not installed.', 'Install Fake', null]) + expect(shown({ downloadBytes: 111_456_962 })).toEqual([ '112 MB download.', 'Install Fake', null ]) - expect(shown({ operation: op({}) }, 'chat')).toEqual([ + expect(shown({ operation: op({}) })).toEqual([ 'Downloading 42.0 MB of 111.0 MB.', 'Cancel installation', null ]) - expect(shown({ operation: op({ phase: 'verifying' }) }, 'settings')).toEqual([ + expect(shown({ operation: op({ phase: 'verifying' }) })).toEqual([ 'Checking the downloaded runtime.', 'Cancel installation', null @@ -62,44 +57,45 @@ describe('managedRuntimeInstallView', () => { const cancelling = managedRuntimeInstallView( { kind: 'known', state: { ...idle, operation: op({}) } }, 'Fake', - 'chat', true ) expect(cancelling.status).toBe('Cancelling…') - expect(shown({ operation: op({ phase: 'cancelled' }) }, 'chat')).toEqual([ + expect(shown({ operation: op({ phase: 'cancelled' }) })).toEqual([ 'Installation cancelled.', 'Retry installation', null ]) - expect( - shown({ operation: op({ phase: 'failed', error: 'No space left.' }) }, 'settings') - ).toEqual(['Not installed.', 'Retry installation', 'No space left.']) - expect(shown(installed, 'settings')).toEqual(['Installed.', 'Reinstall Fake', null]) - expect( - shown({ ...installed, installedVersion: '1.2.0', installedCurrent: false }, 'settings') - ).toEqual(['Installed.', 'Update Fake', null]) - expect(shown(installed, 'chat')[1]).toBeNull() - expect(shown({ available: false, version: null }, 'settings')).toEqual([ + expect(shown({ operation: op({ phase: 'failed', error: 'No space left.' }) })).toEqual([ + 'Not installed.', + 'Retry installation', + 'No space left.' + ]) + expect(shown(installed)).toEqual(['Installed.', 'Reinstall Fake', null]) + expect(shown({ ...installed, installedVersion: '1.2.0', installedCurrent: false })).toEqual([ + 'Installed.', + 'Update Fake', + null + ]) + expect(shown({ available: false, version: null })).toEqual([ 'Fake is not available on this host.', null, null ]) - expect(shown({ damaged: true }, 'chat')).toEqual([ + expect(shown({ damaged: true })).toEqual([ 'This copy of Fake is damaged.', 'Install Fake', null ]) const external = { source: 'external' as const } - expect(shown(external, 'settings')).toEqual([ + expect(shown(external)).toEqual([ 'Uses the copy of Fake found on this host.', "Install Orca's copy", null ]) - expect(shown(external, 'chat')[1]).toBeNull() }) it('never reads a failed status read as not installed', () => { - const view = managedRuntimeInstallView({ kind: 'readFailed' }, 'Fake', 'chat') + const view = managedRuntimeInstallView({ kind: 'readFailed' }, 'Fake') expect(view.action).toBeNull() expect(view.error).toBe('Could not check Fake on this host. Retrying.') }) diff --git a/src/renderer/src/components/managed-runtime/managed-runtime-install-view.ts b/src/renderer/src/components/managed-runtime/managed-runtime-install-view.ts index 0356c046c3f..f651abd2820 100644 --- a/src/renderer/src/components/managed-runtime/managed-runtime-install-view.ts +++ b/src/renderer/src/components/managed-runtime/managed-runtime-install-view.ts @@ -2,8 +2,6 @@ import { translate } from '@/i18n/i18n' import type { ManagedRuntimeInstallState } from '../../../../shared/managed-runtime-install' import type { ManagedRuntimeInstallSnapshot } from '@/runtime/managed-runtime-install-status' -export type ManagedRuntimeInstallSurface = 'settings' | 'chat' - export type ManagedRuntimeInstallAction = | { kind: 'install'; label: string } | { kind: 'cancel'; label: string; operationId: string } @@ -17,7 +15,7 @@ export type ManagedRuntimeInstallView = { action: ManagedRuntimeInstallAction | null } -const megabytes = (bytes: number): string => (bytes / 1_000_000).toFixed(1) +export const megabytes = (bytes: number): string => (bytes / 1_000_000).toFixed(1) function runningView( operation: NonNullable, @@ -52,11 +50,7 @@ function runningView( } } -function settledView( - state: ManagedRuntimeInstallState, - agent: string, - surface: ManagedRuntimeInstallSurface -): ManagedRuntimeInstallView { +function settledView(state: ManagedRuntimeInstallState, agent: string): ManagedRuntimeInstallView { const { operation } = state const failed = operation?.phase === 'failed' const error = failed @@ -70,9 +64,7 @@ function settledView( action: label === null ? null : { kind: 'install', label } }) const install = translate('components.managed-runtime.install', 'Install {{agent}}', { agent }) - // Install, Reinstall and Update belong to Settings; a chat that can start shows its composer. - const settings = surface === 'settings' - // A chat already runs a copy found outside Orca; Settings offers Orca's, which then runs first. + // A copy found outside Orca already runs; Orca's own, once installed, runs first. if (state.source === 'external') { return view( translate( @@ -80,7 +72,7 @@ function settledView( 'Uses the copy of {{agent}} found on this host.', { agent } ), - settings && state.available + state.available ? translate('components.managed-runtime.installManaged', "Install Orca's copy") : null ) @@ -89,10 +81,7 @@ function settledView( const label = state.installedCurrent ? translate('components.managed-runtime.reinstall', 'Reinstall {{agent}}', { agent }) : translate('components.managed-runtime.update', 'Update {{agent}}', { agent }) - return view( - translate('components.managed-runtime.installed', 'Installed.'), - settings ? label : null - ) + return view(translate('components.managed-runtime.installed', 'Installed.'), label) } if (!state.available) { return view( @@ -113,16 +102,14 @@ function settledView( install ) } - // The chat card's heading already says it is not installed; the size says what Install costs. + // The size says what Install costs. const size = state.downloadBytes const idle = size !== undefined ? translate('components.managed-runtime.downloadSize', '{{size}} MB download.', { size: Math.ceil(size / 1_000_000) }) - : settings - ? translate('components.managed-runtime.notInstalled', 'Not installed.') - : null + : translate('components.managed-runtime.notInstalled', 'Not installed.') const retry = translate('components.managed-runtime.retry', 'Retry installation') if (operation?.kind === 'install' && operation.phase === 'cancelled') { return view(translate('components.managed-runtime.cancelled', 'Installation cancelled.'), retry) @@ -136,7 +123,6 @@ function settledView( export function managedRuntimeInstallView( snapshot: ManagedRuntimeInstallSnapshot, agent: string, - surface: ManagedRuntimeInstallSurface, cancelling = false ): ManagedRuntimeInstallView { if (snapshot.kind !== 'known') { @@ -157,6 +143,6 @@ export function managedRuntimeInstallView( const { operation } = snapshot.state return ( (operation ? runningView(operation, agent, cancelling) : null) ?? - settledView(snapshot.state, agent, surface) + settledView(snapshot.state, agent) ) } diff --git a/src/renderer/src/components/managed-runtime/use-managed-runtime-install-card.ts b/src/renderer/src/components/managed-runtime/use-managed-runtime-install-card.ts new file mode 100644 index 00000000000..7f218845c7c --- /dev/null +++ b/src/renderer/src/components/managed-runtime/use-managed-runtime-install-card.ts @@ -0,0 +1,66 @@ +import { useState } from 'react' +import { translate } from '@/i18n/i18n' +import { + cancelManagedRuntimeInstall, + installManagedRuntime +} from '@/runtime/managed-runtime-install-status' +import type { ManagedRuntimeInstallObservation } from '@/runtime/use-managed-runtime-install' +import type { ManagedRuntimeInstallState } from '../../../../shared/managed-runtime-install' +import { + managedRuntimeInstallCard, + type ManagedRuntimeInstallCard, + type ManagedRuntimeInstallLayout +} from './managed-runtime-install-card' +import type { ManagedRuntimeInstallAction } from './managed-runtime-install-view' + +/** The install card and its Install / Cancel clicks. A click holds its button (`pending`) until + * the host answers, so a double click starts nothing twice; `refusal` is why the host turned the + * last click down. */ +export function useManagedRuntimeInstallCard( + observation: Extract, + state: ManagedRuntimeInstallState, + agentLabel: string, + layout: ManagedRuntimeInstallLayout +): { + card: ManagedRuntimeInstallCard + pending: boolean + refusal: string | null + act: (action: ManagedRuntimeInstallAction) => void +} { + const [pending, setPending] = useState(null) + const [refusal, setRefusal] = useState(null) + const act = (action: ManagedRuntimeInstallAction): void => { + setPending(action.kind) + setRefusal(null) + const { subject } = observation + const call = + action.kind === 'cancel' + ? cancelManagedRuntimeInstall(subject, action.operationId) + : installManagedRuntime(subject) + void call + .catch((cause: unknown) => + setRefusal( + cause instanceof Error + ? cause.message + : action.kind === 'cancel' + ? translate( + 'components.managed-runtime.cancelFailed', + "Couldn't cancel the installation. Try again." + ) + : translate('components.managed-runtime.failed', 'Installation failed. Try again.') + ) + ) + .finally(() => setPending(null)) + } + const card = managedRuntimeInstallCard(state, agentLabel, layout, pending === 'cancel') + return { + // An offer's hint points at Settings › Chat › Providers, which installs on this computer only. + card: + card.tone === 'offer' && observation.subject.target.kind !== 'local' + ? { ...card, hint: null } + : card, + pending: pending !== null, + refusal, + act + } +} diff --git a/src/renderer/src/components/native-chat/NativeChatComposer.tsx b/src/renderer/src/components/native-chat/NativeChatComposer.tsx index 3b3431a7b1e..02c32b65d0f 100644 --- a/src/renderer/src/components/native-chat/NativeChatComposer.tsx +++ b/src/renderer/src/components/native-chat/NativeChatComposer.tsx @@ -54,6 +54,7 @@ const NativeChatComposerPane = forwardRef
) : null} diff --git a/src/renderer/src/components/settings/NativeChatProvidersSetting.tsx b/src/renderer/src/components/settings/NativeChatProvidersSetting.tsx index 52ef66588e6..e83a421ea7d 100644 --- a/src/renderer/src/components/settings/NativeChatProvidersSetting.tsx +++ b/src/renderer/src/components/settings/NativeChatProvidersSetting.tsx @@ -104,7 +104,6 @@ function ProviderDetail(props: {
) diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index 061cf9d118a..f0286bea6cf 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -19422,7 +19422,6 @@ "cancel": "Cancel installation", "cancelled": "Installation cancelled.", "cancelling": "Cancelling…", - "chatTitle": "Install {{agent}} to start chatting.", "damaged": "This copy of {{agent}} is damaged.", "downloadSize": "{{size}} MB download.", "downloading": "Downloading {{done}} MB.", @@ -19441,7 +19440,28 @@ "retry": "Retry installation", "unavailable": "{{agent}} is not available on this host.", "update": "Update {{agent}}", - "verifying": "Checking the downloaded runtime." + "verifying": "Checking the downloaded runtime.", + "installTitle": "Install {{agent}} to start chatting", + "continueTitle": "Install {{agent}} to continue", + "heroBody": "One-time {{size}} MB download. It's shared by {{agent}} chats on the computer running this chat.", + "heroBodyNoSize": "One-time download. It's shared by {{agent}} chats on the computer running this chat.", + "stripSize": "{{size}} MB download on the computer running this chat", + "settingsHint": "Also in Settings › Chat › Providers", + "installShort": "Install", + "tryAgain": "Try again", + "cancelShort": "Cancel", + "failedTitle": "Couldn't install {{agent}}", + "cancelledTitle": "Installation cancelled", + "cancelledBody": "Nothing was installed.", + "downloadingTitle": "Downloading {{agent}}", + "unpackingTitle": "Unpacking {{agent}}", + "checkingTitle": "Checking the download", + "progressOf": "{{done}} of {{total}} MB", + "unpackingDetail": "Unpacking the runtime", + "checkingDetail": "Checking the downloaded runtime", + "stepDownload": "Download", + "stepUnpack": "Unpack", + "stepCheck": "Check" } }, "dashboardPopout": { diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json index 7310a34089a..e6fd77516b2 100644 --- a/src/renderer/src/i18n/locales/es.json +++ b/src/renderer/src/i18n/locales/es.json @@ -19189,7 +19189,6 @@ "cancel": "Cancelar instalación", "cancelled": "Instalación cancelada.", "cancelling": "Cancelando…", - "chatTitle": "Instala {{agent}} para empezar a chatear.", "damaged": "Esta copia de {{agent}} está dañada.", "downloadSize": "Descarga de {{size}} MB.", "downloading": "Descargando {{done}} MB.", @@ -19208,7 +19207,28 @@ "retry": "Reintentar instalación", "unavailable": "{{agent}} no está disponible en este host.", "update": "Actualizar {{agent}}", - "verifying": "Comprobando el entorno descargado." + "verifying": "Comprobando el entorno descargado.", + "installTitle": "Instala {{agent}} para empezar a chatear", + "continueTitle": "Instala {{agent}} para continuar", + "heroBody": "Descarga única de {{size}} MB. La comparten los chats de {{agent}} en el equipo que ejecuta este chat.", + "heroBodyNoSize": "Descarga única. La comparten los chats de {{agent}} en el equipo que ejecuta este chat.", + "stripSize": "Descarga de {{size}} MB en el equipo que ejecuta este chat", + "settingsHint": "También en Ajustes › Chat › Proveedores", + "installShort": "Instalar", + "tryAgain": "Reintentar", + "cancelShort": "Cancelar", + "failedTitle": "No se pudo instalar {{agent}}", + "cancelledTitle": "Instalación cancelada", + "cancelledBody": "No se instaló nada.", + "downloadingTitle": "Descargando {{agent}}", + "unpackingTitle": "Descomprimiendo {{agent}}", + "checkingTitle": "Comprobando la descarga", + "progressOf": "{{done}} de {{total}} MB", + "unpackingDetail": "Descomprimiendo el entorno de ejecución", + "checkingDetail": "Comprobando el entorno de ejecución descargado", + "stepDownload": "Descargar", + "stepUnpack": "Descomprimir", + "stepCheck": "Comprobar" } }, "dashboardPopout": { diff --git a/src/renderer/src/i18n/locales/fr.json b/src/renderer/src/i18n/locales/fr.json index a58096bb210..42fc8dd87f2 100644 --- a/src/renderer/src/i18n/locales/fr.json +++ b/src/renderer/src/i18n/locales/fr.json @@ -19305,7 +19305,6 @@ "cancel": "Annuler l'installation", "cancelled": "Installation annulée.", "cancelling": "Annulation…", - "chatTitle": "Installez {{agent}} pour commencer à discuter.", "damaged": "Cette copie de {{agent}} est endommagée.", "downloadSize": "Téléchargement de {{size}} Mo.", "downloading": "Téléchargement de {{done}} Mo.", @@ -19324,7 +19323,28 @@ "retry": "Réessayer l'installation", "unavailable": "{{agent}} n'est pas disponible sur cet hôte.", "update": "Mettre à jour {{agent}}", - "verifying": "Vérification de l’environnement téléchargé." + "verifying": "Vérification de l’environnement téléchargé.", + "installTitle": "Installez {{agent}} pour commencer à discuter", + "continueTitle": "Installez {{agent}} pour continuer", + "heroBody": "Téléchargement unique de {{size}} Mo, partagé par les conversations {{agent}} sur l’ordinateur qui exécute cette conversation.", + "heroBodyNoSize": "Téléchargement unique, partagé par les conversations {{agent}} sur l’ordinateur qui exécute cette conversation.", + "stripSize": "Téléchargement de {{size}} Mo sur l’ordinateur qui exécute cette conversation", + "settingsHint": "Aussi dans Paramètres › Chat › Fournisseurs", + "installShort": "Installer", + "tryAgain": "Réessayer", + "cancelShort": "Annuler", + "failedTitle": "Impossible d’installer {{agent}}", + "cancelledTitle": "Installation annulée", + "cancelledBody": "Rien n'a été installé.", + "downloadingTitle": "Téléchargement de {{agent}}", + "unpackingTitle": "Décompression de {{agent}}", + "checkingTitle": "Vérification du téléchargement", + "progressOf": "{{done}} sur {{total}} Mo", + "unpackingDetail": "Décompression de l'environnement d'exécution", + "checkingDetail": "Vérification de l'environnement d'exécution téléchargé", + "stepDownload": "Télécharger", + "stepUnpack": "Décompresser", + "stepCheck": "Vérifier" } }, "dashboardPopout": { diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index 1d191dc26d8..6a24ae44e5c 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -19201,7 +19201,6 @@ "cancel": "インストールをキャンセル", "cancelled": "インストールをキャンセルしました。", "cancelling": "キャンセルしています…", - "chatTitle": "チャットを始めるには {{agent}} をインストールしてください。", "damaged": "この {{agent}} のコピーは破損しています。", "downloadSize": "{{size}} MB のダウンロード。", "downloading": "{{done}} MB をダウンロード中です。", @@ -19220,7 +19219,28 @@ "retry": "インストールを再試行", "unavailable": "{{agent}} はこのホストでは利用できません。", "update": "{{agent}} を更新", - "verifying": "ダウンロードしたランタイムを確認しています。" + "verifying": "ダウンロードしたランタイムを確認しています。", + "installTitle": "チャットを始めるには {{agent}} をインストールしてください", + "continueTitle": "続けるには {{agent}} をインストールしてください", + "heroBody": "{{size}} MB の一度きりのダウンロードです。このチャットを実行するコンピューター上の {{agent}} チャットで共有されます。", + "heroBodyNoSize": "一度きりのダウンロードです。このチャットを実行するコンピューター上の {{agent}} チャットで共有されます。", + "stripSize": "このチャットを実行するコンピューターに {{size}} MB をダウンロード", + "settingsHint": "設定 › チャット › プロバイダー からも行えます", + "installShort": "インストール", + "tryAgain": "再試行", + "cancelShort": "キャンセル", + "failedTitle": "{{agent}} をインストールできませんでした", + "cancelledTitle": "インストールをキャンセルしました", + "cancelledBody": "何もインストールされていません。", + "downloadingTitle": "{{agent}} をダウンロード中", + "unpackingTitle": "{{agent}} を展開中", + "checkingTitle": "ダウンロードを確認中", + "progressOf": "{{total}} MB 中 {{done}} MB", + "unpackingDetail": "ランタイムを展開しています", + "checkingDetail": "ダウンロードしたランタイムを確認しています", + "stepDownload": "ダウンロード", + "stepUnpack": "展開", + "stepCheck": "確認" } }, "dashboardPopout": { diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index f2b37ae5011..0353b159bb0 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -19201,7 +19201,6 @@ "cancel": "설치 취소", "cancelled": "설치가 취소되었습니다.", "cancelling": "취소하는 중…", - "chatTitle": "채팅을 시작하려면 {{agent}}을(를) 설치하세요.", "damaged": "이 {{agent}} 사본이 손상되었습니다.", "downloadSize": "{{size}}MB 다운로드.", "downloading": "{{done}}MB 다운로드 중입니다.", @@ -19220,7 +19219,28 @@ "retry": "설치 다시 시도", "unavailable": "{{agent}}은(는) 이 호스트에서 사용할 수 없습니다.", "update": "{{agent}} 업데이트", - "verifying": "다운로드한 런타임을 확인하는 중입니다." + "verifying": "다운로드한 런타임을 확인하는 중입니다.", + "installTitle": "채팅을 시작하려면 {{agent}}을(를) 설치하세요", + "continueTitle": "계속하려면 {{agent}}을(를) 설치하세요", + "heroBody": "한 번만 {{size}}MB를 다운로드합니다. 이 채팅을 실행하는 컴퓨터의 {{agent}} 채팅이 함께 사용합니다.", + "heroBodyNoSize": "한 번만 다운로드합니다. 이 채팅을 실행하는 컴퓨터의 {{agent}} 채팅이 함께 사용합니다.", + "stripSize": "이 채팅을 실행하는 컴퓨터에 {{size}}MB 다운로드", + "settingsHint": "설정 › 채팅 › 공급자에서도 설치할 수 있습니다", + "installShort": "설치", + "tryAgain": "다시 시도", + "cancelShort": "취소", + "failedTitle": "{{agent}}을(를) 설치하지 못했습니다", + "cancelledTitle": "설치가 취소되었습니다", + "cancelledBody": "아무것도 설치되지 않았습니다.", + "downloadingTitle": "{{agent}} 다운로드 중", + "unpackingTitle": "{{agent}} 압축 해제 중", + "checkingTitle": "다운로드 확인 중", + "progressOf": "{{total}}MB 중 {{done}}MB", + "unpackingDetail": "런타임 압축을 푸는 중", + "checkingDetail": "다운로드한 런타임을 확인하는 중", + "stepDownload": "다운로드", + "stepUnpack": "압축 해제", + "stepCheck": "확인" } }, "dashboardPopout": { diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index 015c734eec1..bffa9b41c59 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -19201,7 +19201,6 @@ "cancel": "取消安装", "cancelled": "已取消安装。", "cancelling": "正在取消…", - "chatTitle": "安装 {{agent}} 即可开始聊天。", "damaged": "此 {{agent}} 副本已损坏。", "downloadSize": "下载大小 {{size}} MB。", "downloading": "正在下载 {{done}} MB。", @@ -19220,7 +19219,28 @@ "retry": "重试安装", "unavailable": "{{agent}} 在此主机上不可用。", "update": "更新 {{agent}}", - "verifying": "正在检查已下载的运行时。" + "verifying": "正在检查已下载的运行时。", + "installTitle": "安装 {{agent}} 以开始聊天", + "continueTitle": "安装 {{agent}} 以继续", + "heroBody": "一次性下载 {{size}} MB,由运行此聊天的电脑上的 {{agent}} 聊天共用。", + "heroBodyNoSize": "一次性下载,由运行此聊天的电脑上的 {{agent}} 聊天共用。", + "stripSize": "在运行此聊天的电脑上下载 {{size}} MB", + "settingsHint": "也可在 设置 › 聊天 › 供应商 中安装", + "installShort": "安装", + "tryAgain": "重试", + "cancelShort": "取消", + "failedTitle": "无法安装 {{agent}}", + "cancelledTitle": "安装已取消", + "cancelledBody": "未安装任何内容。", + "downloadingTitle": "正在下载 {{agent}}", + "unpackingTitle": "正在解压 {{agent}}", + "checkingTitle": "正在检查下载内容", + "progressOf": "{{done}} / {{total}} MB", + "unpackingDetail": "正在解压运行时", + "checkingDetail": "正在检查已下载的运行时", + "stepDownload": "下载", + "stepUnpack": "解压", + "stepCheck": "检查" } }, "dashboardPopout": { diff --git a/src/renderer/src/store/slices/floating-editor-file-id-layout-owner.test.ts b/src/renderer/src/store/slices/floating-editor-file-id-layout-owner.test.ts index 6bf0e61e98a..467078d2585 100644 --- a/src/renderer/src/store/slices/floating-editor-file-id-layout-owner.test.ts +++ b/src/renderer/src/store/slices/floating-editor-file-id-layout-owner.test.ts @@ -36,18 +36,16 @@ function openFloatingFile(): string { activeGroupIdByWorktree: { [WT]: 'g1' }, unifiedTabsByWorktree: { [WT]: [] } }) - return store - .getState() - .openFile( - { - filePath: '/notes/a.md', - relativePath: 'a.md', - worktreeId: WT, - language: 'md', - mode: 'edit' - }, - { preview: false } - ) + return store.getState().openFile( + { + filePath: '/notes/a.md', + relativePath: 'a.md', + worktreeId: WT, + language: 'md', + mode: 'edit' + }, + { preview: false } + ) } beforeEach(() => {