From 4576ddf77f430dfc8a3ea93e4355df8f6bffadf6 Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Tue, 21 Jul 2026 11:28:27 -0700 Subject: [PATCH] Implement data recovery for failed agent catalog v1 migrations Add recovery-point inventory and atomic restore, data-recovery IPC surface, and app-level migration-blocked notice that persists until retry succeeds or a pre-v1 backup is restored. Offline CLI writes are schema-transparent: they refuse pre-v1 file downgrades and never stamp v1 fields without the pinned backup contract. Mobile surfaces the blocked state in the agent picker. Web clients probe host identity-launch capability and degrade to legacy paths on pre-identity hosts, failing fast for custom agents or stored defaults that cannot resolve. --- .github/workflows/e2e.yml | 4 +- config/tsconfig.cli.json | 1 + .../custom-agents-forward-rollback-runbook.md | 132 ++- mobile/src/components/NewWorktreeModal.tsx | 42 +- .../components/NewWorktreeModalDrawers.tsx | 2 + mobile/src/components/PickerListDrawer.tsx | 9 + .../mobile-agent-catalog-projection.test.ts | 15 +- .../tasks/mobile-agent-catalog-projection.ts | 16 + src/cli/handlers/agent-hooks.test.ts | 78 ++ src/cli/handlers/agent-hooks.ts | 31 + .../agent-catalog-base-disable-impact.ts | 44 + .../agent-catalog-service.test.ts | 32 + .../agent-launch/agent-catalog-service.ts | 56 +- .../data-recovery/recovery-points.test.ts | 118 +++ src/main/data-recovery/recovery-points.ts | 133 +++ src/main/ipc/data-recovery.ts | 42 + .../register-core-handlers.test.ts | 17 +- .../register-core-handlers.ts | 4 +- ...ersistence-agent-catalog-migration.test.ts | 40 + .../loading-store/profile-preferences.ts | 35 +- src/main/persistence/loading-store/store.ts | 8 + src/preload/api-types.ts | 23 +- src/preload/index.ts | 8 + src/renderer/src/App.tsx | 2 + .../data-recovery/DataRecoveryDialog.tsx | 204 +++++ .../DataRecoveryMigrationNotice.test.tsx | 100 +++ .../DataRecoveryMigrationNotice.tsx | 89 ++ .../data-recovery/DataRecoverySettingsRow.tsx | 46 + .../settings/AgentCatalogSection.test.tsx | 16 + .../settings/AgentCatalogSection.tsx | 31 +- .../src/hooks/useLocalAgentCatalog.ts | 32 +- src/renderer/src/i18n/locales/en.json | 785 ++---------------- src/renderer/src/i18n/locales/es.json | 190 +---- src/renderer/src/i18n/locales/ja.json | 190 +---- src/renderer/src/i18n/locales/ko.json | 275 ++---- src/renderer/src/i18n/locales/zh.json | 260 ++---- .../src/web/preload-api/web-worktrees-api.ts | 34 +- .../web/worktree-create-launch-compat.test.ts | 49 ++ .../src/web/worktree-create-launch-compat.ts | 45 + src/shared/agent-catalog-snapshot.ts | 8 + src/shared/data-recovery.ts | 23 + 41 files changed, 1800 insertions(+), 1469 deletions(-) create mode 100644 src/main/agent-launch/agent-catalog-base-disable-impact.ts create mode 100644 src/main/data-recovery/recovery-points.test.ts create mode 100644 src/main/data-recovery/recovery-points.ts create mode 100644 src/main/ipc/data-recovery.ts create mode 100644 src/renderer/src/components/data-recovery/DataRecoveryDialog.tsx create mode 100644 src/renderer/src/components/data-recovery/DataRecoveryMigrationNotice.test.tsx create mode 100644 src/renderer/src/components/data-recovery/DataRecoveryMigrationNotice.tsx create mode 100644 src/renderer/src/components/data-recovery/DataRecoverySettingsRow.tsx create mode 100644 src/renderer/src/web/worktree-create-launch-compat.test.ts create mode 100644 src/renderer/src/web/worktree-create-launch-compat.ts create mode 100644 src/shared/data-recovery.ts diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 158600c10ed..7526cfee307 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -361,9 +361,7 @@ jobs: # into the REMOTE worktree, and observes the spawned process's argv+env from # /proc inside the container — proving one host resolution produced one remote # PTY with the exact custom executable/args/env, and that reconnect never - # duplicates the launch. Ubuntu-only because macOS/Windows GitHub runners - # cannot host a Linux Docker container (the platform matrix in pr.yml covers - # the pure-resolution cross-OS cells instead). + # duplicates the launch. ssh-custom-agent: name: e2e ssh custom agent needs: build diff --git a/config/tsconfig.cli.json b/config/tsconfig.cli.json index 93d556602f8..44f58cae6c5 100644 --- a/config/tsconfig.cli.json +++ b/config/tsconfig.cli.json @@ -18,6 +18,7 @@ "../src/main/agent-hooks/runtime-home-hook-command.ts", "../src/main/agent-hooks/windows-powershell-hook-launcher.ts", "../src/main/amp/agent-status-plugin-source.ts", + "../src/main/agent-launch/agent-catalog-schema-migration.ts", "../src/main/amp/hook-service.ts", "../src/main/amp/managed-plugin-install-status.ts", "../src/main/antigravity/hook-events.ts", diff --git a/docs/reference/custom-agents-forward-rollback-runbook.md b/docs/reference/custom-agents-forward-rollback-runbook.md index bc7f034b6b8..fb27ba09e32 100644 --- a/docs/reference/custom-agents-forward-rollback-runbook.md +++ b/docs/reference/custom-agents-forward-rollback-runbook.md @@ -9,7 +9,7 @@ rollback contract) and acceptance oracle 39. The v1 data file is **forward-readable but not honestly writable** by an older Orca binary: pre-v1 code does not understand custom identities in defaults, owner records, or resume attribution. Runtime compatibility projections protect old -*clients*; they do not make a downgraded desktop binary schema-aware. +_clients_; they do not make a downgraded desktop binary schema-aware. ## Rules @@ -20,18 +20,102 @@ records, or resume attribution. Runtime compatibility projections protect old must not resolve a feature incident this way. 3. **Explicit user downgrade = restore the pinned backup** (below). This is the only supported pre-v1 binary rollback and discards settings/workspace metadata - written after the backup point. + written after the backup point. Users perform this through **Data recovery**; + direct file manipulation is a support-only fallback. +4. **Offline CLI writes are schema-transparent.** CLI commands that edit the + data file while the app is not running (e.g. `orca agent hooks on|off`) must + never add, upgrade, or default any agent-catalog schema field, and must + refuse the write when the file carries a newer schema than the binary knows. + Only the app's migration path may perform the first v1 write. +5. **The hard protocol-floor bump ships only after the rollback window closes.** + While a forward-rollback build is a supported production state, raising + `MIN_COMPATIBLE_DESKTOP_VERSION` (or retiring the + `agent-launch.identity.v1` soft capability phase) would strand updated + clients against a rolled-back host. ## The pinned pre-v1 backup - Written once, before the first v1 write, beside the rotating backups at `.pre-agent-catalog-v1.backup` (`pinnedPreV1BackupPath`). - Same filesystem permissions as the data file; `fsync` + atomic rename. -- If it cannot be created, migration performs **no v1 write** and Settings reports a - local migration error; launch behavior stays on the clean built-in baseline. -- Never synced, never exposed through RPC, never given weaker permissions. +- If it cannot be created, migration performs **no v1 write**; launch behavior + stays on the clean built-in baseline and catalog/reference writes remain + blocked until migration succeeds. +- Never synced, never given weaker permissions, and never exposed to the renderer + as raw contents or a filesystem path. The renderer receives only recovery-point + metadata and invokes main-process-owned retry/restore operations. - Removed only after the documented one-release rollback window (see follow-up). +## Migration user experience + +The v1 migration runs during profile load; users do not need to open Settings to +start it or learn that it failed. A successful migration is silent. A failed +backup or migration produces a persistent, app-level notice +(`DataRecoveryMigrationNotice`) as soon as the profile is loaded. The notice +explains that: + +- Orca left the existing data unchanged; +- built-in agent launches remain available; and +- custom-agent catalog and reference changes are read-only until recovery. + +The notice provides **Retry migration** and **Open Data recovery** actions plus +copyable local error details. It remains visible across navigation and relaunches +until retry succeeds or the user restores a recovery point. Settings mirrors the +same status and actions, but is not the only place that reports the failure. + +## General Data recovery UI + +Backup restoration is a reusable main-process capability +(`src/main/data-recovery/recovery-points.ts`), not an agent-catalog-specific +file-copy instruction. **Data recovery** inventories recovery points for this +and future migrations (new migrations register their points there). Each entry +shows its migration identifier, creation time, compatibility target, and a +plain-language summary of data that will be lost. The renderer receives +metadata only — never a filesystem path or backup contents. + +After explicit confirmation, the main process validates the selected backup, +suspends writes, creates a separate pre-restore safety copy, and atomically +replaces the live data file. Cancellation or failure leaves both the current file +and recovery point intact and reports an actionable local error. + +For a recovery point compatible with the running build, the final action is +**Restore and restart**. For the pinned pre-v1 point, it is **Prepare downgrade**: +Orca restores the point and quits without relaunching, preventing the current v1 +build from immediately migrating the restored file again. + +## Remote hosts (`orca serve`) + +Catalogs are per-host. A serve host runs the same profile load, migration, and +pinned backup against its own data file on its own disk; every rule above +applies to each host independently. A desktop and a remote host are rolled back +separately and may skew — runtime projections and the protocol floor cover the +skew, not this runbook. + +Serve-host contract: + +- The migration-blocked state is projected env-free over runtime RPC as a + `migrationBlocked` boolean on the remote catalog snapshot — never the local + error text — so a connected client (mobile, paired web, remote desktop) can + show that the host's catalog is read-only pending recovery. Mobile surfaces + it in the agent picker. +- The headless downgrade replaces **Prepare downgrade**: stop the serve process + and disable any supervisor auto-restart, restore the pinned backup, install + the pre-v1 binary, then start the service. A supervisor that relaunches the + v1 binary after the restore re-runs the migration and defeats the downgrade. +- New desktop/paired-web clients follow the same probe-and-degrade contract as + mobile and the CLI (compat design, client column): probe + `agent-launch.identity.v1` before sending an identity-only `agentLaunch` + (`worktree-create-launch-compat.ts`); a pre-identity host gets the legacy + built-in id fallback, and custom/default selections fail fast with an + update-the-host error. + +## Mobile clients + +Mobile needs no rollback procedure of its own: it never writes the catalog, +holds only an in-memory env-free per-host cache, and old clients are protected +by the legacy projections and the capability/protocol floor. Its half of a host +incident is displaying the projected migration-blocked state above. + ## Crash safety (oracle 39) Schema migration, backup creation, catalog/reference mutation, and snapshot @@ -42,20 +126,50 @@ mid-migration always finds an intact v0 file to restart from. ## Explicit downgrade procedure -1. Quit Orca. -2. Copy `.pre-agent-catalog-v1.backup` over ``. -3. Install the pre-v1 binary and relaunch. +1. Open **Data recovery** from the app-level migration notice or Settings. +2. Select the recovery point labeled as the state before agent-catalog v1. +3. Choose **Prepare downgrade**, review the post-backup data-loss summary, and + confirm. Orca restores the point atomically and quits. +4. Install the pre-v1 binary before opening Orca again, then relaunch. The restored file is byte-identical to the pre-v1 state; all post-backup metadata is intentionally discarded. +### Support-only manual fallback + +Use this only when Orca cannot open Data recovery: + +1. Quit every running Orca process, including terminal daemons and any `orca` + CLI invocation — a v1-era CLI's offline write between restore and downgrade + can re-stamp the schema. +2. Preserve the current `` separately, then copy + `.pre-agent-catalog-v1.backup` over ``. +3. Install the pre-v1 binary before opening Orca again, then relaunch. + +Support must verify the profile and paths before copying; the UI flow above is the +normal user procedure. + ## Verification Rollback verification runs on a **disposable** profile, never on user data: `src/main/agent-launch/agent-catalog-forward-rollback-fixture.test.ts` exercises migrate → v1-reference resolve → forward-rollback resolve → pinned-backup restore → crash-before-write restart. Unit field-mapping coverage lives in -`agent-catalog-schema-migration.test.ts`. +`agent-catalog-schema-migration.test.ts`. CLI offline-write schema transparency +(Rule 4) is covered in `src/cli/handlers/agent-hooks.test.ts`; load-time +surfacing of the blocked state in `agent-catalog-service.test.ts` and +`AgentCatalogSection.test.tsx`; retry in +`persistence-agent-catalog-migration.test.ts`; inventory/atomic +restore/safety-copy/failure-unfreeze in +`src/main/data-recovery/recovery-points.test.ts`; the app-level notice, retry +actions, and Prepare-downgrade flow in `DataRecoveryMigrationNotice.test.tsx`; +the client probe-and-degrade in `worktree-create-launch-compat.test.ts`; the +serve `migrationBlocked` projection in `agent-catalog-service.test.ts` and +`mobile-agent-catalog-projection.test.ts`. + +Before release, end-to-end coverage should still exercise the full +restore-quit-relaunch cycle against a packaged build (component/unit tests +above stop at the IPC boundary). ## Follow-up (fill before merge) diff --git a/mobile/src/components/NewWorktreeModal.tsx b/mobile/src/components/NewWorktreeModal.tsx index 2ca4288f8fd..23f184fabda 100644 --- a/mobile/src/components/NewWorktreeModal.tsx +++ b/mobile/src/components/NewWorktreeModal.tsx @@ -1,4 +1,4 @@ -import { useMemo, useState } from 'react' +import { useMemo, useRef, useState } from 'react' import { Keyboard } from 'react-native' import { getComposerRepoWorktreeBranches } from '../../../src/shared/composer-branch-selection' import { getProjectIdentityKey } from '../../../src/shared/project-host-setup-projection' @@ -7,10 +7,8 @@ import type { SmartModeAvailabilityInput } from '../tasks/mobile-smart-source-mo import { deriveRepoSlug, type PasteRepoCandidate } from '../tasks/smart-source-paste-intent' import { useMobileComposerSource } from '../tasks/use-mobile-composer-source' import { useNewWorktreeRuntimeCapabilities } from '../tasks/worktree-create-capability' -import { - buildRetiredWorktreeNamesRefreshKey, - useRetiredWorktreeNames -} from '../worktree/use-retired-worktree-names' +import { useRetiredWorktreeNames } from '../worktree/use-retired-worktree-names' +import { hostAgentCatalogReadOnlyNotice } from '../tasks/mobile-agent-catalog-projection' import { BottomDrawerModalHost } from './bottom-drawer-modal-host' import { getMobileWorkspaceRepoBadgeColor, @@ -34,25 +32,26 @@ import { useNewWorkspaceSetupScript } from './use-new-workspace-setup-script' import { useNewWorktreeDrawerNavigation } from './use-new-worktree-drawer-navigation' export function NewWorktreeModal(props: NewWorktreeModalProps) { + const openEpochRef = useRef(0) + const wasVisibleRef = useRef(false) + const clientEpochRef = useRef({ client: props.client, epoch: 0 }) + // Why: each drawer opening is a fresh form session; remounting resets local // form state before paint instead of clearing it in a visible-prop Effect. - // State, not a ref: react-native-screens freezes a blurred screen by suspending - // this subtree, and a counter bumped during a render React then throws away - // would restart the session for an opening that never committed. - const [session, setSession] = useState({ openEpoch: 0, visible: props.visible }) - if (session.visible !== props.visible) { - setSession({ - openEpoch: props.visible ? session.openEpoch + 1 : session.openEpoch, - visible: props.visible - }) + if (props.visible && !wasVisibleRef.current) { + openEpochRef.current += 1 + } + wasVisibleRef.current = props.visible + if (clientEpochRef.current.client !== props.client) { + clientEpochRef.current = { client: props.client, epoch: clientEpochRef.current.epoch + 1 } } - // Why: key the session on the HOST, never on the RpcClient object. A reconnect, - // forceReconnect, or foreground revival swaps that object for the same host - // (see useHostClient), and keying on it silently remounted this form mid-edit - // and threw away the picked source. Every client-scoped hook below already - // drops responses from a superseded client, so no remount is needed for that. - return + return ( + + ) } function NewWorktreeModalContent(props: NewWorktreeModalProps) { @@ -94,7 +93,7 @@ function NewWorktreeModalContent(props: NewWorktreeModalProps) { agentCatalog }) const retiredNamesRefreshKey = useMemo( - () => buildRetiredWorktreeNamesRefreshKey(existingWorktreePaths), + () => (existingWorktreePaths ?? []).toSorted().join('\0'), [existingWorktreePaths] ) const retiredWorktreeNames = useRetiredWorktreeNames( @@ -249,6 +248,7 @@ function NewWorktreeModalContent(props: NewWorktreeModalProps) { selectedProjectId={selectedProjectId} runTargetPickerItems={runTargetPickerItems} pickerAgentOptions={agentSelection.pickerAgentOptions} + agentPickerSubtitle={hostAgentCatalogReadOnlyNotice(agentCatalog)} selectedAgent={agentSelection.selectedAgent} setupTrustPrompt={createSubmit.setupTrustPrompt} creating={createSubmit.creating} diff --git a/mobile/src/components/NewWorktreeModalDrawers.tsx b/mobile/src/components/NewWorktreeModalDrawers.tsx index 70ce22cb807..5b7cef0e9ca 100644 --- a/mobile/src/components/NewWorktreeModalDrawers.tsx +++ b/mobile/src/components/NewWorktreeModalDrawers.tsx @@ -34,6 +34,7 @@ export function NewWorktreeModalDrawers(props: { selectedProjectId: string | null runTargetPickerItems: NewWorkspaceRunTargetOption[] pickerAgentOptions: NewWorktreeAgentOption[] + agentPickerSubtitle?: string selectedAgent: NewWorktreeAgentOption setupTrustPrompt: SetupTrustPrompt | null creating: boolean @@ -94,6 +95,7 @@ export function NewWorktreeModalDrawers(props: { = { visible: boolean title: string + /** Muted line under the title for host-state context (e.g. read-only catalog). */ + subtitle?: string items: T[] selectedId: string onSelect: (item: T) => void @@ -21,6 +23,7 @@ type Props = { export function PickerListDrawer({ visible, title, + subtitle, items, selectedId, onSelect, @@ -72,6 +75,7 @@ export function PickerListDrawer({ > {title} + {subtitle ? {subtitle} : null} { const custom = rows.find((row) => row.isCustom) expect(Object.keys(custom ?? {}).sort()).toEqual(['baseAgent', 'id', 'isCustom', 'label']) }) + + it('reads the host migration-blocked flag as a boolean-only projection', () => { + expect(isHostAgentCatalogMigrationBlocked(null)).toBe(false) + expect(isHostAgentCatalogMigrationBlocked(snapshot())).toBe(false) + expect(isHostAgentCatalogMigrationBlocked(projectionError)).toBe(false) + const blocked = snapshot({ migrationBlocked: true }) + expect(isHostAgentCatalogMigrationBlocked(blocked)).toBe(true) + // The wire value carries no error text or path — boolean only. + expect(JSON.stringify(blocked)).not.toMatch(/error|\/Users\//i) + }) }) diff --git a/mobile/src/tasks/mobile-agent-catalog-projection.ts b/mobile/src/tasks/mobile-agent-catalog-projection.ts index 50a1b05721c..1c3ae58a0d9 100644 --- a/mobile/src/tasks/mobile-agent-catalog-projection.ts +++ b/mobile/src/tasks/mobile-agent-catalog-projection.ts @@ -71,6 +71,22 @@ function groupEnabledReadyCustoms( return grouped } +/** True while the host's agent catalog is fail-closed pending recovery (failed + * pre-v1 backup): catalog edits are read-only ON THE HOST and customs are + * absent. Boolean projection only — the error detail never crosses the wire. */ +export function isHostAgentCatalogMigrationBlocked(snapshot: AgentCatalogValue | null): boolean { + return isCatalogSnapshot(snapshot) && snapshot.migrationBlocked === true +} + +/** Picker subtitle for a blocked host, or undefined when healthy. */ +export function hostAgentCatalogReadOnlyNotice( + snapshot: AgentCatalogValue | null +): string | undefined { + return isHostAgentCatalogMigrationBlocked(snapshot) + ? 'Agent settings on this host are read-only until its data migration is recovered.' + : undefined +} + /** Build the ordered picker rows. With no usable snapshot or with customs gated * off, returns exactly the static built-in rows. Otherwise each enabled, ready * custom is inserted directly below its base harness in the built-in order. */ diff --git a/src/cli/handlers/agent-hooks.test.ts b/src/cli/handlers/agent-hooks.test.ts index 4fcc186b0d8..5f8eba58ac8 100644 --- a/src/cli/handlers/agent-hooks.test.ts +++ b/src/cli/handlers/agent-hooks.test.ts @@ -47,9 +47,16 @@ vi.mock('../runtime-client', () => { } } + class RuntimeRpcFailureError extends RuntimeClientError { + constructor(readonly response: unknown) { + super('rpc_failure', 'rpc failure') + } + } + return { RuntimeClient, RuntimeClientError, + RuntimeRpcFailureError, getDefaultUserDataPath: getDefaultUserDataPathMock } }) @@ -81,6 +88,7 @@ describe('agent hooks CLI handler', () => { beforeEach(() => { userDataPath = mkdtempSync(join(tmpdir(), 'orca-agent-hooks-cli-')) + applyAgentStatusHooksEnabledMock.mockReset() applyAgentStatusHooksEnabledMock.mockReturnValue([]) callMock.mockReset() getCliStatusMock.mockClear() @@ -228,4 +236,74 @@ describe('agent hooks CLI handler', () => { timeoutMs: 1_000 }) }) + + const AGENT_CATALOG_KEYS = [ + 'agentCatalogSchemaVersion', + 'agentCatalogRevision', + 'agentReferenceRevision', + 'defaultTuiAgent', + 'disabledTuiAgents', + 'customTuiAgents', + 'deletedCustomTuiAgents' + ] as const + + it('never stamps agent-catalog schema fields onto an existing pre-v1 file', async () => { + const existing = getDefaultPersistedState(userDataPath) + const settings = existing.settings as unknown as Record + for (const key of AGENT_CATALOG_KEYS) { + delete settings[key] + } + // Pre-v1 legacy shape: an explicit null default, no schema stamp. + settings.defaultTuiAgent = null + writeDataFile(userDataPath, existing) + + await runAgentHooksOff(userDataPath) + + const persisted = readDataFile(userDataPath).settings as unknown as Record + expect(persisted.agentStatusHooksEnabled).toBe(false) + expect(persisted.defaultTuiAgent).toBeNull() + for (const key of AGENT_CATALOG_KEYS) { + if (key === 'defaultTuiAgent') { + continue + } + expect(persisted, key).not.toHaveProperty(key) + } + // Non-catalog defaults still normalize. + expect(persisted.experimentalNewWorktreeCardStyle).toBe(false) + }) + + it('round-trips an existing v1 catalog byte-for-byte through an offline update', async () => { + const existing = getDefaultPersistedState(userDataPath) + const settings = existing.settings as unknown as Record + settings.agentCatalogSchemaVersion = 1 + settings.agentCatalogRevision = 7 + settings.agentReferenceRevision = 3 + settings.customTuiAgents = [{ id: 'custom-abc', label: 'My Agent', baseAgent: 'claude' }] + settings.deletedCustomTuiAgents = [{ id: 'custom-gone' }] + writeDataFile(userDataPath, existing) + + await runAgentHooksOff(userDataPath) + + const persisted = readDataFile(userDataPath).settings as unknown as Record + expect(persisted.agentCatalogSchemaVersion).toBe(1) + expect(persisted.agentCatalogRevision).toBe(7) + expect(persisted.agentReferenceRevision).toBe(3) + expect(persisted.customTuiAgents).toEqual([ + { id: 'custom-abc', label: 'My Agent', baseAgent: 'claude' } + ]) + expect(persisted.deletedCustomTuiAgents).toEqual([{ id: 'custom-gone' }]) + }) + + it('refuses the offline write when the file carries a newer agent-catalog schema', async () => { + const existing = getDefaultPersistedState(userDataPath) + ;(existing.settings as unknown as Record).agentCatalogSchemaVersion = 2 + writeDataFile(userDataPath, existing) + const before = readFileSync(join(userDataPath, 'orca-data.json'), 'utf-8') + + await runAgentHooksOff(userDataPath) + + expect(process.exitCode).toBe(1) + expect(readFileSync(join(userDataPath, 'orca-data.json'), 'utf-8')).toBe(before) + expect(applyAgentStatusHooksEnabledMock).not.toHaveBeenCalled() + }) }) diff --git a/src/cli/handlers/agent-hooks.ts b/src/cli/handlers/agent-hooks.ts index bf44211b4a7..e5250f972f0 100644 --- a/src/cli/handlers/agent-hooks.ts +++ b/src/cli/handlers/agent-hooks.ts @@ -20,6 +20,20 @@ import { getManagedAgentHookStatuses, prepareManagedCodexHomeBeforeShellLaunch } from '../../main/agent-hooks/managed-agent-hook-controls' +import { AGENT_CATALOG_SCHEMA_VERSION } from '../../main/agent-launch/agent-catalog-schema-migration' + +// Why: offline writes must be schema-transparent for the agent catalog — merging +// defaults would stamp agentCatalogSchemaVersion on a pre-v1 file without the +// pinned pre-v1 backup the app's migration contract requires before any v1 write. +const AGENT_CATALOG_SETTINGS_KEYS = [ + 'agentCatalogSchemaVersion', + 'agentCatalogRevision', + 'agentReferenceRevision', + 'defaultTuiAgent', + 'disabledTuiAgents', + 'customTuiAgents', + 'deletedCustomTuiAgents' +] as const type AgentHookCommandResult = { enabled: boolean @@ -132,12 +146,29 @@ function updateEnabledOnDisk(enabled: boolean): { settings: Pick } { const dataPath = getDataPath() + const fileExists = existsSync(dataPath) const state = readPersistedState(dataPath) + const originalSettings: Record = { ...state.settings } + const rawVersion = originalSettings.agentCatalogSchemaVersion + if (typeof rawVersion === 'number' && rawVersion > AGENT_CATALOG_SCHEMA_VERSION) { + throw new RuntimeClientError( + 'runtime_error', + `${dataPath} uses agent-catalog schema ${rawVersion}, newer than this CLI supports (${AGENT_CATALOG_SCHEMA_VERSION}). Update the Orca CLI or change this setting from the app.` + ) + } state.settings = { ...getDefaultPersistedState(homedir()).settings, ...state.settings, agentStatusHooksEnabled: enabled } + if (fileExists) { + const merged = state.settings as unknown as Record + for (const key of AGENT_CATALOG_SETTINGS_KEYS) { + if (!(key in originalSettings)) { + delete merged[key] + } + } + } writePersistedState(dataPath, state) return { settingsPath: dataPath, diff --git a/src/main/agent-launch/agent-catalog-base-disable-impact.ts b/src/main/agent-launch/agent-catalog-base-disable-impact.ts new file mode 100644 index 00000000000..10655ea6083 --- /dev/null +++ b/src/main/agent-launch/agent-catalog-base-disable-impact.ts @@ -0,0 +1,44 @@ +// §973 base-disable impact: the counts of persisted-owner references and +// resumable sessions that will block when a built-in base is disabled. Saved +// references include the base id and any live custom derivative of it (a +// derivative can't launch without its harness); sessions are counted by base +// and excluded from the reference scan so the two counts never overlap. Counts +// only — never a label or config. Enabled-derivative counts stay client-side. + +import type { BuiltInTuiAgent, GlobalSettings } from '../../shared/types' +import type { BaseDisableImpact } from '../../shared/agent-reference-snapshot' +import type { AgentTombstoneReferenceIndex } from './agent-tombstone-reference-index' +import { normalizeCatalogFromSettings } from './agent-catalog-projections' +import { getHostAgentSessionRecordStore } from './agent-session-record-store-host' + +export function computeBaseDisableImpact( + settings: GlobalSettings, + referenceIndex: AgentTombstoneReferenceIndex, + base: BuiltInTuiAgent +): BaseDisableImpact { + const catalog = normalizeCatalogFromSettings(settings) + const derivativeIds = new Set() + for (const agent of catalog.liveCustomAgents) { + if (agent.baseAgent === base) { + derivativeIds.add(agent.id) + } + } + const matches = (value: unknown): boolean => + value === base || (typeof value === 'string' && derivativeIds.has(value)) + const saved = referenceIndex.countMatchingReferences(matches, { + excludeOwners: new Set(['session']) + }) + let resumableSessions: BaseDisableImpact['resumableSessions'] + try { + resumableSessions = { + count: getHostAgentSessionRecordStore().countRecordsByBase(base), + atLeast: false + } + } catch { + resumableSessions = { count: 0, atLeast: true } + } + return { + savedReferences: { count: saved.count, atLeast: !saved.complete }, + resumableSessions + } +} diff --git a/src/main/agent-launch/agent-catalog-service.test.ts b/src/main/agent-launch/agent-catalog-service.test.ts index eb8a1b2ace8..16c7987b93d 100644 --- a/src/main/agent-launch/agent-catalog-service.test.ts +++ b/src/main/agent-launch/agent-catalog-service.test.ts @@ -619,6 +619,38 @@ describe('pre-v1 migration gate', () => { // No v1 write of any kind may land on the unbacked-up profile. expect(state.settings).toBe(before) }) + + it('carries the block into the local snapshot so Settings surfaces it on load', () => { + const state: StoreStubState = { + settings: baseSettings(), + repos: [], + automations: [], + agentCatalogMigrationError: 'disk full' + } + const service = new AgentCatalogService(makeStoreStub(state)) + expect(service.getLocalSnapshot().migrationBlockedError).toBe('disk full') + + state.agentCatalogMigrationError = null + expect(service.getLocalSnapshot().migrationBlockedError).toBeUndefined() + }) + + it('projects the block to remote clients as a boolean only — never the error text', () => { + const state: StoreStubState = { + settings: baseSettings(), + repos: [], + automations: [], + agentCatalogMigrationError: 'disk full at /Users/someone/Library' + } + const service = new AgentCatalogService(makeStoreStub(state)) + const remote = service.getRemoteSnapshot() + expect('customAgents' in remote && remote.migrationBlocked).toBe(true) + expect(JSON.stringify(remote)).not.toContain('disk full') + expect(JSON.stringify(remote)).not.toContain('/Users/') + + state.agentCatalogMigrationError = null + const healthy = service.getRemoteSnapshot() + expect('migrationBlocked' in healthy).toBe(false) + }) }) describe('reference payload budget (L1-#2)', () => { diff --git a/src/main/agent-launch/agent-catalog-service.ts b/src/main/agent-launch/agent-catalog-service.ts index baafb7d0dc7..7e4efa740cf 100644 --- a/src/main/agent-launch/agent-catalog-service.ts +++ b/src/main/agent-launch/agent-catalog-service.ts @@ -39,7 +39,7 @@ import { isSecurityReducingMutation, type AgentCatalogMigrationBlockedError } from './agent-catalog-write-policy' -import { getHostAgentSessionRecordStore } from './agent-session-record-store-host' +import { computeBaseDisableImpact } from './agent-catalog-base-disable-impact' import { applyAgentReferenceMutation } from './agent-reference-mutations' import type { AgentReferenceMutationRequest, @@ -91,11 +91,19 @@ export class AgentCatalogService { } getLocalSnapshot(): LocalAgentCatalogSnapshot { - return buildLocalAgentCatalogSnapshot(this.store.getSettings(), this.repairTokens) + const snapshot = buildLocalAgentCatalogSnapshot(this.store.getSettings(), this.repairTokens) + const blocked = agentCatalogMigrationBlockedError(this.store) + return blocked ? { ...snapshot, migrationBlockedError: blocked.migrationError } : snapshot } getRemoteSnapshot(): ReturnType { - return buildAgentCatalogSnapshot(this.store.getSettings()) + const snapshot = buildAgentCatalogSnapshot(this.store.getSettings()) + // Boolean-only block flag (the error text never crosses the wire); a + // projection error never carries it — a blocked host is pre-v1, never oversize. + if (!('customAgents' in snapshot) || !agentCatalogMigrationBlockedError(this.store)) { + return snapshot + } + return { ...snapshot, migrationBlocked: true } } /** Local-desktop-only reference summary for delete confirmation and "Review @@ -104,38 +112,8 @@ export class AgentCatalogService { return this.referenceIndex.summarizeReferences(id) } - /** §973 base-disable impact: the counts of persisted-owner references and - * resumable sessions that will block when a built-in base is disabled. Saved - * references include the base id and any live custom derivative of it (a - * derivative can't launch without its harness); sessions are counted by base - * and excluded from the reference scan so the two counts never overlap. Counts - * only — never a label or config. Enabled-derivative counts stay client-side. */ getBaseDisableImpact(base: BuiltInTuiAgent): BaseDisableImpact { - const catalog = normalizeCatalogFromSettings(this.store.getSettings()) - const derivativeIds = new Set() - for (const agent of catalog.liveCustomAgents) { - if (agent.baseAgent === base) { - derivativeIds.add(agent.id) - } - } - const matches = (value: unknown): boolean => - value === base || (typeof value === 'string' && derivativeIds.has(value)) - const saved = this.referenceIndex.countMatchingReferences(matches, { - excludeOwners: new Set(['session']) - }) - let resumableSessions: BaseDisableImpact['resumableSessions'] - try { - resumableSessions = { - count: getHostAgentSessionRecordStore().countRecordsByBase(base), - atLeast: false - } - } catch { - resumableSessions = { count: 0, atLeast: true } - } - return { - savedReferences: { count: saved.count, atLeast: !saved.complete }, - resumableSessions - } + return computeBaseDisableImpact(this.store.getSettings(), this.referenceIndex, base) } /** Single-record full-env editor read, access-checked by the preload boundary @@ -217,18 +195,12 @@ export class AgentCatalogService { } } - /** Non-null while the profile is pinned pre-v1 by a failed backup; all - * catalog/reference mutations are gated on it. */ - private getMigrationBlockedError(): AgentCatalogMigrationBlockedError | null { - return agentCatalogMigrationBlockedError(this.store) - } - mutateReferences( request: AgentReferenceMutationRequest ): AgentReferenceMutationResult | AgentCatalogMigrationBlockedError { // The failed-backup invariant is "no v1 write": reference mutations stamp // agentReferenceRevision, so they are blocked alongside catalog mutations. - const blocked = this.getMigrationBlockedError() + const blocked = agentCatalogMigrationBlockedError(this.store) if (blocked) { return blocked } @@ -311,7 +283,7 @@ export class AgentCatalogService { ): AgentCatalogMutationResult | AgentCatalogMigrationBlockedError { // A failed pinned pre-v1 backup means no v1 write may land on the profile; // fail closed here so authoring cannot bypass the migration invariant. - const blocked = this.getMigrationBlockedError() + const blocked = agentCatalogMigrationBlockedError(this.store) if (blocked) { return blocked } diff --git a/src/main/data-recovery/recovery-points.test.ts b/src/main/data-recovery/recovery-points.test.ts new file mode 100644 index 00000000000..4e94ae70112 --- /dev/null +++ b/src/main/data-recovery/recovery-points.test.ts @@ -0,0 +1,118 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' +import { + chmodSync, + existsSync, + mkdtempSync, + readFileSync, + rmSync, + statSync, + writeFileSync +} from 'node:fs' +import { join } from 'node:path' +import { tmpdir } from 'node:os' +import { + PRE_RESTORE_SAFETY_SUFFIX, + listRecoveryPoints, + restoreRecoveryPoint +} from './recovery-points' + +const PINNED_SUFFIX = '.pre-agent-catalog-v1.backup' + +describe('data recovery points', () => { + let dir = '' + let dataFile = '' + + beforeEach(() => { + dir = mkdtempSync(join(tmpdir(), 'orca-data-recovery-')) + dataFile = join(dir, 'orca-data.json') + }) + + afterEach(() => { + chmodSync(dir, 0o755) + rmSync(dir, { recursive: true, force: true }) + }) + + function makeStore() { + return { + getDataFilePath: () => dataFile, + freezeWrites: vi.fn(), + unfreezeWrites: vi.fn(), + waitForPendingWrite: vi.fn(() => Promise.resolve()) + } + } + + it('lists no points when no backup exists', () => { + expect(listRecoveryPoints(dataFile)).toEqual([]) + }) + + it('lists the pinned pre-v1 point with metadata only — no path, no contents', () => { + writeFileSync(`${dataFile}${PINNED_SUFFIX}`, JSON.stringify({ settings: {} })) + const points = listRecoveryPoints(dataFile) + expect(points).toHaveLength(1) + expect(points[0].id).toBe('agent-catalog-pre-v1') + expect(points[0].compatibility).toBe('previous-binary') + expect(points[0].sizeBytes).toBeGreaterThan(0) + expect(points[0].createdAtMs).toBeGreaterThan(0) + expect(JSON.stringify(points)).not.toContain(dir) + expect(JSON.stringify(points)).not.toContain('settings') + }) + + it('restores atomically: freeze before replace, safety copy kept, point preserved', async () => { + const preV1 = JSON.stringify({ settings: { defaultTuiAgent: null } }) + const current = JSON.stringify({ settings: { agentCatalogSchemaVersion: 1 } }) + writeFileSync(dataFile, current, { mode: 0o600 }) + writeFileSync(`${dataFile}${PINNED_SUFFIX}`, preV1, { mode: 0o600 }) + + const store = makeStore() + const result = await restoreRecoveryPoint(store, 'agent-catalog-pre-v1') + + expect(result).toEqual({ ok: true }) + expect(store.freezeWrites).toHaveBeenCalledTimes(1) + expect(store.waitForPendingWrite).toHaveBeenCalledTimes(1) + expect(store.unfreezeWrites).not.toHaveBeenCalled() + // Byte-identical restore; the point and a safety copy of the v1 file both survive. + expect(readFileSync(dataFile, 'utf-8')).toBe(preV1) + expect(readFileSync(`${dataFile}${PINNED_SUFFIX}`, 'utf-8')).toBe(preV1) + expect(readFileSync(`${dataFile}${PRE_RESTORE_SAFETY_SUFFIX}`, 'utf-8')).toBe(current) + expect(statSync(dataFile).mode & 0o777).toBe(0o600) + }) + + it('rejects a missing point without touching anything', async () => { + writeFileSync(dataFile, '{}') + const store = makeStore() + const result = await restoreRecoveryPoint(store, 'agent-catalog-pre-v1') + expect(result.ok).toBe(false) + expect(store.freezeWrites).not.toHaveBeenCalled() + expect(readFileSync(dataFile, 'utf-8')).toBe('{}') + }) + + it('rejects a corrupt backup before suspending writes', async () => { + writeFileSync(dataFile, '{"settings":{}}') + writeFileSync(`${dataFile}${PINNED_SUFFIX}`, 'not json {') + const store = makeStore() + const result = await restoreRecoveryPoint(store, 'agent-catalog-pre-v1') + expect(result.ok).toBe(false) + expect(store.freezeWrites).not.toHaveBeenCalled() + expect(readFileSync(dataFile, 'utf-8')).toBe('{"settings":{}}') + expect(existsSync(`${dataFile}${PRE_RESTORE_SAFETY_SUFFIX}`)).toBe(false) + }) + + // Why skip on win32: chmod cannot make a directory read-only on Windows. + it.skipIf(process.platform === 'win32')( + 're-enables writes and leaves the live file intact when the replace fails', + async () => { + const current = '{"settings":{"agentCatalogSchemaVersion":1}}' + writeFileSync(dataFile, current, { mode: 0o600 }) + writeFileSync(`${dataFile}${PINNED_SUFFIX}`, '{"settings":{}}', { mode: 0o600 }) + const store = makeStore() + // Read-only directory: the safety-copy tmp file cannot be created. + chmodSync(dir, 0o500) + const result = await restoreRecoveryPoint(store, 'agent-catalog-pre-v1') + chmodSync(dir, 0o755) + expect(result.ok).toBe(false) + expect(store.freezeWrites).toHaveBeenCalledTimes(1) + expect(store.unfreezeWrites).toHaveBeenCalledTimes(1) + expect(readFileSync(dataFile, 'utf-8')).toBe(current) + } + ) +}) diff --git a/src/main/data-recovery/recovery-points.ts b/src/main/data-recovery/recovery-points.ts new file mode 100644 index 00000000000..dc30bba6468 --- /dev/null +++ b/src/main/data-recovery/recovery-points.ts @@ -0,0 +1,133 @@ +// Recovery-point inventory and atomic restore for migration backups (runbook: +// "General Data recovery UI"). Only metadata crosses to the renderer — never a +// filesystem path or raw backup contents; restore/retry are main-owned. + +import { + closeSync, + existsSync, + fsyncSync, + openSync, + readFileSync, + renameSync, + statSync, + unlinkSync, + writeSync +} from 'node:fs' +import { pinnedPreV1BackupPath } from '../agent-launch/agent-catalog-schema-migration' +import type { RecoveryPointDto, RecoveryPointId } from '../../shared/data-recovery' + +export type { RecoveryPointDto, RecoveryPointId } from '../../shared/data-recovery' + +export const PRE_RESTORE_SAFETY_SUFFIX = '.pre-restore-safety.backup' + +type RestoreStore = { + getDataFilePath(): string + freezeWrites(): void + unfreezeWrites(): void + waitForPendingWrite(): Promise +} + +function recoveryPointPath(dataFile: string, id: RecoveryPointId): string { + switch (id) { + case 'agent-catalog-pre-v1': + return pinnedPreV1BackupPath(dataFile) + } +} + +export function listRecoveryPoints(dataFile: string): RecoveryPointDto[] { + const points: RecoveryPointDto[] = [] + const pinned = pinnedPreV1BackupPath(dataFile) + if (existsSync(pinned)) { + let createdAtMs: number | null = null + let sizeBytes: number | null = null + try { + const stat = statSync(pinned) + createdAtMs = stat.birthtimeMs > 0 ? stat.birthtimeMs : stat.mtimeMs + sizeBytes = stat.size + } catch { + // Metadata is best-effort; the point is still restorable. + } + points.push({ + id: 'agent-catalog-pre-v1', + compatibility: 'previous-binary', + createdAtMs, + sizeBytes + }) + } + return points +} + +function writeFileAtomicSync(targetPath: string, contents: string, mode: number): void { + const tmpPath = `${targetPath}.tmp` + const fd = openSync(tmpPath, 'w', mode) + try { + writeSync(fd, contents) + fsyncSync(fd) + } finally { + closeSync(fd) + } + try { + renameSync(tmpPath, targetPath) + } catch (error) { + try { + unlinkSync(tmpPath) + } catch { + // Best-effort tmp cleanup; surface the rename error instead. + } + throw error + } +} + +export type RestoreRecoveryPointResult = { ok: true } | { ok: false; error: string } + +/** Validates the selected backup, suspends writes, keeps a pre-restore safety + * copy, and atomically replaces the live data file. Failure or invalid input + * leaves the current file and the recovery point intact and re-enables writes; + * the caller owns the post-success app action (restart or quit). */ +export async function restoreRecoveryPoint( + store: RestoreStore, + id: RecoveryPointId +): Promise { + const dataFile = store.getDataFilePath() + const backupPath = recoveryPointPath(dataFile, id) + if (!existsSync(backupPath)) { + return { ok: false, error: 'The selected recovery point no longer exists.' } + } + let backupContents: string + try { + backupContents = readFileSync(backupPath, 'utf-8') + const parsed: unknown = JSON.parse(backupContents) + if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) { + return { ok: false, error: 'The recovery point is not a valid Orca data file.' } + } + } catch (error) { + return { + ok: false, + error: `The recovery point could not be validated: ${error instanceof Error ? error.message : String(error)}` + } + } + + // Why freeze before the safety copy: an in-flight or quit-time save landing + // after the copy would make the safety copy stale and could clobber the + // restored file between rename and process exit. + store.freezeWrites() + try { + await store.waitForPendingWrite() + const mode = existsSync(dataFile) ? statSync(dataFile).mode & 0o777 : 0o600 + if (existsSync(dataFile)) { + writeFileAtomicSync( + `${dataFile}${PRE_RESTORE_SAFETY_SUFFIX}`, + readFileSync(dataFile, 'utf-8'), + mode + ) + } + writeFileAtomicSync(dataFile, backupContents, mode) + return { ok: true } + } catch (error) { + store.unfreezeWrites() + return { + ok: false, + error: `Restore failed and no changes were made: ${error instanceof Error ? error.message : String(error)}` + } + } +} diff --git a/src/main/ipc/data-recovery.ts b/src/main/ipc/data-recovery.ts new file mode 100644 index 00000000000..13e7b3d578f --- /dev/null +++ b/src/main/ipc/data-recovery.ts @@ -0,0 +1,42 @@ +// Local preload IPC for Data recovery (runbook release requirement): migration +// status, retry, recovery-point inventory, and main-owned restore. Never a +// runtime RPC method; the renderer sees metadata only. + +import { app, ipcMain } from 'electron' +import type { Store } from '../persistence' +import type { DataRecoveryMigrationStatus } from '../../shared/data-recovery' +import { listRecoveryPoints, restoreRecoveryPoint } from '../data-recovery/recovery-points' + +export function registerDataRecoveryHandlers(store: Store): void { + ipcMain.handle( + 'dataRecovery:migrationStatus', + (): DataRecoveryMigrationStatus => ({ + agentCatalogMigrationError: store.getAgentCatalogMigrationError() + }) + ) + + ipcMain.handle('dataRecovery:retryAgentCatalogMigration', () => + store.retryAgentCatalogMigration() + ) + + ipcMain.handle('dataRecovery:listPoints', () => listRecoveryPoints(store.getDataFilePath())) + + ipcMain.handle('dataRecovery:restore', async (_event, args: { id?: unknown; mode?: unknown }) => { + // The pinned pre-v1 point is only restorable as Prepare downgrade: a + // restart would relaunch the v1 build and immediately re-migrate the + // restored file, undoing the restore. + if (args?.id !== 'agent-catalog-pre-v1' || args?.mode !== 'prepare-downgrade') { + return { ok: false, error: 'Invalid restore request.' } + } + const result = await restoreRecoveryPoint(store, args.id) + if (!result.ok) { + return result + } + // Reply first, then quit without relaunching; the frozen store cannot + // write over the restored file during shutdown. + setImmediate(() => { + app.quit() + }) + return result + }) +} diff --git a/src/main/ipc/register-core-handlers/register-core-handlers.test.ts b/src/main/ipc/register-core-handlers/register-core-handlers.test.ts index 557fb7136bb..25f972bf82c 100644 --- a/src/main/ipc/register-core-handlers/register-core-handlers.test.ts +++ b/src/main/ipc/register-core-handlers/register-core-handlers.test.ts @@ -16,6 +16,8 @@ const { registerDeveloperPermissionHandlersMock, registerComputerUsePermissionHandlersMock, registerSettingsHandlersMock, + registerAgentCatalogHandlersMock, + registerDataRecoveryHandlersMock, registerKeybindingHandlersMock, registerTelemetryHandlersMock, registerDiagnosticsHandlersMock, @@ -59,7 +61,6 @@ const { registerTerminalPreviewHandlersMock, registerSpeechHandlersMock, registerSkillsHandlersMock, - registerAgentCatalogHandlersMock, registerWorkspaceSpaceHandlersMock, registerWorkspacePortHandlersMock, registerLocalhostWorktreeLabelHandlersMock, @@ -82,6 +83,8 @@ const { registerDeveloperPermissionHandlersMock: vi.fn(), registerComputerUsePermissionHandlersMock: vi.fn(), registerSettingsHandlersMock: vi.fn(), + registerAgentCatalogHandlersMock: vi.fn(), + registerDataRecoveryHandlersMock: vi.fn(), registerKeybindingHandlersMock: vi.fn(), registerTelemetryHandlersMock: vi.fn(), registerDiagnosticsHandlersMock: vi.fn(), @@ -125,7 +128,6 @@ const { registerTerminalPreviewHandlersMock: vi.fn(), registerSpeechHandlersMock: vi.fn(), registerSkillsHandlersMock: vi.fn(), - registerAgentCatalogHandlersMock: vi.fn(), registerWorkspaceSpaceHandlersMock: vi.fn(), registerWorkspacePortHandlersMock: vi.fn(), registerLocalhostWorktreeLabelHandlersMock: vi.fn(), @@ -224,14 +226,18 @@ vi.mock('./settings', () => ({ registerSettingsHandlers: registerSettingsHandlersMock })) -vi.mock('./skills', () => ({ - registerSkillsHandlers: registerSkillsHandlersMock +vi.mock('./data-recovery', () => ({ + registerDataRecoveryHandlers: registerDataRecoveryHandlersMock })) vi.mock('./agent-catalog', () => ({ registerAgentCatalogHandlers: registerAgentCatalogHandlersMock })) +vi.mock('./skills', () => ({ + registerSkillsHandlers: registerSkillsHandlersMock +})) + vi.mock('./workspace-space', () => ({ registerWorkspaceSpaceHandlers: registerWorkspaceSpaceHandlersMock })) @@ -535,7 +541,8 @@ describe('registerCoreHandlers', () => { expect(registerTerminalPreviewHandlersMock).toHaveBeenCalledWith(runtime) expect(registerSettingsHandlersMock).toHaveBeenCalledWith(store, agentAwakeService) expect(registerAgentCatalogHandlersMock).toHaveBeenCalledWith(store) - expect(registerSkillsHandlersMock).toHaveBeenCalledWith(store, runtime) + expect(registerSkillsHandlersMock).toHaveBeenCalledWith(store, runtime) + expect(registerDataRecoveryHandlersMock).toHaveBeenCalledWith(store) expect(registerWorkspaceSpaceHandlersMock).toHaveBeenCalledWith(store) expect(registerWorkspacePortHandlersMock).toHaveBeenCalledWith(store) expect(registerLocalhostWorktreeLabelHandlersMock).toHaveBeenCalledWith(store) diff --git a/src/main/ipc/register-core-handlers/register-core-handlers.ts b/src/main/ipc/register-core-handlers/register-core-handlers.ts index c8f5da34919..1d7d2864aff 100644 --- a/src/main/ipc/register-core-handlers/register-core-handlers.ts +++ b/src/main/ipc/register-core-handlers/register-core-handlers.ts @@ -37,9 +37,10 @@ import { setAgentBrowserBridgeRef, registerBrowserHandlers } from './browser' import { setTrustedBrowserRendererWebContentsId } from './browser-renderer-trust' import { registerSessionHandlers } from './session' import { registerSettingsHandlers } from './settings' +import { registerAgentCatalogHandlers } from './agent-catalog' +import { registerDataRecoveryHandlers } from './data-recovery' import { registerDiagnosticsHandlers } from './diagnostics' import { registerSkillsHandlers } from './skills' -import { registerAgentCatalogHandlers } from './agent-catalog' import { registerWorkspaceSpaceHandlers } from './workspace-space' import { registerWorkspacePortHandlers } from './workspace-ports' import { registerLocalhostWorktreeLabelHandlers } from './localhost-worktree-labels' @@ -178,6 +179,7 @@ export function registerCoreHandlers( registerSettingsHandlers(store, agentAwakeService) registerAgentCatalogHandlers(store) registerSkillsHandlers(store, runtime) + registerDataRecoveryHandlers(store) if (automations) { registerAutomationHandlers(store, automations) } diff --git a/src/main/persistence-agent-catalog-migration.test.ts b/src/main/persistence-agent-catalog-migration.test.ts index aa128be7a35..9f9a3988984 100644 --- a/src/main/persistence-agent-catalog-migration.test.ts +++ b/src/main/persistence-agent-catalog-migration.test.ts @@ -146,4 +146,44 @@ describe('agent-catalog v1 migration through the real Store', () => { expect(store.getSettings().agentCatalogSchemaVersion).toBe(1) expect(existsSync(`${dataFile}${PINNED_BACKUP_SUFFIX}`)).toBe(false) }) + + // Why skip on win32: chmod cannot make a directory read-only on Windows. + it.skipIf(process.platform === 'win32')( + 'retryAgentCatalogMigration completes a blocked migration once the cause clears', + async () => { + const original = JSON.stringify({ settings: { defaultTuiAgent: null } }) + writeFileSync(dataFile, original, { mode: 0o600 }) + chmodSync(dir, 0o500) + const store = await createStore(dataFile) + expect(store.getAgentCatalogMigrationError()).not.toBeNull() + + // Still failing: retry reports the error and stays pre-v1. + const stillBlocked = store.retryAgentCatalogMigration() + expect(stillBlocked.ok).toBe(false) + expect(store.getAgentCatalogMigrationError()).not.toBeNull() + expect(store.getSettings().agentCatalogSchemaVersion).toBeUndefined() + + chmodSync(dir, 0o755) + const result = store.retryAgentCatalogMigration() + expect(result).toEqual({ ok: true }) + expect(store.getAgentCatalogMigrationError()).toBeNull() + expect(store.getSettings().agentCatalogSchemaVersion).toBe(1) + expect(store.getSettings().defaultTuiAgent).toBe('auto') + // The backup captured the untouched pre-v1 bytes before any v1 write. + expect(readFileSync(`${dataFile}${PINNED_BACKUP_SUFFIX}`, 'utf-8')).toBe(original) + store.flushOrThrow() + const persisted = JSON.parse(readFileSync(dataFile, 'utf-8')) + expect(persisted.settings.agentCatalogSchemaVersion).toBe(1) + } + ) + + it('retryAgentCatalogMigration is a no-op when the migration is not blocked', async () => { + writeFileSync(dataFile, JSON.stringify({ settings: { defaultTuiAgent: null } }), { + mode: 0o600 + }) + const store = await createStore(dataFile) + expect(store.getAgentCatalogMigrationError()).toBeNull() + expect(store.retryAgentCatalogMigration()).toEqual({ ok: true }) + expect(store.getSettings().agentCatalogSchemaVersion).toBe(1) + }) }) diff --git a/src/main/persistence/loading-store/profile-preferences.ts b/src/main/persistence/loading-store/profile-preferences.ts index 1bd1f5bcbe3..e2f9796cd66 100644 --- a/src/main/persistence/loading-store/profile-preferences.ts +++ b/src/main/persistence/loading-store/profile-preferences.ts @@ -1,3 +1,4 @@ +import { existsSync, readFileSync } from 'node:fs' import type { GlobalSettings } from '../../../shared/global-settings-types' import type { OnboardingChecklistState } from '../../../shared/onboarding-state-types' import type { PersistedState } from '../../../shared/persisted-state-types' @@ -17,12 +18,16 @@ import { import type { StoreRuntimeState } from './store-runtime-state' import type { WriteSchedulingOperations } from './write-scheduling' import { scheduleSave } from './write-scheduling' -import { bumpLocalWorktreeScanGeneration } from '../../local-worktree-scan-generation' +import { + createPinnedPreV1Backup, + migrateAgentCatalogSchema +} from '../../agent-launch/agent-catalog-schema-migration' type ProfilePreferencesRuntime = Pick< StoreRuntimeState, | 'activeViewPreference' | 'agentCatalogMigrationError' + | 'dataFile' | 'githubCacheDirty' | 'githubCacheGeneration' | 'protectedSecrets' @@ -52,6 +57,33 @@ export class ProfilePreferences { return this[profilePreferencesContext].runtime.agentCatalogMigrationError } + retryAgentCatalogMigration(): { ok: true } | { ok: false; error: string } { + const runtime = this[profilePreferencesContext].runtime + if (runtime.agentCatalogMigrationError === null) { + return { ok: true } + } + let raw: string | null + try { + raw = existsSync(runtime.dataFile) ? readFileSync(runtime.dataFile, 'utf-8') : null + } catch (error) { + const message = `Could not read the data file: ${error instanceof Error ? error.message : String(error)}` + runtime.agentCatalogMigrationError = message + return { ok: false, error: message } + } + const migration = migrateAgentCatalogSchema({ + settings: runtime.state.settings, + preV1RawContents: raw, + createBackup: () => createPinnedPreV1Backup(runtime.dataFile, raw ?? '') + }) + if (migration.backupError) { + runtime.agentCatalogMigrationError = migration.backupError + return { ok: false, error: migration.backupError } + } + runtime.agentCatalogMigrationError = null + this.updateSettings(migration.settingsPatch) + return { ok: true } + } + onSettingsChanged( listener: ( updates: Partial, @@ -161,7 +193,6 @@ export function getSettingsMutationOperations( ): SettingsMutationOperations { return { state: owner[profilePreferencesContext].runtime.state, - bumpLocalWorktreeScanGeneration, removeRetainedBlob: (slot) => owner[profilePreferencesContext].runtime.protectedSecrets.removeRetainedBlob(slot), scheduleSave: () => scheduleSave(owner[profilePreferencesContext].scheduling), diff --git a/src/main/persistence/loading-store/store.ts b/src/main/persistence/loading-store/store.ts index 888c0092ed2..e2d128358b9 100644 --- a/src/main/persistence/loading-store/store.ts +++ b/src/main/persistence/loading-store/store.ts @@ -87,6 +87,10 @@ export class Store { return dirname(this.runtime.dataFile) } + getDataFilePath(): string { + return this.runtime.dataFile + } + freezeWrites(): void { this.runtime.writesFrozen = true if (this.runtime.writeTimer) { @@ -94,6 +98,10 @@ export class Store { this.runtime.writeTimer = null } } + + unfreezeWrites(): void { + this.runtime.writesFrozen = false + } } // oxlint-disable-next-line typescript-eslint/consistent-type-definitions -- declaration merging derives Store's prototype API directly from the exact concrete domain classes installed below diff --git a/src/preload/api-types.ts b/src/preload/api-types.ts index 632e53005ae..5cf28b9c61c 100644 --- a/src/preload/api-types.ts +++ b/src/preload/api-types.ts @@ -6,7 +6,7 @@ import type { GrokAccountsApi, MinimaxCredentialsApi } from './api/agent-account-api' -import type { HooksApi } from './api/agent-hook-api' +import type { AgentHooksApi, HooksApi } from './api/agent-hook-api' import type { SkillsApi } from './api/agent-skill-api' import type { AgentAwakeApi, AgentStatusApi, AgentTrustApi } from './api/agent-status-api' import type { @@ -22,7 +22,13 @@ import type { BrowserApi } from './api/browser-api' import type { CliApi } from './api/cli-install-api' import type { CrashReportsApi, FeedbackApi } from './api/crash-report-api' import type { DashboardApi, TerminalPreviewApi } from './api/dashboard-api' -import type { DocPreviewApi } from './api/doc-preview-api' +import type { + DataRecoveryMigrationStatus, + DataRecoveryOperationResult, + RecoveryPointDto, + RecoveryPointId, + RestoreRecoveryPointMode +} from '../shared/data-recovery' import type { EmulatorApi } from './api/emulator-api' import type { EphemeralVmApi } from './api/ephemeral-vm-api' import type { ExportApi, FilesystemApi } from './api/filesystem-api' @@ -104,6 +110,7 @@ export type PreloadApi = { claudeAccounts: ClaudeAccountsApi cli: CliApi codexConfigSync: CodexConfigSyncApi + agentHooks: AgentHooksApi agentTrust: AgentTrustApi preflight: PreflightApi notifications: NotificationsApi @@ -125,7 +132,6 @@ export type PreloadApi = { remoteWorkspace: WorkspaceSessionApi['remoteWorkspace'] updater: UpdaterApi notebook: FilesystemApi['notebook'] - docPreview: DocPreviewApi['docPreview'] stats: StatsApi memory: MemoryApi claudeUsage: ClaudeUsageApi @@ -150,11 +156,20 @@ export type PreloadApi = { agentStatus: AgentStatusApi mobile: MobileApi speech: SpeechApi + /** Desktop-only data recovery; paired web clients do not expose this surface. */ + dataRecovery?: { + migrationStatus: () => Promise + retryAgentCatalogMigration: () => Promise + listPoints: () => Promise + restore: (args: { + id: RecoveryPointId + mode: RestoreRecoveryPointMode + }) => Promise + } } export type { ClaudeUsageApi, CodexUsageApi, OpenCodeUsageApi } from './api/agent-usage-api' export type { AiVaultApi } from './api/ai-vault-api' -export type { AutomationsApi, ExternalAutomationManagerResult } from './api/automation-api' export type { AppApi } from './api/app-api' export type { BrowserApi, DetectedBrowserInfo, DetectedBrowserProfileInfo } from './api/browser-api' export type { EmulatorApi } from './api/emulator-api' diff --git a/src/preload/index.ts b/src/preload/index.ts index cd96e849f32..4cb12d42488 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -7069,6 +7069,14 @@ const api = { ipcRenderer.invoke('localhostWorktreeLabels:register', args) } satisfies PreloadApi['localhostWorktreeLabels'], + dataRecovery: { + migrationStatus: (): Promise => ipcRenderer.invoke('dataRecovery:migrationStatus'), + retryAgentCatalogMigration: (): Promise => + ipcRenderer.invoke('dataRecovery:retryAgentCatalogMigration'), + listPoints: (): Promise => ipcRenderer.invoke('dataRecovery:listPoints'), + restore: (args: unknown): Promise => ipcRenderer.invoke('dataRecovery:restore', args) + }, + keybindings: { get: (): Promise => ipcRenderer.invoke('keybindings:get'), ensureFile: (): Promise => ipcRenderer.invoke('keybindings:ensureFile'), diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 903c8b81447..cbba05c9abe 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -6,6 +6,7 @@ import { BrowserWebAuthnAccountDialog } from './components/browser-webauthn-acco import { DocPreviewExternalLinkConfirmation } from './components/browser-pane/workspace-doc/doc-preview-external-link-confirmation' import { LinkRoutingPreferenceDialogProvider } from './components/link-routing-preference-dialog' import { SkillFreshnessNudge } from './components/skills/SkillFreshnessNudge' +import { DataRecoveryMigrationNotice } from './components/data-recovery/DataRecoveryMigrationNotice' import PinnedTabCloseDialog from './components/terminal-pane/PinnedTabCloseDialog' import RunningTerminalCloseDialog from './components/terminal-pane/RunningTerminalCloseDialog' import WorktreeBaseFallbackDialog from './components/WorktreeBaseFallbackDialog' @@ -105,6 +106,7 @@ function App(): React.JSX.Element { + diff --git a/src/renderer/src/components/data-recovery/DataRecoveryDialog.tsx b/src/renderer/src/components/data-recovery/DataRecoveryDialog.tsx new file mode 100644 index 00000000000..20b31139e35 --- /dev/null +++ b/src/renderer/src/components/data-recovery/DataRecoveryDialog.tsx @@ -0,0 +1,204 @@ +import { useEffect, useState } from 'react' +import { Button } from '@/components/ui/button' +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle +} from '@/components/ui/dialog' +import { translate } from '@/i18n/i18n' +import type { RecoveryPointDto } from '../../../../shared/data-recovery' + +export type DataRecoveryDialogProps = { + open: boolean + onOpenChange: (open: boolean) => void +} + +function pointTitle(point: RecoveryPointDto): string { + switch (point.id) { + case 'agent-catalog-pre-v1': + return translate( + 'auto.components.dataRecovery.pointAgentCatalogPreV1Title', + 'Before the custom-agents update (agent catalog v1)' + ) + } +} + +function pointLossSummary(point: RecoveryPointDto): string { + switch (point.id) { + case 'agent-catalog-pre-v1': + return translate( + 'auto.components.dataRecovery.pointAgentCatalogPreV1Loss', + 'Restoring discards all settings and workspace metadata saved after this point, including any custom agents.' + ) + } +} + +/** Main-owned restore flow (runbook: General Data recovery UI). Lists recovery + * points by metadata only; the pinned pre-v1 point restores via Prepare + * downgrade — Orca restores atomically and quits without relaunching. */ +export function DataRecoveryDialog({ open, onOpenChange }: DataRecoveryDialogProps) { + const [points, setPoints] = useState(null) + const [confirming, setConfirming] = useState(null) + const [restoring, setRestoring] = useState(false) + const [error, setError] = useState(null) + + useEffect(() => { + if (!open) { + setConfirming(null) + setError(null) + return + } + let cancelled = false + void window.api.dataRecovery + ?.listPoints() + .then((list) => { + if (!cancelled) { + setPoints(list) + } + }) + .catch(() => { + if (!cancelled) { + setPoints([]) + } + }) + return () => { + cancelled = true + } + }, [open]) + + const handleRestore = async (point: RecoveryPointDto): Promise => { + setRestoring(true) + setError(null) + try { + const result = await window.api.dataRecovery?.restore({ + id: point.id, + mode: 'prepare-downgrade' + }) + // On success Orca quits; only failures come back to this dialog. + if (result && !result.ok) { + setError(result.error) + } + } catch (err) { + setError(err instanceof Error ? err.message : String(err)) + } finally { + setRestoring(false) + } + } + + return ( + + + + + {translate('auto.components.dataRecovery.title', 'Data recovery')} + + + {translate( + 'auto.components.dataRecovery.description', + 'Restore this profile from a recovery point created before a data migration. Restoring never deletes the recovery point.' + )} + + + + {points === null ? ( +

+ {translate('auto.components.dataRecovery.loading', 'Looking for recovery points…')} +

+ ) : points.length === 0 ? ( +

+ {translate( + 'auto.components.dataRecovery.empty', + 'No recovery points exist for this profile.' + )} +

+ ) : ( +
    + {points.map((point) => ( +
  • +

    {pointTitle(point)}

    +

    + {point.createdAtMs + ? translate('auto.components.dataRecovery.createdAt', 'Created {{date}}', { + date: new Date(point.createdAtMs).toLocaleString() + }) + : translate( + 'auto.components.dataRecovery.createdUnknown', + 'Creation time unknown' + )} +

    + {point.compatibility === 'previous-binary' ? ( +

    + {translate( + 'auto.components.dataRecovery.previousBinary', + 'After restoring, Orca quits. Install the previous Orca version before opening it again.' + )} +

    + ) : null} +

    {pointLossSummary(point)}

    + {confirming?.id === point.id ? ( +
    + + +
    + ) : ( + + )} +
  • + ))} +
+ )} + + {error ? ( +

+ {translate( + 'auto.components.dataRecovery.restoreFailed', + 'Restore failed and no changes were made: {{error}}', + { error } + )} +

+ ) : null} + + + + +
+
+ ) +} diff --git a/src/renderer/src/components/data-recovery/DataRecoveryMigrationNotice.test.tsx b/src/renderer/src/components/data-recovery/DataRecoveryMigrationNotice.test.tsx new file mode 100644 index 00000000000..6b513718b5e --- /dev/null +++ b/src/renderer/src/components/data-recovery/DataRecoveryMigrationNotice.test.tsx @@ -0,0 +1,100 @@ +// @vitest-environment happy-dom +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { cleanup, fireEvent, render, screen } from '@testing-library/react' +import { DataRecoveryMigrationNotice } from './DataRecoveryMigrationNotice' + +type ApiStub = { + migrationStatus: ReturnType + retryAgentCatalogMigration: ReturnType + listPoints: ReturnType + restore: ReturnType +} + +function installApi(overrides: Partial = {}): ApiStub { + const api: ApiStub = { + migrationStatus: vi.fn().mockResolvedValue({ agentCatalogMigrationError: 'disk full' }), + retryAgentCatalogMigration: vi.fn().mockResolvedValue({ ok: true }), + listPoints: vi.fn().mockResolvedValue([ + { + id: 'agent-catalog-pre-v1', + compatibility: 'previous-binary', + createdAtMs: 1_752_000_000_000, + sizeBytes: 1024 + } + ]), + restore: vi.fn().mockResolvedValue({ ok: true }), + ...overrides + } + ;(window as unknown as { api: unknown }).api = { dataRecovery: api } + return api +} + +describe('DataRecoveryMigrationNotice', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + afterEach(() => { + cleanup() + }) + + it('renders nothing when the migration is healthy', async () => { + installApi({ + migrationStatus: vi.fn().mockResolvedValue({ agentCatalogMigrationError: null }) + }) + render() + await vi.waitFor(() => expect(screen.queryByRole('alert')).toBeNull()) + }) + + it('renders nothing on paired web where the dataRecovery surface is absent', async () => { + ;(window as unknown as { api: unknown }).api = {} + render() + await vi.waitFor(() => expect(screen.queryByRole('alert')).toBeNull()) + }) + + it('shows the blocked state at load with error details and both actions', async () => { + installApi() + render() + expect(await screen.findByRole('alert')).toBeTruthy() + expect(screen.getByText('disk full')).toBeTruthy() + expect(screen.getByText('Retry migration')).toBeTruthy() + expect(screen.getByText('Open Data recovery')).toBeTruthy() + }) + + it('clears the notice when retry succeeds and keeps it when retry fails', async () => { + const api = installApi({ + retryAgentCatalogMigration: vi + .fn() + .mockResolvedValueOnce({ ok: false, error: 'still failing' }) + .mockResolvedValueOnce({ ok: true }) + }) + api.migrationStatus + .mockResolvedValueOnce({ agentCatalogMigrationError: 'disk full' }) + .mockResolvedValue({ agentCatalogMigrationError: 'still failing' }) + render() + const retry = await screen.findByText('Retry migration') + + fireEvent.click(retry) + await vi.waitFor(() => expect(screen.getByText('still failing')).toBeTruthy()) + + fireEvent.click(screen.getByText('Retry migration')) + await vi.waitFor(() => expect(screen.queryByRole('alert')).toBeNull()) + }) + + it('opens Data recovery listing the pinned point with Prepare downgrade', async () => { + const api = installApi() + render() + fireEvent.click(await screen.findByText('Open Data recovery')) + expect( + await screen.findByText('Before the custom-agents update (agent catalog v1)') + ).toBeTruthy() + fireEvent.click(screen.getByText('Prepare downgrade…')) + fireEvent.click(screen.getByText('Restore and quit')) + await vi.waitFor(() => + expect(api.restore).toHaveBeenCalledWith({ + id: 'agent-catalog-pre-v1', + mode: 'prepare-downgrade' + }) + ) + }) +}) diff --git a/src/renderer/src/components/data-recovery/DataRecoveryMigrationNotice.tsx b/src/renderer/src/components/data-recovery/DataRecoveryMigrationNotice.tsx new file mode 100644 index 00000000000..d5abb891490 --- /dev/null +++ b/src/renderer/src/components/data-recovery/DataRecoveryMigrationNotice.tsx @@ -0,0 +1,89 @@ +import { useCallback, useEffect, useState } from 'react' +import { AlertTriangle } from 'lucide-react' +import { Button } from '@/components/ui/button' +import { translate } from '@/i18n/i18n' +import { DataRecoveryDialog } from './DataRecoveryDialog' + +/** App-level persistent notice for a blocked agent-catalog migration (runbook: + * Migration user experience). Shown at profile load — not only in Settings — + * and stays until Retry succeeds or a recovery point is restored. Renders + * nothing on paired web (no dataRecovery preload surface) or when healthy. */ +export function DataRecoveryMigrationNotice() { + const [migrationError, setMigrationError] = useState(null) + const [retrying, setRetrying] = useState(false) + const [recoveryOpen, setRecoveryOpen] = useState(false) + + const refresh = useCallback(async () => { + try { + const status = await window.api.dataRecovery?.migrationStatus() + setMigrationError(status?.agentCatalogMigrationError ?? null) + } catch { + setMigrationError(null) + } + }, []) + + useEffect(() => { + void refresh() + }, [refresh]) + + if (migrationError === null) { + return null + } + + const handleRetry = async (): Promise => { + setRetrying(true) + try { + const result = await window.api.dataRecovery?.retryAgentCatalogMigration() + if (result?.ok) { + setMigrationError(null) + } else { + await refresh() + } + } finally { + setRetrying(false) + } + } + + return ( +
+ +
+

+ {translate( + 'auto.components.dataRecovery.noticeTitle', + 'Orca could not back up your data before a required update' + )} +

+

+ {translate( + 'auto.components.dataRecovery.noticeBody', + 'Your existing data is unchanged and built-in agents keep working, but custom-agent settings are read-only until this is resolved.' + )} +

+

{migrationError}

+
+
+ + + +
+ +
+ ) +} diff --git a/src/renderer/src/components/data-recovery/DataRecoverySettingsRow.tsx b/src/renderer/src/components/data-recovery/DataRecoverySettingsRow.tsx new file mode 100644 index 00000000000..18fed39d9b1 --- /dev/null +++ b/src/renderer/src/components/data-recovery/DataRecoverySettingsRow.tsx @@ -0,0 +1,46 @@ +import { useEffect, useState } from 'react' +import { Button } from '@/components/ui/button' +import { translate } from '@/i18n/i18n' +import { DataRecoveryDialog } from './DataRecoveryDialog' + +/** Settings-side entry into Data recovery (runbook: downgrade step 1 offers + * "the app-level migration notice or Settings"). Renders nothing when no + * recovery point exists or the surface is absent (paired web). */ +export function DataRecoverySettingsRow() { + const [hasPoints, setHasPoints] = useState(false) + const [open, setOpen] = useState(false) + + useEffect(() => { + let cancelled = false + void window.api.dataRecovery + ?.listPoints() + .then((points) => { + if (!cancelled) { + setHasPoints(points.length > 0) + } + }) + .catch(() => {}) + return () => { + cancelled = true + } + }, []) + + if (!hasPoints) { + return null + } + + return ( +
+ + {translate( + 'auto.components.dataRecovery.settingsRowHint', + 'A pre-update recovery point exists for this profile.' + )} + + + +
+ ) +} diff --git a/src/renderer/src/components/settings/AgentCatalogSection.test.tsx b/src/renderer/src/components/settings/AgentCatalogSection.test.tsx index 169f0a2fe97..a31a9fe3f15 100644 --- a/src/renderer/src/components/settings/AgentCatalogSection.test.tsx +++ b/src/renderer/src/components/settings/AgentCatalogSection.test.tsx @@ -105,6 +105,22 @@ describe('AgentCatalogSection (connected)', () => { expect(mutate).toHaveBeenCalledTimes(1) }) + it('surfaces the migration block from the load-time snapshot before any mutation', async () => { + const getLocal = vi + .fn() + .mockResolvedValue(buildLocalCatalogSnapshot({ migrationBlockedError: 'disk full' })) + ;(window as unknown as { api: unknown }).api = { + settings: { + agentCatalog: { getLocal }, + onChanged: () => () => {} + } + } + render() + expect(await screen.findByRole('alert')).toBeTruthy() + expect(screen.getByText('Agent settings are temporarily read-only')).toBeTruthy() + expect(screen.getByText('disk full')).toBeTruthy() + }) + it('keeps catalog search usable in read-only mode while controls stay disabled', async () => { render() const search = (await screen.findByLabelText('Search agents')) as HTMLInputElement diff --git a/src/renderer/src/components/settings/AgentCatalogSection.tsx b/src/renderer/src/components/settings/AgentCatalogSection.tsx index aaf5aae7340..353d675d415 100644 --- a/src/renderer/src/components/settings/AgentCatalogSection.tsx +++ b/src/renderer/src/components/settings/AgentCatalogSection.tsx @@ -1,4 +1,4 @@ -import { useMemo, useState } from 'react' +import { useEffect, useMemo, useState } from 'react' import type { BuiltInTuiAgent, CustomTuiAgentId, @@ -22,6 +22,7 @@ import { AgentCatalogMigrationBlockedNotice, asAgentCatalogMigrationBlocked } from './agent-catalog-migration-blocked' +import { DataRecoverySettingsRow } from '../data-recovery/DataRecoverySettingsRow' import { CustomAgentEditorDialog } from './CustomAgentEditorDialog' import { BuiltInLaunchSettingsDialog } from './BuiltInLaunchSettingsDialog' import { CustomAgentDeleteDialog } from './CustomAgentDeleteDialog' @@ -70,7 +71,7 @@ export function AgentCatalogSection({ detectionTarget, readOnly }: AgentCatalogSectionProps): React.JSX.Element { - const { snapshot, refetch } = useLocalAgentCatalog() + const { snapshot, unavailable, refetch } = useLocalAgentCatalog() const { detectedIds, isRefreshing, refresh } = useDetectedAgents(detectionTarget) const detectedSet = useMemo | null>( () => (detectedIds ? new Set(detectedIds) : null), @@ -81,6 +82,18 @@ export function AgentCatalogSection({ // write; without surfacing that, the toggles/default picker look like silent // no-ops. A later success clears the notice (the block lifted). const [migrationBlockedError, setMigrationBlockedError] = useState(null) + // Why: the block is a load-time state (failed pre-v1 backup), not just a + // mutation outcome — a user who never edits must still learn writes are + // fail-closed. Each real snapshot is authoritative both ways: it arms the + // notice and clears one left stale by an app-banner Retry. + useEffect(() => { + if (!snapshot) { + return + } + setMigrationBlockedError( + typeof snapshot.migrationBlockedError === 'string' ? snapshot.migrationBlockedError : null + ) + }, [snapshot]) const surfaceMigrationBlock = (result: { ok: boolean }): void => { const blocked = asAgentCatalogMigrationBlocked(result) if (blocked) { @@ -257,6 +270,19 @@ export function AgentCatalogSection({ void refresh() } + if (unavailable) { + // Paired web: the local catalog surface doesn't exist here; the pane-level + // read-only notice explains where to manage agents. + return ( +

+ {translate( + 'auto.components.settings.AgentCatalogSection.unavailable', + 'Custom agents are managed on the desktop host.' + )} +

+ ) + } + if (!snapshot) { return (

@@ -289,6 +315,7 @@ export function AgentCatalogSection({ onRepairReplace={handleRepairReplace} onRepairDuplicate={handleRepairDuplicate} /> + {!readOnly ? : null} {editorMode.kind === 'built-in-launch' ? ( void /** Adopt the authoritative snapshot a local mutation just returned, without a @@ -24,16 +27,27 @@ export type UseLocalAgentCatalog = { */ export function useLocalAgentCatalog(): UseLocalAgentCatalog { const [snapshot, setSnapshot] = useState(null) + const [unavailable, setUnavailable] = useState(false) const requestTokenRef = useRef(0) const mountedRef = useRef(true) const load = useCallback(() => { const token = (requestTokenRef.current += 1) - void window.api.settings.agentCatalog.getLocal().then((next) => { - if (mountedRef.current && token === requestTokenRef.current) { - setSnapshot(next) - } - }) + void window.api.settings.agentCatalog + .getLocal() + .then((next) => { + if (mountedRef.current && token === requestTokenRef.current) { + setSnapshot(next) + setUnavailable(false) + } + }) + .catch(() => { + // Paired web rejects (not_available_on_paired_web): surface an honest + // read-only empty state rather than a perpetual loading spinner. + if (mountedRef.current && token === requestTokenRef.current) { + setUnavailable(true) + } + }) }, []) const applySnapshot = useCallback((next: LocalAgentCatalogSnapshot) => { @@ -65,5 +79,11 @@ export function useLocalAgentCatalog(): UseLocalAgentCatalog { } }, [load]) - return { snapshot, loading: snapshot === null, refetch: load, applySnapshot } + return { + snapshot, + loading: snapshot === null && !unavailable, + unavailable, + refetch: load, + applySnapshot + } } diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index 76a0b3a42fa..b34ecc8cbc8 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -27,83 +27,11 @@ "certificateChallengeExpired": "This certificate approval expired. Retry the page to request a new one.", "certificateChallengeChanged": "The certificate request changed. Retry the page and review the new warning.", "certificateChallengeUnavailable": "This certificate request is no longer available. Retry the page to request a new one.", - "certificateProceedFailed": "Orca could not approve this certificate request. Retry the page and try again.", - "sshRoutedHint": "This page browses through the workspace's SSH host — the host may be disconnected or unable to reach this site.", - "recheckSshRoute": "Run connection check" + "certificateProceedFailed": "Orca could not approve this certificate request. Retry the page and try again." }, "guestRecovery": { "title": "Browser page stopped", "failed": "The browser page stopped unexpectedly. Retry to restore it." - }, - "clientHosted": { - "preparationFailed": "Couldn't start the remote browser on this desktop. Check the paired connection and try again.", - "unavailableTitle": "Client-hosted browser unavailable", - "unavailableDescription": "This page is attached to a different desktop or is no longer available.", - "download": { - "started": "Downloading {{filename}}…", - "completed": "Downloaded {{filename}}", - "canceled": "Download canceled.", - "failed": "Couldn't finish the download." - }, - "popupBlocked": "Popup blocked: {{origin}}", - "hostPaneOfflineTitle": "That device is no longer connected", - "hostPaneNamedTitle": "Showing on {{device}}", - "hostPaneTitle": "Showing on another device", - "hostPaneOfflineDescription": "This page stays listed so you can close it from here. It will come back when the device reconnects.", - "hostPaneDescription": "This page renders on the paired desktop that opened it.", - "hostRowClose": "Close hosted page", - "hostRowCloseFailed": "Couldn't close this page. The device hosting it may be busy — try again.", - "hostRowOfflineNamed": "Offline — was hosted on {{device}}", - "hostRowOffline": "Offline — the device hosting it quit", - "hostRowNamed": "Hosted on {{device}}", - "hostRow": "Hosted on another device" - }, - "navigation": { - "back": "Back", - "forward": "Forward", - "reload": "Reload" - }, - "reopenOnServer": { - "action": "Reopen on server", - "pending": "Reopening…", - "caveat": "This opens a new page on the remote host at this page’s last address. Signed-in and other transient page state may differ, and a page that came from a form submission opens blank.", - "failed": "Couldn't open this page on the remote host. Check the connection and try again." - }, - "sshRoute": { - "preparingTitle": "Connecting through the SSH host", - "errorTitle": "SSH browser routing unavailable", - "errorDescription": "Pages in this workspace browse through its SSH host, and that connection is not available right now.", - "retry": "Retry", - "tryAnyway": "Try anyway", - "browseLocally": "Browse from this device instead", - "forwardingBlockedTitle": "The SSH server blocks browser traffic", - "sshUnavailableTitle": "SSH connection unavailable", - "forwardingBlockedDescription": "This server refuses TCP forwarding (often “AllowTcpForwarding no” in its sshd config), which browsing through it requires. Ask its administrator to allow forwarding, or browse from this device instead.", - "sshUnavailableDescription": "Pages in this workspace browse through its SSH host, and that connection isn't available right now. Reconnect the host, then retry.", - "showDetails": "Show details" - }, - "sshEgress": { - "routedTooltip": "Browsing through {{value0}}", - "localTooltip": "Browsing from this device, not {{value0}}", - "localChip": "This device", - "routedDetail": "Network traffic and DNS go through the workspace's SSH host.", - "localDetail": "Pages load from this machine and its network.", - "settingsLink": "Routing settings" - }, - "remoteEgress": { - "clientHostedTooltip": "Browsing through {{value0}}", - "streamedTooltip": "Browsing on {{value0}}", - "clientHostedDetail": "The page renders on this device. Network traffic and DNS go through the remote host.", - "streamedDetail": "The page runs on the remote host and streams to this device." - }, - "remote": { - "findUnavailable": "Find in page is not available while this page streams from the remote host." - } - }, - "editor": { - "richMarkdown": { - "tooLarge": "File is larger than the {{limit}} rich editing limit. Showing source mode instead.", - "openAnyway": "Open anyway" } }, "githubChecks": { @@ -138,37 +66,6 @@ "title": "Show Menu Bar Icon", "description": "Keep an Orca shortcut and activity indicator in the macOS menu bar." } - }, - "browser": { - "clientHostedRemote": { - "title": "Host remote browser pages on this device", - "description": "Render remote workspace pages on this desktop; network traffic still goes through the remote host. Applies to new pages only.", - "optionDevice": "This device", - "optionDeviceTooltip": "Pages render on this desktop, so input and popups behave natively.", - "optionServer": "Server (streamed)", - "optionServerTooltip": "Pages render on the remote server and stream to this device.", - "rowTitle": "Remote server workspaces", - "rowDescription": "Where pages render. Traffic always goes through the remote server. Applies to new pages only." - }, - "sshWorkspaceRouting": { - "title": "Browse through SSH workspace hosts", - "description": "Browser pages in SSH workspaces send their traffic through the workspace's SSH host, with DNS resolved there. Off means pages browse from this machine.", - "disabledHosts": "Hosts browsing from this device instead:", - "enableHost": "Route again", - "optionHost": "SSH host", - "optionHostTooltip": "Traffic and DNS go through the workspace's SSH host.", - "optionDevice": "This device", - "optionDeviceTooltip": "Pages browse from this machine's network.", - "rowTitle": "SSH workspaces", - "rowDescription": "Where browser traffic leaves from. Pages always render on this device.", - "useHost": "Use SSH host", - "probeSkippedHosts": "Hosts routed without the connection check (Try anyway):", - "probeAgain": "Check again" - }, - "remoteBrowsing": { - "heading": "Remote browsing", - "headingDescription": "Where remote workspace pages render, and where their network traffic leaves from." - } } }, "menu": { @@ -227,8 +124,7 @@ "createHint": "Create worktree from Linear issue", "loadingHint": "Loading Linear issue…" }, - "renderCapOverflow": "{{value0}} more", - "seeMore": "See more", + "renderCapOverflow": "{{value0}} more — scroll or keep typing to narrow", "filter": { "emptyTitle": "No results match the active filter", "emptySubtitle": "Clear the filter above, or widen it to more hosts and projects.", @@ -357,6 +253,10 @@ "07f788de83": "WebSocket" } } + }, + "worktreeCreate": { + "hostPredatesDefaultLaunch": "This Orca host predates default-agent launch. Pick a specific built-in agent or update Orca on the host.", + "hostPredatesCustomAgents": "This Orca host predates custom agents. Pick a built-in agent or update Orca on the host." } }, "store": { @@ -450,19 +350,11 @@ "meta": { "persist": { "877e3638d8": "Update the remote runtime to change this workspace’s linked issue", - "4367540861": "Update the remote runtime to link Linear issues", - "github": { - "pr": { - "suppression": "Update the remote runtime to unlink GitHub pull requests" - } - } + "4367540861": "Update the remote runtime to link Linear issues" } } } - }, - "createdWithoutParentNesting": "Created without nesting under \"{{value0}}\"", - "createdWithoutParentNestingUnnamed": "Created without nesting under the selected parent", - "createdWithoutParentNestingDetail": "The parent workspace was no longer available. You can set it from the workspace menu." + } }, "repos": { "2975400634": "Update Orca server to open non-Git folders on this runtime.", @@ -477,9 +369,7 @@ "15cf5319ec": "{{path}} was checked on {{hostName}}, but that host did not report a usable folder.", "2dcd706774": "Project also exists in another profile", "presenceProfileOverflow": "{{names}} +{{count}} more", - "removeProjectFailed": "Failed to remove project", - "wslFilesystemBoundaryTitle": "This project is stored on a Windows drive", - "wslFilesystemBoundaryDescription": "Git for this project runs in {{distro}}, which reaches Windows drives over the WSL filesystem bridge. Expect git to be roughly 20x slower than it would be on a copy stored inside {{distro}}." + "removeProjectFailed": "Failed to remove project" }, "settings": { "e12dab333b": "Failed to switch servers", @@ -912,12 +802,7 @@ "undecryptableUnknown": "{{value0}} cookies could not be decrypted and were skipped. Close the source browser completely and try the import again.", "unrecognizedWarning": "The cookie import finished with a warning this version of Orca does not recognize. Update Orca to see the details, then check this profile before relying on its cookies.", "undecryptableUnrecognizedReason": "{{value0}} cookies could not be decrypted and were skipped for a reason this version of Orca does not recognize. Update Orca to see the details, then try the import again.", - "partitionSkipped": "{{value0}} cookies were not imported because their site-partition could not be read. Sign in to those sites again in Orca.", - "locationClientHosted": "Read from this device and stored here for the {{value0}} workspace.", - "locationRemoteHost": "Read from browsers on {{value0}} and stored there.", - "googleCookiesSkippedLocal": "Google cookies were not imported. Open a browser in Orca with this profile, then sign into Google.", - "googleCookiesSkippedClientHosted": "Google cookies were not imported. Open a browser tab in the {{value0}} workspace with this profile — it opens on this device — then sign into Google.", - "googleCookiesSkippedRemoteWorkspace": "Google cookies were not imported. Open a browser tab in the {{value0}} workspace with this profile, then sign into Google." + "partitionSkipped": "{{value0}} cookies were not imported because their site-partition could not be read. Sign in to those sites again in Orca." } } } @@ -939,16 +824,8 @@ } } }, - "activateAiVaultStructuredSession": { - "unavailable": "The structured agent session is not available yet. Retry in a moment." - }, "ephemeralVmWorktreeCreation": { "sparseCheckoutUnsupported": "Provisioned-root recipes do not support sparse checkout." - }, - "file": { - "preview": { - "pairedOutsideWorktree": "Files outside the workspace can't be previewed on a paired server yet." - } } }, "hooks": { @@ -956,8 +833,7 @@ "59718b120b": "The target workspace is no longer available.", "16a21d6413": "SSH reconnect requires interactive credentials.", "386db94f3e": "The target project is no longer available.", - "3ad7d77f57": "The target workspace is on a different host than this automation run target.", - "workspaceHostUnresolved": "The target workspace spans more than one host, so this run has no single host to use." + "3ad7d77f57": "The target workspace is on a different host than this automation run target." }, "useComposerState": { "7eb3f44ff7": "Selected agent is disabled. Choose an enabled agent before creating.", @@ -1107,13 +983,6 @@ "description": "Running Orca terminals are hosted by a daemon started by a previous Orca installation. macOS may not apply Orca’s Accessibility, Automation, or protected-file permissions to them. Restart the daemon from Manage Sessions to restore access. This will close all running Orca terminals.", "openManageSessions": "Open Manage Sessions", "dismiss": "Dismiss" - }, - "ipc": { - "events": { - "browserStateIpcBridge": { - "docPreviewLinkFailed": "Could not open this link in Orca Browser." - } - } } }, "components": { @@ -1409,8 +1278,7 @@ "3a051b8ade": "Work item", "32f8bef818": "No log output.", "4168eb2c51": "Resolve", - "commentTooLarge": "Comment is too large to submit safely.", - "gitlabActionFailed": "GitLab action failed." + "commentTooLarge": "Comment is too large to submit safely." }, "JiraIssueWorkspace": { "b0b92666c9": "Comment", @@ -2235,8 +2103,7 @@ "61ed600d29": "\"{{value0}}\" has unsaved changes. Do you want to save before closing?", "cdc9ac4b2d": "editor", "e57db40c11": "Could not build launch command for {{value0}}.", - "5b2c1a9e44": "No agent CLI detected — install one or pick a default agent in Settings.", - "quitSnapshotSaveFailed": "Quit canceled: the session snapshot could not be saved ({{value0}})." + "5b2c1a9e44": "No agent CLI detected — install one or pick a default agent in Settings." }, "TerminalSearch": { "db234b7519": "Close", @@ -3095,9 +2962,7 @@ "copyLink": "Copy link", "copied": "Copied", "copiedLink": "Copied link", - "copyLinkFailed": "Failed to copy link", - "downloadOpenWithDefaultApp": "Download & open with default app", - "downloadOpenFailed": "Failed to download '{{value0}}'." + "copyLinkFailed": "Failed to copy link" }, "TerminalSessionStateSaveFailureDialog": { "6bee0c8f17": "Open Disk Space Analyzer", @@ -3359,15 +3224,14 @@ "25dc1cd653": "Open file", "7cdf8ee0c8": "Open URL", "b27864279e": "Launch agent", + "0e5b7a3f16": "Search open tabs, files, URLs, agents…", "8f0a1c4d92": "Switch to tab", "2c38630a01": "Workspace no longer exists", "4f0d9a71c2": "Tab no longer exists", "d7d496a451": "Browser page no longer exists", "7726ce9970": "Mobile emulator tab no longer exists", "chooseAction": "Choose an action.", - "searchProvider": "Search {{value0}}", - "openPage": "Open page", - "omniboxPlaceholderWithHistory": "Search open tabs, history, files, URLs, agents…" + "searchProvider": "Search {{value0}}" }, "TabBarQuickCommandsButton": { "a2c7a33831": "Command", @@ -3406,6 +3270,7 @@ "classifier": { "42e6262ae9": "No action available.", "097a982ee0": "Loading files...", + "c41f8d20b7": "Search open tabs, files, URLs, agents…", "90eb94dc48": "Enter an http:// or https:// URL.", "5553b283ce": "Enter a URL or file path.", "queryTooLarge": "Search text is too large.", @@ -3549,8 +3414,8 @@ "81cd37af99": "Main", "fa6d36758d": "Kill session {{value0}}", "b8f4a2c1d0e3": "{{value0}} orphans", - "c7e3b1a0d9f2": "End {{value0}} orphan terminal", - "d8f4c2b1e0a3": "End {{value0}} orphan terminals", + "c7e3b1a0d9f2": "Kill {{value0}} orphan terminal", + "d8f4c2b1e0a3": "Kill {{value0}} orphan terminals", "e9a5d3c2b1f0": "Kill {{value0}}?" }, "SshStatusSegment": { @@ -3571,7 +3436,6 @@ "remote_server": "Remote Server", "runtime_checking": "Checking", "runtime_online": "Connected", - "runtime_unavailable_transport_up": "Orca unavailable", "runtime_unavailable": "Disconnected", "runtime_connect_unavailable": "Remote host is not reachable", "runtime_disconnect_failed": "Disconnect failed", @@ -3869,8 +3733,7 @@ "resource": { "memory": { "metric": { - "workingSetDescription": "Summed working set (WS): pages resident in RAM right now. Shared pages can appear in more than one process, and memory Windows has paged out is not counted here.", - "privateBytesDescription": "Summed private bytes: memory these processes have committed, counted whether it is resident or paged out. This is what the host charges against its commit limit, so it keeps rising while the working set above shrinks under paging.", + "workingSetDescription": "Summed working set (WS). Shared pages can appear in more than one process.", "rssDescription": "Summed resident set size (RSS). Shared or aliased pages can appear in more than one process." } }, @@ -3897,18 +3760,6 @@ "onDescription": "Keep this computer awake continuously", "autoDescription": "Stay awake while an agent is working", "offDescription": "Allow normal system sleep behavior" - }, - "RuntimeHostStatusRow": { - "previous_connection_closed": "The previous connection closed", - "workspace_window_closed": "The workspace window is closed", - "checking_host": "Orca is checking whether this host is reachable", - "restoring_connection": "Orca is trying to restore the connection", - "host_unreachable": "Orca isn’t reachable on this host", - "runtime_unavailable": "SSH transport is connected, but the Orca runtime is unavailable", - "runtime_unavailable_explanation": "The remote host may still be running; only the Orca runtime connection is unavailable.", - "contact_note": "The host may still be running; only the Orca connection is unavailable.", - "last_connected": "Last connected {{value0}}", - "reconnect_attempt": "Attempt {{value0}}" } } }, @@ -4293,8 +4144,7 @@ "retry": "Retry", "remoteShareNotice": "These skills live on {{host}}. Open Skills on that machine to share them.", "viewSwitch": "Show", - "searchLinks": "Search links", - "deleteSkills": "Delete skills…" + "searchLinks": "Search links" }, "SkillFreshnessNudge": { "titleOne": "An installed Orca skill is out of date", @@ -4417,8 +4267,7 @@ "3af85f6add": "Done", "readyDescriptionV2": "Anyone with this unlisted link can inspect and install the skills.", "descriptionV2": "Review the exact files, then publish an immutable version behind an unlisted link.", - "publishBundle": "Publish bundle", - "cancelRequestFailed": "Orca could not send the cancellation request. The upload may still finish." + "publishBundle": "Publish bundle" }, "SkillCard": { "d25a1b8ae6": "Share skill", @@ -4628,9 +4477,7 @@ "detailSource": "Source", "detailContents": "Contents", "skillActions": "Actions for {{value0}}", - "viewDetails": "View details", - "deleteSkill": "Delete…", - "notDeletable": "Not deletable" + "viewDetails": "View details" }, "SkillSelectionBar": { "selectAll": "Select all {{count}} eligible", @@ -4670,15 +4517,7 @@ "retryOne": "Retry {{count}} skill", "retryOther": "Retry {{count}} skills", "linkOne": "{{count}} link", - "linkOther": "{{count}} links", - "deleteOne": "Delete {{count}} skill", - "deleteOther": "Delete {{count}} skills", - "deletedOne": "Deleted {{count}} skill", - "deletedOther": "Deleted {{count}} skills", - "deleteFolderOne": "{{count}} folder", - "deleteFolderOther": "{{count}} folders", - "deleteLinkOne": "{{count}} link", - "deleteLinkOther": "{{count}} links" + "linkOther": "{{count}} links" }, "host": { "local": "This machine", @@ -4711,23 +4550,6 @@ "showMore": "Show more" }, "install": { - "authorizing": "Authorizing package access…", - "installing": "Downloading, verifying, and installing…", - "selectSkill": "Select at least one skill.", - "chooseWorkspace": "Choose a workspace.", - "reconnectBeforeInstalling": "Reconnect your Orca account before installing.", - "bundleVerificationFailed": "Installation failed before Orca could verify the requested bundle.", - "destinationAlreadyFinished": "The destination had already finished this installation.", - "enterShareLink": "Enter an Orca skill share link.", - "shareUnavailable": "This share is unavailable. The link may be invalid, expired, or revoked.", - "requestedVersionVerificationFailed": "Installation failed before Orca could verify the requested version.", - "versionVerificationFailed": "Orca could not verify the requested version.", - "inspectManagedFailed": "Orca could not inspect managed installs on this machine.", - "reconnectForVersionHistory": "Reconnect your Orca account to load version history.", - "versionHistoryUnavailable": "Version history is unavailable for this skill.", - "bundleSkillsMissing": "This version does not contain any of the installed bundle skills.", - "reconnectBeforeVersionChange": "Reconnect your Orca account before changing versions.", - "removeFailed": "Orca could not safely remove this skill.", "agentsLabel": "Agents", "agentsCanonical": "Always installed for {{value0}}, which read {{value1}}.", "agentsNoneChosen": "No extra agents", @@ -4772,8 +4594,7 @@ "exitTooltip": "Exit selection · Esc", "title": "Select skills to share", "selectAll": "Select all {{count}} eligible", - "clear": "Clear", - "deleteTitle": "Select skills to delete" + "clear": "Clear" }, "SkillDetailDialog": { "agents": "Agents", @@ -4827,32 +4648,6 @@ "supportingFilesDescription": "Includes readable reference files beyond the main instructions.", "runnableFilesDescription": "Includes scripts that an agent may run with your access.", "binaryFilesDescription": "Includes opaque assets and an executable binary." - }, - "SkillDelete": { - "dismissResults": "Dismiss", - "reasonBundled": "Bundled with Orca — it would be restored", - "reasonPlugin": "Installed by a plugin — remove the plugin instead", - "reasonUnowned": "This skill lives outside Orca’s skill folders — delete it where it is stored", - "reasonMissing": "This skill is no longer on disk", - "reasonStale": "This skill changed since the list was loaded — refresh and try again", - "blockedLine": "{{count}} × {{reason}}", - "resultLine": "{{count}} × {{label}}", - "statusSkipped": "Skipped", - "statusBusy": "Busy — another skill operation is in progress", - "statusPartial": "Partly removed — some files are still on disk under a hidden name", - "statusFailed": "Failed — nothing changed", - "statusDeleted": "Deleted", - "confirmHost": "On {{host}}.", - "confirmPermanent": "This cannot be undone.", - "failed": "Could not delete skills", - "hostUnavailable": "Could not reach the selected machine. Refresh and try again.", - "hostUnresolved": "Still finding the machine that owns these skills.", - "hostUpdateRequired": "Update Orca on the selected machine to delete skills.", - "nothingOne": "That skill cannot be deleted from here.", - "nothingOther": "None of the {{count}} selected skills can be deleted from here.", - "placementSummaryParts": "Removes {{parts}} across {{roots}}.", - "placementJoin": " and ", - "retainedSource": "The skill itself stays at {{path}}." } }, "sidebar": { @@ -5428,7 +5223,6 @@ "eace1d2cf6": "neutral", "dbe2d18972": "More {{value0}} actions", "ae76907ca6": "Unlink {{value0}}", - "openInOrcaBrowser": "Open in Orca browser", "ad25c3ff05": "View on {{value0}}", "2c67730e07": "Open in Orca", "e42941631a": "View on Linear", @@ -5582,11 +5376,7 @@ "382fd11a3e": "Edit Worktree Details", "2174f17011": "Save", "61d6f612cf": "Saving...", - "a0d191b7a7": "Edit issue links, pull request links, and notes for this workspace.", - "gitlabDescription": "Edit issue links, merge request links, and notes for this workspace.", - "gitlabMR": "GitLab MR", - "gitlabPlaceholder": "MR ! or GitLab URL", - "gitlabHelp": "Paste a merge request URL, or enter a number. Leave blank to remove the link." + "a0d191b7a7": "Edit issue links, pull request links, and notes for this workspace." }, "WorktreeOpenInMenu": { "localOnly": "Local only", @@ -6905,17 +6695,13 @@ "nativeChat": { "title": "Chat UI", "description": "Preview the desktop chat surface for supported agent terminal sessions.", - "copy": "Enables the experimental Chat UI for newly created supported local sessions. Existing terminal sessions keep the terminal chat path while we tune transcript fidelity, streaming, and parity.", + "copy": "Adds a Chat UI view you can switch to from supported agent terminal panes. Experimental while we tune transcript fidelity, streaming, and terminal parity.", "toggleLabel": "Toggle Chat UI", "defaultTitle": "Default view", "defaultCopy": "Choose how new supported agent terminal tabs open.", "defaultViewLabel": "Default Chat UI view", "defaultViewTerminal": "Terminal chat", - "defaultViewNative": "Chat UI", - "structuredTitle": "Use updated structured native chat", - "structuredCopy": "Opt in to the host-owned structured Codex runtime. Off keeps the existing terminal-backed chat path.", - "structuredScope": "Local macOS and Linux sessions only for now. Windows, WSL, and remote execution hosts (including SSH) continue to use terminal chat.", - "structuredToggleLabel": "Toggle updated structured native chat" + "defaultViewNative": "Chat UI" }, "agentDashboard": { "title": "Agent Dashboard", @@ -7172,9 +6958,10 @@ "compareBaseBranchUpstream": "Branch upstream" }, "HiddenExperimentalGroup": { - "7c4e18d2f6": "Arms the bold-glitch capture gestures on this machine: {{samplingShortcut}} starts a sampling burst; {{captureShortcut}} captures pane evidence to disk.", - "b09f24a51d": "Terminal render diagnostics", - "f2c81d904a": "Hidden experimental settings" + "d0f914a528": "Placeholder toggle", + "1014ddbfaf": "Does nothing today. Reserved as the first slot for hidden experimental options.", + "232cf83de8": "Unlisted toggles for internal testing. Nothing here is supported.", + "3e9e827ca5": "Hidden experimental" }, "InputPane": { "db15068196": "Enabled by default on Linux and macOS. Linux uses the system selection clipboard; other platforms use a private buffer.", @@ -7506,9 +7293,7 @@ "9bedd2a6e5": "Enables agents to hand off context and coordinate work through Orca.", "07641b9768": "Orchestration skill", "2aacdb0517": "Coordinate coding agents across handoffs, worktree handovers, and child-agent work.", - "191ac34567": "Agent Orchestration", - "nestedWorkerDepthTitle": "Nested worker depth", - "nestedWorkerDepthDescription": "How many generations of dispatched workers may spawn their own workers. 1 keeps the agent tree flat: a coordinator dispatches workers, and those workers do not dispatch." + "191ac34567": "Agent Orchestration" }, "OrchestrationSetupCard": { "e7d2a5146c": "Enables agents to hand off context and coordinate work through Orca.", @@ -7922,11 +7707,7 @@ "3f67e8078a": "Use this computer by default. Choose a saved server only when you want supported projects, files, terminals, provider checks, and browser/mobile handoff to run through that server.", "2c85efb3e8": "Selecting a saved server makes this browser use that paired Orca runtime as its default Host.", "serverConnected": "Connected", - "serverWorkspaceWindowClosed": "Workspace window closed", - "serverRuntimeUnavailable": "Orca unavailable", - "serverRuntimeUnavailableDescription": "SSH transport is connected, but the Orca runtime did not answer. The host may still be running.", "serverChecking": "Checking…", - "serverReconnecting": "Reconnecting", "serverDisconnected": "Disconnected", "disconnectedServer": "Disconnected from {{value0}}.", "connectToRemoteServers": "Connect to remote servers", @@ -8258,11 +8039,7 @@ "8a349e3fac": "Passphrase for {{value0}}", "cab3d5f5a5": "Password for {{value0}}", "1f3dde805d": "SSH Key Passphrase", - "106bd57f4a": "SSH Password", - "a21f9e74c0": "SSH Verification", - "981352fb42": "Complete the verification challenge for", - "456516603b": "Enter response", - "c624f64b86": "Continue" + "106bd57f4a": "SSH Password" }, "SshTargetCard": { "a883f5a00f": "terminal timeout: {{value0}}", @@ -10910,7 +10687,8 @@ "defaultTitle": "Default agent", "nullDefaultAttention": "Choose a default agent. Until you do, attended launches can't start and will ask you to pick one.", "migrationBlockedTitle": "Agent settings are temporarily read-only", - "migrationBlockedDescription": "Orca could not back up your existing agent settings before updating them, so changes are blocked to protect your data. Resolve the problem below, then try again." + "migrationBlockedDescription": "Orca could not back up your existing agent settings before updating them, so changes are blocked to protect your data. Resolve the problem below, then try again.", + "unavailable": "Custom agents are managed on the desktop host." }, "AgentCatalogStatusBadge": { "enabled": "Enabled", @@ -11754,9 +11532,6 @@ }, "shortcutDefinitionCatalog": { "missionControlConflict": "Blocked by Mission Control. Remap here or change it in System Settings." - }, - "NativeChatSupportedAgents": { - "label": "Supported agents:" } }, "right": { @@ -11816,10 +11591,7 @@ }, "7e4b2a19c0": "Could not resolve the GitHub PR to reply on.", "430f1a62d4": "Could not resolve the selected thread on the host.", - "updateReactionFailed": "Failed to update reaction.", - "gitlabMoreActions": "More MR actions", - "gitlabUnlink": "Unlink MR", - "gitlabLinkAnother": "Link another MR" + "updateReactionFailed": "Failed to update reaction." }, "CreatePullRequestDialog": { "2bc1b4345e": "Cancel", @@ -11947,12 +11719,7 @@ "3de88351c5": "GitHub will add this pull request and every pull request below it to the merge queue.", "a32fe6dba6": "GitHub will merge this pull request and every pull request below it in the stack.", "73e0e1819d": "Queueing stack...", - "e555a41d32": "Merging stack...", - "markingReady": "Marking ready...", - "markReady": "Mark ready for review", - "draftMoreActions": "More {{value0}} actions", - "closeDraft": "Close", - "readyToast": "{{value0}} marked ready for review" + "e555a41d32": "Merging stack..." }, "PortsPanel": { "3ea4a02a8f": "Cancel", @@ -12179,8 +11946,6 @@ "7a09d7f9d2": "base", "383cf92c73": "tree", "d7ae61269b": "Committed on Branch", - "branchFilesChangedVsBaseOne": "1 file changed vs {{ref}}", - "branchFilesChangedVsBaseOther": "{{count}} files changed vs {{ref}}", "48a003c1b1": "Staged Changes", "d4ef4bafc5": "Changes", "522f44dce5": "Untracked Files", @@ -12255,10 +12020,10 @@ "b3c8f1a902": "Filter files by name", "d4f8c2a901": "Clear and close filter", "e8a1c4b203": "vs", - "compareBaseCommitsAheadOne": "1 commit ahead of {{ref}}", - "compareBaseCommitsAheadOther": "{{count}} commits ahead of {{ref}}", - "compareBaseCommitsBehindOne": "1 commit behind {{ref}}", - "compareBaseCommitsBehindOther": "{{count}} commits behind {{ref}}", + "f9b2441bb6": "1 commit ahead of {{value0}}", + "b715ef615b": "{{value0}} commits ahead of {{value1}}", + "c1a8f3e204": "1 commit behind {{value0}}", + "d2b9g4f315": "{{value0}} commits behind {{value1}}", "4b4a7de138": "Open review page in browser", "createPrIntentCommitBlockedSummary": "Commit blocked: {{value0}} Fix the issue, then retry Create PR.", "pushRecovery": { @@ -12281,6 +12046,7 @@ "a4e93c21d7": "Current branch: {{value0}}", "c7d4e2f801": "Change base ref: {{value0}}", "f3a1b8c204": "upstream", + "createPrIntentEmptyGeneratedBody": "Generated review details did not include a description. Retry Create PR.", "createPrIntentGenerateDetailsFailed": "Could not generate review details. Retry Create PR." }, "SourceControlAgentActionDialog": { @@ -14100,30 +13866,6 @@ }, "feature": { "wall": { - "setup": { - "checklist": { - "localized": { - "copy": { - "ec0a363633": "Multi-task", - "62bac8f43c": "Work in 2 different worktrees at once. Each one is isolated (even in the same project). Perfect for working on 2 features at once.", - "908898c3ee": "Use Orca's browser", - "43781563c3": "Browse your web app without leaving Orca. Grab any element and send its exact source and styles to an agent with one click.", - "29aa2c2077": "Turn on notifications", - "71bd9a8c95": "Know the moment an agent finishes, needs attention, or gets blocked.", - "46db810da8": "Choose your default agent", - "b8e5bae17f": "Start new work faster with your preferred agent already selected.", - "fee5557b02": "Enable Orca CLI", - "7bcb4097fa": "Register the Orca shell command and install agent skills for browser, computer, and orchestration workflows.", - "ad342dd4c6": "Connect integrations", - "06fe30fdb0": "Start an agent from a task in one click and keep PR status in view.", - "eddc532e58": "Automate workspace setup", - "56049b74c2": "Run install and setup commands automatically so every new worktree is ready for agents.", - "2cf795433b": "Start work in multiple repos", - "42525ba8a4": "Bring your key repos into Orca so you can start agent work without hunting for folders." - } - } - } - }, "usage": { "tracking": { "b94ec70eda": "Tracking not set up", @@ -14488,9 +14230,8 @@ "7e7ca60816": "changed files", "b6c3b84476": "Show file tree", "39f8007549": "Review conflicts", - "skippedConflictsExcluded": "{{count}} unresolved conflicts were excluded from this diff view.", - "skippedConflictsExcluded_one": "{{count}} unresolved conflict was excluded from this diff view.", - "skippedConflictsExcluded_other": "{{count}} unresolved conflicts were excluded from this diff view.", + "39e73e7181": "were excluded from this diff view.", + "689b99f8ad": "unresolved conflict", "820ec01f24": "Conflicted files are reviewed separately", "fd8892b120": "No changes to display", "eb5f40e49c": "This diff view excludes unresolved conflicts because the normal two-way diff pipeline is not conflict-safe.", @@ -14999,16 +14740,14 @@ "d5a8c2f1b9": "Start the default AI agent to fix this check", "e2b4d7c8a1": "Choose an agent for this check", "f1c9e3a6d4": "Choose agent to fix check", - "actionRequired": "Action required", - "copyOutput": "Copy output" + "actionRequired": "Action required" }, "CheckRunJobs": { "1c0a4d7e02": "succeeded", "2d3b8f1a55": "skipped", "3e6c9a2b71": "pending", "4f7d0c3e88": "steps failed", - "5a8e1d4f23": " · ", - "copy": "Copy jobs" + "5a8e1d4f23": " · " }, "check": { "run": { @@ -15088,47 +14827,6 @@ "insertColumnRight": "Insert column right", "deleteRow": "Delete row", "deleteColumn": "Delete column" - }, - "CheckRunAnnotations": { - "copy": "Copy annotations" - }, - "CheckRunCopyButton": { - "copied": "Copied", - "failed": "Couldn't copy", - "empty": "Nothing to copy" - }, - "HtmlDocPreview": { - "documentTooLargePanel": "This document is too large to preview. Open it in the editor instead.", - "documentUnreadablePanel": "Orca could not read this file from the workspace.", - "multipleAssetsFailedNotice": "{{count}} files in this document could not be loaded.", - "assetTooLargeNotice": "{{path}} is too large to load in this preview.", - "assetUnsupportedNotice": "This workspace cannot send {{path}} to a preview.", - "assetUnreadableNotice": "Orca could not read {{path}} from the workspace.", - "previewAriaLabel": "HTML preview", - "reloadPreviewControl": "Reload preview", - "previewUnavailableTitle": "Preview unavailable", - "copyDocumentPathControl": "Copy file path", - "documentPathCopied": "Copied", - "workspaceFileChipLabel": "Workspace file", - "previewMenuControl": "Preview options", - "openSourceControl": "Open source file", - "openExternallyControl": "Open with default app", - "copyDocumentRelativePathControl": "Copy relative path", - "openExternallyUnknownHostError": "Can't open '{{value0}}': the host that owns it is no longer known.", - "downloadBlockedNotice": "Downloads are disabled in document previews.", - "directoryAuthorizationFailed": "Could not allow access to this directory.", - "directoryAccessRequest": "This preview wants to read files in {{path}}.", - "dismissAccessRequest": "Dismiss", - "allowDirectory": "Allow folder", - "directoryAccessRequestOverflow": "{{folders}}, and {{count}} more", - "directoryAccessRequestMultiple": "This preview wants to read files in {{folders}}.", - "allowDirectories": "Allow {{count}} folders", - "editAddressControl": "Edit address" - }, - "LargeDiffLoadPrompt": { - "a0af0198aa": "Large diffs are not rendered by default.", - "c3d9f4a712": "This diff's size isn't known yet, so it loads on request.", - "f7fa7a40d0": "Load diff" } }, "diff": { @@ -15259,16 +14957,6 @@ "title": "Find the results", "body": "Runs show when automations ran, what happened, and where to inspect their output." } - }, - "client": { - "hosted": { - "browser": { - "intro": { - "title": "This page renders on your desktop", - "body": "Remote browser tabs now render on this device. Network traffic still goes through the remote host." - } - } - } } } } @@ -15448,8 +15136,7 @@ "6e776f9ef9": "Download failed", "756bfc25c9": "Open", "09a9489aa5": "Show", - "2a4c4b8e1f": "Copy", - "fileUrlUnsupported": "This browser tab cannot open local files. Use \"Open Preview to the Side\" on the file instead." + "2a4c4b8e1f": "Copy" }, "BrowserToolbarMenu": { "429ef481f9": "Cancel", @@ -15501,9 +15188,6 @@ } } }, - "download": { - "savedToRemote": "Saved to {{value1}} on {{value2}}" - }, "annotate": { "use": { "browser": { @@ -15517,18 +15201,6 @@ } } } - }, - "browser": { - "page": { - "annotation": { - "tray": { - "09a7c1df70": "Edit annotation {{value0}}", - "c16f932cb3": "Save", - "024467ceac": "Cancel", - "449d2c2629": "Annotation intent" - } - } - } } }, "navigate": { @@ -15584,7 +15256,6 @@ "showMore": "Show more" }, "AutomationDetail": { - "host": "Host", "007c8ad874": "Prompt", "a1d52c2189": "Usage coverage", "449fc83bf7": "Tokens", @@ -15678,8 +15349,7 @@ "8faaa00726": "Run", "53fc5f07ab": "Run history", "a00e38d1a3": "n/a", - "fdb3caa8fb": "known", - "historyUnavailable": "Run history is unavailable from this host. This does not mean the automation failed or has no runs." + "fdb3caa8fb": "known" }, "AutomationRunPageFrame": { "40a511bed4": "Run context", @@ -15753,9 +15423,6 @@ "b11170a008": "Failed to save automation.", "2a20596d6b": "Automation saved.", "244727e655": "Automation updated.", - "moved": "Automation moved to {host}.", - "moveOriginalKept": "Created on {host}, but the original could not be deleted. Remove it on the old host.", - "moveOriginalUnverified": "Created on {host}, but the original deletion could not be verified. Check the old host before retrying.", "77b81bc4ac": "Hermes automation created.", "08efc3ae12": "Hermes automation updated.", "e431bb85d4": "Choose a workspace on the same host as this Hermes automation.", @@ -15799,15 +15466,12 @@ "backToList": "All automations", "tableName": "Name", "tableProject": "Project", - "tableHost": "Host", "tableStatus": "Status", "tableActions": "Actions", "newAutomation": "New Automation", "rowActions": "Automation actions", "tableLastRun": "Last run", - "noListMatches": "No automations match.", - "destinationProjectUnavailable": "Choose a project owned by the selected automation destination.", - "ownerChanged": "This automation changed hosts. Refresh and try again." + "noListMatches": "No automations match." }, "AutomationsPageSkeleton": { "55527b7bcf": "Loading automations" @@ -15968,111 +15632,6 @@ "AutomationListSortHeader": { "sortedAscending": "{{value0}}, sorted ascending", "sortedDescending": "{{value0}}, sorted descending" - }, - "hostSummary": { - "partialFailure": "{{failed}} of {{total}} hosts could not be loaded" - }, - "emptyState": { - "unknownHost": "this host", - "hostLoading": "Loading host…", - "hostLoadingDetail": "Waiting for {{hostLabel}} to report its automations.", - "hostError": "Automations could not be loaded from {{hostLabel}}", - "hostUnavailableDetail": "The host could not be reached, so its automations are unknown.", - "hostIncompatibleDetail": "This server is too old to scope automations by host.", - "hostStaleDetail": "The most recent refresh did not complete.", - "hostNotConnected": "{{hostLabel}} is not connected", - "hostNotConnectedDetail": "Connect to this host to see the automations stored for it.", - "hostEmpty": "No automations on {{hostLabel}}", - "searchNoMatch": "No automations match your search", - "filtersNoMatch": "No automations match your filters", - "allHostsEmpty": "No automations across loaded hosts" - }, - "hostNotice": { - "loading": "Loading host…", - "unavailable": "{{hostLabel}} could not be reached. Showing the automations last loaded from it.", - "ghost": "{{hostLabel}} was removed. Automations still assigned to it remain listed.", - "removed": "The selected host is no longer available. Showing all hosts." - }, - "hostPicker": { - "allHosts": "All hosts", - "loadingHost": "Loading host…" - }, - "ownerConflict": { - "dismiss": "Dismiss" - }, - "capturedOwner": { - "orphan": "This automation has no host to run on. Delete it, or re-add the host it belongs to.", - "unfenced": "Orca on this host needs an update before you can view run history or manage this automation. Scheduled runs may still continue on the host." - }, - "hostStatus": { - "authority": { - "loading": "Loading", - "loadingDescription": "Loading automations from this host.", - "fresh": "Up to date", - "freshDescription": "Automations loaded from this host.", - "refreshing": "Refreshing", - "refreshingDescription": "Checking this host for changes.", - "staleError": "Not refreshed", - "staleErrorDescription": "Showing the automations last loaded from this host. The most recent refresh did not complete.", - "unavailable": "Unreachable", - "unavailableDescription": "This host could not be reached, so its automations could not be loaded.", - "incompatible": "Update server", - "incompatibleDescription": "This server does not support host-scoped automation queries. Update it to load automations for this host." - }, - "execution": { - "connected": "Connected", - "connectedDescription": "This host is connected and can run automations.", - "connecting": "Connecting", - "connectingDescription": "Connecting to this host.", - "disconnected": "Not connected", - "disconnectedDescription": "Automations on this host cannot run until the connection is restored.", - "unavailable": "Unavailable", - "unavailableDescription": "This execution target is no longer available.", - "unknown": "Connection unknown", - "unknownDescription": "The connection state for this host has not loaded yet." - }, - "query": { - "legacyUnscopedDescription": "This server lists automations without host scoping, so they cannot be edited or run from here.", - "incompatible": "Update server", - "incompatibleDescription": "This server is too old to scope automations by host. Update it to manage automations here.", - "viewOnly": "View only", - "targetRemovedDescription": "This host was removed, so its automations can no longer be edited or run from here.", - "targetUnverifiedDescription": "This host has not been verified since its connection dropped, so its automations cannot be edited or run from here.", - "targetUnregisteredDescription": "This host has no registration record, so its automations cannot be edited or run from here." - }, - "action": { - "retry": "Retry", - "reconnect": "Reconnect", - "updateServer": "Update server" - } - }, - "ownerAction": { - "failed": "That automation action did not complete." - }, - "externalScope": { - "unknownManager": "This external automation is no longer listed for any host in view.", - "uncheckedHost": "External automation managers on {{hostLabel}} could not be checked.", - "uncheckedHosts": "External automation managers on {{count}} hosts could not be checked." - }, - "enablement": { - "paused": "Paused" - }, - "runHistory": { - "occurrencesWithLatest": "{{times}} times, most recently {{date}}", - "occurrences": "{{times}} times" - }, - "createDestination": { - "label": "Host", - "placeholder": "Select a host", - "storedOn": "Stored and scheduled by {authority}.", - "unselected": "Choose the host that stores and schedules this automation.", - "orphan": "Automations with no host cannot hold new ones. Choose a host to create this automation on.", - "unavailable": "That host cannot hold a new automation yet. Choose another host to create this one on.", - "stale": "{host} changed while this form was open. Choose it again before saving.", - "projectMismatch": "That project is not on {host}. Choose a project on that host, or another host.", - "noProjects": "No projects are set up on {host}. Add one there, or choose another host.", - "updateRequired": "Update the Orca server on {hosts} to store automations there.", - "move": "Saving creates this automation on {host} and deletes the original and its run history." } }, "agent": { @@ -16292,114 +15851,6 @@ "chooseSource": "Choose task source" } } - }, - "page": { - "chrome": { - "task": { - "page": { - "gitlab": { - "filters": { - "e157d7ce4d": "MRs", - "2328f6a40c": "My Todos" - } - } - } - } - }, - "dialogs": { - "new": { - "github": { - "issue": { - "dialog": { - "e02508846c": "this repository" - } - } - }, - "linear": { - "issue": { - "dialog": { - "dialogTitle": "New Linear issue", - "dialogDescription": "Create a Linear issue for the selected team." - } - } - } - }, - "task": { - "page": { - "connect": { - "dialogs": { - "353c7dc71d": "Update access" - } - } - } - } - }, - "github": { - "github": { - "detail": { - "host": { - "8d5cde4770": "Pull requests", - "312ca15778": "GitHub list" - } - }, - "issue": { - "label": { - "selector": { - "2a1862c470": "Loading labels…" - } - } - }, - "work": { - "item": { - "row": { - "draftPullRequest": "Draft pull request", - "pullRequest": "Pull request", - "issue": "Issue" - } - } - } - } - }, - "hooks": { - "use": { - "task": { - "page": { - "jira": { - "create": { - "dialog": { - "jiraRequiredFieldsLoadFailed": "Failed to load required Jira fields." - } - } - }, - "linear": { - "active": { - "collection": { - "d8b3cd9488": "Project: {{value0}}", - "68462f8b29": "View: {{value0}}" - } - } - }, - "session": { - "resume": { - "savedLinearProjectMissing": "Saved Linear project was not found.", - "savedLinearProjectRestoreFailed": "Failed to restore saved Linear project.", - "savedLinearViewMissing": "Saved Linear view was not found.", - "savedLinearViewRestoreFailed": "Failed to restore saved Linear view." - } - } - } - } - } - }, - "linear": { - "linear": { - "connect": { - "empty": { - "3e00e8ebd4": "Loading Linear" - } - } - } - } } }, "taskPageEmptyState": { @@ -16462,15 +15913,7 @@ "allWorkspacesTitle": "Select one workspace", "allWorkspacesBody": "Status, assignee, and label filters use ids from a single Linear workspace. Choose one workspace to filter by those attributes.", "optionsFromTeam": "Options from {{team}}", - "clearAll": "Clear all filters", - "partialCoverage": "partial", - "partialCoverageTitle": "Some teams may be left out of this filter. Open Filters for details." - }, - "linear-issue-attribute-filter-coverage-notice": { - "statusPartialTeamCoverage": "Filtering {{value0}} of {{value1}} team statuses — issues from the remaining teams are not included.", - "labelsPartialTeamCoverage": "Filtering {{value0}} of {{value1}} team labels — issues from the remaining teams are not included.", - "statusAtIdLimit": "Filtering the most this can carry: {{value0}} team statuses. Rows picked past that are left out.", - "labelsAtIdLimit": "Filtering the most this can carry: {{value0}} team labels. Rows picked past that are left out." + "clearAll": "Clear all filters" }, "linear-issue-attribute-filter-sections": { "status": "Status", @@ -16479,7 +15922,6 @@ "unassigned": "Unassigned", "labels": "Labels", "countSelected": "{{count}} selected", - "partialCoverageMarker": "· partial", "selected": "selected", "searchPriority": "Filter priority…", "searchStatus": "Filter status…", @@ -16637,7 +16079,7 @@ "publishingOffDescription": "Learn about public links and enable sharing in Settings.", "openSettings": "Open Artifacts settings", "sharing": "Sharing…", - "sharePublicLink": "Generate link", + "sharePublicLink": "Share public link", "publishedDescription": "Anyone with this link can view the shared file.", "checkingLink": "Checking for an existing link…", "checkFailed": "Could not check for an existing link.", @@ -16651,7 +16093,7 @@ "2fc727c831": "Artifact updated", "5cb4f5ec36": "Copy link", "fbb5018602": "This file is empty.", - "6112db5a1c": "This artifact is too large to share.", + "6112db5a1c": "Artifacts shared from Orca must be smaller than 800 KB.", "e2ed5acd8c": "Orca couldn't read this file. Open it from a workspace and try again.", "6d475e9b25": "Only local HTML and Markdown files can be shared as artifacts.", "29a406be09": "Artifacts must contain text." @@ -16685,41 +16127,6 @@ "typeMarkdown": "Markdown", "typeHtml": "HTML" }, -<<<<<<< HEAD - "BrowserCookieImportMachineNotice": { - "clientLabel": "Browsers on this device", - "remoteLabel": "Browsers on {{value0}}", - "clientNoteLocalStorage": "Imports read this device’s browsers. Cookies are stored locally.", - "remoteNoteRemoteStorage": "Imports read browsers on {{value0}}. Cookies are stored on that machine, and a permission prompt may appear on its screen." - }, - "native": { - "chat": { - "NativeChatStructuredSession": { - "1f772bb5d0": "Message delivery is unconfirmed.", - "93ef441197": "Message was not sent.", - "a5e7f14068": "Retry" - } - } - }, - "ComposerParentWorktreePicker": { - "label": "Parent worktree", - "noParent": "No parent", - "description": "Nests this workspace under another in the sidebar. Does not change the base branch.", - "searchPlaceholder": "Search workspaces...", - "noMatches": "No matches." - }, - "browserPane": { - "workspaceDoc": { - "externalLinkTitle": "Open link to {{host}}?", - "externalLinkConfirm": "Open link", - "externalLinkCancel": "Cancel" - } - }, - "HostedReviewUnlinkMenuItem": { - "label": "Unlink {{value0}} from workspace", - "description": "Orca will hide {{value0}} {{value1}} details for this workspace. The {{value0}} and branch on {{value2}} won’t be changed." -||||||| parent of ebaa81ab2f (Rebase custom-agents onto main (2/4): renderer) -======= "AgentLaunchNotices": { "disabledFallback": "{{value0}} is disabled. Started stock {{value1}} with no custom executable, custom arguments, or custom agent environment.", "missingFallback": "{{value0}} was deleted. Started stock {{value1}} with no custom executable, custom arguments, or custom agent environment.", @@ -16742,7 +16149,31 @@ "openTerminal": "Open terminal", "forgetLaunch": "Forget launch…", "manageAgents": "Manage agents" ->>>>>>> ebaa81ab2f (Rebase custom-agents onto main (2/4): renderer) + }, + "dataRecovery": { + "pointAgentCatalogPreV1Title": "Before the custom-agents update (agent catalog v1)", + "pointAgentCatalogPreV1Loss": "Restoring discards all settings and workspace metadata saved after this point, including any custom agents.", + "title": "Data recovery", + "description": "Restore this profile from a recovery point created before a data migration. Restoring never deletes the recovery point.", + "loading": "Looking for recovery points…", + "empty": "No recovery points exist for this profile.", + "createdAt": "Created {{date}}", + "createdUnknown": "Creation time unknown", + "previousBinary": "After restoring, Orca quits. Install the previous Orca version before opening it again.", + "restoring": "Restoring…", + "confirmRestore": "Restore and quit", + "cancel": "Cancel", + "prepareDowngrade": "Prepare downgrade…", + "restoreFailed": "Restore failed and no changes were made: {{error}}", + "close": "Close", + "noticeTitle": "Orca could not back up your data before a required update", + "noticeBody": "Your existing data is unchanged and built-in agents keep working, but custom-agent settings are read-only until this is resolved.", + "retrying": "Retrying…", + "retryMigration": "Retry migration", + "openDataRecovery": "Open Data recovery", + "copyDetails": "Copy details", + "settingsRowHint": "A pre-update recovery point exists for this profile.", + "settingsRowOpen": "Data recovery…" } }, "i18n": { @@ -16781,9 +16212,6 @@ }, "githubCheckDetailsTimeout": { "timedOut": "Timed out loading check details." - }, - "gitlabIpcTimeout": { - "timedOut": "Timed out talking to GitLab." } }, "ssh": { @@ -16810,11 +16238,6 @@ } }, "components": { - "jiraUserPicker": { - "select": "Select {{value0}}", - "search": "Search users", - "empty": "No users found" - }, "native-chat": { "composer": { "imageUnsupported": "Image paste is not supported for this agent.", @@ -16873,38 +16296,16 @@ "ultra": "Ultra", "on": "On", "off": "Off" - }, - "pendingAttachmentLimit": "Too many attachments are waiting. Finish composing before attaching more.", - "viewAttachment": "View image", - "imagePreview": "Full-size image preview", - "imagePreviewUnavailable": "Preview unavailable" + } }, "tool": { "running": "Running…", "result": "Result", "countOne": "1 tool call", - "countN": "{{value0}} tool calls", - "runningPreview": "Running {{preview}}", - "runningCommand": "Running command", - "runningNamedPreview": "Running {{toolName}} {{preview}}", - "runningNamed": "Running {{toolName}}", - "ranCommandOneToolSummary": "Ran {{commandCount}} command and used {{toolCount}} tool", - "ranCommandManyToolsSummary": "Ran {{commandCount}} command and used {{toolCount}} tools", - "ranCommandsOneToolSummary": "Ran {{commandCount}} commands and used {{toolCount}} tool", - "ranCommandsManyToolsSummary": "Ran {{commandCount}} commands and used {{toolCount}} tools", - "usedOneSummary": "Used 1 tool", - "usedManySummary": "Used {{toolCount}} tools" - }, - "providerFrame": { - "byteLength": "{{value0}} bytes" + "countN": "{{value0}} tool calls" }, "status": { - "responding": "Agent is responding", - "working": "Working…", - "thinking": "Thinking", - "workingFor": "Working for {{value0}} seconds", - "workedFor": "Worked for {{value0}} seconds", - "toggleDetails": "Toggle turn details" + "responding": "Agent is responding" }, "jumpToLatest": "Jump to latest", "toggle": { @@ -16953,15 +16354,7 @@ "allow": "Allow", "deny": "Deny" }, - "launchPromptNotDelivered": "Not delivered — check the terminal", - "orchestrationPaused": { - "label": "Orchestration paused", - "message": "Structured Chat blocks terminal prompts and sends. Orchestration messages remain queued; switch to Terminal, then check the Orca inbox with", - "command": "orca orchestration check" - }, - "structuredSessionCloseFailed": "Could not close this Codex chat", - "structuredSessionLaunchFailed": "Could not open Codex chat", - "structuredSessionCloseFailedDescription": "The terminal stayed open so the provider remains recoverable." + "launchPromptNotDelivered": "Not delivered — check the terminal" }, "tab": { "bar": { @@ -17448,19 +16841,6 @@ "quickOpen": { "moreMatchesAvailable": "More matches may be available. Refine your search to narrow the results." }, -<<<<<<< HEAD - "checksPanel": { - "unlinked": { - "title": "Pull request unlinked", - "description": "PR #{{number}} is hidden for this workspace. Link it again to restore checks and review details.", - "relink": "Link PR #{{number}}" - } - }, - "sourceControl": { - "unlinkedPr": { - "status": "PR #{{number}} unlinked" -||||||| parent of ebaa81ab2f (Rebase custom-agents onto main (2/4): renderer) -======= "agentLaunch": { "failure": { "unknownAgent": "That agent no longer exists. Pick another agent and try again.", @@ -17534,7 +16914,6 @@ "absent": "Not running", "unknown": "Unreachable" } ->>>>>>> ebaa81ab2f (Rebase custom-agents onto main (2/4): renderer) } } } diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json index 26dfe71176e..bc97738f929 100644 --- a/src/renderer/src/i18n/locales/es.json +++ b/src/renderer/src/i18n/locales/es.json @@ -185,6 +185,10 @@ "07f788de83": "WebSocket" } } + }, + "worktreeCreate": { + "hostPredatesDefaultLaunch": "This Orca host predates default-agent launch. Pick a specific built-in agent or update Orca on the host.", + "hostPredatesCustomAgents": "This Orca host predates custom agents. Pick a built-in agent or update Orca on the host." } }, "store": { @@ -263,10 +267,7 @@ "3b57982bf6": "Git no pudo eliminar de forma segura la rama \"{{value0}}\" después de eliminar el espacio de trabajo \"{{value1}}\", así que Orca la conservó para evitar perder commits locales.", "81f13f48d2": "Git no pudo eliminar de forma segura la rama \"{{value0}}\" después de eliminar el worktree \"{{value1}}\", así que Orca la conservó para evitar perder commits locales.", "runtimeScopeForbiddenTitle": "Esta conexión tiene acceso limitado (móvil)", - "runtimeScopeForbiddenDescription": "Los espacios de trabajo no están disponibles con un emparejamiento de acceso móvil. Vuelve a conectarte con el enlace del navegador desde Ajustes → Servidores remotos de Orca → Comparte este servidor Orca.", - "createdWithoutParentNesting": "Creado sin anidarlo bajo \"{{value0}}\"", - "createdWithoutParentNestingUnnamed": "Creado sin anidarlo bajo el worktree padre seleccionado", - "createdWithoutParentNestingDetail": "El espacio de trabajo padre ya no estaba disponible. Puedes establecerlo desde el menú del espacio de trabajo." + "runtimeScopeForbiddenDescription": "Los espacios de trabajo no están disponibles con un emparejamiento de acceso móvil. Vuelve a conectarte con el enlace del navegador desde Ajustes → Servidores remotos de Orca → Comparte este servidor Orca." }, "repos": { "2975400634": "Actualiza el servidor Orca para abrir carpetas que no sean Git en este host.", @@ -3013,8 +3014,7 @@ "25dc1cd653": "Abrir archivo", "7cdf8ee0c8": "Abrir URL", "b27864279e": "Iniciar agente", - "omniboxPlaceholderWithHistory": "Buscar pestañas abiertas, historial, archivos, URL y agentes…", - "openPage": "Abrir página", + "0e5b7a3f16": "Buscar pestañas abiertas, archivos, URL y agentes…", "8f0a1c4d92": "Cambiar a la pestaña", "2c38630a01": "El espacio de trabajo ya no existe", "4f0d9a71c2": "La pestaña ya no existe", @@ -3060,6 +3060,7 @@ "classifier": { "42e6262ae9": "No hay acción disponible.", "097a982ee0": "Cargando archivos...", + "c41f8d20b7": "Buscar pestañas abiertas, archivos, URL y agentes…", "90eb94dc48": "Introduce una URL http:// o https://.", "5553b283ce": "Introduce una URL o ruta de archivo.", "queryTooLarge": "El texto de búsqueda es demasiado grande.", @@ -3505,8 +3506,7 @@ "resource": { "memory": { "metric": { - "workingSetDescription": "Suma del conjunto de trabajo (WS): las páginas residentes en RAM en este momento. Las páginas compartidas pueden aparecer en más de un proceso, y la memoria que Windows ha paginado a disco no se cuenta aquí.", - "privateBytesDescription": "Suma de bytes privados: la memoria que estos procesos han confirmado, contada tanto si está residente como si está paginada a disco. Es lo que el host imputa a su límite de confirmación, así que sigue subiendo mientras el conjunto de trabajo de arriba se reduce por la paginación.", + "workingSetDescription": "Suma del conjunto de trabajo (WS). Las páginas compartidas pueden aparecer en más de un proceso.", "rssDescription": "Suma del tamaño del conjunto residente (RSS). Las páginas compartidas o con alias pueden aparecer en más de un proceso." } }, @@ -3877,97 +3877,6 @@ } }, "skills": { - "SkillsPage": { - "cb142070b4": "Actualizar", - "a68dee6a32": "Buscar skills", - "f43ad6edf3": "Skills", - "ea72d6185b": "No se pudieron analizar las skills", - "dc4c3328ee": "Mostrar archivo", - "9963dff6d3": "No se encontró ninguna descripción.", - "995fde8337": "No se pudo mostrar el archivo de la skill", - "4acd6d68ec": "No se encontraron skills", - "6a62a0168c": "Sin coincidencias", - "08a321a984": "Ninguna skill coincide con la búsqueda y los filtros actuales.", - "cd7893fbc1": "Analizando skills", - "35b9a724a0": "Disponible", - "c13b82793c": "Gestionar instalaciones", - "aee7b99cc6": "Instalar desde un enlace", - "filterProvider": "Filtrar por agente", - "filterSource": "Filtrar por origen", - "allSources": "Todo", - "clearFilters": "Borrar filtros", - "closeSkills": "Cerrar skills", - "closeTooltip": "Cerrar · Esc", - "moreActions": "Más acciones", - "sharedLinks": "Enlaces compartidos", - "emptyCopy": "Las carpetas de skills analizadas están vacías. Instala un paquete compartido o actualiza después de añadir una skill.", - "retry": "Reintentar", - "remoteShareNotice": "Estas skills están en {{host}}. Abre Skills en esa máquina para compartirlas.", - "viewSwitch": "Mostrar", - "searchLinks": "Buscar enlaces", - "deleteSkills": "Eliminar skills…" - }, - "SkillShareSelectionControls": { - "01c5a15e02": "Compartir skills" - }, - "SkillRow": { - "updatedUnknown": "Sin fecha", - "pathCopied": "Ruta copiada", - "copyPath": "Copiar ruta", - "detailPath": "Ruta", - "skillActions": "Acciones para {{value0}}", - "viewDetails": "Ver detalles", - "deleteSkill": "Eliminar…" - }, - "SkillsList": { - "listLabel": "Skills" - }, - "sourceStatus": { - "missing": "Carpeta no encontrada", - "remoteRepo": "Repositorio remoto — sin analizar", - "unavailable": "Sin analizar" - }, - "sources": { - "heading": "Carpetas de skills" - }, - "sourceKind": { - "home": "Inicio", - "workspace": "Espacio de trabajo", - "bundled": "Incluida", - "plugin": "Plugin" - }, - "count": { - "skillOne": "{{count}} skill", - "skillOther": "{{count}} skills", - "sourceOne": "{{count}} origen", - "sourceOther": "{{count}} orígenes", - "fileOne": "{{count}} archivo", - "fileOther": "{{count}} archivos", - "resultOne": "{{count}} resultado", - "resultOther": "{{count}} resultados", - "selected": "{{count}} seleccionadas", - "shareOne": "Compartir {{count}} skill", - "shareOther": "Compartir {{count}} skills", - "linkOne": "{{count}} enlace", - "linkOther": "{{count}} enlaces" - }, - "filter": { - "allAgents": "Todos los agentes", - "sharedAgent": "Compartido (.agents)" - }, - "SkillsSelectionHeader": { - "exit": "Salir de la selección", - "exitTooltip": "Salir de la selección · Esc", - "title": "Seleccionar skills para compartir", - "selectAll": "Seleccionar las {{count}} aptas", - "clear": "Borrar", - "deleteTitle": "Seleccionar skills para eliminar" - }, - "SkillDetailDialog": { - "agents": "Agentes", - "updated": "Actualizado", - "copy": "Copiar" - }, "SkillFreshnessNudge": { "titleOne": "Una skill de Orca instalada está desactualizada", "titleMany": "{{value0}} skills de Orca instaladas están desactualizadas", @@ -4064,10 +3973,6 @@ "SkillUpdateRow": { "oneLocation": "1 location", "manyLocations": "{{value0}} locations" - }, - "host": { - "local": "Esta máquina", - "remote": "Entorno conectado" } }, "sidebar": { @@ -6177,9 +6082,10 @@ "compareBaseBranchUpstream": "Upstream de la rama" }, "HiddenExperimentalGroup": { - "7c4e18d2f6": "Activa los gestos de captura del fallo de negrita en esta máquina: {{samplingShortcut}} inicia una ráfaga de muestreo; {{captureShortcut}} captura evidencia del panel en disco.", - "b09f24a51d": "Diagnósticos de renderizado del terminal", - "f2c81d904a": "Configuración experimental oculta" + "d0f914a528": "Alternar marcador de posición", + "1014ddbfaf": "No hace nada hoy. Reservado como primer espacio para opciones experimentales ocultas.", + "232cf83de8": "Alternadores no listados para pruebas internas. Aquí no se admite nada.", + "3e9e827ca5": "experimental oculto" }, "InputPane": { "db15068196": "Habilitado de forma predeterminada en Linux y macOS. Linux usa el portapapeles de selección del sistema; otras plataformas utilizan un búfer privado.", @@ -9882,7 +9788,8 @@ "defaultTitle": "Agente predeterminado", "nullDefaultAttention": "Elige un agente predeterminado. Hasta que lo hagas, los inicios atendidos no podrán comenzar y te pedirán que elijas uno.", "migrationBlockedTitle": "Los ajustes de agentes son temporalmente de solo lectura", - "migrationBlockedDescription": "Orca no pudo hacer una copia de seguridad de tus ajustes de agentes existentes antes de actualizarlos, por lo que los cambios están bloqueados para proteger tus datos. Resuelve el problema que aparece abajo y vuelve a intentarlo." + "migrationBlockedDescription": "Orca no pudo hacer una copia de seguridad de tus ajustes de agentes existentes antes de actualizarlos, por lo que los cambios están bloqueados para proteger tus datos. Resuelve el problema que aparece abajo y vuelve a intentarlo.", + "unavailable": "Custom agents are managed on the desktop host." }, "AgentCatalogStatusBadge": { "enabled": "Activado", @@ -10915,8 +10822,6 @@ "7a09d7f9d2": "base", "383cf92c73": "árbol", "d7ae61269b": "Commits en la rama", - "branchFilesChangedVsBaseOne": "1 archivo cambiado respecto a {{ref}}", - "branchFilesChangedVsBaseOther": "{{count}} archivos cambiados respecto a {{ref}}", "48a003c1b1": "Cambios preparados", "d4ef4bafc5": "Cambios", "522f44dce5": "Archivos sin seguimiento", @@ -10991,10 +10896,10 @@ "b3c8f1a902": "Filtrar archivos por nombre", "d4f8c2a901": "Limpiar y cerrar filtro", "e8a1c4b203": "vs", - "compareBaseCommitsAheadOne": "1 commit por delante de {{ref}}", - "compareBaseCommitsAheadOther": "{{count}} commits por delante de {{ref}}", - "compareBaseCommitsBehindOne": "1 commit por detrás de {{ref}}", - "compareBaseCommitsBehindOther": "{{count}} commits por detrás de {{ref}}", + "f9b2441bb6": "1 commit por delante de {{value0}}", + "b715ef615b": "{{value0}} commits por delante de {{value1}}", + "c1a8f3e204": "1 commit por detrás de {{value0}}", + "d2b9g4f315": "{{value0}} commits por detrás de {{value1}}", "4b4a7de138": "Abrir página de revisión en el navegador", "createPrIntentCommitBlockedSummary": "Commit bloqueado: {{value0}} Corrige el issue y reintenta crear PR.", "pushRecovery": { @@ -11017,6 +10922,7 @@ "a4e93c21d7": "Rama actual: {{value0}}", "c7d4e2f801": "Cambiar ref base: {{value0}}", "f3a1b8c204": "upstream", + "createPrIntentEmptyGeneratedBody": "Los detalles de revisión generados no incluyeron una descripción. Reintentar Crear PR.", "createPrIntentGenerateDetailsFailed": "No se pudieron generar los detalles de revisión. Reintentar Crear PR." }, "SourceControlAgentActionDialog": { @@ -13003,9 +12909,8 @@ "7e7ca60816": "archivos cambiados", "b6c3b84476": "Mostrar árbol de archivos", "39f8007549": "Revisar conflictos", - "skippedConflictsExcluded": "Se excluyeron {{count}} conflictos no resueltos de esta vista de diff.", - "skippedConflictsExcluded_one": "Se excluyó {{count}} conflicto no resuelto de esta vista de diff.", - "skippedConflictsExcluded_other": "Se excluyeron {{count}} conflictos no resueltos de esta vista de diff.", + "39e73e7181": "se excluyeron de esta vista de diff.", + "689b99f8ad": "conflicto no resuelto", "820ec01f24": "Los archivos en conflicto se revisan por separado", "fd8892b120": "No hay cambios para mostrar", "eb5f40e49c": "Esta vista de diff excluye los conflictos sin resolver porque el pipeline normal de diff de dos vías no es seguro para conflictos.", @@ -13967,7 +13872,6 @@ "showMore": "Mostrar más" }, "AutomationDetail": { - "host": "Host", "007c8ad874": "Inmediato", "a1d52c2189": "Cobertura de uso", "449fc83bf7": "Fichas", @@ -14131,9 +14035,6 @@ "b11170a008": "No se pudo guardar la automatización.", "2a20596d6b": "Automatización guardada.", "244727e655": "Automatización actualizada.", - "moved": "Automatización movida a {host}.", - "moveOriginalKept": "Se creó en {host}, pero no se pudo eliminar la original. Elimínela en el host anterior.", - "moveOriginalUnverified": "Se creó en {host}, pero no se pudo verificar la eliminación de la original. Compruebe el host anterior antes de volver a intentarlo.", "77b81bc4ac": "Se crea la automatización de Hermes.", "08efc3ae12": "Automatización Hermes actualizada.", "e431bb85d4": "Elija un espacio de trabajo en el mismo host que esta automatización de Hermes.", @@ -14177,7 +14078,6 @@ "backToList": "Todas las automatizaciones", "tableName": "Nombre", "tableProject": "Proyecto", - "tableHost": "Host", "tableStatus": "Estado", "tableActions": "Acciones", "newAutomation": "Nueva automatización", @@ -14330,19 +14230,6 @@ "filters": "Filtros", "clear": "Limpiar filtros", "removeFilter": "Eliminar filtro de {{value0}}" - }, - "createDestination": { - "label": "Host", - "placeholder": "Seleccionar un host", - "storedOn": "Almacenada y programada por {authority}.", - "unselected": "Elija el host que almacena y programa esta automatización.", - "orphan": "Las automatizaciones sin host no pueden alojar otras nuevas. Elija un host en el que crear esta automatización.", - "unavailable": "Ese host todavía no puede alojar una nueva automatización. Elija otro host para crear esta.", - "stale": "{host} cambió mientras este formulario estaba abierto. Elíjalo de nuevo antes de guardar.", - "projectMismatch": "Ese proyecto no está en {host}. Elija un proyecto en ese host, u otro host.", - "noProjects": "No hay proyectos configurados en {host}. Agregue uno allí o elija otro host.", - "updateRequired": "Actualice el servidor de Orca en {hosts} para almacenar automatizaciones allí.", - "move": "Al guardar se crea esta automatización en {host} y se eliminan la original y su historial de ejecuciones." } }, "agent": { @@ -14697,15 +14584,6 @@ "delete": "Eliminar" } }, -<<<<<<< HEAD - "ComposerParentWorktreePicker": { - "label": "Worktree padre", - "noParent": "Sin worktree padre", - "description": "Anida este espacio de trabajo bajo otro en la barra lateral. No cambia la rama base.", - "searchPlaceholder": "Buscar espacios de trabajo...", - "noMatches": "No hay coincidencias." -||||||| parent of ebaa81ab2f (Rebase custom-agents onto main (2/4): renderer) -======= "AgentLaunchNotices": { "disabledFallback": "{{value0}} está deshabilitado. Se inició {{value1}} estándar sin ejecutable personalizado, argumentos personalizados ni entorno de agente personalizado.", "missingFallback": "{{value0}} se eliminó. Se inició {{value1}} estándar sin ejecutable personalizado, argumentos personalizados ni entorno de agente personalizado.", @@ -14728,7 +14606,31 @@ "openTerminal": "Abrir terminal", "forgetLaunch": "Olvidar lanzamiento…", "manageAgents": "Administrar agentes" ->>>>>>> ebaa81ab2f (Rebase custom-agents onto main (2/4): renderer) + }, + "dataRecovery": { + "pointAgentCatalogPreV1Title": "Before the custom-agents update (agent catalog v1)", + "pointAgentCatalogPreV1Loss": "Restoring discards all settings and workspace metadata saved after this point, including any custom agents.", + "title": "Data recovery", + "description": "Restore this profile from a recovery point created before a data migration. Restoring never deletes the recovery point.", + "loading": "Looking for recovery points…", + "empty": "No recovery points exist for this profile.", + "createdAt": "Created {{date}}", + "createdUnknown": "Creation time unknown", + "previousBinary": "After restoring, Orca quits. Install the previous Orca version before opening it again.", + "restoring": "Restoring…", + "confirmRestore": "Restore and quit", + "cancel": "Cancel", + "prepareDowngrade": "Prepare downgrade…", + "restoreFailed": "Restore failed and no changes were made: {{error}}", + "close": "Close", + "noticeTitle": "Orca could not back up your data before a required update", + "noticeBody": "Your existing data is unchanged and built-in agents keep working, but custom-agent settings are read-only until this is resolved.", + "retrying": "Retrying…", + "retryMigration": "Retry migration", + "openDataRecovery": "Open Data recovery", + "copyDetails": "Copy details", + "settingsRowHint": "A pre-update recovery point exists for this profile.", + "settingsRowOpen": "Data recovery…" } }, "i18n": { diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index c77b8914291..98c95517f76 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -185,6 +185,10 @@ "07f788de83": "ウェブソケット" } } + }, + "worktreeCreate": { + "hostPredatesDefaultLaunch": "This Orca host predates default-agent launch. Pick a specific built-in agent or update Orca on the host.", + "hostPredatesCustomAgents": "This Orca host predates custom agents. Pick a built-in agent or update Orca on the host." } }, "store": { @@ -263,10 +267,7 @@ "3b57982bf6": "ワークスペース\"{{value1}}\"を削除した後、Git はブランチ\"{{value0}}\"を安全に削除できなかったため、Orca はローカルコミットを失わないように保持しました。", "81f13f48d2": "ワークツリー\"{{value1}}\"を削除した後、Git はブランチ\"{{value0}}\"を安全に削除できなかったため、Orca はローカルコミットを失わないように保持しました。", "runtimeScopeForbiddenTitle": "この接続は制限付き(モバイル)アクセスです", - "runtimeScopeForbiddenDescription": "モバイル向けペアリングではワークスペースを使用できません。[設定] → [リモート Orca サーバー] → [この Orca サーバーを共有する] から、ブラウザ用リンクで再接続してください。", - "createdWithoutParentNesting": "「{{value0}}」の配下にネストせずに作成しました", - "createdWithoutParentNestingUnnamed": "選択した親ワークツリーの配下にネストせずに作成しました", - "createdWithoutParentNestingDetail": "親ワークスペースは利用できなくなりました。ワークスペースメニューから設定できます。" + "runtimeScopeForbiddenDescription": "モバイル向けペアリングではワークスペースを使用できません。[設定] → [リモート Orca サーバー] → [この Orca サーバーを共有する] から、ブラウザ用リンクで再接続してください。" }, "repos": { "2975400634": "Orca サーバーを更新して、このランタイムで Git 以外のフォルダーを開くようにします。", @@ -3013,8 +3014,7 @@ "25dc1cd653": "ファイルを開く", "7cdf8ee0c8": "URLを開く", "b27864279e": "Agent を起動", - "omniboxPlaceholderWithHistory": "開いているタブ、履歴、ファイル、URL、Agent を検索…", - "openPage": "ページを開く", + "0e5b7a3f16": "開いているタブ、ファイル、URL、Agent を検索…", "8f0a1c4d92": "タブに切り替える", "2c38630a01": "ワークスペースは存在しません", "4f0d9a71c2": "タブは存在しません", @@ -3060,6 +3060,7 @@ "classifier": { "42e6262ae9": "利用可能な操作はありません。", "097a982ee0": "ファイルを読み込み中…", + "c41f8d20b7": "開いているタブ、ファイル、URL、Agent を検索…", "90eb94dc48": "http:// または https:// URL を入力します。", "5553b283ce": "URL またはファイルパスを入力します。", "queryTooLarge": "検索テキストが大きすぎます。", @@ -3505,8 +3506,7 @@ "resource": { "memory": { "metric": { - "workingSetDescription": "合計ワーキングセット(WS): 現在 RAM に常駐しているページです。共有ページが複数のプロセスに表示されることがあり、Windows がページアウトしたメモリはここには含まれません。", - "privateBytesDescription": "合計プライベートバイト: これらのプロセスがコミットしたメモリで、常駐中かページアウト済みかを問わず計上されます。ホストがコミット制限に対して計上する値であるため、ページングによって上のワーキングセットが縮小しても増え続けます。", + "workingSetDescription": "合計ワーキングセット(WS)。共有ページが複数のプロセスに表示されることがあります。", "rssDescription": "合計レジデントセットサイズ(RSS)。共有またはエイリアスされたページが複数のプロセスに表示されることがあります。" } }, @@ -3877,97 +3877,6 @@ } }, "skills": { - "SkillsPage": { - "cb142070b4": "更新", - "a68dee6a32": "スキルを検索", - "f43ad6edf3": "スキル", - "ea72d6185b": "スキルをスキャンできませんでした", - "dc4c3328ee": "ファイルを表示", - "9963dff6d3": "説明が見つかりません。", - "995fde8337": "スキルファイルを表示できませんでした", - "4acd6d68ec": "スキルが見つかりません", - "6a62a0168c": "一致する項目はありません", - "08a321a984": "現在の検索条件とフィルターに一致するスキルはありません。", - "cd7893fbc1": "スキルをスキャン中", - "35b9a724a0": "利用可能", - "c13b82793c": "インストールを管理", - "aee7b99cc6": "リンクからインストール", - "filterProvider": "Agent で絞り込み", - "filterSource": "ソースで絞り込み", - "allSources": "すべて", - "clearFilters": "フィルターをクリア", - "closeSkills": "スキルを閉じる", - "closeTooltip": "閉じる · Esc", - "moreActions": "その他の操作", - "sharedLinks": "共有リンク", - "emptyCopy": "スキャンしたスキルフォルダーは空です。共有バンドルをインストールするか、スキルを追加後に更新してください。", - "retry": "再試行", - "remoteShareNotice": "これらのスキルは {{host}} にあります。共有するには、そのマシンでスキルを開いてください。", - "viewSwitch": "表示", - "searchLinks": "リンクを検索", - "deleteSkills": "スキルを削除…" - }, - "SkillShareSelectionControls": { - "01c5a15e02": "スキルを共有" - }, - "SkillRow": { - "updatedUnknown": "日付なし", - "pathCopied": "パスをコピーしました", - "copyPath": "パスをコピー", - "detailPath": "パス", - "skillActions": "{{value0}} の操作", - "viewDetails": "詳細を表示", - "deleteSkill": "削除…" - }, - "SkillsList": { - "listLabel": "スキル" - }, - "sourceStatus": { - "missing": "フォルダーが見つかりません", - "remoteRepo": "リモートリポジトリ — 未スキャン", - "unavailable": "未スキャン" - }, - "sources": { - "heading": "スキルフォルダー" - }, - "sourceKind": { - "home": "ホーム", - "workspace": "ワークスペース", - "bundled": "バンドル済み", - "plugin": "プラグイン" - }, - "count": { - "skillOne": "{{count}} 件のスキル", - "skillOther": "{{count}} 件のスキル", - "sourceOne": "{{count}} 件のソース", - "sourceOther": "{{count}} 件のソース", - "fileOne": "{{count}} 個のファイル", - "fileOther": "{{count}} 個のファイル", - "resultOne": "{{count}} 件の結果", - "resultOther": "{{count}} 件の結果", - "selected": "{{count}} 件を選択", - "shareOne": "{{count}} 件のスキルを共有", - "shareOther": "{{count}} 件のスキルを共有", - "linkOne": "{{count}} 件のリンク", - "linkOther": "{{count}} 件のリンク" - }, - "filter": { - "allAgents": "すべての Agent", - "sharedAgent": "共有 (.agents)" - }, - "SkillsSelectionHeader": { - "exit": "選択を終了", - "exitTooltip": "選択を終了 · Esc", - "title": "共有するスキルを選択", - "selectAll": "対象の {{count}} 件をすべて選択", - "clear": "クリア", - "deleteTitle": "削除するスキルを選択" - }, - "SkillDetailDialog": { - "agents": "Agent", - "updated": "更新日", - "copy": "コピー" - }, "SkillFreshnessNudge": { "titleOne": "インストール済みの Orca スキルが古くなっています", "titleMany": "インストール済みの Orca スキル {{value0}} 件が古くなっています", @@ -4064,10 +3973,6 @@ "SkillUpdateRow": { "oneLocation": "1 件の場所", "manyLocations": "{{value0}} 件の場所" - }, - "host": { - "local": "このマシン", - "remote": "接続済みランタイム" } }, "sidebar": { @@ -6199,9 +6104,10 @@ "compareBaseBranchUpstream": "ブランチの upstream" }, "HiddenExperimentalGroup": { - "7c4e18d2f6": "このマシンで太字グリッチのキャプチャジェスチャーを有効にします。{{samplingShortcut}} でサンプリングバーストを開始し、{{captureShortcut}} でペインの証拠をディスクに保存します。", - "b09f24a51d": "ターミナル描画診断", - "f2c81d904a": "隠された実験的機能の設定" + "d0f914a528": "プレースホルダーの切り替え", + "1014ddbfaf": "今日は何もしません。隠された実験的機能オプションの最初のスロットとして予約されています。", + "232cf83de8": "内部テスト用の非公開トグル。ここでは何もサポートされていません。", + "3e9e827ca5": "隠された実験的機能" }, "InputPane": { "db15068196": "Linux および macOS ではデフォルトで有効になっています。 Linux はシステム選択クリップボードを使用します。他のプラットフォームはプライベートバッファを使用します。", @@ -9882,7 +9788,8 @@ "defaultTitle": "デフォルトエージェント", "nullDefaultAttention": "デフォルトのエージェントを選択してください。選択するまで、対話的な起動は開始できず、選択を求められます。", "migrationBlockedTitle": "エージェント設定は一時的に読み取り専用です", - "migrationBlockedDescription": "Orca は既存のエージェント設定を更新する前にバックアップを作成できなかったため、データを保護するために変更がブロックされています。以下の問題を解決してから、もう一度お試しください。" + "migrationBlockedDescription": "Orca は既存のエージェント設定を更新する前にバックアップを作成できなかったため、データを保護するために変更がブロックされています。以下の問題を解決してから、もう一度お試しください。", + "unavailable": "Custom agents are managed on the desktop host." }, "AgentCatalogStatusBadge": { "enabled": "有効", @@ -10915,8 +10822,6 @@ "7a09d7f9d2": "ベース", "383cf92c73": "木", "d7ae61269b": "コミット先のブランチ", - "branchFilesChangedVsBaseOne": "{{ref}} と比べて 1 ファイルが変更されています", - "branchFilesChangedVsBaseOther": "{{ref}} と比べて {{count}} ファイルが変更されています", "48a003c1b1": "ステージ済みの変更", "d4ef4bafc5": "変更点", "522f44dce5": "追跡されていないファイル", @@ -10991,10 +10896,10 @@ "b3c8f1a902": "ファイル名でフィルター", "d4f8c2a901": "フィルターをクリアして閉じる", "e8a1c4b203": "vs", - "compareBaseCommitsAheadOne": "{{ref}} より 1 コミット進んでいます", - "compareBaseCommitsAheadOther": "{{ref}}より{{count}}コミット進んでいます", - "compareBaseCommitsBehindOne": "{{ref}} より 1 コミット遅れています", - "compareBaseCommitsBehindOther": "{{ref}}より{{count}}コミット遅れています", + "f9b2441bb6": "{{value0}} より 1 コミット進んでいます", + "b715ef615b": "{{value1}}より{{value0}}コミット進んでいます", + "c1a8f3e204": "{{value0}} より 1 コミット遅れています", + "d2b9g4f315": "{{value1}}より{{value0}}コミット遅れています", "4b4a7de138": "ブラウザでレビューページを開く", "createPrIntentCommitBlockedSummary": "コミットがブロックされました:{{value0}} Issue を修正して、PR 作成を再試行してください。", "pushRecovery": { @@ -11017,6 +10922,7 @@ "a4e93c21d7": "現在のブランチ: {{value0}}", "c7d4e2f801": "ベース ref を変更: {{value0}}", "f3a1b8c204": "upstream", + "createPrIntentEmptyGeneratedBody": "生成されたレビューの詳細に説明が含まれていませんでした。PR の作成を再試行してください。", "createPrIntentGenerateDetailsFailed": "レビューの詳細を生成できませんでした。PR の作成を再試行してください。" }, "SourceControlAgentActionDialog": { @@ -13003,9 +12909,8 @@ "7e7ca60816": "変更されたファイル", "b6c3b84476": "ファイルツリーを表示", "39f8007549": "競合をレビューする", - "skippedConflictsExcluded": "未解決の競合 {{count}} 件がこの差分ビューから除外されました。", - "skippedConflictsExcluded_one": "未解決の競合 {{count}} 件がこの差分ビューから除外されました。", - "skippedConflictsExcluded_other": "未解決の競合 {{count}} 件がこの差分ビューから除外されました。", + "39e73e7181": "はこの差分ビューから除外されました。", + "689b99f8ad": "未解決の競合", "820ec01f24": "競合したファイルは個別にレビューされます", "fd8892b120": "表示に変更はありません", "eb5f40e49c": "通常の双方向 diff パイプラインは競合に対して安全ではないため、この diff ビューでは未解決の競合が除外されます。", @@ -13967,7 +13872,6 @@ "showMore": "さらに表示" }, "AutomationDetail": { - "host": "ホスト", "007c8ad874": "プロンプト", "a1d52c2189": "使用範囲", "449fc83bf7": "トークン", @@ -14131,9 +14035,6 @@ "b11170a008": "オートメーションを保存できませんでした。", "2a20596d6b": "オートメーションが保存されました。", "244727e655": "自動化が更新されました。", - "moved": "自動化を {host} に移動しました。", - "moveOriginalKept": "{host} に作成しましたが、元の自動化を削除できませんでした。元のホストで削除してください。", - "moveOriginalUnverified": "{host} に作成しましたが、元の自動化の削除を確認できませんでした。再試行する前に元のホストを確認してください。", "77b81bc4ac": "Hermes オートメーションが作成されました。", "08efc3ae12": "Hermes オートメーションが更新されました。", "e431bb85d4": "この Hermes オートメーションと同じホスト上のワークスペースを選択します。", @@ -14177,7 +14078,6 @@ "backToList": "すべての自動化", "tableName": "名前", "tableProject": "プロジェクト", - "tableHost": "ホスト", "tableStatus": "ステータス", "tableActions": "操作", "newAutomation": "新しい自動化", @@ -14330,19 +14230,6 @@ "filters": "フィルター", "clear": "フィルターをクリア", "removeFilter": "{{value0}} フィルターを解除" - }, - "createDestination": { - "label": "ホスト", - "placeholder": "ホストを選択", - "storedOn": "{authority} が保存してスケジュールします。", - "unselected": "この自動化を保存してスケジュールするホストを選択します。", - "orphan": "ホストのない自動化は新しい自動化を保持できません。この自動化を作成するホストを選択してください。", - "unavailable": "そのホストはまだ新しい自動化を保持できません。これを作成する別のホストを選択してください。", - "stale": "このフォームを開いている間に {host} が変更されました。保存する前にもう一度選択してください。", - "projectMismatch": "そのプロジェクトは {host} にありません。そのホスト上のプロジェクト、または別のホストを選択してください。", - "noProjects": "{host} にはプロジェクトが設定されていません。そこに追加するか、別のホストを選択してください。", - "updateRequired": "{hosts} の Orca サーバーを更新すると、そこに自動化を保存できます。", - "move": "保存すると、この自動化は {host} に作成され、元の自動化と実行履歴は削除されます。" } }, "agent": { @@ -14697,15 +14584,6 @@ "delete": "削除" } }, -<<<<<<< HEAD - "ComposerParentWorktreePicker": { - "label": "親ワークツリー", - "noParent": "親なし", - "description": "このワークスペースをサイドバー内の別のワークスペースの配下にネストします。ベースブランチは変更されません。", - "searchPlaceholder": "ワークスペースを検索...", - "noMatches": "一致する項目がありません。" -||||||| parent of ebaa81ab2f (Rebase custom-agents onto main (2/4): renderer) -======= "AgentLaunchNotices": { "disabledFallback": "{{value0}} は無効です。カスタム実行ファイル、カスタム引数、カスタムエージェント環境なしで標準の {{value1}} を起動しました。", "missingFallback": "{{value0}} は削除されました。カスタム実行ファイル、カスタム引数、カスタムエージェント環境なしで標準の {{value1}} を起動しました。", @@ -14728,7 +14606,31 @@ "openTerminal": "ターミナルを開く", "forgetLaunch": "起動を破棄…", "manageAgents": "エージェントを管理" ->>>>>>> ebaa81ab2f (Rebase custom-agents onto main (2/4): renderer) + }, + "dataRecovery": { + "pointAgentCatalogPreV1Title": "Before the custom-agents update (agent catalog v1)", + "pointAgentCatalogPreV1Loss": "Restoring discards all settings and workspace metadata saved after this point, including any custom agents.", + "title": "Data recovery", + "description": "Restore this profile from a recovery point created before a data migration. Restoring never deletes the recovery point.", + "loading": "Looking for recovery points…", + "empty": "No recovery points exist for this profile.", + "createdAt": "Created {{date}}", + "createdUnknown": "Creation time unknown", + "previousBinary": "After restoring, Orca quits. Install the previous Orca version before opening it again.", + "restoring": "Restoring…", + "confirmRestore": "Restore and quit", + "cancel": "Cancel", + "prepareDowngrade": "Prepare downgrade…", + "restoreFailed": "Restore failed and no changes were made: {{error}}", + "close": "Close", + "noticeTitle": "Orca could not back up your data before a required update", + "noticeBody": "Your existing data is unchanged and built-in agents keep working, but custom-agent settings are read-only until this is resolved.", + "retrying": "Retrying…", + "retryMigration": "Retry migration", + "openDataRecovery": "Open Data recovery", + "copyDetails": "Copy details", + "settingsRowHint": "A pre-update recovery point exists for this profile.", + "settingsRowOpen": "Data recovery…" } }, "i18n": { diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index b1ce0a838ee..58b7d943cda 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -185,6 +185,10 @@ "07f788de83": "웹소켓" } } + }, + "worktreeCreate": { + "hostPredatesDefaultLaunch": "This Orca host predates default-agent launch. Pick a specific built-in agent or update Orca on the host.", + "hostPredatesCustomAgents": "This Orca host predates custom agents. Pick a built-in agent or update Orca on the host." } }, "store": { @@ -266,10 +270,7 @@ "3b57982bf6": "\"{{value1}}\" 워크스페이스를 삭제한 후 Git이 \"{{value0}}\" 브랜치를 안전하게 삭제할 수 없어 Orca가 로컬 커밋 손실을 방지하기 위해 유지했습니다.", "81f13f48d2": "\"{{value1}}\" 워크트리를 삭제한 후 Git이 \"{{value0}}\" 브랜치를 안전하게 삭제할 수 없어 Orca가 로컬 커밋 손실을 방지하기 위해 유지했습니다.", "runtimeScopeForbiddenTitle": "이 연결은 제한된(모바일) 액세스입니다", - "runtimeScopeForbiddenDescription": "모바일 범위 페어링에서는 워크스페이스를 사용할 수 없습니다. 설정 → 원격 Orca 서버 → 이 Orca 서버 공유에서 브라우저 액세스 링크로 다시 연결하세요.", - "createdWithoutParentNesting": "\"{{value0}}\" 아래에 중첩하지 않고 생성했습니다", - "createdWithoutParentNestingUnnamed": "선택한 상위 워크트리 아래에 중첩하지 않고 생성했습니다", - "createdWithoutParentNestingDetail": "상위 워크스페이스를 더 이상 사용할 수 없습니다. 워크스페이스 메뉴에서 설정할 수 있습니다." + "runtimeScopeForbiddenDescription": "모바일 범위 페어링에서는 워크스페이스를 사용할 수 없습니다. 설정 → 원격 Orca 서버 → 이 Orca 서버 공유에서 브라우저 액세스 링크로 다시 연결하세요." }, "repos": { "2975400634": "이 런타임에서 Git이 아닌 폴더를 열려면 Orca 서버를 업데이트하세요.", @@ -3018,8 +3019,7 @@ "25dc1cd653": "파일 열기", "7cdf8ee0c8": "URL 열기", "b27864279e": "agent 실행", - "omniboxPlaceholderWithHistory": "열린 탭, 방문 기록, 파일, URL, agent 검색…", - "openPage": "페이지 열기", + "0e5b7a3f16": "열린 탭, 파일, URL, agent 검색…", "8f0a1c4d92": "탭으로 전환", "2c38630a01": "워크스페이스가 더 이상 존재하지 않습니다", "4f0d9a71c2": "탭이 더 이상 존재하지 않습니다", @@ -3065,6 +3065,7 @@ "classifier": { "42e6262ae9": "사용할 수 있는 작업이 없습니다.", "097a982ee0": "파일 로드 중...", + "c41f8d20b7": "열린 탭, 파일, URL, agent 검색…", "90eb94dc48": "http:// 또는 https:// URL을 입력하세요.", "5553b283ce": "URL 또는 파일 경로를 입력하세요.", "queryTooLarge": "검색 텍스트가 너무 큽니다.", @@ -3510,8 +3511,7 @@ "resource": { "memory": { "metric": { - "workingSetDescription": "합산된 워킹 세트(WS): 지금 RAM에 상주 중인 페이지입니다. 공유 페이지가 둘 이상의 프로세스에 나타날 수 있으며, Windows가 페이지 아웃한 메모리는 여기에 포함되지 않습니다.", - "privateBytesDescription": "합산된 프라이빗 바이트: 이 프로세스들이 커밋한 메모리로, 상주 여부와 관계없이 계산됩니다. 호스트가 커밋 한도에 반영하는 값이므로, 페이징으로 위의 워킹 세트가 줄어드는 동안에도 계속 늘어납니다.", + "workingSetDescription": "합산된 워킹 세트(WS). 공유 페이지가 둘 이상의 프로세스에 나타날 수 있습니다.", "rssDescription": "합산된 레지던트 세트 크기(RSS). 공유 또는 별칭된 페이지가 둘 이상의 프로세스에 나타날 수 있습니다." } }, @@ -3882,84 +3882,6 @@ } }, "skills": { - "SkillsPage": { - "cb142070b4": "새로 고침", - "a68dee6a32": "스킬 검색", - "f43ad6edf3": "스킬", - "ea72d6185b": "스킬을 검색하지 못했습니다", - "dc4c3328ee": "파일 표시", - "9963dff6d3": "설명을 찾을 수 없습니다.", - "995fde8337": "스킬 파일을 표시하지 못했습니다", - "4acd6d68ec": "스킬을 찾을 수 없습니다", - "6a62a0168c": "일치하는 항목 없음", - "08a321a984": "현재 검색 및 필터와 일치하는 스킬이 없습니다.", - "cd7893fbc1": "스킬 검색 중", - "35b9a724a0": "사용 가능", - "c13b82793c": "설치 관리", - "aee7b99cc6": "링크에서 설치", - "filterProvider": "에이전트별 필터", - "filterSource": "소스별 필터", - "allSources": "전체", - "clearFilters": "필터 지우기", - "closeSkills": "스킬 닫기", - "closeTooltip": "닫기 · Esc", - "moreActions": "추가 작업", - "sharedLinks": "공유 링크", - "emptyCopy": "검색한 스킬 폴더가 비어 있습니다. 공유 번들을 설치하거나 스킬을 추가한 후 새로 고치세요.", - "retry": "다시 시도", - "remoteShareNotice": "이 스킬은 {{host}}에 있습니다. 공유하려면 해당 머신에서 스킬을 여세요.", - "viewSwitch": "표시", - "searchLinks": "링크 검색", - "deleteSkills": "스킬 삭제…" - }, - "SkillShareSelectionControls": { "01c5a15e02": "스킬 공유" }, - "SkillRow": { - "updatedUnknown": "날짜 없음", - "pathCopied": "경로 복사됨", - "copyPath": "경로 복사", - "detailPath": "경로", - "skillActions": "{{value0}} 작업", - "viewDetails": "세부 정보 보기", - "deleteSkill": "삭제…" - }, - "SkillsList": { "listLabel": "스킬" }, - "sourceStatus": { - "missing": "폴더를 찾을 수 없음", - "remoteRepo": "원격 리포지토리 — 검색 안 됨", - "unavailable": "검색 안 됨" - }, - "sources": { "heading": "스킬 폴더" }, - "sourceKind": { - "home": "홈", - "workspace": "워크스페이스", - "bundled": "번들", - "plugin": "플러그인" - }, - "count": { - "skillOne": "스킬 {{count}}개", - "skillOther": "스킬 {{count}}개", - "sourceOne": "소스 {{count}}개", - "sourceOther": "소스 {{count}}개", - "fileOne": "파일 {{count}}개", - "fileOther": "파일 {{count}}개", - "resultOne": "결과 {{count}}개", - "resultOther": "결과 {{count}}개", - "selected": "{{count}}개 선택됨", - "shareOne": "스킬 {{count}}개 공유", - "shareOther": "스킬 {{count}}개 공유", - "linkOne": "링크 {{count}}개", - "linkOther": "링크 {{count}}개" - }, - "filter": { "allAgents": "모든 에이전트", "sharedAgent": "공유됨 (.agents)" }, - "SkillsSelectionHeader": { - "exit": "선택 나가기", - "exitTooltip": "선택 나가기 · Esc", - "title": "공유할 스킬 선택", - "selectAll": "가능한 {{count}}개 모두 선택", - "clear": "지우기", - "deleteTitle": "삭제할 스킬 선택" - }, - "SkillDetailDialog": { "agents": "에이전트", "updated": "업데이트됨", "copy": "복사" }, "SkillFreshnessNudge": { "titleOne": "설치된 Orca 스킬이 오래되었습니다", "titleMany": "설치된 Orca 스킬 {{value0}}개가 오래되었습니다", @@ -4056,10 +3978,6 @@ "SkillUpdateRow": { "oneLocation": "1 location", "manyLocations": "{{value0}} locations" - }, - "host": { - "local": "이 컴퓨터", - "remote": "연결된 런타임" } }, "sidebar": { @@ -4371,7 +4289,7 @@ "489d1c8c9f": "프로젝트 검색...", "ee240a39eb": "필터 편집", "automationCreated": "자동화로 생성된 워크스페이스 숨기기", - "cliCreated": "CLI로 생성된 워크스페이스 숨기기", + "cliCreated": "Hide CLI-created", "detachedHead": "분리된 HEAD 숨기기", "keepDefaultBranch": "기본 브랜치는 제외", "keepDefaultBranchAria": "슬립 중인 워크스페이스를 숨겨도 기본 브랜치는 계속 표시" @@ -4440,7 +4358,7 @@ "ed1611b65b": "슬립 중인 항목 숨기기", "82594419ba": "필터", "automationCreated": "자동화로 생성된 워크스페이스 숨기기", - "cliCreated": "CLI로 생성된 워크스페이스 숨기기", + "cliCreated": "Hide CLI-created", "detachedHead": "분리된 HEAD 숨기기", "keepDefaultBranch": "기본 브랜치는 제외", "keepDefaultBranchAria": "슬립 중인 워크스페이스를 숨겨도 기본 브랜치는 계속 표시" @@ -4613,10 +4531,10 @@ "issueLinkLabel": "이슈 링크", "reviewLinkLabel": "{{value0}} 링크", "cliHeader": "Orca CLI", - "cliCreatedFromAgent": "Agent가 `orca worktree create`로 생성", - "cliCreatedFromShell": "`orca worktree create`로 생성", - "cliStartupAgent": "{{value0}}(으)로 시작됨", - "cliCreated": "Orca CLI로 생성됨", + "cliCreatedFromAgent": "Created by an agent via `orca worktree create`", + "cliCreatedFromShell": "Created via `orca worktree create`", + "cliStartupAgent": "Started with {{value0}}", + "cliCreated": "Created by Orca CLI", "jiraIssue": "Jira {{value0}}", "viewOnJira": "Jira에서 보기", "linkedJira": "연결된 Jira {{value0}}" @@ -6154,9 +6072,10 @@ "compareBaseBranchUpstream": "브랜치 업스트림" }, "HiddenExperimentalGroup": { - "7c4e18d2f6": "이 컴퓨터에서 굵게 표시 결함 캡처 제스처를 활성화합니다. {{samplingShortcut}}하면 샘플링 버스트가 시작되고, {{captureShortcut}}하면 창 증거를 디스크에 캡처합니다.", - "b09f24a51d": "터미널 렌더링 진단", - "f2c81d904a": "숨겨진 실험 설정" + "d0f914a528": "자리 표시자 토글", + "1014ddbfaf": "오늘은 아무것도 하지 않습니다. 숨겨진 실험 옵션을 위한 첫 번째 슬롯으로 예약되어 있습니다.", + "232cf83de8": "내부 테스트를 위한 비공개 토글입니다. 여기에는 아무것도 지원되지 않습니다.", + "3e9e827ca5": "숨겨진 실험" }, "InputPane": { "db15068196": "Linux 및 macOS에서는 기본적으로 활성화되어 있습니다. Linux는 시스템 선택 클립보드를 사용합니다. 다른 플랫폼은 개인 버퍼를 사용합니다.", @@ -9874,7 +9793,8 @@ "defaultTitle": "기본 에이전트", "nullDefaultAttention": "기본 에이전트를 선택하세요. 선택하기 전에는 대화형 실행을 시작할 수 없으며 에이전트 선택을 요청받습니다.", "migrationBlockedTitle": "에이전트 설정이 일시적으로 읽기 전용입니다", - "migrationBlockedDescription": "Orca가 기존 에이전트 설정을 업데이트하기 전에 백업할 수 없었기 때문에 데이터를 보호하기 위해 변경이 차단되었습니다. 아래 문제를 해결한 후 다시 시도하세요." + "migrationBlockedDescription": "Orca가 기존 에이전트 설정을 업데이트하기 전에 백업할 수 없었기 때문에 데이터를 보호하기 위해 변경이 차단되었습니다. 아래 문제를 해결한 후 다시 시도하세요.", + "unavailable": "Custom agents are managed on the desktop host." }, "AgentCatalogStatusBadge": { "enabled": "활성화됨", @@ -10923,8 +10843,6 @@ "7a09d7f9d2": "베이스", "383cf92c73": "트리", "d7ae61269b": "브랜치에 Commit 됨", - "branchFilesChangedVsBaseOne": "{{ref}} 대비 파일 1개 변경됨", - "branchFilesChangedVsBaseOther": "{{ref}} 대비 파일 {{count}}개 변경됨", "48a003c1b1": "스테이징된 변경 사항", "d4ef4bafc5": "변경 사항", "522f44dce5": "추적되지 않은 파일", @@ -10999,10 +10917,10 @@ "b3c8f1a902": "파일 이름으로 필터링", "d4f8c2a901": "필터 지우고 닫기", "e8a1c4b203": "vs", - "compareBaseCommitsAheadOne": "{{ref}}보다 1 commit 앞서 있음", - "compareBaseCommitsAheadOther": "{{ref}}보다 {{count}} commits 앞서 있음", - "compareBaseCommitsBehindOne": "{{ref}}보다 1 commit 뒤처짐", - "compareBaseCommitsBehindOther": "{{ref}}보다 {{count}} commits 뒤처짐", + "f9b2441bb6": "{{value0}}보다 1 commit 앞서 있음", + "b715ef615b": "{{value1}}보다 {{value0}} commits 앞서 있음", + "c1a8f3e204": "{{value0}}보다 1 commit 뒤처짐", + "d2b9g4f315": "{{value1}}보다 {{value0}} commits 뒤처짐", "4b4a7de138": "브라우저에서 검토 페이지 열기", "createPrIntentCommitBlockedSummary": "커밋이 차단됨: {{value0}} 이슈를 수정한 후 PR 만들기를 다시 시도하세요.", "pushRecovery": { @@ -11025,6 +10943,7 @@ "a4e93c21d7": "현재 브랜치: {{value0}}", "c7d4e2f801": "베이스 ref 변경: {{value0}}", "f3a1b8c204": "upstream", + "createPrIntentEmptyGeneratedBody": "생성된 검토 세부 정보에 설명이 포함되지 않았습니다. PR 생성을 다시 시도하세요.", "createPrIntentGenerateDetailsFailed": "검토 세부 정보를 생성할 수 없습니다. PR 생성을 다시 시도하세요." }, "SourceControlAgentActionDialog": { @@ -12647,30 +12566,6 @@ }, "feature": { "wall": { - "setup": { - "checklist": { - "localized": { - "copy": { - "ec0a363633": "동시 작업", - "62bac8f43c": "동시에 2개의 서로 다른 워크트리에서 작업하세요. 같은 프로젝트 내에서도 각각 완벽히 격리되어 있어 2개의 기능을 동시에 작업하기에 적합합니다.", - "908898c3ee": "Orca 브라우저 사용", - "43781563c3": "Orca를 벗어나지 않고 웹 앱을 탐색하세요. 원하는 요소를 선택하고 클릭 한 번으로 정확한 소스와 스타일을 에이전트에 전달할 수 있습니다.", - "29aa2c2077": "알림 켜기", - "71bd9a8c95": "에이전트가 작업을 완료했거나, 확인이 필요하거나, 차단된 순간을 즉시 알 수 있습니다.", - "46db810da8": "기본 에이전트 선택", - "b8e5bae17f": "선호하는 에이전트를 미리 선택하여 새로운 작업을 더 빠르게 시작하세요.", - "fee5557b02": "Orca CLI 활성화", - "7bcb4097fa": "Orca 셸 명령을 등록하고 브라우저, 컴퓨터 및 오케스트레이션 워크플로를 위한 에이전트 스킬을 설치하세요.", - "ad342dd4c6": "서비스 연동", - "06fe30fdb0": "클릭 한 번으로 작업에서 에이전트를 시작하고 PR 상태를 한눈에 확인하세요.", - "eddc532e58": "워크스페이스 설정 자동화", - "56049b74c2": "설치 및 설정 명령을 자동으로 실행하여 모든 새 워크트리가 에이전트 작업을 바로 수행할 수 있도록 준비합니다.", - "2cf795433b": "여러 저장소에서 작업 시작", - "42525ba8a4": "주요 저장소를 Orca로 가져와 폴더를 일일이 찾을 필요 없이 에이전트 작업을 시작하세요." - } - } - } - }, "usage": { "tracking": { "b94ec70eda": "추적이 설정되지 않았습니다.", @@ -13035,9 +12930,8 @@ "7e7ca60816": "변경된 파일", "b6c3b84476": "파일 트리 표시", "39f8007549": "충돌 리뷰", - "skippedConflictsExcluded": "미해결 충돌 {{count}}개가 이 차이점 보기에서 제외되었습니다.", - "skippedConflictsExcluded_one": "미해결 충돌 {{count}}개가 이 차이점 보기에서 제외되었습니다.", - "skippedConflictsExcluded_other": "미해결 충돌 {{count}}개가 이 차이점 보기에서 제외되었습니다.", + "39e73e7181": "이 차이점 보기에서 제외되었습니다.", + "689b99f8ad": "미해결 충돌", "820ec01f24": "충돌하는 파일은 별도로 리뷰됩니다.", "fd8892b120": "표시할 변경사항이 없습니다.", "eb5f40e49c": "일반적인 양방향 diff 파이프라인은 충돌로부터 안전하지 않기 때문에 이 diff 보기는 해결되지 않은 충돌을 제외합니다.", @@ -14010,7 +13904,6 @@ "showMore": "더 보기" }, "AutomationDetail": { - "host": "호스트", "007c8ad874": "프롬프트", "a1d52c2189": "사용 범위", "449fc83bf7": "토큰", @@ -14174,9 +14067,6 @@ "b11170a008": "자동화를 저장하지 못했습니다.", "2a20596d6b": "자동화가 저장되었습니다.", "244727e655": "자동화가 업데이트되었습니다.", - "moved": "자동화를 {host}(으)로 이동했습니다.", - "moveOriginalKept": "{host}에 생성했지만 원본을 삭제하지 못했습니다. 이전 호스트에서 삭제하세요.", - "moveOriginalUnverified": "{host}에 생성했지만 원본 삭제를 확인할 수 없습니다. 다시 시도하기 전에 이전 호스트를 확인하세요.", "77b81bc4ac": "Hermes 자동화가 생성되었습니다.", "08efc3ae12": "Hermes 자동화가 업데이트되었습니다.", "e431bb85d4": "이 Hermes 자동화와 동일한 호스트에서 워크스페이스를 선택하세요.", @@ -14220,7 +14110,6 @@ "backToList": "모든 자동화", "tableName": "이름", "tableProject": "프로젝트", - "tableHost": "호스트", "tableStatus": "상태", "tableActions": "작업", "newAutomation": "새 자동화", @@ -14373,19 +14262,6 @@ "filters": "필터", "clear": "필터 지우기", "removeFilter": "{{value0}} 필터 제거" - }, - "createDestination": { - "label": "호스트", - "placeholder": "호스트 선택", - "storedOn": "{authority}에서 저장하고 예약합니다.", - "unselected": "이 자동화를 저장하고 예약할 호스트를 선택하세요.", - "orphan": "호스트가 없는 자동화는 새 자동화를 보유할 수 없습니다. 이 자동화를 만들 호스트를 선택하세요.", - "unavailable": "해당 호스트는 아직 새 자동화를 보유할 수 없습니다. 이 자동화를 만들 다른 호스트를 선택하세요.", - "stale": "이 양식이 열려 있는 동안 {host}이(가) 변경되었습니다. 저장하기 전에 다시 선택하세요.", - "projectMismatch": "해당 프로젝트는 {host}에 없습니다. 그 호스트의 프로젝트를 선택하거나 다른 호스트를 선택하세요.", - "noProjects": "{host}에 설정된 프로젝트가 없습니다. 거기에 추가하거나 다른 호스트를 선택하세요.", - "updateRequired": "{hosts}의 Orca 서버를 업데이트하면 거기에 자동화를 저장할 수 있습니다.", - "move": "저장하면 이 자동화가 {host}에 생성되고 원본과 실행 기록은 삭제됩니다." } }, "agent": { @@ -14740,15 +14616,6 @@ "delete": "삭제" } }, -<<<<<<< HEAD - "ComposerParentWorktreePicker": { - "label": "상위 워크트리", - "noParent": "상위 워크트리 없음", - "description": "이 워크스페이스를 사이드바의 다른 워크스페이스 아래에 중첩합니다. 기준 브랜치는 변경되지 않습니다.", - "searchPlaceholder": "워크스페이스 검색...", - "noMatches": "일치하는 항목이 없습니다." -||||||| parent of ebaa81ab2f (Rebase custom-agents onto main (2/4): renderer) -======= "AgentLaunchNotices": { "disabledFallback": "{{value0}}이(가) 비활성화되어 있습니다. 사용자 지정 실행 파일, 사용자 지정 인수 또는 사용자 지정 에이전트 환경 없이 기본 {{value1}}을(를) 시작했습니다.", "missingFallback": "{{value0}}이(가) 삭제되었습니다. 사용자 지정 실행 파일, 사용자 지정 인수 또는 사용자 지정 에이전트 환경 없이 기본 {{value1}}을(를) 시작했습니다.", @@ -14771,7 +14638,31 @@ "openTerminal": "터미널 열기", "forgetLaunch": "실행 지우기…", "manageAgents": "에이전트 관리" ->>>>>>> ebaa81ab2f (Rebase custom-agents onto main (2/4): renderer) + }, + "dataRecovery": { + "pointAgentCatalogPreV1Title": "Before the custom-agents update (agent catalog v1)", + "pointAgentCatalogPreV1Loss": "Restoring discards all settings and workspace metadata saved after this point, including any custom agents.", + "title": "Data recovery", + "description": "Restore this profile from a recovery point created before a data migration. Restoring never deletes the recovery point.", + "loading": "Looking for recovery points…", + "empty": "No recovery points exist for this profile.", + "createdAt": "Created {{date}}", + "createdUnknown": "Creation time unknown", + "previousBinary": "After restoring, Orca quits. Install the previous Orca version before opening it again.", + "restoring": "Restoring…", + "confirmRestore": "Restore and quit", + "cancel": "Cancel", + "prepareDowngrade": "Prepare downgrade…", + "restoreFailed": "Restore failed and no changes were made: {{error}}", + "close": "Close", + "noticeTitle": "Orca could not back up your data before a required update", + "noticeBody": "Your existing data is unchanged and built-in agents keep working, but custom-agent settings are read-only until this is resolved.", + "retrying": "Retrying…", + "retryMigration": "Retry migration", + "openDataRecovery": "Open Data recovery", + "copyDetails": "Copy details", + "settingsRowHint": "A pre-update recovery point exists for this profile.", + "settingsRowOpen": "Data recovery…" } }, "i18n": { @@ -14968,16 +14859,29 @@ }, "workspace": { "cleanup": { + "browse": { + "selectionWithheldOne": "선택한 워크스페이스 1개가 현재 필터에 의해 숨겨져 선택이 해제되었습니다.", + "selectionWithheld": "선택한 워크스페이스 {{value0}}개가 현재 필터에 의해 숨겨져 선택이 해제되었습니다.", + "selectAllCountOne": "안전 검사를 통과한 워크스페이스 1개 선택", + "selectAllCount": "안전 검사를 통과한 워크스페이스 {{value0}}개 모두 선택" + }, + "scan": { + "readyOne": "워크스페이스 1개를 찾았습니다.", + "readyMany": "워크스페이스 {{value0}}개를 찾았습니다." + }, + "presentationFixtures": { + "reviewAlphaCleanup": "알파 정리 검토" + }, + "presentation": { + "gitlabMergeRequestNumber": "MR #{{value0}}", + "githubPullRequestNumber": "PR #{{value0}}" + }, "browse": { "gitStatusCheckFailed": "Git 상태 확인 실패", "gitStatusUnverified": "Git 상태를 확인할 수 없음", "deleteAnyway": "그래도 삭제", "forceDeleteProjectionOne": "현재 워크스페이스 {{count}}개에 위험 요소가 표시되며 강제 삭제가 필요할 수 있습니다", "forceDeleteProjectionMany": "현재 워크스페이스 {{count}}개에 위험 요소가 표시되며 강제 삭제가 필요할 수 있습니다", - "selectionWithheldOne": "선택한 워크스페이스 1개가 현재 필터에 의해 숨겨져 선택이 해제되었습니다.", - "selectionWithheld": "선택한 워크스페이스 {{value0}}개가 현재 필터에 의해 숨겨져 선택이 해제되었습니다.", - "selectAllCountOne": "안전 검사를 통과한 워크스페이스 1개 선택", - "selectAllCount": "안전 검사를 통과한 워크스페이스 {{value0}}개 모두 선택", "appliedFilters": "적용된 필터", "removeFilter": "{{value0}} 필터 제거", "chip": { @@ -15024,17 +14928,6 @@ "retainedAgents": "완료된 에이전트" } } - }, - "scan": { - "readyOne": "워크스페이스 1개를 찾았습니다.", - "readyMany": "워크스페이스 {{value0}}개를 찾았습니다." - }, - "presentationFixtures": { - "reviewAlphaCleanup": "알파 정리 검토" - }, - "presentation": { - "gitlabMergeRequestNumber": "MR #{{value0}}", - "githubPullRequestNumber": "PR #{{value0}}" } } }, @@ -15088,24 +14981,24 @@ "agentCount": "에이전트 {{count}}개" }, "placeholder": { - "title": "에이전트 대시보드", - "description": "모든 에이전트를 한눈에 볼 수 있는 곳입니다. 보드는 곧 제공됩니다." + "title": "Agent dashboard", + "description": "This is where all your agents will show up at a glance. The board is coming soon." }, "recoverableError": { - "title": "Orca 대시보드에서 오류가 발생했습니다.", - "description": "대시보드 렌더링을 완료하지 못했습니다. 다시 시도해 다시 마운트하거나 다시 열어 보세요." + "title": "Orca dashboard hit an error.", + "description": "The dashboard could not finish rendering. Retry to remount it, or reopen it." }, "bucket": { - "attention": "확인 필요", - "working": "작업 중", - "idle": "유휴", - "empty": "없음", + "attention": "Needs You", + "working": "Working", + "idle": "Idle", + "empty": "None", "done": "완료" }, - "title": "에이전트", - "total": "총 {{count}}개", + "title": "Agents", + "total": "{{count}} total", "card": { - "you": "나", + "you": "You", "time": { "justNow": "방금", "minutes": "{{count}}분", @@ -15122,11 +15015,11 @@ "subagents_other": "서브에이전트 {{count}}개" }, "terminal": { - "closed": "실시간 터미널이 없습니다 — 이 에이전트의 창이 닫혔습니다.", - "focusWorktree": "워크트리 열기", - "close": "닫기" + "closed": "No live terminal — this agent's pane has closed.", + "focusWorktree": "Open worktree", + "close": "Close" }, - "close": "대시보드 닫기", + "close": "Close dashboard", "settingsTooltip": "보드 설정", "filters": { "remove": "{{label}} 필터 제거", diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index a86c86e0f2d..2cdd5d7a4b1 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -185,6 +185,10 @@ "07f788de83": "WebSocket" } } + }, + "worktreeCreate": { + "hostPredatesDefaultLaunch": "This Orca host predates default-agent launch. Pick a specific built-in agent or update Orca on the host.", + "hostPredatesCustomAgents": "This Orca host predates custom agents. Pick a built-in agent or update Orca on the host." } }, "store": { @@ -266,10 +270,7 @@ "3b57982bf6": "删除工作区\"{{value1}}\"后,Git 无法安全删除分支\"{{value0}}\",因此 Orca 将其保留以避免丢失本地提交。", "81f13f48d2": "删除工作树\"{{value1}}\"后,Git 无法安全删除分支\"{{value0}}\",因此 Orca 将其保留以避免丢失本地提交。", "runtimeScopeForbiddenTitle": "此连接具有受限(移动端)访问权限", - "runtimeScopeForbiddenDescription": "移动端范围的配对无法使用工作区。请通过设置 → 远程 Orca 服务器 → 分享此 Orca 服务器中的浏览器访问链接重新连接。", - "createdWithoutParentNesting": "已创建,但未嵌套在“{{value0}}”下", - "createdWithoutParentNestingUnnamed": "已创建,但未嵌套在所选父工作树下", - "createdWithoutParentNestingDetail": "父工作树已不可用。您可以从工作区菜单中重新设置。" + "runtimeScopeForbiddenDescription": "移动端范围的配对无法使用工作区。请通过设置 → 远程 Orca 服务器 → 分享此 Orca 服务器中的浏览器访问链接重新连接。" }, "repos": { "2975400634": "请更新 Orca 服务器,以便在此运行时打开非 Git 文件夹。", @@ -3028,8 +3029,7 @@ "25dc1cd653": "打开文件", "7cdf8ee0c8": "打开网址", "b27864279e": "启动智能体", - "omniboxPlaceholderWithHistory": "搜索打开的标签页、历史记录、文件、URL 和智能体…", - "openPage": "打开页面", + "0e5b7a3f16": "搜索打开的标签页、文件、URL 和智能体…", "8f0a1c4d92": "切换到标签页", "2c38630a01": "工作区已不存在", "4f0d9a71c2": "标签页已不存在", @@ -3075,6 +3075,7 @@ "classifier": { "42e6262ae9": "没有可用的操作。", "097a982ee0": "正在加载文件...", + "c41f8d20b7": "搜索打开的标签页、文件、URL 和智能体…", "90eb94dc48": "输入 http:// 或 https:// URL。", "5553b283ce": "输入 URL 或文件路径。", "queryTooLarge": "搜索文本过长。", @@ -3520,8 +3521,7 @@ "resource": { "memory": { "metric": { - "workingSetDescription": "工作集 (WS) 的总和:当前驻留在 RAM 中的页。共享页可能会出现在多个进程中,被 Windows 换出到页面文件的内存不计入其中。", - "privateBytesDescription": "专用字节的总和:这些进程已提交的内存,无论驻留还是已换出都会计入。这是主机计入提交限制的数值,因此在分页导致上方工作集缩小时它仍会继续上升。", + "workingSetDescription": "工作集 (WS) 的总和。共享页可能会出现在多个进程中。", "rssDescription": "驻留集大小 (RSS) 的总和。共享页或别名映射页可能会出现在多个进程中。" } }, @@ -3892,92 +3892,6 @@ } }, "skills": { - "SkillsPage": { - "cb142070b4": "刷新", - "a68dee6a32": "搜索技能", - "f43ad6edf3": "技能", - "ea72d6185b": "无法扫描技能", - "dc4c3328ee": "显示文件", - "9963dff6d3": "未找到说明。", - "995fde8337": "无法显示技能文件", - "4acd6d68ec": "未找到技能", - "6a62a0168c": "无匹配项", - "08a321a984": "没有技能匹配当前搜索和筛选条件。", - "cd7893fbc1": "正在扫描技能", - "35b9a724a0": "可用", - "c13b82793c": "管理安装", - "aee7b99cc6": "从链接安装", - "filterProvider": "按 Agent 筛选", - "filterSource": "按来源筛选", - "allSources": "全部", - "clearFilters": "清除筛选", - "closeSkills": "关闭技能", - "closeTooltip": "关闭 · Esc", - "moreActions": "更多操作", - "sharedLinks": "共享链接", - "emptyCopy": "扫描的技能文件夹为空。请安装共享包,或添加技能后刷新。", - "retry": "重试", - "remoteShareNotice": "这些技能位于 {{host}}。请在该机器上打开“技能”以进行共享。", - "viewSwitch": "显示", - "searchLinks": "搜索链接", - "deleteSkills": "删除技能…" - }, - "SkillShareSelectionControls": { "01c5a15e02": "共享技能" }, - "SkillRow": { - "updatedUnknown": "无日期", - "pathCopied": "路径已复制", - "copyPath": "复制路径", - "detailPath": "路径", - "skillActions": "{{value0}} 的操作", - "viewDetails": "查看详情", - "deleteSkill": "删除…" - }, - "SkillsList": { "listLabel": "技能" }, - "sourceStatus": { - "missing": "未找到文件夹", - "remoteRepo": "远程仓库 — 未扫描", - "unavailable": "未扫描" - }, - "sources": { "heading": "技能文件夹" }, - "sourceKind": { - "home": "主目录", - "workspace": "工作区", - "bundled": "内置", - "plugin": "插件" - }, - "count": { - "skillOne": "{{count}} 个技能", - "skillOther": "{{count}} 个技能", - "sourceOne": "{{count}} 个来源", - "sourceOther": "{{count}} 个来源", - "fileOne": "{{count}} 个文件", - "fileOther": "{{count}} 个文件", - "resultOne": "{{count}} 个结果", - "resultOther": "{{count}} 个结果", - "selected": "已选择 {{count}} 个", - "shareOne": "共享 {{count}} 个技能", - "shareOther": "共享 {{count}} 个技能", - "linkOne": "{{count}} 个链接", - "linkOther": "{{count}} 个链接", - "deleteOne": "删除 {{count}} 个技能", - "deleteOther": "删除 {{count}} 个技能", - "deletedOne": "已删除 {{count}} 个技能", - "deletedOther": "已删除 {{count}} 个技能", - "deleteFolderOne": "{{count}} 个文件夹", - "deleteFolderOther": "{{count}} 个文件夹", - "deleteLinkOne": "{{count}} 个链接", - "deleteLinkOther": "{{count}} 个链接" - }, - "filter": { "allAgents": "所有 Agent", "sharedAgent": "共享 (.agents)" }, - "SkillsSelectionHeader": { - "exit": "退出选择", - "exitTooltip": "退出选择 · Esc", - "title": "选择要共享的技能", - "selectAll": "选择全部 {{count}} 个符合条件的项目", - "clear": "清除", - "deleteTitle": "选择要删除的技能" - }, - "SkillDetailDialog": { "agents": "Agent", "updated": "已更新", "copy": "复制" }, "SkillFreshnessNudge": { "titleOne": "已安装的 Orca 技能已过期", "titleMany": "{{value0}} 个已安装的 Orca 技能已过期", @@ -4074,35 +3988,6 @@ "SkillUpdateRow": { "oneLocation": "1 location", "manyLocations": "{{value0}} locations" - }, - "host": { - "local": "此机器", - "remote": "已连接的运行时" - }, - "SkillDelete": { - "dismissResults": "关闭", - "reasonBundled": "Orca 内置技能 — 删除后会恢复", - "reasonPlugin": "由插件安装 — 请改为移除插件", - "reasonUnowned": "此技能位于 Orca 技能文件夹之外 — 请在其存储位置删除", - "reasonMissing": "此技能已不在磁盘上", - "reasonStale": "此技能在列表加载后发生了变化 — 请刷新后重试", - "placementSummaryParts": "将在 {{roots}} 中删除 {{parts}}。", - "placementJoin": " 和 ", - "retainedSource": "技能本体仍保留在 {{path}}。", - "blockedLine": "{{count}} × {{reason}}", - "resultLine": "{{count}} × {{label}}", - "statusSkipped": "已跳过", - "statusBusy": "忙碌 — 另一项技能操作正在进行", - "statusPartial": "部分移除 — 某些文件仍以隐藏名称保留在磁盘上", - "statusFailed": "失败 — 未进行任何更改", - "statusDeleted": "已删除", - "confirmHost": "位于 {{host}}。", - "confirmPermanent": "此操作无法撤销。", - "failed": "无法删除技能", - "hostUnresolved": "仍在查找存储这些技能的计算机。", - "hostUpdateRequired": "请更新所选机器上的 Orca 以删除技能。", - "nothingOne": "无法从这里删除该技能。", - "nothingOther": "所选的 {{count}} 个技能均无法从这里删除。" } }, "sidebar": { @@ -6020,7 +5905,7 @@ "80c454e8a6": "将此与您的提供商的缓存 TTL 相匹配。" }, "GeneralEditorSettingsSection": { - "f80603d293": "在富文本编辑器模式和智能体切换操作中显示本地 Markdown 注释控件。", + "f80603d293": "在丰富的编辑器模式和智能体切换操作中显示本地 Markdown 注释控件。", "4edc104f0f": "Markdown 评审笔记", "5f02e6fb21": "在富文本编辑器模式下显示本地 Markdown 评审笔记控件。", "51161d1647": "编辑文件时显示小地图概述。", @@ -6197,9 +6082,10 @@ "compareBaseBranchUpstream": "分支上游" }, "HiddenExperimentalGroup": { - "7c4e18d2f6": "在本机启用粗体故障捕获手势:{{samplingShortcut}} 开始采样突发;{{captureShortcut}} 将窗格证据捕获到磁盘。", - "b09f24a51d": "终端渲染诊断", - "f2c81d904a": "隐藏实验设置" + "d0f914a528": "占位符切换", + "1014ddbfaf": "今天什么也不做。保留作为隐藏实验选项的第一个插槽。", + "232cf83de8": "用于内部测试的未列出的切换。这里什么都不支持。", + "3e9e827ca5": "隐藏实验" }, "InputPane": { "db15068196": "在 Linux 和 macOS 上默认启用。 Linux使用系统选择剪贴板;其他平台使用私有缓冲区。", @@ -9917,7 +9803,8 @@ "defaultTitle": "默认智能体", "nullDefaultAttention": "请选择一个默认智能体。在此之前,需人工干预的启动将无法开始,并会要求你选择一个。", "migrationBlockedTitle": "智能体设置暂时为只读状态", - "migrationBlockedDescription": "Orca 在更新前无法备份你现有的智能体设置,因此变更已被阻止以保护你的数据。请先解决下面的问题,然后重试。" + "migrationBlockedDescription": "Orca 在更新前无法备份你现有的智能体设置,因此变更已被阻止以保护你的数据。请先解决下面的问题,然后重试。", + "unavailable": "Custom agents are managed on the desktop host." }, "AgentCatalogStatusBadge": { "enabled": "已启用", @@ -10958,8 +10845,6 @@ "7a09d7f9d2": "根据", "383cf92c73": "树", "d7ae61269b": "已提交的更改", - "branchFilesChangedVsBaseOne": "与 {{ref}} 相比更改了 1 个文件", - "branchFilesChangedVsBaseOther": "与 {{ref}} 相比更改了 {{count}} 个文件", "48a003c1b1": "已暂存的更改", "d4ef4bafc5": "更改", "522f44dce5": "未跟踪文件", @@ -11034,10 +10919,10 @@ "b3c8f1a902": "按文件名筛选", "d4f8c2a901": "清除并关闭筛选器", "e8a1c4b203": "vs", - "compareBaseCommitsAheadOne": "领先 {{ref}} 1 个提交", - "compareBaseCommitsAheadOther": "领先 {{ref}} {{count}} 个提交", - "compareBaseCommitsBehindOne": "落后 {{ref}} 1 个提交", - "compareBaseCommitsBehindOther": "落后 {{ref}} {{count}} 个提交", + "f9b2441bb6": "领先 {{value0}} 1 个提交", + "b715ef615b": "领先 {{value1}} {{value0}} 个提交", + "c1a8f3e204": "落后 {{value0}} 1 个提交", + "d2b9g4f315": "落后 {{value1}} {{value0}} 个提交", "4b4a7de138": "在浏览器中打开审查页面", "createPrIntentCommitBlockedSummary": "提交被阻止:{{value0}} 修复问题后重试创建 PR。", "pushRecovery": { @@ -11060,6 +10945,7 @@ "a4e93c21d7": "当前分支:{{value0}}", "c7d4e2f801": "更改基引用:{{value0}}", "f3a1b8c204": "upstream", + "createPrIntentEmptyGeneratedBody": "生成的评审详情未包含描述。请重试创建 PR。", "createPrIntentGenerateDetailsFailed": "无法生成评审详情。请重试创建 PR。" }, "SourceControlAgentActionDialog": { @@ -13046,9 +12932,8 @@ "7e7ca60816": "更改的文件", "b6c3b84476": "显示文件树", "39f8007549": "评审冲突", - "skippedConflictsExcluded": "{{count}} 个未解决的冲突已被排除在此差异视图之外。", - "skippedConflictsExcluded_one": "{{count}} 个未解决的冲突已被排除在此差异视图之外。", - "skippedConflictsExcluded_other": "{{count}} 个未解决的冲突已被排除在此差异视图之外。", + "39e73e7181": "被排除在此差异视图之外。", + "689b99f8ad": "未解决的冲突", "820ec01f24": "冲突文件单独评审", "fd8892b120": "没有显示任何变化", "eb5f40e49c": "此差异视图排除了未解决的冲突,因为普通的双向差异管道不是冲突安全的。", @@ -13116,9 +13001,9 @@ "8915726e93": "复制路径" }, "EditorContent": { - "56dba34e1a": "(在源码模式下编辑)", + "56dba34e1a": "(在源模式下编辑)", "e4b074749d": "前言", - "9640d1d3db": "预览此差异的修改版本。切换到源码模式以检查更改。", + "9640d1d3db": "预览此差异的修改版本。切换到源模式以检查更改。", "78541e254e": "二进制文件已更改", "c88c73a0d3": "加载差异...", "b9de81ba52": "二进制文件——无法显示", @@ -13142,12 +13027,12 @@ "84cdc0794b": "重命名", "1bb1e226ec": "重命名文件 {{value0}}", "5447c4f68f": "目录", - "146cb5473c": "目录可在富文本模式或预览模式下使用", + "146cb5473c": "目录可在丰富模式或预览模式下使用", "e836faacfa": "切换到并排差异", "94756f08ba": "切换到内联差异", "c98ce191da": "此差异没有可打开的修改端文件", "9b80bbe1de": "打开文件选项卡", - "f0fd4174b5": "打开文件选项卡以使用富文本 Markdown 编辑", + "f0fd4174b5": "打开文件选项卡以使用丰富的 Markdown 编辑", "a10d9b8337": "打开文件", "2076ecfc9c": "上一个更改", "631dab0df3": "下一个更改" @@ -13169,8 +13054,8 @@ "4837f3f578": "更改", "ac3bb87913": "编辑", "0d193dc03c": "预览", - "aff15f94f5": "富文本编辑器", - "4d6ccb7ba6": "源码" + "aff15f94f5": "丰富的编辑器", + "4d6ccb7ba6": "来源" }, "ImageDiffViewer": { "a651be62b0": "修改的", @@ -13342,8 +13227,8 @@ }, "RichMarkdownErrorBoundary": { "aad0998127": "重试", - "4a5de9f2f0": "切换到源码模式,或单击“重试”以重新加载富文本视图。", - "dfdf1cacd4": "富文本 Markdown 编辑器遇到了意外错误,并被重置以保持 Orca 的其余部分响应。" + "4a5de9f2f0": "切换到源模式,或单击“重试”以重新加载丰富视图。", + "dfdf1cacd4": "丰富的 Markdown 编辑器遇到了意外错误,并被重置以保持 Orca 的其余部分响应。" }, "RichMarkdownLinkBubble": { "1c99b726e0": "删除链接", @@ -14010,7 +13895,6 @@ "showMore": "显示更多" }, "AutomationDetail": { - "host": "主机", "007c8ad874": "提示词", "a1d52c2189": "使用范围", "449fc83bf7": "token", @@ -14174,9 +14058,6 @@ "b11170a008": "无法保存自动化。", "2a20596d6b": "自动化已保存。", "244727e655": "自动化已更新。", - "moved": "自动化已移动到 {host}。", - "moveOriginalKept": "已在 {host} 上创建,但无法删除原有自动化。请在原主机上删除它。", - "moveOriginalUnverified": "已在 {host} 上创建,但无法确认是否删除了原有自动化。重试前请检查原主机。", "77b81bc4ac": "Hermes 自动化创建。", "08efc3ae12": "Hermes 自动化已更新。", "e431bb85d4": "选择与此 Hermes 自动化位于同一主机上的工作区。", @@ -14220,7 +14101,6 @@ "backToList": "全部自动化", "tableName": "名称", "tableProject": "项目", - "tableHost": "主机", "tableStatus": "状态", "tableActions": "操作", "newAutomation": "新建自动化", @@ -14373,19 +14253,6 @@ "filters": "筛选器", "clear": "清除筛选器", "removeFilter": "移除 {{value0}} 筛选器" - }, - "createDestination": { - "label": "主机", - "placeholder": "选择主机", - "storedOn": "由 {authority} 存储和调度。", - "unselected": "选择存储和调度此自动化的主机。", - "orphan": "没有主机的自动化无法容纳新的自动化。请选择要在其上创建此自动化的主机。", - "unavailable": "该主机尚无法容纳新的自动化。请选择另一台主机来创建它。", - "stale": "此表单打开期间 {host} 已更改。保存前请重新选择。", - "projectMismatch": "该项目不在 {host} 上。请选择该主机上的项目,或选择另一台主机。", - "noProjects": "{host} 上未设置任何项目。请在该主机上添加一个,或选择另一台主机。", - "updateRequired": "更新 {hosts} 上的 Orca 服务器即可在其中存储自动化。", - "move": "保存后会在 {host} 上创建此自动化,并删除原有自动化及其运行历史。" } }, "agent": { @@ -14740,15 +14607,6 @@ "delete": "删除" } }, -<<<<<<< HEAD - "ComposerParentWorktreePicker": { - "label": "父工作树", - "noParent": "无父工作树", - "description": "在侧边栏中将此工作区嵌套到另一个工作树下。不会更改基础分支。", - "searchPlaceholder": "搜索工作树...", - "noMatches": "无匹配项。" -||||||| parent of ebaa81ab2f (Rebase custom-agents onto main (2/4): renderer) -======= "AgentLaunchNotices": { "disabledFallback": "{{value0}} 已禁用。已启动标准 {{value1}},未使用自定义可执行文件、自定义参数或自定义代理环境。", "missingFallback": "{{value0}} 已删除。已启动标准 {{value1}},未使用自定义可执行文件、自定义参数或自定义代理环境。", @@ -14771,7 +14629,31 @@ "openTerminal": "打开终端", "forgetLaunch": "忘记启动…", "manageAgents": "管理代理" ->>>>>>> ebaa81ab2f (Rebase custom-agents onto main (2/4): renderer) + }, + "dataRecovery": { + "pointAgentCatalogPreV1Title": "Before the custom-agents update (agent catalog v1)", + "pointAgentCatalogPreV1Loss": "Restoring discards all settings and workspace metadata saved after this point, including any custom agents.", + "title": "Data recovery", + "description": "Restore this profile from a recovery point created before a data migration. Restoring never deletes the recovery point.", + "loading": "Looking for recovery points…", + "empty": "No recovery points exist for this profile.", + "createdAt": "Created {{date}}", + "createdUnknown": "Creation time unknown", + "previousBinary": "After restoring, Orca quits. Install the previous Orca version before opening it again.", + "restoring": "Restoring…", + "confirmRestore": "Restore and quit", + "cancel": "Cancel", + "prepareDowngrade": "Prepare downgrade…", + "restoreFailed": "Restore failed and no changes were made: {{error}}", + "close": "Close", + "noticeTitle": "Orca could not back up your data before a required update", + "noticeBody": "Your existing data is unchanged and built-in agents keep working, but custom-agent settings are read-only until this is resolved.", + "retrying": "Retrying…", + "retryMigration": "Retry migration", + "openDataRecovery": "Open Data recovery", + "copyDetails": "Copy details", + "settingsRowHint": "A pre-update recovery point exists for this profile.", + "settingsRowOpen": "Data recovery…" } }, "i18n": { @@ -14968,16 +14850,29 @@ }, "workspace": { "cleanup": { + "browse": { + "selectionWithheldOne": "当前筛选条件隐藏了 1 个已选工作区,已取消选择。", + "selectionWithheld": "当前筛选条件隐藏了 {{value0}} 个已选工作区,已取消选择。", + "selectAllCountOne": "选择 1 个通过安全检查的工作区", + "selectAllCount": "选择全部 {{value0}} 个通过安全检查的工作区" + }, + "scan": { + "readyOne": "找到 1 个工作区。", + "readyMany": "找到 {{value0}} 个工作区。" + }, + "presentationFixtures": { + "reviewAlphaCleanup": "评审 Alpha 清理" + }, + "presentation": { + "gitlabMergeRequestNumber": "MR #{{value0}}", + "githubPullRequestNumber": "PR #{{value0}}" + }, "browse": { "gitStatusCheckFailed": "Git 状态检查失败", "gitStatusUnverified": "无法验证 Git 状态", "deleteAnyway": "仍然删除", "forceDeleteProjectionOne": "当前有 {{count}} 个工作区显示风险,可能需要强制删除", "forceDeleteProjectionMany": "当前有 {{count}} 个工作区显示风险,可能需要强制删除", - "selectionWithheldOne": "当前筛选条件隐藏了 1 个已选工作区,已取消选择。", - "selectionWithheld": "当前筛选条件隐藏了 {{value0}} 个已选工作区,已取消选择。", - "selectAllCountOne": "选择 1 个通过安全检查的工作区", - "selectAllCount": "选择全部 {{value0}} 个通过安全检查的工作区", "appliedFilters": "已应用的筛选", "removeFilter": "移除筛选 {{value0}}", "chip": { @@ -15024,17 +14919,6 @@ "retainedAgents": "已完成的代理" } } - }, - "scan": { - "readyOne": "找到 1 个工作区。", - "readyMany": "找到 {{value0}} 个工作区。" - }, - "presentationFixtures": { - "reviewAlphaCleanup": "评审 Alpha 清理" - }, - "presentation": { - "gitlabMergeRequestNumber": "MR #{{value0}}", - "githubPullRequestNumber": "PR #{{value0}}" } } }, diff --git a/src/renderer/src/web/preload-api/web-worktrees-api.ts b/src/renderer/src/web/preload-api/web-worktrees-api.ts index ba8b494d02d..89e1495c18b 100644 --- a/src/renderer/src/web/preload-api/web-worktrees-api.ts +++ b/src/renderer/src/web/preload-api/web-worktrees-api.ts @@ -1,5 +1,6 @@ import type { PreloadApi } from '../../../../preload/api-types' import type { + CreateWorktreeResult, ForceDeleteWorktreeBranchResult, RemoveWorktreeResult } from '../../../../shared/worktree/create-types' @@ -25,6 +26,28 @@ import type { WorktreeRetryAgentLaunchResult } from '../../../../shared/agent-launch-worktree-recovery' import type { PendingAgentLaunchSummary } from '../../../../shared/agent-launch-pending-summary' +import type { AgentLaunchSpawnRequest } from '../../../../shared/agent-launch-spawn-request' +import { AGENT_LAUNCH_IDENTITY_RUNTIME_CAPABILITY } from '../../../../shared/protocol-version' +import { + resolveRemoteWorktreeCreateLaunchParams, + type RemoteWorktreeCreateLaunchParams +} from '../worktree-create-launch-compat' +import { getRemoteRuntimeStatus } from './web-runtime-calls' + +async function remoteHostSupportsAgentLaunchIdentity(): Promise { + try { + const status = await getRemoteRuntimeStatus() + return status.capabilities?.includes(AGENT_LAUNCH_IDENTITY_RUNTIME_CAPABILITY) === true + } catch { + return false + } +} + +function resolveWebWorktreeCreateLaunchParams( + agentLaunch: AgentLaunchSpawnRequest | undefined +): Promise { + return resolveRemoteWorktreeCreateLaunchParams(agentLaunch, remoteHostSupportsAgentLaunchIdentity) +} export function createWorktreesApi(): NonNullable['worktrees']> { return { @@ -53,7 +76,9 @@ export function createWorktreesApi(): NonNullable['worktrees listAll: () => listAllRuntimeWorktrees(), create: async (args) => { invalidateRuntimeWorktreeCaches() - const owned = await callRuntimeResultWithOwner<{ worktree: Worktree }>('worktree.create', { + const launchParams = await resolveWebWorktreeCreateLaunchParams(args.agentLaunch) + const dropEmptyStartupCommand = 'startupAgent' in launchParams && args.startup?.command === '' + const owned = await callRuntimeResultWithOwner('worktree.create', { repo: args.repoId, name: args.name, // Absent means user-typed, which is what the host must assume — so send it only when true. @@ -80,7 +105,7 @@ export function createWorktreesApi(): NonNullable['worktrees pendingFirstAgentMessageRename: args.pendingFirstAgentMessageRename, ...(args.startup ? { - startupCommand: args.startup.command, + ...(dropEmptyStartupCommand ? {} : { startupCommand: args.startup.command }), ...(args.startup.env ? { startupEnv: args.startup.env } : {}), ...(args.startup.launchConfig ? { startupLaunchConfig: args.startup.launchConfig } @@ -97,8 +122,11 @@ export function createWorktreesApi(): NonNullable['worktrees workspaceStatus: args.workspaceStatus, manualOrder: args.manualOrder, automationProvenanceRequest: args.automationProvenanceRequest, - ...(args.agentLaunch ? { agentLaunch: args.agentLaunch } : {}) + ...launchParams }) + if (!('worktree' in owned.result)) { + return owned.result + } return { ...owned.result, worktree: withRuntimeWorktreeOwner(owned.result.worktree, owned.hostId) diff --git a/src/renderer/src/web/worktree-create-launch-compat.test.ts b/src/renderer/src/web/worktree-create-launch-compat.test.ts new file mode 100644 index 00000000000..2582237a39c --- /dev/null +++ b/src/renderer/src/web/worktree-create-launch-compat.test.ts @@ -0,0 +1,49 @@ +import { describe, expect, it } from 'vitest' +import { resolveRemoteWorktreeCreateLaunchParams } from './worktree-create-launch-compat' +import type { AgentLaunchSpawnRequest } from '../../../shared/agent-launch-spawn-request' + +const supported = () => Promise.resolve(true) +const unsupported = () => Promise.resolve(false) + +function identityLaunch( + selection: AgentLaunchSpawnRequest['selection'], + prompt?: string +): AgentLaunchSpawnRequest { + return { selection, ...(prompt !== undefined ? { prompt } : {}) } +} + +describe('resolveRemoteWorktreeCreateLaunchParams', () => { + it('passes no launch through untouched', async () => { + expect(await resolveRemoteWorktreeCreateLaunchParams(undefined, unsupported)).toEqual({}) + }) + + it('sends identity-only agentLaunch to a capable host', async () => { + const launch = identityLaunch({ kind: 'agent', agent: 'claude' }, 'hello') + expect(await resolveRemoteWorktreeCreateLaunchParams(launch, supported)).toEqual({ + agentLaunch: launch + }) + }) + + it('falls back to the legacy startupAgent id for a built-in on a pre-identity host', async () => { + const launch = identityLaunch({ kind: 'agent', agent: 'claude' }, 'hello') + expect(await resolveRemoteWorktreeCreateLaunchParams(launch, unsupported)).toEqual({ + startupAgent: 'claude', + startupPrompt: 'hello' + }) + }) + + it('fails fast for a stored-default selection on a pre-identity host', async () => { + await expect( + resolveRemoteWorktreeCreateLaunchParams(identityLaunch({ kind: 'default' }), unsupported) + ).rejects.toThrow(/predates default-agent launch/) + }) + + it('fails fast for a custom id on a pre-identity host — never a client-assembled command', async () => { + await expect( + resolveRemoteWorktreeCreateLaunchParams( + identityLaunch({ kind: 'agent', agent: 'custom-agent:claude:abc' }), + unsupported + ) + ).rejects.toThrow(/predates custom agents/) + }) +}) diff --git a/src/renderer/src/web/worktree-create-launch-compat.ts b/src/renderer/src/web/worktree-create-launch-compat.ts new file mode 100644 index 00000000000..d6c8646e0fb --- /dev/null +++ b/src/renderer/src/web/worktree-create-launch-compat.ts @@ -0,0 +1,45 @@ +// Client-side skew contract for worktree.create against a remote host (compat +// design: "old host × new client"). A pre-identity host's schema silently +// strips the unknown `agentLaunch` key (bare-terminal worktree), so the client +// probes `agent-launch.identity.v1` and falls back to the legacy host-resolved +// `startupAgent` id, mirroring the CLI's resolveWorktreeCreateLaunchParams. + +import { isBuiltInTuiAgent } from '../../../shared/tui-agent-config' +import { translate } from '@/i18n/i18n' +import type { AgentLaunchSpawnRequest } from '../../../shared/agent-launch-spawn-request' + +export type RemoteWorktreeCreateLaunchParams = + | Record + | { agentLaunch: AgentLaunchSpawnRequest } + | { startupAgent: string; startupPrompt: string } + +export async function resolveRemoteWorktreeCreateLaunchParams( + agentLaunch: AgentLaunchSpawnRequest | undefined, + hostSupportsIdentity: () => Promise +): Promise { + if (!agentLaunch) { + return {} + } + if (await hostSupportsIdentity()) { + return { agentLaunch } + } + const selection = agentLaunch.selection + // FAIL-FAST: a pre-identity host cannot resolve a stored default or a custom id. + if (selection.kind !== 'agent') { + throw new Error( + translate( + 'auto.web.worktreeCreate.hostPredatesDefaultLaunch', + 'This Orca host predates default-agent launch. Pick a specific built-in agent or update Orca on the host.' + ) + ) + } + if (!isBuiltInTuiAgent(selection.agent)) { + throw new Error( + translate( + 'auto.web.worktreeCreate.hostPredatesCustomAgents', + 'This Orca host predates custom agents. Pick a built-in agent or update Orca on the host.' + ) + ) + } + return { startupAgent: selection.agent, startupPrompt: agentLaunch.prompt ?? '' } +} diff --git a/src/shared/agent-catalog-snapshot.ts b/src/shared/agent-catalog-snapshot.ts index cf78d71db17..9a0ad4a098d 100644 --- a/src/shared/agent-catalog-snapshot.ts +++ b/src/shared/agent-catalog-snapshot.ts @@ -60,6 +60,10 @@ export type AgentCatalogSnapshot = { disabledAgents: TuiAgent[] customAgents: SyncedCustomTuiAgent[] deletedCustomAgents: DeletedCustomTuiAgent[] + /** Set while the host's v1 migration is fail-closed behind a failed pinned + * backup: catalog/reference writes are read-only pending recovery ON THE + * HOST. Boolean only — the error text stays host-local, never on the wire. */ + migrationBlocked?: true } export type AgentProjectionStatus = @@ -96,6 +100,10 @@ export type LocalAgentCatalogSnapshot = Omit