From 7c77ccab7dc858cd5dd0538688638e9cfd1cb7c8 Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Sat, 4 Jul 2026 00:10:53 -0700 Subject: [PATCH 01/45] Fix notes send targets for manual agents (#7300) * Fix notes send targets for manual agents * Split agent title merging into manual and launch-agent paths Separate the merging logic for terminal titles depending on whether the tab has a launch agent or is a manually started CLI. - Launch-agent tabs carry an owner bit, allowing their live titles to promote a stale status row on the same pane. - Manually started agents have no owner bit, so they only ever add a row and must not override existing status evidence. - Remove the temporary TitleHintAgentTarget type and the need to strip metadata when pushing targets. --- .../src/lib/notes-send-agent-targets.test.ts | 105 +++++++++++++++++- .../src/lib/notes-send-agent-targets.ts | 90 +++++++++------ src/shared/terminal-title-agent-type.ts | 2 +- 3 files changed, 158 insertions(+), 39 deletions(-) diff --git a/src/renderer/src/lib/notes-send-agent-targets.test.ts b/src/renderer/src/lib/notes-send-agent-targets.test.ts index a87ba7392c9..e7b4178df2b 100644 --- a/src/renderer/src/lib/notes-send-agent-targets.test.ts +++ b/src/renderer/src/lib/notes-send-agent-targets.test.ts @@ -14,6 +14,7 @@ import { const WORKTREE_ID = 'wt-1' const STATUS_TAB_ID = 'tab-status' const LAUNCH_TAB_ID = 'tab-launch' +const MANUAL_TAB_ID = 'tab-manual' const LEAF_A = '11111111-1111-4111-8111-111111111111' const LEAF_B = '22222222-2222-4222-8222-222222222222' const NOW = 10_000 @@ -201,6 +202,70 @@ describe('notes send agent targets', () => { ]) }) + it('lists a manual agent tab with a recognized idle pane title and live PTY', () => { + const targets = deriveNotesSendAgentTargets( + state({ + tabsByWorktree: { + [WORKTREE_ID]: [tab(MANUAL_TAB_ID, { title: 'Terminal 2' })] + }, + terminalLayoutsByTabId: { [MANUAL_TAB_ID]: leafLayout(LEAF_B, 'pty-b') }, + runtimePaneTitlesByTabId: { [MANUAL_TAB_ID]: { 1: 'Codex ready' } } + }), + WORKTREE_ID, + NOW + ) + + expect(targets).toEqual([ + { + paneKey: makePaneKey(MANUAL_TAB_ID, LEAF_B), + tabId: MANUAL_TAB_ID, + leafId: LEAF_B, + agentType: 'codex', + tabTitle: 'Terminal 2', + status: 'eligible' + } + ]) + }) + + it('skips a manual agent tab with an unrecognized booting pane title', () => { + const targets = deriveNotesSendAgentTargets( + state({ + tabsByWorktree: { + [WORKTREE_ID]: [tab(MANUAL_TAB_ID, { title: 'Terminal 2' })] + }, + terminalLayoutsByTabId: { [MANUAL_TAB_ID]: leafLayout(LEAF_B, 'pty-b') }, + runtimePaneTitlesByTabId: { [MANUAL_TAB_ID]: { 1: 'zsh' } } + }), + WORKTREE_ID, + NOW + ) + + expect(targets).toEqual([]) + }) + + it('disables a manual agent tab with a permission pane title', () => { + const targets = deriveNotesSendAgentTargets( + state({ + tabsByWorktree: { + [WORKTREE_ID]: [tab(MANUAL_TAB_ID, { title: 'Terminal 2' })] + }, + terminalLayoutsByTabId: { [MANUAL_TAB_ID]: leafLayout(LEAF_B, 'pty-b') }, + runtimePaneTitlesByTabId: { [MANUAL_TAB_ID]: { 1: 'Codex - action required' } } + }), + WORKTREE_ID, + NOW + ) + + expect(targets).toEqual([ + expect.objectContaining({ + paneKey: makePaneKey(MANUAL_TAB_ID, LEAF_B), + agentType: 'codex', + status: 'disabled', + disabledReason: 'Agent needs permission' + }) + ]) + }) + it('recognizes a launch-agent tab by its explicit ready tab title when no pane title is set', () => { const targets = deriveNotesSendAgentTargets( state({ @@ -351,6 +416,31 @@ describe('notes send agent targets', () => { }) }) + it('does not duplicate a status-backed tab with a manual title fallback', () => { + const paneKey = makePaneKey(MANUAL_TAB_ID, LEAF_A) + const targets = deriveNotesSendAgentTargets( + state({ + agentStatusByPaneKey: { [paneKey]: entry(paneKey, 'working') }, + tabsByWorktree: { + [WORKTREE_ID]: [tab(MANUAL_TAB_ID, { title: 'Codex ready' })] + }, + terminalLayoutsByTabId: { + [MANUAL_TAB_ID]: splitLayout(LEAF_B, { [LEAF_A]: 'pty-a', [LEAF_B]: 'pty-b' }) + }, + runtimePaneTitlesByTabId: { [MANUAL_TAB_ID]: { 2: 'Codex ready' } } + }), + WORKTREE_ID, + NOW + ) + + expect(targets).toHaveLength(1) + expect(targets[0]).toMatchObject({ + tabId: MANUAL_TAB_ID, + leafId: LEAF_A, + status: 'eligible' + }) + }) + it('promotes a stale status-backed launch-agent pane when live title and PTY prove it is sendable', () => { const paneKey = makePaneKey(LAUNCH_TAB_ID, LEAF_B) const targets = deriveNotesSendAgentTargets( @@ -545,17 +635,22 @@ describe('notes send agent targets', () => { expect(targets).toEqual([]) }) - it('does not list a plain terminal tab without a launch agent or status', () => { + it('recognizes a manual agent tab by its explicit ready tab title when no pane title is set', () => { const targets = deriveNotesSendAgentTargets( state({ - tabsByWorktree: { [WORKTREE_ID]: [tab(LAUNCH_TAB_ID, { title: 'Codex ready' })] }, - terminalLayoutsByTabId: { [LAUNCH_TAB_ID]: leafLayout(LEAF_B, 'pty-b') }, - runtimePaneTitlesByTabId: { [LAUNCH_TAB_ID]: { 1: 'Codex ready' } } + tabsByWorktree: { [WORKTREE_ID]: [tab(MANUAL_TAB_ID, { title: 'Codex ready' })] }, + terminalLayoutsByTabId: { [MANUAL_TAB_ID]: leafLayout(LEAF_B, 'pty-b') } }), WORKTREE_ID, NOW ) - expect(targets).toEqual([]) + expect(targets).toEqual([ + expect.objectContaining({ + paneKey: makePaneKey(MANUAL_TAB_ID, LEAF_B), + agentType: 'codex', + status: 'eligible' + }) + ]) }) }) diff --git a/src/renderer/src/lib/notes-send-agent-targets.ts b/src/renderer/src/lib/notes-send-agent-targets.ts index 3ce0e61f7fa..163907d99fc 100644 --- a/src/renderer/src/lib/notes-send-agent-targets.ts +++ b/src/renderer/src/lib/notes-send-agent-targets.ts @@ -1,6 +1,7 @@ import type { AgentType } from '../../../shared/agent-status-types' import type { AppState } from '@/store/types' import { isTerminalLeafId, makePaneKey } from '../../../shared/stable-pane-id' +import { resolveTerminalTitleAgentType } from '../../../shared/terminal-title-agent-type' import type { TerminalTab } from '../../../shared/types' import { detectAgentSendTitleStatus } from './agent-send-title-status' import { @@ -25,19 +26,26 @@ export type NotesSendAgentTarget = { disabledReason?: string } -function detectLaunchAgentPaneStatus( +type AgentTitleEvidence = { + status: NonNullable> + title: string +} + +function detectTitleHintPaneEvidence( paneTitleResolution: RuntimePaneTitleLeafResolution, tabTitle: string -) { +): AgentTitleEvidence | null { if (paneTitleResolution.title !== null) { - return detectAgentSendTitleStatus(paneTitleResolution.title) + const status = detectAgentSendTitleStatus(paneTitleResolution.title) + return status ? { status, title: paneTitleResolution.title } : null } // Why: mirror isTerminalRunningAgent — the OSC-enriched tab title only counts // when the leaf has no runtime pane title of its own yet. if (paneTitleResolution.hasAnyPaneTitle) { return null } - return detectAgentSendTitleStatus(tabTitle) + const status = detectAgentSendTitleStatus(tabTitle) + return status ? { status, title: tabTitle } : null } /** @@ -46,11 +54,12 @@ function detectLaunchAgentPaneStatus( * Why this exists on top of deriveRunningAgentSendTargets: that derivation only * sees panes with a live status entry, so a freshly launched (still idle) agent * stays invisible until its first hook event — i.e. until the user talks to it. - * We augment it with launch-agent tabs whose pane still has a live PTY: - * TerminalTab.launchAgent records the harness Orca started and is the same - * pre-hook signal the tab bar already trusts for its provider icon. + * We augment it with recognized agent-title tabs whose pane still has a live + * PTY. TerminalTab.launchAgent records the harness Orca started; manually typed + * CLIs do not have that owner bit, so their runtime title is the only pre-hook + * signal available. * - * The launch hint is gated on a recognized agent title (pane or tab) — the same + * The title hint is gated on a recognized agent title (pane or tab) — the same * signal isTerminalRunningAgent checks — so a freshly spawned tab is only listed * once the runtime would actually accept the send. Without that gate, clicking a * still-booting pane fails with "not a recognized agent session". @@ -73,12 +82,19 @@ export function deriveNotesSendAgentTargets( ) for (const tab of state.tabsByWorktree[worktreeId] ?? []) { - const launchTarget = deriveLaunchAgentTarget(state, tab) - if (!launchTarget) { + const titleHintTarget = deriveTitleHintAgentTarget(state, tab) + if (!titleHintTarget) { continue } - mergeLaunchAgentTarget(targets, launchTarget) + // Why: a launchAgent tab carries an owner bit, so its live title can promote a + // stale status row on the same pane. A manually started CLI has no owner bit, + // so it only ever adds a row and must not override existing status evidence. + if (tab.launchAgent) { + mergeLaunchAgentTitleTarget(targets, titleHintTarget) + } else { + mergeManualAgentTitleTarget(targets, titleHintTarget) + } } return targets @@ -94,14 +110,10 @@ function resolveNotesTargetAgentType( return launchAgent ?? entryAgentType } -function deriveLaunchAgentTarget( +function deriveTitleHintAgentTarget( state: NotesSendAgentTargetState, tab: TerminalTab ): NotesSendAgentTarget | null { - if (!tab.launchAgent) { - return null - } - const layout = state.terminalLayoutsByTabId[tab.id] const leafId = layout?.activeLeafId if (!leafId || !isTerminalLeafId(leafId)) { @@ -115,32 +127,44 @@ function deriveLaunchAgentTarget( const paneTitles = state.runtimePaneTitlesByTabId[tab.id] const paneTitleResolution = resolveRuntimePaneTitleLeafResolution(layout, paneTitles, leafId) - const launchStatus = detectLaunchAgentPaneStatus(paneTitleResolution, tab.title) - if (!launchStatus) { + const titleEvidence = detectTitleHintPaneEvidence(paneTitleResolution, tab.title) + if (!titleEvidence) { // Why: launchAgent is set the instant Orca spawns the tab, but the runtime - // only accepts a send once the pane reads as an agent. Skipping until the - // title is recognized keeps "listed ⇒ sendable" and avoids the boot-window - // "not a recognized agent session" error. + // only accepts a send once the pane reads as an agent; manually started + // CLIs need the same title proof before appearing in the send menu. return null } - const disabledReason = launchStatus === 'permission' ? 'Agent needs permission' : undefined + const disabledReason = + titleEvidence.status === 'permission' ? 'Agent needs permission' : undefined return { paneKey: makePaneKey(tab.id, leafId), tabId: tab.id, leafId, - agentType: tab.launchAgent, + agentType: tab.launchAgent ?? resolveTerminalTitleAgentType(titleEvidence.title), tabTitle: tab.title, status: disabledReason ? 'disabled' : 'eligible', ...(disabledReason ? { disabledReason } : {}) } } -function mergeLaunchAgentTarget( +// Why: a manual title hint is the weakest signal, so never duplicate a tab that +// already has a status-backed or launch-agent row — even a disabled one. +function mergeManualAgentTitleTarget( targets: NotesSendAgentTarget[], - launchTarget: NotesSendAgentTarget + target: NotesSendAgentTarget ): void { - const samePaneIndex = targets.findIndex((target) => target.paneKey === launchTarget.paneKey) + if (targets.some((existing) => existing.tabId === target.tabId)) { + return + } + targets.push(target) +} + +function mergeLaunchAgentTitleTarget( + targets: NotesSendAgentTarget[], + target: NotesSendAgentTarget +): void { + const samePaneIndex = targets.findIndex((existing) => existing.paneKey === target.paneKey) if (samePaneIndex !== -1) { const existing = targets[samePaneIndex] if (existing.status === 'eligible' || existing.disabledReason === 'Agent needs permission') { @@ -151,12 +175,12 @@ function mergeLaunchAgentTarget( // same live launch-agent pane has a fresh title proof, prefer the sendable // runtime evidence over the stale retained status row. targets[samePaneIndex] = { - ...launchTarget, + ...target, agentType: existing.agentType && existing.agentType !== 'unknown' ? existing.agentType - : launchTarget.agentType, - tabTitle: existing.tabTitle || launchTarget.tabTitle + : target.agentType, + tabTitle: existing.tabTitle || target.tabTitle } return } @@ -165,13 +189,13 @@ function mergeLaunchAgentTarget( // be a split shell pane, which would list a second bogus row for the same tab. if ( targets.some( - (target) => - target.tabId === launchTarget.tabId && - (target.status === 'eligible' || target.disabledReason === 'Agent needs permission') + (existing) => + existing.tabId === target.tabId && + (existing.status === 'eligible' || existing.disabledReason === 'Agent needs permission') ) ) { return } - targets.push(launchTarget) + targets.push(target) } diff --git a/src/shared/terminal-title-agent-type.ts b/src/shared/terminal-title-agent-type.ts index 6e453e5cb75..3671ef92998 100644 --- a/src/shared/terminal-title-agent-type.ts +++ b/src/shared/terminal-title-agent-type.ts @@ -52,7 +52,7 @@ function isGenericClaudeStatusClaim(title: string, titleAgent: TuiAgent | null): ) } -function resolveTerminalTitleAgentType(title: string): TuiAgent | null { +export function resolveTerminalTitleAgentType(title: string): TuiAgent | null { const label = getAgentLabel(title) return label ? (TITLE_LABEL_TO_AGENT[label] ?? null) : null } From 0982882436f4dfd896544e6485c1e50bb46e0de4 Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Sat, 4 Jul 2026 00:21:55 -0700 Subject: [PATCH 02/45] Exclude submodule worktree-only changes from Stage All action (#7298) - Prevent the "Stage All" button from being enabled when only nested submodule worktree changes are present. - Remove the disabled stage button on submodule worktree-only rows, replacing it with an explanatory tooltip. - Fix WSL terminal environment assertion in daemon PTY tests to allow inherited agent-hook environment variables. --- src/main/daemon/pty-subprocess.test.ts | 17 ++-- .../SourceControl.preview-open.test.tsx | 78 +++++++++++++++++-- .../right-sidebar/SourceControl.tsx | 35 +++++---- 3 files changed, 102 insertions(+), 28 deletions(-) diff --git a/src/main/daemon/pty-subprocess.test.ts b/src/main/daemon/pty-subprocess.test.ts index 98e0b863b47..85366a1377b 100644 --- a/src/main/daemon/pty-subprocess.test.ts +++ b/src/main/daemon/pty-subprocess.test.ts @@ -2028,15 +2028,14 @@ describe('createPtySubprocess', () => { } } - expect(spawnMock).toHaveBeenCalledWith( - 'wsl.exe', - expect.any(Array), - expect.objectContaining({ - env: expect.objectContaining({ - ORCA_TERMINAL_HANDLE: 'term_wsl', - WSLENV: 'FOO/u:ORCA_TERMINAL_HANDLE/u:POWERLEVEL9K_DISABLE_CONFIGURATION_WIZARD' - }) - }) + const spawnCall = spawnMock.mock.calls.at(-1)! + expect(spawnCall[0]).toBe('wsl.exe') + expect(spawnCall[1]).toEqual(expect.any(Array)) + expect(spawnCall[2].env.ORCA_TERMINAL_HANDLE).toBe('term_wsl') + // Why: the daemon inherits optional agent-hook env in development. This + // test owns only the terminal handle and Powerlevel10k WSLENV contract. + expect(spawnCall[2].env.WSLENV?.split(':')).toEqual( + expect.arrayContaining(['FOO/u', 'ORCA_TERMINAL_HANDLE/u', POWERLEVEL10K_WIZARD_DISABLE_ENV]) ) }) diff --git a/src/renderer/src/components/right-sidebar/SourceControl.preview-open.test.tsx b/src/renderer/src/components/right-sidebar/SourceControl.preview-open.test.tsx index 61bb6ac3634..6d0121157bb 100644 --- a/src/renderer/src/components/right-sidebar/SourceControl.preview-open.test.tsx +++ b/src/renderer/src/components/right-sidebar/SourceControl.preview-open.test.tsx @@ -47,6 +47,7 @@ const mocks = vi.hoisted(() => { openBranchDiff: vi.fn(), createEmptySplitGroup: vi.fn(), discardRuntimeGitPath: vi.fn(), + bulkStageRuntimeGitPaths: vi.fn(), refreshGitStatusForWorktree: vi.fn(), requestEditorSaveQuiesce: vi.fn(), notifyEditorExternalFileChange: vi.fn() @@ -85,7 +86,8 @@ vi.mock('@/runtime/runtime-git-client', async (importOriginal) => { const actual = await importOriginal>() return { ...actual, - discardRuntimeGitPath: mocks.calls.discardRuntimeGitPath + discardRuntimeGitPath: mocks.calls.discardRuntimeGitPath, + bulkStageRuntimeGitPaths: mocks.calls.bulkStageRuntimeGitPaths } }) @@ -140,6 +142,7 @@ function resetState(overrides: Partial> = {}): void { vi.clearAllMocks() mocks.calls.createEmptySplitGroup.mockReturnValue('group-2') mocks.calls.discardRuntimeGitPath.mockResolvedValue(undefined) + mocks.calls.bulkStageRuntimeGitPaths.mockResolvedValue(undefined) mocks.calls.refreshGitStatusForWorktree.mockResolvedValue(undefined) mocks.calls.requestEditorSaveQuiesce.mockResolvedValue(undefined) mocks.state = { @@ -437,13 +440,76 @@ describe('SourceControl preview row opens', () => { const row = container.querySelector( '[data-source-control-path="packages/nested"]' ) - expect(row?.textContent).toContain('Submodule changes - stage inside submodule') + expect(row?.textContent).toContain('Stage inside submodule') + expect( + row?.querySelector('[title*="cannot stage file changes inside a submodule"]') + ).not.toBeNull() - const stageButton = row?.querySelector( - 'button[aria-label="Stage these changes inside the submodule"]' + expect(row?.querySelector('button[aria-label="Stage"]')).toBeNull() + }) + + it('does not render a commit-area Stage All primary for submodule worktree-only rows', () => { + resetState({ + gitStatusByWorktree: { + [mocks.activeWorktree.id]: [ + gitEntry({ + path: 'june-11th-launch', + submodule: { commitChanged: false, trackedChanges: true, untrackedChanges: false } + }) + ] + } + }) + renderSourceControl() + + const row = container.querySelector( + '[data-source-control-path="june-11th-launch"]' + ) + expect(row?.textContent).toContain('Stage inside submodule') + + const stageAllButton = [...container.querySelectorAll('button')].find( + (button) => button.textContent?.trim() === 'Stage All' + ) + expect(stageAllButton).toBeUndefined() + + const commitButton = [...container.querySelectorAll('button')].find( + (button) => button.textContent?.trim() === 'Commit' + ) + expect(commitButton).toBeDefined() + expect(commitButton?.disabled).toBe(true) + + act(() => { + commitButton?.dispatchEvent(new MouseEvent('click', { bubbles: true })) + }) + expect(mocks.calls.bulkStageRuntimeGitPaths).not.toHaveBeenCalled() + }) + + it('keeps the commit-area Stage All primary for changed submodule gitlinks', async () => { + resetState({ + gitStatusByWorktree: { + [mocks.activeWorktree.id]: [ + gitEntry({ + path: 'packages/nested', + submodule: { commitChanged: true, trackedChanges: false, untrackedChanges: false } + }) + ] + } + }) + renderSourceControl() + + const stageAllButton = [...container.querySelectorAll('button')].find( + (button) => button.textContent?.trim() === 'Stage All' + ) + expect(stageAllButton).toBeDefined() + expect(stageAllButton?.disabled).toBe(false) + + await act(async () => { + stageAllButton?.dispatchEvent(new MouseEvent('click', { bubbles: true })) + await Promise.resolve() + }) + expect(mocks.calls.bulkStageRuntimeGitPaths).toHaveBeenCalledWith( + expect.objectContaining({ worktreeId: mocks.activeWorktree.id, worktreePath: '/repo/wt' }), + ['packages/nested'] ) - expect(stageButton).not.toBeNull() - expect(stageButton?.getAttribute('aria-disabled')).toBe('true') }) it('passes preview=true when a plain branch row click opens a branch diff tab', () => { diff --git a/src/renderer/src/components/right-sidebar/SourceControl.tsx b/src/renderer/src/components/right-sidebar/SourceControl.tsx index 59a44672999..11803eee85e 100644 --- a/src/renderer/src/components/right-sidebar/SourceControl.tsx +++ b/src/renderer/src/components/right-sidebar/SourceControl.tsx @@ -536,8 +536,9 @@ const SOURCE_CONTROL_TREE_DIRECTORY_PADDING_PX = 8 const SOURCE_CONTROL_TREE_FILE_PADDING_PX = 20 const EMPTY_GIT_HISTORY_STATE: GitHistoryPanelState = { status: 'idle' } const DEFAULT_COLLAPSED_SECTIONS = ['history'] as const -const SUBMODULE_WORKTREE_ONLY_LABEL = 'Submodule changes - stage inside submodule' -const SUBMODULE_WORKTREE_ONLY_STAGE_TOOLTIP = 'Stage these changes inside the submodule' +const SUBMODULE_WORKTREE_ONLY_LABEL = 'Stage inside submodule' +const SUBMODULE_WORKTREE_ONLY_TOOLTIP = + 'The parent repo (including Stage All) cannot stage file changes inside a submodule' const SUBMODULE_LOADING_LABEL = 'Loading submodule changes…' const SUBMODULE_EMPTY_LABEL = 'No changes in submodule' const SUBMODULE_ERROR_LABEL = 'Failed to load submodule changes' @@ -3995,7 +3996,12 @@ function SourceControlInner(): React.JSX.Element { ]) const hasUnstagedChanges = grouped.unstaged.length > 0 || grouped.untracked.length > 0 - const hasStageableChanges = hasUnstagedChanges + const hasStageableChanges = useMemo( + () => + grouped.unstaged.some(isStageableStatusEntry) || + grouped.untracked.some(isStageableStatusEntry), + [grouped.unstaged, grouped.untracked] + ) const hasPartiallyStagedChanges = useMemo(() => { if (grouped.staged.length === 0 || grouped.unstaged.length === 0) { return false @@ -8115,9 +8121,17 @@ const UncommittedEntryRow = React.memo(function UncommittedEntryRow({ {dirPath} )} - {(conflictLabel || isSubmoduleWorktreeOnly) && ( -
- {conflictLabel ?? SUBMODULE_WORKTREE_ONLY_LABEL} + {conflictLabel && ( +
{conflictLabel}
+ )} + {isSubmoduleWorktreeOnly && ( + // Why: parent git can stage a changed gitlink, but not nested + // worktree dirtiness. Keep that boundary visible in the row. +
+ {SUBMODULE_WORKTREE_ONLY_LABEL}
)}
@@ -8176,19 +8190,14 @@ const UncommittedEntryRow = React.memo(function UncommittedEntryRow({ }} /> )} - {(canStage || isSubmoduleWorktreeOnly) && ( + {canStage && ( { event.stopPropagation() void onStage(entry.path) }} - disabled={isSubmoduleWorktreeOnly} /> )} {canUnstage && ( From 0ebfc989cb812c35ce29d1e5f5ab506bbf2b5fbf Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 4 Jul 2026 00:25:21 -0700 Subject: [PATCH 03/45] Fix flaky terminal-rendering-golden repo-load race on macOS CI (#7330) The release-blocking `terminal rendering golden mac` job was failing ~40% of Cut Release runs with `Expected e2e repo to be loaded`, leaving the RC stuck as a draft (publish-release depends on this job). Root cause: the sharedPage fixture did a single-shot fetchRepos() + find() + throw. window.api.repos.add() fires a repos:changed echo that triggers a concurrent fetchRepos() in the renderer; the store's reposFetchGeneration guard then drops the fixture's own awaited fetch result, leaving `repos` briefly stale, so find() returns undefined and throws. The repo lands a few ms later (the failure screenshot's sidebar actually shows it). Wrap the repo load in expect.poll (matching the seeded-worktree poll right below it) so it retries fetchRepos until the repo lands, then runs the idempotent updateRepo. Also harden the single-shot hasWebgl/cursorHidden diagnostics reads in the golden spec: WebGL reattaches asynchronously after a worktree switch, so poll those eventually-consistent fields until they settle before the golden asserts. Regression detection is preserved: a real WebGL/cursor regression times out the poll and still fails the test; the geometry/wrap/overpaint golden checks stay single-shot. --- tests/e2e/helpers/orca-app.ts | 49 ++++++++++++------- ...nal-raw-emoji-table-scroll-restore.spec.ts | 46 +++++++++++++++-- 2 files changed, 74 insertions(+), 21 deletions(-) diff --git a/tests/e2e/helpers/orca-app.ts b/tests/e2e/helpers/orca-app.ts index ecfa238d9d4..64dfe9d2148 100644 --- a/tests/e2e/helpers/orca-app.ts +++ b/tests/e2e/helpers/orca-app.ts @@ -314,23 +314,38 @@ export const test = base.extend({ await window.api.repos.add({ path: repoPath }) }, repoPath) - // Fetch repos in the renderer store so it picks up the new repo - await page.evaluate(async (repoPath) => { - const store = window.__store - if (!store) { - return - } - - await store.getState().fetchRepos() - const repo = store.getState().repos.find((candidate) => candidate.path === repoPath) - if (!repo) { - throw new Error(`Expected e2e repo to be loaded: ${repoPath}`) - } - // Why: the fixture deliberately creates external Git worktrees. New - // repos hide those by default after the visibility rollout, so opt this - // disposable repo into showing them before specs assert on worktree state. - await store.getState().updateRepo(repo.id, { externalWorktreeVisibility: 'show' }) - }, repoPath) + // Fetch repos in the renderer store so it picks up the new repo, then opt + // this disposable repo into showing external worktrees. + // Why: repos.add() fires a repos:changed echo that triggers a *concurrent* + // fetchRepos() in the renderer; the store's generation guard can then drop + // this awaited fetch's result, leaving `repos` briefly stale. Poll the + // public fetch path until the repo lands instead of asserting on the first + // tick (mirrors the seeded-worktree poll below). updateRepo is idempotent, + // so running it once the repo appears is safe across poll ticks. + await playwrightExpect + .poll( + () => + page.evaluate(async (repoPath) => { + const store = window.__store + if (!store) { + return false + } + await store.getState().fetchRepos() + const repo = store.getState().repos.find((candidate) => candidate.path === repoPath) + if (!repo) { + return false + } + // Why: the fixture deliberately creates external Git worktrees. New + // repos hide those by default after the visibility rollout. + await store.getState().updateRepo(repo.id, { externalWorktreeVisibility: 'show' }) + return true + }, repoPath), + { + timeout: 30_000, + message: `Expected e2e repo to be loaded: ${repoPath}` + } + ) + .toBe(true) // Wait for the repo to appear and fetch its worktrees await page.evaluate(async () => { diff --git a/tests/e2e/terminal-raw-emoji-table-scroll-restore.spec.ts b/tests/e2e/terminal-raw-emoji-table-scroll-restore.spec.ts index 043fbe61012..238bd4f9d26 100644 --- a/tests/e2e/terminal-raw-emoji-table-scroll-restore.spec.ts +++ b/tests/e2e/terminal-raw-emoji-table-scroll-restore.spec.ts @@ -486,9 +486,28 @@ test.describe('Terminal raw emoji table scroll restore repro', () => { await sendToTerminal(orcaPage, ptyId, `printf ${JSON.stringify(`${marker}\\n`)}\r`) await waitForTerminalOutput(orcaPage, marker, 10_000) - const diagnostics = await readTerminalRenderDiagnostics(orcaPage) + const expectedWebgl = await expectAutoWebgl(orcaPage) + // Why: WebGL (re)attaches asynchronously via React visibility effects and a + // transient ESC[?25l during a redraw can momentarily set cursorHidden. Let + // those eventually-consistent fields settle before the single-shot golden + // asserts so runner timing can't flake-block the release. hasComplexScriptOutput + // stays single-shot: its not-ready default is also false, so timing can't + // turn it into a false failure. + let diagnostics = await readTerminalRenderDiagnostics(orcaPage) + await expect + .poll( + async () => { + diagnostics = await readTerminalRenderDiagnostics(orcaPage) + return diagnostics.hasWebgl === expectedWebgl && diagnostics.cursorHidden === false + }, + { + timeout: 15_000, + message: `terminal render diagnostics did not settle (expected hasWebgl=${expectedWebgl}, cursorHidden=false)` + } + ) + .toBe(true) expect(diagnostics.hasComplexScriptOutput).toBe(false) - expect(diagnostics.hasWebgl).toBe(await expectAutoWebgl(orcaPage)) + expect(diagnostics.hasWebgl).toBe(expectedWebgl) expect(diagnostics.cursorHidden).toBe(false) }) @@ -553,7 +572,26 @@ test.describe('Terminal raw emoji table scroll restore repro', () => { await scrollActiveTerminalToText(orcaPage, 'Singer') await closeFeatureTips(orcaPage) - const diagnostics = await readTerminalRenderDiagnostics(orcaPage) + const expectedWebgl = await expectAutoWebgl(orcaPage) + // Why: after the worktree switch, WebGL reattaches asynchronously (React + // visibility effect + attach backoff) and a transient ESC[?25l during the + // restore redraw can momentarily set cursorHidden. Let those settle before + // the single-shot golden asserts so runner timing can't flake-block the + // release; the geometry/wrap/overpaint checks below stay single-shot as the + // real regression signal. + let diagnostics = await readTerminalRenderDiagnostics(orcaPage) + await expect + .poll( + async () => { + diagnostics = await readTerminalRenderDiagnostics(orcaPage) + return diagnostics.hasWebgl === expectedWebgl && diagnostics.cursorHidden === false + }, + { + timeout: 15_000, + message: `terminal render diagnostics did not settle (expected hasWebgl=${expectedWebgl}, cursorHidden=false)` + } + ) + .toBe(true) const overpaint = await readTerminalRightEdgeOverpaint(orcaPage) const wrapDiagnostics = await readTerminalBoxTableWrapDiagnostics(orcaPage) const singerGeometry = await readVisibleSingerRowGeometry(orcaPage) @@ -579,7 +617,7 @@ test.describe('Terminal raw emoji table scroll restore repro', () => { expect(wrapDiagnostics.cols).toBeGreaterThanOrEqual(RAW_EMOJI_BOX_TABLE_WIDTH) expect(diagnostics.hasComplexScriptOutput).toBe(false) - expect(diagnostics.hasWebgl).toBe(await expectAutoWebgl(orcaPage)) + expect(diagnostics.hasWebgl).toBe(expectedWebgl) expect(diagnostics.cursorHidden).toBe(false) expect(overpaint.offenders).toEqual([]) expect(wrapDiagnostics.wrappedBoxLines).toEqual([]) From 8070e657787f0d76fc4a56dfd29d6bfcc6140bca Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 4 Jul 2026 07:27:11 +0000 Subject: [PATCH 04/45] release: v1.4.122-rc.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8d4dab0a577..52cae65e873 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "orca", - "version": "1.4.122-rc.2", + "version": "1.4.122-rc.3", "description": "Next-gen IDE for parallel agentic development", "homepage": "https://github.com/stablyai/orca", "author": "stablyai", From f215a48064cf41231f76139f12d66b72094fe11b Mon Sep 17 00:00:00 2001 From: "buf0-bot[bot]" <252831055+buf0-bot[bot]@users.noreply.github.com> Date: Sat, 4 Jul 2026 01:26:55 -0700 Subject: [PATCH 05/45] fix: pr-bug-scan validated finding from #6952 (#7180) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: address pr-bug-scan validated finding from #6952 throwNodeNotFound() now re-raises AbortError when the shared signal is aborted, so a signal-cancelled node probe no longer launders into 'Node.js not found'; sequential fallback runs. * fix(ssh): make session-limited (MaxSessions=1) relay deploys actually succeed Review of #7180 verified the parent fallback end-to-end against a real MaxSessions=1 sshd and found the connect still failed. Four gaps, in order of discovery: - isSshSessionLimitError missed stock OpenSSH, which refuses session channels over MaxSessions with SSH2_OPEN_CONNECT_FAILED (2) and 'open failed' — reason 4 never matched, so the fallback never triggered. - execCommand settled aborted commands before the channel finished closing, so the sequential fallback reissued execs while sshd still counted the old session. - SshConnection.waitForSshCallback rejected aborts mid-channel-open immediately, leaking a confirmed-late channel that held the only session slot; it now settles after the late channel closes (bounded) and drains its streams so ssh2 emits 'close'. - Session channel opens now retry transient session-limit refusals (sshd frees the slot only after processing our close-ack, which the next open can beat by microseconds), and the remote orca CLI shim install is non-fatal like the managed-hook install — after the relay bridge occupies the sole session slot, raw-connection extras must degrade instead of failing the connection. Verified live against Docker sshd (OpenSSH 9.2, MaxSessions=1): fresh deploy (upload + native deps + launch), reconnect cycles, and a PTY round-trip all succeed; unrestricted-sshd regression run also passes. Co-authored-by: Orca * Handle ssh execution aborts immediately during retry backoff or hangs - Cancel the session-limit retry delay immediately if the operation is aborted during backoff. - Limit the wait time to a 5-second grace period when aborted during a channel open that is hung and never invokes its callback, rather than waiting for the full connection timeout. --------- Co-authored-by: orca-bug-scan-bot Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Co-authored-by: Orca --- src/main/ssh/ssh-connection.test.ts | 134 +++++++++++++++++ src/main/ssh/ssh-connection.ts | 141 +++++++++++++++--- src/main/ssh/ssh-relay-deploy-helpers.test.ts | 23 ++- src/main/ssh/ssh-relay-deploy-helpers.ts | 20 ++- src/main/ssh/ssh-relay-session.ts | 14 +- .../ssh/ssh-remote-node-resolution.test.ts | 19 +++ src/main/ssh/ssh-remote-node-resolution.ts | 16 +- src/main/ssh/ssh-session-limit-error.test.ts | 15 +- src/main/ssh/ssh-session-limit-error.ts | 5 +- .../ResourceUsageStatusSegment.rows.test.tsx | 12 +- src/renderer/src/lib/new-workspace.test.ts | 6 +- 11 files changed, 359 insertions(+), 46 deletions(-) diff --git a/src/main/ssh/ssh-connection.test.ts b/src/main/ssh/ssh-connection.test.ts index f40d27077a8..28bc9714db2 100644 --- a/src/main/ssh/ssh-connection.test.ts +++ b/src/main/ssh/ssh-connection.test.ts @@ -22,6 +22,7 @@ type MockSshClient = { _sock: Socket | undefined lastExecCommand?: string lastConnectConfig?: unknown + exec: (cmd: string, cb: (err: Error | undefined, channel: unknown) => void) => void } let clientInstances: MockSshClient[] = [] @@ -707,6 +708,139 @@ describe('SshConnection', () => { } }) + it('retries a session-limit-refused exec open and succeeds on a later attempt', async () => { + const conn = new SshConnection(createTarget(), createCallbacks()) + await conn.connect() + const channel = { close: vi.fn() } + const execMock = vi + .fn<(cmd: string, cb: (err: Error | undefined, ch: unknown) => void) => void>() + .mockImplementationOnce((_cmd, cb) => { + cb( + Object.assign(new Error('(SSH) Channel open failure: open failed'), { reason: 2 }), + undefined + ) + }) + .mockImplementation((_cmd, cb) => cb(undefined, channel)) + clientInstances[0].exec = execMock as never + + await expect(conn.exec('printf ready')).resolves.toBe(channel) + expect(execMock).toHaveBeenCalledTimes(2) + }) + + it('surfaces the session-limit error once open retries are exhausted', async () => { + const conn = new SshConnection(createTarget(), createCallbacks()) + await conn.connect() + const refusal = Object.assign(new Error('(SSH) Channel open failure: open failed'), { + reason: 2 + }) + const execMock = vi + .fn<(cmd: string, cb: (err: Error | undefined, ch: unknown) => void) => void>() + .mockImplementation((_cmd, cb) => cb(refusal, undefined)) + clientInstances[0].exec = execMock as never + + await expect(conn.exec('printf ready')).rejects.toBe(refusal) + expect(execMock).toHaveBeenCalledTimes(4) + }) + + it('does not retry non-session-limit exec open failures', async () => { + const conn = new SshConnection(createTarget(), createCallbacks()) + await conn.connect() + const failure = new Error('Not connected') + const execMock = vi + .fn<(cmd: string, cb: (err: Error | undefined, ch: unknown) => void) => void>() + .mockImplementation((_cmd, cb) => cb(failure, undefined)) + clientInstances[0].exec = execMock as never + + await expect(conn.exec('printf ready')).rejects.toBe(failure) + expect(execMock).toHaveBeenCalledTimes(1) + }) + + it('bounds an aborted exec to the close grace when ssh2 never invokes the open callback', async () => { + const conn = new SshConnection(createTarget(), createCallbacks()) + await conn.connect() + execBehavior = 'pending' + const controller = new AbortController() + + vi.useFakeTimers() + try { + const outcomePromise = conn + .exec('printf ready', { signal: controller.signal }) + .then(() => 'opened') + .catch((error: Error) => error.name) + + controller.abort() + // Why: a hung socket must not pin the aborted caller for the full 30s + // connect timeout — the abort settles at the 5s grace bound instead. + await vi.advanceTimersByTimeAsync(5_000) + + await expect(outcomePromise).resolves.toBe('AbortError') + } finally { + vi.useRealTimers() + } + }) + + it('rejects without waiting out the backoff when aborted during a session-limit retry delay', async () => { + const conn = new SshConnection(createTarget(), createCallbacks()) + await conn.connect() + const controller = new AbortController() + const refusal = Object.assign(new Error('(SSH) Channel open failure: open failed'), { + reason: 2 + }) + const execMock = vi + .fn<(cmd: string, cb: (err: Error | undefined, ch: unknown) => void) => void>() + .mockImplementation((_cmd, cb) => cb(refusal, undefined)) + clientInstances[0].exec = execMock as never + + vi.useFakeTimers() + try { + const outcomePromise = conn + .exec('printf ready', { signal: controller.signal }) + .then(() => 'opened') + .catch((error: Error) => error.name) + + // Flush microtasks so the first refused attempt lands in the backoff. + await vi.advanceTimersByTimeAsync(0) + controller.abort() + + // No timer advance: the abort alone must release the backoff delay. + await expect(outcomePromise).resolves.toBe('AbortError') + expect(execMock).toHaveBeenCalledTimes(1) + } finally { + vi.useRealTimers() + } + }) + + it('settles an abort during channel open only after the late channel closes', async () => { + const conn = new SshConnection(createTarget(), createCallbacks()) + await conn.connect() + execBehavior = 'pending' + const controller = new AbortController() + const lateChannel = Object.assign(new EventEmitter(), { + close: vi.fn(), + resume: vi.fn(), + stderr: { resume: vi.fn() } + }) + + const outcomePromise = conn + .exec('printf ready', { signal: controller.signal }) + .then(() => 'opened') + .catch((error: Error) => error.name) + + await Promise.resolve() + controller.abort() + pendingExecCallback?.(undefined, lateChannel) + + // Why: the sshd session slot is freed only when the channel finishes + // closing — settling before 'close' lets the next open race the close. + const early = await Promise.race([outcomePromise, Promise.resolve('pending')]) + expect(early).toBe('pending') + expect(lateChannel.close).toHaveBeenCalledTimes(1) + expect(lateChannel.resume).toHaveBeenCalled() + + lateChannel.emit('close') + await expect(outcomePromise).resolves.toBe('AbortError') + }) + it('times out when ssh2 never opens an SFTP channel', async () => { const conn = new SshConnection(createTarget(), createCallbacks()) await conn.connect() diff --git a/src/main/ssh/ssh-connection.ts b/src/main/ssh/ssh-connection.ts index 528f03c4c58..eb2e54909c9 100644 --- a/src/main/ssh/ssh-connection.ts +++ b/src/main/ssh/ssh-connection.ts @@ -35,12 +35,25 @@ import { type SshConnectionCallbacks } from './ssh-connection-utils' import type { RemoteHostPlatform } from './ssh-remote-platform' +import { isSshSessionLimitError } from './ssh-session-limit-error' export type { SshConnectionCallbacks } from './ssh-connection-utils' type SshRemoteFileOptions = { hostPlatform?: RemoteHostPlatform } +// Upper bound on waiting, after an abort, for the in-flight open callback or +// for an aborted late-opened channel to finish closing before rejecting +// anyway. Normal opens and closes complete in one network round-trip. +const ABORTED_CHANNEL_CLOSE_GRACE_MS = 5_000 + +// Why: on session-limited servers (MaxSessions), a channel open can be refused +// transiently — e.g. our next CHANNEL_OPEN reaching sshd a few microseconds +// before it finishes processing the previous channel's close. A refused open +// never started the command, so retrying is always safe. +const SESSION_LIMIT_OPEN_RETRIES = 4 +const SESSION_LIMIT_OPEN_RETRY_DELAY_MS = 150 + function cloneResolvedConfig(config: SshResolvedConfig | null): SshResolvedConfig | null { if (!config) { return null @@ -132,10 +145,14 @@ export class SshConnection { } const client = this.client const remoteCommand = options?.wrapCommand === false ? cmd : wrapRemoteCommandForPosixShell(cmd) - return this.waitForSshCallback( - 'SSH exec channel timed out', - (callback) => client.exec(remoteCommand, callback), - (channel) => channel.close(), + return this.openSessionChannelWithRetry( + () => + this.waitForSshCallback( + 'SSH exec channel timed out', + (callback) => client.exec(remoteCommand, callback), + (channel) => channel.close(), + options?.signal + ), options?.signal ) } @@ -148,13 +165,50 @@ export class SshConnection { throw new Error('Not connected') } const client = this.client - return this.waitForSshCallback( - 'SSH SFTP channel timed out', - (callback) => client.sftp(callback), - (sftp) => sftp.end() + return this.openSessionChannelWithRetry(() => + this.waitForSshCallback( + 'SSH SFTP channel timed out', + (callback) => client.sftp(callback), + (sftp) => sftp.end() + ) ) } + private async openSessionChannelWithRetry( + open: () => Promise, + signal?: AbortSignal + ): Promise { + let lastError: unknown + for (let attempt = 0; attempt < SESSION_LIMIT_OPEN_RETRIES; attempt++) { + if (attempt > 0) { + // Why: an abort must release the backoff immediately, not after it. + if (!signal?.aborted) { + await new Promise((resolve) => { + const onDelayDone = (): void => { + clearTimeout(delayTimer) + signal?.removeEventListener('abort', onDelayDone) + resolve() + } + const delayTimer = setTimeout(onDelayDone, SESSION_LIMIT_OPEN_RETRY_DELAY_MS) + signal?.addEventListener('abort', onDelayDone, { once: true }) + }) + } + if (signal?.aborted) { + throw createSshOperationAbortError() + } + } + try { + return await open() + } catch (err) { + if (!isSshSessionLimitError(err)) { + throw err + } + lastError = err + } + } + throw lastError + } + private waitForSshCallback( timeoutMessage: string, register: (callback: (error: Error | undefined, value: T) => void) => void, @@ -163,23 +217,66 @@ export class SshConnection { ): Promise { return new Promise((resolve, reject) => { let settled = false + // Why: rejecting the instant the signal aborts lets the caller proceed + // while the in-flight channel open completes in the background and holds + // a server-side session slot (MaxSessions). Mark the abort and settle + // from the open callback, after the late resource has been closed. + let abortRequested = false + let abortDeadlineTimer: NodeJS.Timeout | undefined const cleanup = (): void => { clearTimeout(timer) + clearTimeout(abortDeadlineTimer) signal?.removeEventListener('abort', onAbort) } const onAbort = (): void => { - if (settled) { - return - } - settled = true - cleanup() - reject(createSshOperationAbortError()) + abortRequested = true + // Why: a hung socket may never invoke the open callback — bound the + // aborted caller's wait instead of pinning it for CONNECT_TIMEOUT_MS. + abortDeadlineTimer = setTimeout(() => { + settled = true + cleanup() + reject(createSshOperationAbortError()) + }, ABORTED_CHANNEL_CLOSE_GRACE_MS) } const timer = setTimeout(() => { settled = true - signal?.removeEventListener('abort', onAbort) - reject(new Error(timeoutMessage)) + cleanup() + reject(abortRequested ? createSshOperationAbortError() : new Error(timeoutMessage)) }, CONNECT_TIMEOUT_MS) + const rejectAfterClose = (value: T): void => { + const abortError = createSshOperationAbortError() + const emitter = value as Partial & { + resume?: () => void + stderr?: { resume?: () => void } + } + let finished = false + const done = (): void => { + if (finished) { + return + } + finished = true + clearTimeout(closeGraceTimer) + reject(abortError) + } + // Why: bounded — a remote that never confirms the close must not hang + // the aborted operation forever. + const closeGraceTimer = setTimeout(done, ABORTED_CHANNEL_CLOSE_GRACE_MS) + if (typeof emitter.once === 'function') { + emitter.once('close', done) + } + // Why: ssh2 withholds the 'close' event until the channel's streams + // are drained; nobody else will ever read this discarded channel. + emitter.resume?.() + emitter.stderr?.resume?.() + try { + cleanupLateValue?.(value) + } catch { + /* best effort */ + } + if (typeof emitter.once !== 'function') { + done() + } + } const finish = (error: Error | undefined, value?: T): void => { if (settled) { // Why: ssh2 can invoke the open callback after our timeout has @@ -196,6 +293,14 @@ export class SshConnection { } settled = true cleanup() + if (abortRequested) { + if (!error && value !== undefined) { + rejectAfterClose(value) + } else { + reject(createSshOperationAbortError()) + } + return + } if (error) { reject(error) return @@ -203,7 +308,9 @@ export class SshConnection { resolve(value as T) } if (signal?.aborted) { - onAbort() + // No open is in flight yet, so failing fast leaks nothing. + cleanup() + reject(createSshOperationAbortError()) return } signal?.addEventListener('abort', onAbort, { once: true }) diff --git a/src/main/ssh/ssh-relay-deploy-helpers.test.ts b/src/main/ssh/ssh-relay-deploy-helpers.test.ts index dd55f559d9c..cb5477091d6 100644 --- a/src/main/ssh/ssh-relay-deploy-helpers.test.ts +++ b/src/main/ssh/ssh-relay-deploy-helpers.test.ts @@ -286,8 +286,22 @@ describe('execCommand', () => { await Promise.resolve() controller.abort() - await expect(commandPromise).rejects.toMatchObject({ name: 'AbortError' }) + // Why: sshd frees the MaxSessions slot only when the channel finishes + // closing, so abort must request close and settle from the 'close' event — + // settling immediately lets the sequential fallback race the closing + // channel and get refused. expect(channel.close).toHaveBeenCalledOnce() + const settledEarly = await Promise.race([ + commandPromise.then( + () => 'settled', + () => 'settled' + ), + Promise.resolve('pending') + ]) + expect(settledEarly).toBe('pending') + channel.emit('close', 0) + + await expect(commandPromise).rejects.toMatchObject({ name: 'AbortError' }) expect(channel.listenerCount('error')).toBe(0) expect(channel.listenerCount('data')).toBe(0) expect(channel.listenerCount('close')).toBe(0) @@ -313,8 +327,13 @@ describe('execCommand', () => { controller.abort() resolveExec(channel) - await expect(commandPromise).rejects.toMatchObject({ name: 'AbortError' }) + await Promise.resolve() + await Promise.resolve() expect(channel.close).toHaveBeenCalledOnce() + // Nonzero exit from the killed command must still surface as AbortError. + channel.emit('close', 1) + + await expect(commandPromise).rejects.toMatchObject({ name: 'AbortError' }) expect(channel.listenerCount('error')).toBe(0) expect(channel.listenerCount('data')).toBe(0) expect(channel.listenerCount('close')).toBe(0) diff --git a/src/main/ssh/ssh-relay-deploy-helpers.ts b/src/main/ssh/ssh-relay-deploy-helpers.ts index e4729014a6c..1b1cdd27074 100644 --- a/src/main/ssh/ssh-relay-deploy-helpers.ts +++ b/src/main/ssh/ssh-relay-deploy-helpers.ts @@ -275,11 +275,16 @@ export async function execCommand( fn(val as never) } const fail = (err: Error): void => { - settle(reject, err) + settle(reject, abortRequested ? createSshOperationAbortError() : err) } + // Why: sshd counts the session against MaxSessions until CHANNEL_CLOSE + // completes. Settling on abort before the channel actually closes lets the + // concurrent-bootstrap sequential fallback reissue an exec while the slot + // is still held, so it gets refused again. Close and settle from onClose. + let abortRequested = false const onAbort = (): void => { + abortRequested = true channel.close() - settle(reject, createSshOperationAbortError()) } const onStdoutData = (data: Buffer): void => { stdout += data.toString('utf-8') @@ -288,7 +293,9 @@ export async function execCommand( stderr += data.toString('utf-8') } const onClose = (code: number): void => { - if (code !== 0) { + if (abortRequested) { + settle(reject, createSshOperationAbortError()) + } else if (code !== 0) { // Why: on the system-ssh transport channel.stderr carries local OpenSSH // client noise; preferring it masks the real failure in stdout (2>&1). const output = [stderr.trim(), stdout.trim()].filter(Boolean).join('\n') @@ -299,7 +306,12 @@ export async function execCommand( } const timeout = setTimeout(() => { channel.close() - settle(reject, new Error(`Command "${command}" timed out after ${timeoutMs / 1000}s`)) + settle( + reject, + abortRequested + ? createSshOperationAbortError() + : new Error(`Command "${command}" timed out after ${timeoutMs / 1000}s`) + ) }, timeoutMs) // Why: remote reboot tears down exec channels with stream errors. Without diff --git a/src/main/ssh/ssh-relay-session.ts b/src/main/ssh/ssh-relay-session.ts index b078db39760..58bf3fbf798 100644 --- a/src/main/ssh/ssh-relay-session.ts +++ b/src/main/ssh/ssh-relay-session.ts @@ -531,7 +531,19 @@ export class SshRelaySession { return false } - await this.installRemoteOrcaCliShim() + try { + await this.installRemoteOrcaCliShim() + } catch (error) { + // Why: the remote `orca` CLI shim is a convenience bridge. On session- + // limited remotes (MaxSessions=1) the relay bridge holds the only slot, + // so this raw-connection install can fail — that must not fail the + // whole connection, matching the managed-hook install above. + console.warn( + `[ssh-relay-session] remote orca CLI shim install failed for ${this.targetId}: ${ + error instanceof Error ? error.message : String(error) + }` + ) + } if (shouldContinue && !shouldContinue()) { return false } diff --git a/src/main/ssh/ssh-remote-node-resolution.test.ts b/src/main/ssh/ssh-remote-node-resolution.test.ts index 23ea9097540..b3cad3c0ecc 100644 --- a/src/main/ssh/ssh-remote-node-resolution.test.ts +++ b/src/main/ssh/ssh-remote-node-resolution.test.ts @@ -331,6 +331,25 @@ describe('resolveRemoteNodePath', () => { ).rejects.toBe(sessionLimitError) }) + it('surfaces an AbortError instead of not-found when the shared signal is aborted', async () => { + // Why: in concurrent bootstrap a sibling probe aborts this resolution via + // the shared signal. Each strategy catch swallows the injected AbortError, + // so without re-raising it here the resolver would launder cancellation + // into a fatal "Node.js not found" and defeat the sequential fallback. + const controller = new AbortController() + const abortError = Object.assign(new Error('SSH operation was cancelled'), { + name: 'AbortError' + }) + execCommandMock.mockImplementation(() => { + controller.abort() + return Promise.reject(abortError) + }) + + await expect( + resolveRemoteNodePath(conn, undefined, { signal: controller.signal }) + ).rejects.toMatchObject({ name: 'AbortError' }) + }) + it('throws when every candidate across both strategies is below the minimum', async () => { execCommandMock .mockResolvedValueOnce('/old/node\n') // path probe diff --git a/src/main/ssh/ssh-remote-node-resolution.ts b/src/main/ssh/ssh-remote-node-resolution.ts index 4117ddf97ef..e4bb3181b79 100644 --- a/src/main/ssh/ssh-remote-node-resolution.ts +++ b/src/main/ssh/ssh-remote-node-resolution.ts @@ -1,5 +1,5 @@ import type { SshConnection } from './ssh-connection' -import { shellEscape } from './ssh-connection-utils' +import { createSshOperationAbortError, shellEscape } from './ssh-connection-utils' import type { RemoteHostPlatform } from './ssh-remote-platform' import { isWindowsRemoteHost, normalizeWindowsRemotePath } from './ssh-remote-platform' import { powerShellCommand } from './ssh-remote-powershell' @@ -47,7 +47,7 @@ export async function resolveRemoteNodePath( return loginShellPath } - throwNodeNotFound() + throwNodeNotFound(options) } // Probe the on-disk install directories of every common Node version manager @@ -245,10 +245,18 @@ async function resolveRemoteWindowsNodePath( // Fall through to the shared error below. } - throwNodeNotFound() + throwNodeNotFound(options) } -function throwNodeNotFound(): never { +function throwNodeNotFound(options?: RemoteNodeResolutionOptions): never { + // Why: when a sibling bootstrap probe aborts this resolution via the shared + // signal, the strategy catch-blocks swallow the injected AbortError and fall + // through to here. Surfacing "Node.js not found" would launder a cancellation + // into a fatal non-session failure, defeating the concurrent-bootstrap + // sequential fallback. Re-raise the abort so callers classify it correctly. + if (options?.signal?.aborted) { + throw createSshOperationAbortError() + } throw new Error( 'Node.js not found on remote host. Orca relay requires Node.js 18+. ' + 'Install Node.js on the remote and try again.' diff --git a/src/main/ssh/ssh-session-limit-error.test.ts b/src/main/ssh/ssh-session-limit-error.test.ts index a2aaac5e4b3..65608fd9a6e 100644 --- a/src/main/ssh/ssh-session-limit-error.test.ts +++ b/src/main/ssh/ssh-session-limit-error.test.ts @@ -10,6 +10,16 @@ describe('isSshSessionLimitError', () => { ).toBe(true) }) + it('matches the OpenSSH MaxSessions rejection (SSH2_OPEN_CONNECT_FAILED + "open failed")', () => { + // Why: stock OpenSSH sshd refuses session channels over MaxSessions with + // reason 2, not resource-shortage — observed against OpenSSH 9.2. + expect( + isSshSessionLimitError( + Object.assign(new Error('(SSH) Channel open failure: open failed'), { reason: 2 }) + ) + ).toBe(true) + }) + it('matches OpenSSH mux and MaxSessions failures', () => { expect( isSshSessionLimitError( @@ -29,11 +39,6 @@ describe('isSshSessionLimitError', () => { Object.assign(new Error('(SSH) Channel open failure: open failed'), { reason: 1 }) ) ).toBe(false) - expect( - isSshSessionLimitError( - Object.assign(new Error('(SSH) Channel open failure: open failed'), { reason: 2 }) - ) - ).toBe(false) expect( isSshSessionLimitError( Object.assign(new Error('(SSH) Channel open failure: open failed'), { reason: 3 }) diff --git a/src/main/ssh/ssh-session-limit-error.ts b/src/main/ssh/ssh-session-limit-error.ts index aed848525b8..d3f1c120b11 100644 --- a/src/main/ssh/ssh-session-limit-error.ts +++ b/src/main/ssh/ssh-session-limit-error.ts @@ -4,8 +4,11 @@ export function isSshSessionLimitError(err: unknown): boolean { } const reason = (err as { reason?: unknown }).reason const message = err.message.toLowerCase() + // Why: OpenSSH rejects session channels over MaxSessions with + // SSH2_OPEN_CONNECT_FAILED (2) and the literal description "open failed"; + // reason 4 (resource shortage) covers other server implementations. if ( - reason === 4 && + (reason === 2 || reason === 4) && (message.includes('channel open failure') || message.includes('open failed')) ) { return true diff --git a/src/renderer/src/components/status-bar/ResourceUsageStatusSegment.rows.test.tsx b/src/renderer/src/components/status-bar/ResourceUsageStatusSegment.rows.test.tsx index 5701928ad1e..57f77d1e472 100644 --- a/src/renderer/src/components/status-bar/ResourceUsageStatusSegment.rows.test.tsx +++ b/src/renderer/src/components/status-bar/ResourceUsageStatusSegment.rows.test.tsx @@ -116,12 +116,8 @@ describe('resource manager row presentation', () => { expect(container.textContent).toContain('· remote') const killButtons = container.querySelectorAll('button[aria-label^="Kill session"]') expect(killButtons).toHaveLength(2) - expect( - container.querySelector('button[aria-label="Kill session ssh-a"]') - ).not.toBeNull() - expect( - container.querySelector('button[aria-label="Kill session ssh-b"]') - ).not.toBeNull() + expect(container.querySelector('button[aria-label="Kill session ssh-a"]')).not.toBeNull() + expect(container.querySelector('button[aria-label="Kill session ssh-b"]')).not.toBeNull() }) it('keeps kill affordances on the orphan bucket rows', () => { @@ -133,8 +129,6 @@ describe('resource manager row presentation', () => { }) ) - expect( - container.querySelector('button[aria-label="Kill session orphan-a"]') - ).not.toBeNull() + expect(container.querySelector('button[aria-label="Kill session orphan-a"]')).not.toBeNull() }) }) diff --git a/src/renderer/src/lib/new-workspace.test.ts b/src/renderer/src/lib/new-workspace.test.ts index a9c6c10cc5a..8306ca0d62c 100644 --- a/src/renderer/src/lib/new-workspace.test.ts +++ b/src/renderer/src/lib/new-workspace.test.ts @@ -217,9 +217,9 @@ describe('isGitLabIssueUrl', () => { it('detects modern /-/work_items/ issue URLs (incl. non-default port)', () => { expect(isGitLabIssueUrl('https://gitlab.com/group/project/-/work_items/123')).toBe(true) - expect( - isGitLabIssueUrl('https://gitlab.example.com:8443/group/project/-/work_items/7') - ).toBe(true) + expect(isGitLabIssueUrl('https://gitlab.example.com:8443/group/project/-/work_items/7')).toBe( + true + ) }) it('does not classify GitHub issue URLs as GitLab issues', () => { From 04046fc4f2abf78a0beb53e224730592f63a4fa1 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 4 Jul 2026 01:30:01 -0700 Subject: [PATCH 06/45] Fall back to local base ref when worktree-create refresh fails (#7335) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Creating a workspace hard-failed with "Could not refresh base ref …" whenever the pre-create git fetch of a remote-tracking base (e.g. origin/main) failed — offline, transient, or (common on Linux GUI) the Electron process lacking SSH_AUTH_SOCK so `ssh -o BatchMode=yes` cannot auth. This blocked creation even when a perfectly usable local origin/main already existed and `git worktree add` from it would succeed. Regression introduced by #2310 (5c93579e40), which replaced the prior best-effort fetch + post-create reconcile with a hard network gate. Fix: at all three create sites (local runtime, local IPC, SSH), only throw when the refresh failed AND no usable local base ref exists; otherwise create from the local ref (a possibly stale but valid base). Also: move the SSH local-ref probe after session.registerRoot so relays that gate generic git.exec don't false-negative and defeat the fallback. Co-authored-by: Orca --- src/main/ipc/worktree-remote.ts | 82 ++++++++++++++------ src/main/ipc/worktrees.test.ts | 104 +++++++++++++++++++++++++- src/main/runtime/orca-runtime.test.ts | 52 ++++++++++++- src/main/runtime/orca-runtime.ts | 8 +- 4 files changed, 217 insertions(+), 29 deletions(-) diff --git a/src/main/ipc/worktree-remote.ts b/src/main/ipc/worktree-remote.ts index 86d18129826..c1003a10651 100644 --- a/src/main/ipc/worktree-remote.ts +++ b/src/main/ipc/worktree-remote.ts @@ -623,6 +623,26 @@ function hasRemoteCommitObject( return hasCommitObjectViaGitExec((gitArgs) => provider.exec(gitArgs, repoPath), ref) } +// Why: hasRemoteCommitObject only resolves full SHAs; a remote-tracking base is +// a symbolic ref (refs/remotes/origin/main), so detect its presence directly so +// SSH creates can fall back to an existing local base ref when the refresh +// fetch fails. Require a resolved object id: a missing ref exits non-zero. +async function hasRemoteTrackingRefSsh( + provider: SshGitProvider, + repoPath: string, + ref: string +): Promise { + try { + const { stdout } = await provider.exec( + ['rev-parse', '--verify', '--quiet', `${ref}^{commit}`], + repoPath + ) + return stdout.trim().length > 0 + } catch { + return false + } +} + async function canCheckoutExistingLocalBranchSsh( provider: SshGitProvider, repoPath: string, @@ -1571,32 +1591,14 @@ export async function createRemoteWorktree( } } - if (remoteTrackingBase) { - try { - await refreshRemoteTrackingBaseForWorktreeCreate(provider, repo, remoteTrackingBase) - } catch { - throw new Error( - `Could not refresh base ref "${baseBranch}" from "${remoteTrackingBase.remote}". Check your network and try again.` - ) - } - } else if (!(await hasRemoteCommitObject(provider, repo.path, baseBranch))) { - // Why: local or otherwise non-remote-tracking bases preserve legacy - // best-effort fetch behavior. Verified PR/MR SHA bases already have the - // commit object locally, so a broad remote fetch only updates unrelated refs. - try { - await fetchRemoteForWorktreeCreate(provider, repo, 'origin') - } catch { - /* best-effort */ - } - } - const mux = getActiveMultiplexer(repo.connectionId!) if (!mux) { throw new Error('SSH connection is not available. Please reconnect and try again.') } - // Why: register before the local-base advisory probe as well as addWorktree. - // Fresh/older relays may gate generic git.exec calls on registered roots; if - // the probe runs first it degrades to "no suggestion" even though create works. + // Why: register before any generic git.exec probe (base-ref existence, the + // local-base advisory) as well as addWorktree. Fresh/older relays may gate + // generic git.exec on registered roots; probing first would falsely report a + // ref missing (and defeat the offline fallback below) even though create works. try { await Promise.all([ mux.request('session.registerRoot', { rootPath: repo.path }), @@ -1611,6 +1613,34 @@ export async function createRemoteWorktree( } } + if (remoteTrackingBase) { + try { + await refreshRemoteTrackingBaseForWorktreeCreate(provider, repo, remoteTrackingBase) + } catch { + // Why: a failed refresh must not block creation when a usable local base + // ref already exists — `git worktree add` can still create from that + // (possibly stale but valid) ref, so a transient offline/auth failure does + // not make the workspace uncreatable. Probe AFTER registerRoot so relays + // that gate generic git.exec accept it; only hard-fail when there is no + // local ref to fall back on. Drift is reflected by the compare-to-base + // view once the remote is reachable again. + if (!(await hasRemoteTrackingRefSsh(provider, repo.path, remoteTrackingBase.ref))) { + throw new Error( + `Could not refresh base ref "${baseBranch}" from "${remoteTrackingBase.remote}". Check your network and try again.` + ) + } + } + } else if (!(await hasRemoteCommitObject(provider, repo.path, baseBranch))) { + // Why: local or otherwise non-remote-tracking bases preserve legacy + // best-effort fetch behavior. Verified PR/MR SHA bases already have the + // commit object locally, so a broad remote fetch only updates unrelated refs. + try { + await fetchRemoteForWorktreeCreate(provider, repo, 'origin') + } catch { + /* best-effort */ + } + } + const localBaseRefRefresh = settings.refreshLocalBaseRefOnWorktreeCreate && !checkoutExistingBranch && remoteTrackingBase ? await refreshLocalBaseRefForRemoteWorktreeCreate(provider, repo.path, remoteTrackingBase) @@ -2135,7 +2165,13 @@ export async function createLocalWorktree( if (remoteTrackingRefresh) { await timing.time('refresh_base_ref', async () => { const result = await remoteTrackingRefresh.promise - if (!result.ok) { + if (!result.ok && !remoteTrackingRefresh.hadLocalBaseRef) { + // Why: only block creation when the refresh failed AND there is no local + // base ref to fall back on. An existing local remote-tracking ref lets + // `git worktree add` proceed from a possibly stale but valid base, so a + // transient offline/auth failure must not make the workspace + // uncreatable. The compare-to-base view reflects any drift once the + // remote is reachable again. throw new Error( `Could not refresh base ref "${baseBranch}" from "${remoteTrackingRefresh.base.remote}". Check your network and try again.` ) diff --git a/src/main/ipc/worktrees.test.ts b/src/main/ipc/worktrees.test.ts index 6f2857129e8..f619d078630 100644 --- a/src/main/ipc/worktrees.test.ts +++ b/src/main/ipc/worktrees.test.ts @@ -3668,7 +3668,7 @@ describe('registerWorktreeHandlers', () => { }) }) - it('does not create an SSH worktree when remote-tracking base refresh fails', async () => { + it('does not create an SSH worktree when the refresh fails and no local base ref exists', async () => { const repo = { id: 'repo-ssh', path: '/remote/repo', @@ -3679,6 +3679,7 @@ describe('registerWorktreeHandlers', () => { worktreeBaseRef: 'origin/main' } const provider = { + // Empty rev-parse stdout -> no local remote-tracking base ref to fall back on. exec: vi.fn().mockImplementation(async (args: string[]) => { if (args[0] === 'remote') { return { stdout: 'origin\n', stderr: '' } @@ -3719,6 +3720,70 @@ describe('registerWorktreeHandlers', () => { ) }) + it('creates an SSH worktree from an existing local base ref when the refresh fails', async () => { + // Regression: a failed SSH refresh must fall back to an existing local + // remote-tracking base ref instead of blocking creation. + const repo = { + id: 'repo-ssh', + path: '/remote/repo', + displayName: 'ssh', + badgeColor: '#000', + addedAt: 0, + connectionId: 'conn-1', + worktreeBaseRef: 'origin/main' + } + const provider = { + exec: vi.fn().mockImplementation(async (args: string[]) => { + if (args[0] === 'remote') { + return { stdout: 'origin\n', stderr: '' } + } + // Local remote-tracking base ref resolves -> usable fallback exists. + if (args[0] === 'rev-parse' && args.includes('refs/remotes/origin/main^{commit}')) { + return { stdout: `${'a'.repeat(40)}\n`, stderr: '' } + } + return { stdout: '', stderr: '' } + }), + fetchRemoteTrackingRef: vi.fn().mockRejectedValue(new Error('network unavailable')), + addWorktree: vi.fn().mockResolvedValue(undefined), + listWorktrees: vi + .fn() + .mockResolvedValueOnce([]) + .mockResolvedValueOnce([ + { + path: '/remote/improve-dashboard', + head: 'abc123', + branch: 'refs/heads/improve-dashboard', + isBare: false, + isMainWorktree: false + } + ]) + } + const mux = { + request: vi.fn().mockResolvedValue(undefined), + notify: vi.fn() + } + store.getRepos.mockReturnValue([repo]) + store.getRepo.mockReturnValue(repo) + getSshGitProviderMock.mockReturnValue(provider) + getActiveMultiplexerMock.mockReturnValue(mux) + store.setWorktreeMeta.mockImplementation((_worktreeId, meta) => meta) + + await handlers['worktrees:create'](null, { + repoId: 'repo-ssh', + name: 'improve-dashboard' + }) + + // The refresh must be ATTEMPTED (and fail) before falling back — guards + // against a regression that skips the refresh whenever a local ref exists. + expect(provider.fetchRemoteTrackingRef).toHaveBeenCalledWith( + '/remote/repo', + 'origin', + 'main', + 'refs/remotes/origin/main' + ) + expect(provider.addWorktree).toHaveBeenCalled() + }) + it('reuses a fresh SSH remote-tracking base refresh for repeated creates', async () => { const repo = { id: 'repo-ssh', @@ -4205,7 +4270,9 @@ describe('registerWorktreeHandlers', () => { ) }) - it('does not create when the pre-create remote-tracking refresh fails', async () => { + it('creates from an existing local base ref when the pre-create refresh fails', async () => { + // Regression: a failed refresh must not block creation when a usable local + // remote-tracking base ref already exists. const remoteBase = { remote: 'origin', branch: 'main', @@ -4218,6 +4285,39 @@ describe('registerWorktreeHandlers', () => { ok: false, errorKind: 'git_error' }) + listWorktreesMock.mockResolvedValue([ + { + path: '/workspace/improve-dashboard', + head: 'created-sha', + branch: 'improve-dashboard', + isBare: false, + isMainWorktree: false + } + ]) + gitExecFileAsyncMock.mockResolvedValue({ stdout: 'created-sha\n', stderr: '' }) + + const result = (await handlers['worktrees:create'](null, { + repoId: 'repo-1', + name: 'improve-dashboard' + })) as CreateWorktreeResult + + expect(addWorktreeMock).toHaveBeenCalled() + expect(result.worktree.id).toBe('repo-1::/workspace/improve-dashboard') + }) + + it('does not create when the pre-create refresh fails and no local base ref exists', async () => { + const remoteBase = { + remote: 'origin', + branch: 'main', + ref: 'refs/remotes/origin/main', + base: 'origin/main' + } + runtimeStub.resolveRemoteTrackingBase.mockResolvedValue(remoteBase) + runtimeStub.hasRemoteTrackingRef.mockResolvedValue(false) + runtimeStub.getOrStartRemoteTrackingBaseRefresh.mockResolvedValue({ + ok: false, + errorKind: 'git_error' + }) await expect( handlers['worktrees:create'](null, { diff --git a/src/main/runtime/orca-runtime.test.ts b/src/main/runtime/orca-runtime.test.ts index 87e0352f240..0b410939aba 100644 --- a/src/main/runtime/orca-runtime.test.ts +++ b/src/main/runtime/orca-runtime.test.ts @@ -2355,10 +2355,21 @@ describe('OrcaRuntimeService', () => { } }) - it('does not create runtime local worktrees when remote-tracking base refresh fails', async () => { + it('creates a runtime local worktree from an existing local base ref when the refresh fails', async () => { + // Regression: an offline/auth failure while refreshing the remote-tracking + // base must not block creation when a usable local base ref already exists. const runtime = new OrcaRuntimeService(store) - computeWorktreePathMock.mockReturnValue('/tmp/workspaces/cli-refresh-fails') - ensurePathWithinWorkspaceMock.mockReturnValue('/tmp/workspaces/cli-refresh-fails') + const createdWorktree = { + path: '/tmp/workspaces/cli-refresh-fails', + head: 'base-sha', + branch: 'cli-refresh-fails', + isBare: false, + isMainWorktree: false + } + computeWorktreePathMock.mockReturnValue(createdWorktree.path) + ensurePathWithinWorkspaceMock.mockReturnValue(createdWorktree.path) + vi.mocked(addWorktree).mockResolvedValueOnce({}) + vi.mocked(listWorktrees).mockResolvedValue([createdWorktree]) const gitSpy = vi.spyOn(gitRunner, 'gitExecFileAsync').mockImplementation(async (args) => { if (args[0] === 'remote') { return { stdout: 'origin\n', stderr: '' } @@ -2366,6 +2377,7 @@ describe('OrcaRuntimeService', () => { if (args[0] === 'rev-parse' && args.includes('--git-common-dir')) { return { stdout: '/tmp/repo/.git\n', stderr: '' } } + // Local remote-tracking base ref resolves -> usable fallback exists. if (args[0] === 'rev-parse' && args[1] === '--verify') { return { stdout: 'base-sha\n', stderr: '' } } @@ -2380,6 +2392,40 @@ describe('OrcaRuntimeService', () => { repoSelector: 'id:repo-1', name: 'cli-refresh-fails' }) + ).resolves.toBeDefined() + + expect(addWorktree).toHaveBeenCalled() + } finally { + gitSpy.mockRestore() + } + }) + + it('does not create a runtime local worktree when the refresh fails and no local base ref exists', async () => { + const runtime = new OrcaRuntimeService(store) + computeWorktreePathMock.mockReturnValue('/tmp/workspaces/cli-refresh-no-local') + ensurePathWithinWorkspaceMock.mockReturnValue('/tmp/workspaces/cli-refresh-no-local') + const gitSpy = vi.spyOn(gitRunner, 'gitExecFileAsync').mockImplementation(async (args) => { + if (args[0] === 'remote') { + return { stdout: 'origin\n', stderr: '' } + } + if (args[0] === 'rev-parse' && args.includes('--git-common-dir')) { + return { stdout: '/tmp/repo/.git\n', stderr: '' } + } + // No local remote-tracking base ref -> nothing to fall back on. + if (args[0] === 'rev-parse' && args[1] === '--verify') { + throw new Error('missing ref') + } + if (args[0] === 'fetch') { + throw new Error('network unavailable') + } + return { stdout: '', stderr: '' } + }) + try { + await expect( + runtime.createManagedWorktree({ + repoSelector: 'id:repo-1', + name: 'cli-refresh-no-local' + }) ).rejects.toThrow( 'Could not refresh base ref "origin/main" from "origin". Check your network and try again.' ) diff --git a/src/main/runtime/orca-runtime.ts b/src/main/runtime/orca-runtime.ts index 5ae0c67d065..1ef8374f0d4 100644 --- a/src/main/runtime/orca-runtime.ts +++ b/src/main/runtime/orca-runtime.ts @@ -13075,7 +13075,13 @@ export class OrcaRuntimeService { remoteTrackingBase, ...localWorktreeGitOptionArgs ) - if (!refreshResult.ok) { + if (!refreshResult.ok && !hadLocalBaseRef) { + // Why: only block creation when the refresh failed AND there is no + // usable local base ref to fall back on. If a local remote-tracking ref + // already exists, `git worktree add` can create from it — a possibly + // stale but valid base — so a transient offline/auth failure must not + // make the workspace uncreatable. The compare-to-base view reflects any + // drift once the remote is reachable again. throw new Error( `Could not refresh base ref "${baseBranch}" from "${remoteTrackingBase.remote}". Check your network and try again.` ) From 840bedc90d5145bbfbd9bf961e36f5047f67f38a Mon Sep 17 00:00:00 2001 From: Rod Boev Date: Sat, 4 Jul 2026 04:30:04 -0400 Subject: [PATCH 07/45] fix(automations): start scheduler in headless serve mode (#7296) * fix(automations): start scheduler in headless serve mode * Trigger initial catch-up check for headless scheduled automations Headless serve never receives a renderer-ready IPC, which previously prevented the initial catch-up pass of due scheduled automations from running on startup. Checking for the presence of a headless dispatcher allows starting the evaluation pass immediately. Additionally, refactor the corresponding test to use fake timers for better reliability and deterministic assertions. --------- Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com> --- src/main/automations/service.test.ts | 64 +++++++++++++++++++ src/main/automations/service.ts | 4 +- src/main/index.ts | 2 + .../startup/desktop-startup-ordering.test.ts | 24 +++++++ 4 files changed, 93 insertions(+), 1 deletion(-) diff --git a/src/main/automations/service.test.ts b/src/main/automations/service.test.ts index 9ab1ed45d57..786ff017bb2 100644 --- a/src/main/automations/service.test.ts +++ b/src/main/automations/service.test.ts @@ -348,6 +348,70 @@ describe('AutomationService', () => { ) }) + it('dispatches due scheduled automations headlessly', async () => { + const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone + const beforeRunAt = new Date(2026, 4, 13, 8, 59).getTime() + const scheduledRunAt = new Date(2026, 4, 13, 9, 0).getTime() + const afterRunAt = new Date(2026, 4, 13, 9, 1).getTime() + const nextRunAt = new Date(2026, 4, 14, 9, 0).getTime() + + vi.setSystemTime(beforeRunAt) + const store = await createStore() + const runtimeHostId = toRuntimeExecutionHostId('gpu-server') + store.addRepo(makeRepo({ executionHostId: runtimeHostId })) + const setup = store.getProjectHostSetups()[0]! + const automation = store.createAutomation({ + name: 'Morning check', + prompt: 'Check the repo', + agentId: 'claude', + projectId: 'r1', + runContext: { + kind: 'workspace-run', + projectId: setup.projectId, + hostId: runtimeHostId, + projectHostSetupId: setup.id, + repoId: setup.repoId, + path: setup.path + }, + workspaceMode: 'new_per_run', + timezone, + rrule: 'FREQ=DAILY;BYHOUR=9;BYMINUTE=0', + dtstart: new Date(2026, 4, 12, 0, 0).getTime() + }) + + const headlessDispatcher = vi.fn().mockResolvedValue({ + workspaceId: 'wt1', + workspaceDisplayName: 'Morning check', + terminalSessionId: 'tab-1', + terminalPaneKey: 'pane-1', + terminalPtyId: 'pty-1' + }) + const service = new AutomationService(store, { + tickMs: 60_000, + allowRemoteHostScheduling: true, + headlessDispatcher + }) + + try { + vi.setSystemTime(afterRunAt) + service.start() + await vi.waitFor(() => expect(headlessDispatcher).toHaveBeenCalledTimes(1)) + const run = store.listAutomationRuns(automation.id)[0] + expect(run?.status).toBe('dispatched') + expect(run?.scheduledFor).toBe(scheduledRunAt) + expect(headlessDispatcher).toHaveBeenCalledWith( + expect.objectContaining({ automation: expect.objectContaining({ id: automation.id }) }) + ) + await vi.waitFor(() => + expect(store.listAutomations().find((entry) => entry.id === automation.id)?.nextRunAt).toBe( + nextRunAt + ) + ) + } finally { + service.stop() + } + }) + it('attaches provider usage when a completed run can be attributed', async () => { vi.setSystemTime(new Date('2026-05-13T10:00:00')) const store = await createStore() diff --git a/src/main/automations/service.ts b/src/main/automations/service.ts index 7a192153d1f..dc22550e7ec 100644 --- a/src/main/automations/service.ts +++ b/src/main/automations/service.ts @@ -69,7 +69,9 @@ export class AutomationService { this.timer = setInterval(() => { void this.evaluateDueRuns() }, this.tickMs) - if (this.rendererReady) { + // Why: headless serve never gets a renderer-ready IPC, but due runs still + // need the same startup catch-up pass desktop gets after renderer attach. + if (this.rendererReady || this.headlessDispatcher) { void this.evaluateDueRuns() } } diff --git a/src/main/index.ts b/src/main/index.ts index 9537dc98b38..ff4b249f8dc 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -2095,6 +2095,8 @@ app.whenReady().then(async () => { ) } } + // Why: headless serve never opens a renderer, so arm scheduled automation dispatch here. + automations.start() await printServeReady(serveOptions) return } diff --git a/src/main/startup/desktop-startup-ordering.test.ts b/src/main/startup/desktop-startup-ordering.test.ts index cd1faf35c60..276242040dc 100644 --- a/src/main/startup/desktop-startup-ordering.test.ts +++ b/src/main/startup/desktop-startup-ordering.test.ts @@ -32,4 +32,28 @@ describe('desktop startup ordering', () => { expect(attachIndex).toBeGreaterThanOrEqual(0) expect(startIndex).toBeGreaterThan(attachIndex) }) + + it('starts the automation scheduler before headless serve reports ready', () => { + const source = readFileSync(join(process.cwd(), 'src/main/index.ts'), 'utf8') + const serveStart = source.indexOf('if (serveOptions) {') + const serveReady = source.indexOf('await printServeReady(serveOptions)', serveStart) + const serveReturn = source.indexOf('return', serveReady) + const runtimeRpcStart = source.indexOf('await runtimeRpc.start()', serveStart) + const automationStart = source.indexOf('automations.start()', serveStart) + const desktopSetWebContents = source.indexOf('automations.setWebContents(window.webContents)') + const desktopAutomationStart = source.indexOf( + 'automations.start()', + desktopSetWebContents + 1 + ) + + expect(serveStart).toBeGreaterThanOrEqual(0) + expect(serveReady).toBeGreaterThan(serveStart) + expect(serveReturn).toBeGreaterThan(serveReady) + expect(runtimeRpcStart).toBeGreaterThan(serveStart) + expect(automationStart).toBeGreaterThan(runtimeRpcStart) + expect(automationStart).toBeLessThan(serveReady) + expect(automationStart).toBeLessThan(serveReturn) + expect(desktopSetWebContents).toBeGreaterThanOrEqual(0) + expect(desktopAutomationStart).toBeGreaterThan(desktopSetWebContents) + }) }) From 0d8205ae735d45819dc494c2a069cb32982293f1 Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Sat, 4 Jul 2026 01:31:09 -0700 Subject: [PATCH 08/45] Validate terminal startup CWD paths against symlink escapes (#7334) Resolve terminal CWD and worktree paths to their canonical form using realpath before validating containment. This blocks symlink escape routes for local terminal spawns, returning the default workspace directory if a requested path attempts to escape the worktree. - Local terminal paths are canonicalized using native realpath logic - WSL UNC paths and SSH/remote terminals skip canonicalization - Floating terminal startup directories bypass containment checks - Missing or unresolvable workspace directories default safely --- src/main/ipc/pty.test.ts | 55 ++++++++++++++++------ src/main/ipc/pty.ts | 13 +++-- src/main/pty/terminal-cwd-realpath.test.ts | 43 +++++++++++++++++ src/main/pty/terminal-cwd-realpath.ts | 30 ++++++++++++ src/main/runtime/orca-runtime.ts | 20 ++++++-- src/shared/terminal-startup-cwd.test.ts | 52 ++++++++++++++++++++ src/shared/terminal-startup-cwd.ts | 46 ++++++++++++++++-- 7 files changed, 232 insertions(+), 27 deletions(-) create mode 100644 src/main/pty/terminal-cwd-realpath.test.ts create mode 100644 src/main/pty/terminal-cwd-realpath.ts diff --git a/src/main/ipc/pty.test.ts b/src/main/ipc/pty.test.ts index 71f742a7149..1f6137296ab 100644 --- a/src/main/ipc/pty.test.ts +++ b/src/main/ipc/pty.test.ts @@ -9,6 +9,7 @@ import { TERMINAL_INPUT_MAX_BYTES } from '../../shared/terminal-input' import { CLIPBOARD_TEXT_MEASURE_YIELD_CODE_UNITS } from '../../shared/clipboard-text' +import { FLOATING_TERMINAL_WORKTREE_ID } from '../../shared/constants' const isWindowsHost = process.platform === 'win32' const posixOnlyIt = isWindowsHost ? it.skip : it @@ -1307,6 +1308,7 @@ describe('registerPtyHandlers', () => { // from main and PR #2662 command threading for OMP target selection. spawnArgs?: { cwd?: string + worktreeId?: string shellOverride?: string command?: string envToDelete?: string[] @@ -1500,7 +1502,10 @@ describe('registerPtyHandlers', () => { CODEX_HOME: 'C:\\Users\\test\\AppData\\Roaming\\Orca\\codex-runtime-home\\home', ORCA_CODEX_HOME: 'C:\\Users\\test\\AppData\\Roaming\\Orca\\codex-runtime-home\\home' }, - { cwd: '\\\\wsl.localhost\\Ubuntu\\home\\test\\repo' } + { + cwd: '\\\\wsl.localhost\\Ubuntu\\home\\test\\repo', + worktreeId: 'repo-1::\\\\wsl.localhost\\Ubuntu\\home\\test\\repo' + } ) const { env } = spawnOptions expect(env.CODEX_HOME).toBeUndefined() @@ -4348,7 +4353,7 @@ describe('registerPtyHandlers', () => { cols: 80, rows: 24, cwd: '/tmp', - worktreeId: 'wt-1', + worktreeId: 'repo-1::/tmp', tabId: 'tab-race', leafId, env: { ORCA_PANE_KEY: paneKey }, @@ -4362,12 +4367,12 @@ describe('registerPtyHandlers', () => { cols: 80, rows: 24, cwd: '/tmp', - worktreeId: 'wt-1', + worktreeId: 'repo-1::/tmp', tabId: 'tab-race', leafId, env: { ORCA_TAB_ID: 'tab-race', - ORCA_WORKTREE_ID: 'wt-1' + ORCA_WORKTREE_ID: 'repo-1::/tmp' } }) as Promise<{ id: string }> await Promise.resolve() @@ -4380,7 +4385,7 @@ describe('registerPtyHandlers', () => { ]) expect(providerSpawn).toHaveBeenCalledTimes(1) expect(store.persistPtyBinding).toHaveBeenCalledWith({ - worktreeId: 'wt-1', + worktreeId: 'repo-1::/tmp', tabId: 'tab-race', leafId, ptyId: 'pty-shared', @@ -4462,13 +4467,13 @@ describe('registerPtyHandlers', () => { cols: 80, rows: 24, cwd: '/tmp', - worktreeId: 'wt-1', + worktreeId: 'repo-1::/tmp', tabId: 'tab-race', leafId, env: { ORCA_PANE_KEY: paneKey, ORCA_TAB_ID: 'tab-race', - ORCA_WORKTREE_ID: 'wt-1' + ORCA_WORKTREE_ID: 'repo-1::/tmp' } }) as Promise<{ id: string }> await Promise.resolve() @@ -4478,7 +4483,7 @@ describe('registerPtyHandlers', () => { cols: 80, rows: 24, cwd: '/tmp', - worktreeId: 'wt-1', + worktreeId: 'repo-1::/tmp', tabId: 'tab-race', leafId, env: { ORCA_PANE_KEY: paneKey }, @@ -4494,7 +4499,7 @@ describe('registerPtyHandlers', () => { ]) expect(providerSpawn).toHaveBeenCalledTimes(1) expect(store.persistPtyBinding).toHaveBeenCalledWith({ - worktreeId: 'wt-1', + worktreeId: 'repo-1::/tmp', tabId: 'tab-race', leafId, ptyId: 'pty-renderer', @@ -5838,6 +5843,22 @@ describe('registerPtyHandlers', () => { }) }) + it('passes floating terminal cwds through to the spawned shell', async () => { + // Why: the floating sentinel has no worktree root; its cwd is validated + // against trusted-directory grants before it reaches pty:spawn. + registerPtyHandlers(mainWindow as never) + + await handlers.get('pty:spawn')!(null, { + cols: 80, + rows: 24, + cwd: '/tmp/floating-notes', + worktreeId: FLOATING_TERMINAL_WORKTREE_ID + }) + + const [, , options] = spawnMock.mock.calls.at(-1) as [string, string[], { cwd: string }] + expect(options.cwd).toBe('/tmp/floating-notes') + }) + it('rejects missing WSL worktree cwd instead of validating only the fallback Windows cwd', async () => { const originalPlatform = process.platform const originalUserProfile = process.env.USERPROFILE @@ -5848,8 +5869,10 @@ describe('registerPtyHandlers', () => { }) process.env.USERPROFILE = 'C:\\Users\\jinwo' + // Why: the startup-cwd guard normalizes separators, so the provider sees + // the forward-slash UNC form. existsSyncMock.mockImplementation((targetPath: string) => { - if (targetPath === '\\\\wsl.localhost\\Ubuntu\\home\\jin\\missing') { + if (targetPath === '//wsl.localhost/Ubuntu/home/jin/missing') { return false } return true @@ -5862,10 +5885,11 @@ describe('registerPtyHandlers', () => { handlers.get('pty:spawn')!(null, { cols: 80, rows: 24, - cwd: '\\\\wsl.localhost\\Ubuntu\\home\\jin\\missing' + cwd: '\\\\wsl.localhost\\Ubuntu\\home\\jin\\missing', + worktreeId: 'repo-1::\\\\wsl.localhost\\Ubuntu\\home\\jin' }) ).rejects.toThrow( - 'Working directory "\\\\wsl.localhost\\Ubuntu\\home\\jin\\missing" does not exist.' + 'Working directory "//wsl.localhost/Ubuntu/home/jin/missing" does not exist.' ) expect(spawnMock).not.toHaveBeenCalled() } finally { @@ -7352,7 +7376,8 @@ describe('registerPtyHandlers', () => { const result = await handlers.get('pty:spawn')!(null, { cols: 80, rows: 24, - cwd: '/tmp' + cwd: '/tmp', + worktreeId: 'repo-1::/tmp' }) expect(result).toEqual({ id: expect.any(String), pid: 12345 }) @@ -7392,7 +7417,8 @@ describe('registerPtyHandlers', () => { await handlers.get('pty:spawn')!(null, { cols: 80, rows: 24, - cwd: '/tmp' + cwd: '/tmp', + worktreeId: 'repo-1::/tmp' }) expect(spawnMock).toHaveBeenCalledTimes(1) @@ -7708,6 +7734,7 @@ describe('registerPtyHandlers', () => { cols: 80, rows: 24, cwd: '/tmp', + worktreeId: 'repo-1::/tmp', env: { SHELL: '/opt/homebrew/bin/bash' } }) diff --git a/src/main/ipc/pty.ts b/src/main/ipc/pty.ts index 9538f246415..8a2c109f744 100644 --- a/src/main/ipc/pty.ts +++ b/src/main/ipc/pty.ts @@ -78,6 +78,7 @@ import { parsePaneKey } from '../../shared/stable-pane-id' import { resolveTerminalStartupCwdForWorkspace } from '../../shared/terminal-startup-cwd' +import { localTerminalCwdCanonicalizer } from '../pty/terminal-cwd-realpath' import { clearMigrationUnsupportedPty, clearMigrationUnsupportedPtysForPaneKey @@ -1975,13 +1976,17 @@ export function registerPtyHandlers( const resolveGuardedPtySpawnCwd = ( worktreeId: string | undefined, - cwd: string | undefined + cwd: string | undefined, + connectionId?: string | null ): string | undefined => resolveTerminalStartupCwdForWorkspace({ workspaceId: worktreeId, requestedCwd: cwd, resolveFolderWorkspacePath: (folderWorkspaceId) => - store?.getFolderWorkspace(folderWorkspaceId)?.folderPath + store?.getFolderWorkspace(folderWorkspaceId)?.folderPath, + // Why: realpath only makes sense on the local filesystem; SSH worktree + // paths live on the remote host. + canonicalizePath: localTerminalCwdCanonicalizer(connectionId) }) // Why: the runtime controller must route through getProviderForPty() so that @@ -1994,7 +1999,7 @@ export function registerPtyHandlers( await startupPromise } await assertFolderWorkspacePtyPathUsable(args.worktreeId) - const cwd = resolveGuardedPtySpawnCwd(args.worktreeId, args.cwd) + const cwd = resolveGuardedPtySpawnCwd(args.worktreeId, args.cwd, args.connectionId) const provider = getProvider(args.connectionId) const isClaudeLaunch = !args.connectionId && isClaudeLaunchCommand(args.command) if (isClaudeLaunch && isClaudeAuthSwitchInProgress()) { @@ -2574,7 +2579,7 @@ export function registerPtyHandlers( await startupPromise } await assertFolderWorkspacePtyPathUsable(args.worktreeId) - const cwd = resolveGuardedPtySpawnCwd(args.worktreeId, args.cwd) + const cwd = resolveGuardedPtySpawnCwd(args.worktreeId, args.cwd, args.connectionId) spawnTiming.mark('preflight') const provider = getProvider(args.connectionId) const isClaudeLaunch = !args.connectionId && isClaudeLaunchCommand(args.command) diff --git a/src/main/pty/terminal-cwd-realpath.test.ts b/src/main/pty/terminal-cwd-realpath.test.ts new file mode 100644 index 00000000000..47110648b19 --- /dev/null +++ b/src/main/pty/terminal-cwd-realpath.test.ts @@ -0,0 +1,43 @@ +import { mkdtempSync, mkdirSync, rmSync, symlinkSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterAll, describe, expect, it } from 'vitest' +import { + canonicalizeLocalTerminalPath, + localTerminalCwdCanonicalizer +} from './terminal-cwd-realpath' + +describe('canonicalizeLocalTerminalPath', () => { + const root = mkdtempSync(join(tmpdir(), 'orca-cwd-realpath-')) + + afterAll(() => { + rmSync(root, { recursive: true, force: true }) + }) + + it('resolves symlinks to their target', () => { + const outside = join(root, 'outside') + const worktree = join(root, 'worktree') + mkdirSync(outside) + mkdirSync(worktree) + const link = join(worktree, 'escape') + symlinkSync(outside, link, 'dir') + expect(canonicalizeLocalTerminalPath(link)).toBe(canonicalizeLocalTerminalPath(outside)) + }) + + it('returns null for nonexistent paths', () => { + expect(canonicalizeLocalTerminalPath(join(root, 'does-not-exist'))).toBeNull() + }) + + it('skips WSL UNC paths instead of touching the 9P share', () => { + expect(canonicalizeLocalTerminalPath('\\\\wsl.localhost\\Ubuntu\\home\\jin\\repo')).toBeNull() + expect(canonicalizeLocalTerminalPath('//wsl$/Ubuntu/home/jin/repo')).toBeNull() + }) +}) + +describe('localTerminalCwdCanonicalizer', () => { + it('is disabled for SSH connections and enabled locally', () => { + expect(localTerminalCwdCanonicalizer('ssh-1')).toBeUndefined() + expect(localTerminalCwdCanonicalizer(null)).toBe(canonicalizeLocalTerminalPath) + expect(localTerminalCwdCanonicalizer(undefined)).toBe(canonicalizeLocalTerminalPath) + }) +}) diff --git a/src/main/pty/terminal-cwd-realpath.ts b/src/main/pty/terminal-cwd-realpath.ts new file mode 100644 index 00000000000..f9131d9f01a --- /dev/null +++ b/src/main/pty/terminal-cwd-realpath.ts @@ -0,0 +1,30 @@ +import { realpathSync } from 'node:fs' +import { isWslUncPath } from '../../shared/wsl-paths' + +// Why: terminal startup cwd containment is string-based; realpath closes the +// symlink escape for local paths. Only for local worktrees — SSH/remote paths +// are not resolvable on this filesystem. +export function canonicalizeLocalTerminalPath(targetPath: string): string | null { + // Why: realpath over the WSL 9P share is unreliable and can block the main + // process while the distro boots; keep the string containment check only. + if (isWslUncPath(targetPath)) { + return null + } + try { + return realpathSync.native(targetPath) + } catch { + try { + // Why: realpathSync.native can fail on network/UNC mounts where the + // JS implementation still succeeds (same fallback as git repo scanning). + return realpathSync(targetPath) + } catch { + return null + } + } +} + +export function localTerminalCwdCanonicalizer( + connectionId: string | null | undefined +): ((path: string) => string | null) | undefined { + return connectionId ? undefined : canonicalizeLocalTerminalPath +} diff --git a/src/main/runtime/orca-runtime.ts b/src/main/runtime/orca-runtime.ts index 1ef8374f0d4..cc3e26e590c 100644 --- a/src/main/runtime/orca-runtime.ts +++ b/src/main/runtime/orca-runtime.ts @@ -202,6 +202,7 @@ import { normalizeRuntimePathForComparison } from '../../shared/cross-platform-path' import { resolveTerminalStartupCwd } from '../../shared/terminal-startup-cwd' +import { localTerminalCwdCanonicalizer } from '../pty/terminal-cwd-realpath' import { isWslUncPath } from '../../shared/wsl-paths' import { folderWorkspaceKey, @@ -15493,7 +15494,7 @@ export class OrcaRuntimeService { throw new Error('runtime_unavailable') } const workspace = await this.resolveTerminalWorkspaceLaunchScope(worktreeSelector) - const cwd = resolveTerminalStartupCwd(workspace.path, opts.cwd) ?? workspace.path + const cwd = this.resolveGuardedWorkspaceTerminalCwd(workspace, opts.cwd) ?? workspace.path const preAllocatedHandle = this.createPreAllocatedTerminalHandle() // Why: mint tabId in main before spawn so paneKey is known at PTY env // build time. Hook-based agent status (Claude/Codex/Cursor/Gemini) keys @@ -15678,7 +15679,7 @@ export class OrcaRuntimeService { ? await this.resolveTerminalWorkspaceLaunchScope(worktreeSelector) : null const worktreeId = workspace?.id - const cwd = workspace ? resolveTerminalStartupCwd(workspace.path, opts.cwd) : opts.cwd + const cwd = workspace ? this.resolveGuardedWorkspaceTerminalCwd(workspace, opts.cwd) : opts.cwd const requestId = randomUUID() // Why: terminal creation is a renderer-side Zustand store operation (like @@ -15821,7 +15822,7 @@ export class OrcaRuntimeService { this.assertGraphReady() const workspace = await this.resolveTerminalWorkspaceLaunchScope(worktreeSelector) const worktreeId = workspace.id - const cwd = resolveTerminalStartupCwd(workspace.path, opts.cwd) + const cwd = this.resolveGuardedWorkspaceTerminalCwd(workspace, opts.cwd) this.hydrateHeadlessMobileSessionTabsFromWorkspaceSession(worktreeId) let afterDesktopTabId: string | undefined if (opts.afterTabId) { @@ -16025,7 +16026,7 @@ export class OrcaRuntimeService { } = {} ): Promise { const workspace = await this.resolveTerminalWorkspaceLaunchScope(`id:${worktreeId}`) - const cwd = resolveTerminalStartupCwd(workspace.path, opts.cwd) + const cwd = this.resolveGuardedWorkspaceTerminalCwd(workspace, opts.cwd) // Why: SshPtyProvider treats sessionId as a relay reattach request. Only // synthesize local serve ids; SSH fresh terminals must call pty.spawn. const stableSessionId = @@ -16894,6 +16895,17 @@ export class OrcaRuntimeService { } } + // Why: every terminal-creation path must apply the same symlink-aware cwd + // guard; keep the canonicalizer wiring in one place. + private resolveGuardedWorkspaceTerminalCwd( + workspace: Pick, + requestedCwd?: string | null + ): string | undefined { + return resolveTerminalStartupCwd(workspace.path, requestedCwd, { + canonicalizePath: localTerminalCwdCanonicalizer(workspace.connectionId) + }) + } + private async resolveTerminalWorkspaceLaunchScope( selector: string ): Promise { diff --git a/src/shared/terminal-startup-cwd.test.ts b/src/shared/terminal-startup-cwd.test.ts index e6ac5575ceb..f87ef7d51ce 100644 --- a/src/shared/terminal-startup-cwd.test.ts +++ b/src/shared/terminal-startup-cwd.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from 'vitest' +import { FLOATING_TERMINAL_WORKTREE_ID } from './constants' import { resolveTerminalStartupCwd, resolveTerminalStartupCwdForWorkspace @@ -28,6 +29,31 @@ describe('resolveTerminalStartupCwd', () => { ) }) + it('trims whitespace-padded requested cwds before resolving', () => { + expect(resolveTerminalStartupCwd('/repo/app', ' packages/web ')).toBe('/repo/app/packages/web') + }) + + it('falls back to the default cwd when a symlink escapes the worktree', () => { + const canonicalize = (path: string): string | null => + path === '/repo/app/link' ? '/outside/target' : path + expect( + resolveTerminalStartupCwd('/repo/app', 'link', { canonicalizePath: canonicalize }) + ).toBeUndefined() + }) + + it('accepts cwds under a symlinked worktree root', () => { + const canonicalize = (path: string): string | null => path.replace(/^\/tmp\//, '/private/tmp/') + expect( + resolveTerminalStartupCwd('/tmp/repo', 'packages/web', { canonicalizePath: canonicalize }) + ).toBe('/tmp/repo/packages/web') + }) + + it('skips the symlink re-check when a path cannot be canonicalized', () => { + expect( + resolveTerminalStartupCwd('/repo/app', 'missing', { canonicalizePath: () => null }) + ).toBe('/repo/app/missing') + }) + it('handles Windows path containment without case drift', () => { expect(resolveTerminalStartupCwd('C:\\Repo\\App', 'packages\\web')).toBe( 'C:/Repo/App/packages/web' @@ -52,6 +78,32 @@ describe('resolveTerminalStartupCwd', () => { ).toThrow('Terminal cwd must be inside the selected worktree.') }) + it('passes floating terminal cwds through untouched', () => { + // Why: floating terminal cwds are validated against trusted-directory + // grants in main and have no worktree root to contain within. + expect( + resolveTerminalStartupCwdForWorkspace({ + workspaceId: FLOATING_TERMINAL_WORKTREE_ID, + requestedCwd: '/Volumes/work/notes' + }) + ).toBe('/Volumes/work/notes') + }) + + it('refuses the requested cwd when no workspace root is resolvable', () => { + expect( + resolveTerminalStartupCwdForWorkspace({ + workspaceId: undefined, + requestedCwd: '/anywhere' + }) + ).toBeUndefined() + expect( + resolveTerminalStartupCwdForWorkspace({ + workspaceId: 'opaque-worktree-id', + requestedCwd: '/anywhere' + }) + ).toBeUndefined() + }) + it('validates renderer PTY cwd values against folder workspace keys', () => { expect( resolveTerminalStartupCwdForWorkspace({ diff --git a/src/shared/terminal-startup-cwd.ts b/src/shared/terminal-startup-cwd.ts index d71a0f7c37d..b71b737fa43 100644 --- a/src/shared/terminal-startup-cwd.ts +++ b/src/shared/terminal-startup-cwd.ts @@ -1,20 +1,44 @@ +import { FLOATING_TERMINAL_WORKTREE_ID } from './constants' import { isPathInsideOrEqual, resolveRuntimePath } from './cross-platform-path' import { parseWorkspaceKey } from './workspace-scope' import { splitWorktreeIdForFilesystem } from './worktree-id' +export type TerminalStartupCwdOptions = { + // Why: the string containment check can't see symlinks; only local callers + // can canonicalize — SSH worktree paths live on the remote host. + canonicalizePath?: (path: string) => string | null +} + export function resolveTerminalStartupCwd( worktreePath: string, - requestedCwd?: string | null + requestedCwd?: string | null, + options?: TerminalStartupCwdOptions ): string | undefined { - if (!requestedCwd || requestedCwd.trim().length === 0) { + const trimmedCwd = requestedCwd?.trim() + if (!trimmedCwd) { return undefined } - const resolvedCwd = resolveRuntimePath(worktreePath, requestedCwd) + const resolvedCwd = resolveRuntimePath(worktreePath, trimmedCwd) if (!isPathInsideOrEqual(worktreePath, resolvedCwd)) { // Why: remote/session clients can request terminal cwd; never let that // become a shell outside the selected workspace. throw new Error('Terminal cwd must be inside the selected worktree.') } + const canonicalizePath = options?.canonicalizePath + if (canonicalizePath) { + const canonicalWorktreePath = canonicalizePath(worktreePath) + const canonicalCwd = canonicalizePath(resolvedCwd) + if ( + canonicalWorktreePath && + canonicalCwd && + !isPathInsideOrEqual(canonicalWorktreePath, canonicalCwd) + ) { + // Why: a symlink escaping the worktree can be legitimate (e.g. a pnpm + // store link), so fall back to the default cwd instead of failing the + // spawn — but never grant the requested out-of-worktree shell. + return undefined + } + } return resolvedCwd } @@ -22,18 +46,30 @@ export function resolveTerminalStartupCwdForWorkspace(args: { workspaceId?: string requestedCwd?: string | null resolveFolderWorkspacePath?: (folderWorkspaceId: string) => string | null | undefined + canonicalizePath?: (path: string) => string | null }): string | undefined { if (!args.requestedCwd || args.requestedCwd.trim().length === 0) { return undefined } + if (args.workspaceId === FLOATING_TERMINAL_WORKTREE_ID) { + // Why: floating terminals have no worktree root to contain within; their + // cwd was already validated against the trusted-directory grants in + // resolveFloatingTerminalCwd. + return args.requestedCwd + } const workspacePath = resolveTerminalWorkspacePath( args.workspaceId, args.resolveFolderWorkspacePath ) if (!workspacePath) { - return args.requestedCwd + // Why: without a resolvable workspace root we cannot enforce the + // in-worktree containment check, so refuse the requested cwd rather + // than trusting an unvalidated path. + return undefined } - return resolveTerminalStartupCwd(workspacePath, args.requestedCwd) + return resolveTerminalStartupCwd(workspacePath, args.requestedCwd, { + canonicalizePath: args.canonicalizePath + }) } function resolveTerminalWorkspacePath( From 9964d1ccbf24835986ecfc2b1158fcc98bbce3fe Mon Sep 17 00:00:00 2001 From: Mark Xian Date: Sat, 4 Jul 2026 16:34:40 +0800 Subject: [PATCH 09/45] perf: overlap sidebar-scope loads with worktree scan at startup (#7225) (#7306) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * perf: overlap sidebar-scope loads with worktree scan at startup (#7225) Continues the renderer-chain parallelization proposed in #7225 (proposal 2), on top of the merged #7266: - Run project-groups → folder-workspaces concurrently with the per-repo `git worktree list` fan-out. Neither reads the repos store nor worktrees, so a slow remote host's 15s scope RPCs no longer queue ahead of the scan. - Raise worktree refresh concurrency 5 → 8 so multi-core machines run fewer sequential scan batches, still bounded so one moment can't launch every git probe at once. Co-Authored-By: Claude Opus 4.8 (1M context) * Test overlapping sidebar scope loads and worktree hydration at startup Verify that sidebar scope loads and worktree hydration operations run concurrently before session hydration. This protects against regressions in startup performance under the perf/startup-lag optimization. Additionally, document the rationale for the worktree refresh concurrency limit in the worktrees slice. * Clarify worktree refresh concurrency comment --------- Co-authored-by: Claude Opus 4.8 (1M context) Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com> --- src/renderer/src/App.tsx | 25 +++++++++++----- src/renderer/src/app-startup-routing.test.ts | 29 +++++++++++++++++-- .../src/store/slices/worktrees.test.ts | 9 +++--- src/renderer/src/store/slices/worktrees.ts | 5 +++- 4 files changed, 52 insertions(+), 16 deletions(-) diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index c7b40a929db..72f93b7bd5f 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -878,19 +878,28 @@ function App(): React.JSX.Element { // active one) so a cold start that restored a remote workspace doesn't // hide local repos. The sidebar "All hosts" scope then shows them all. await timeRendererStartupStep('fetch-repos', () => actions.fetchReposForAllHosts()) - await timeRendererStartupStep('fetch-project-groups', () => - actions.fetchProjectGroupsForAllHosts() - ) - await timeRendererStartupStep('fetch-folder-workspaces', () => - actions.fetchFolderWorkspacesForAllHosts() - ) - // Why: both of these fan out `git worktree list` per repo on the main - // process. Running them concurrently lets the main process share one + // Why: project-groups/folder-workspaces read neither the repos store nor + // worktrees, so once repos land they can overlap the per-repo + // `git worktree list` fan-out (#7225) instead of queuing ahead of it — a + // slow remote host's 15s scope RPCs no longer block the scan. folders + // still follow project-groups (they read projectGroups); all settle + // before the hydrate steps below. + const projectScopeChain = (async () => { + await timeRendererStartupStep('fetch-project-groups', () => + actions.fetchProjectGroupsForAllHosts() + ) + await timeRendererStartupStep('fetch-folder-workspaces', () => + actions.fetchFolderWorkspacesForAllHosts() + ) + })() + // Why: worktrees + lineage both fan out `git worktree list` per repo on + // the main process. Running them concurrently lets it share one // in-flight scan per repo instead of paying the process-spawn fan-out // twice back-to-back — the dominant renderer-chain cost on Windows // (issue #7225). Lineage only reads settings + its own slice, so it // does not depend on the worktrees fetch having landed. await Promise.all([ + projectScopeChain, timeRendererStartupStep('fetch-worktrees', () => actions.fetchAllWorktrees()), timeRendererStartupStep('fetch-worktree-lineage', () => actions.fetchWorktreeLineage()) ]) diff --git a/src/renderer/src/app-startup-routing.test.ts b/src/renderer/src/app-startup-routing.test.ts index ad2f480bdd9..81135b32cae 100644 --- a/src/renderer/src/app-startup-routing.test.ts +++ b/src/renderer/src/app-startup-routing.test.ts @@ -6,9 +6,7 @@ describe('renderer startup runtime routing', () => { it('loads settings before repo and worktree hydration', () => { const source = readFileSync(join(process.cwd(), 'src/renderer/src/App.tsx'), 'utf8') const startupBlockStart = source.indexOf('void (async () => {') - const startupBlockEnd = source.indexOf( - "const persistedUI = await timeRendererStartupStep('ui-get'" - ) + const startupBlockEnd = source.indexOf('const persistedUI = await uiGetPromise') const startupBlock = source.slice(startupBlockStart, startupBlockEnd) const settingsIndex = startupBlock.indexOf('actions.fetchSettings()') @@ -17,6 +15,31 @@ describe('renderer startup runtime routing', () => { expect(settingsIndex).toBeLessThan(startupBlock.indexOf('actions.fetchAllWorktrees()')) }) + it('overlaps sidebar scope loads with worktree hydration before session hydration', () => { + const source = readFileSync(join(process.cwd(), 'src/renderer/src/App.tsx'), 'utf8') + const startupBlockStart = source.indexOf('void (async () => {') + const startupBlockEnd = source.indexOf('const persistedUI = await uiGetPromise') + const startupBlock = source.slice(startupBlockStart, startupBlockEnd) + + const reposIndex = startupBlock.indexOf('actions.fetchReposForAllHosts()') + const scopeChainIndex = startupBlock.indexOf('const projectScopeChain = (async () => {') + const projectGroupsIndex = startupBlock.indexOf('actions.fetchProjectGroupsForAllHosts()') + const folderWorkspacesIndex = startupBlock.indexOf('actions.fetchFolderWorkspacesForAllHosts()') + const promiseAllIndex = startupBlock.indexOf('await Promise.all([') + const awaitedScopeChainIndex = startupBlock.indexOf('projectScopeChain', promiseAllIndex) + const worktreesIndex = startupBlock.indexOf('actions.fetchAllWorktrees()') + const lineageIndex = startupBlock.indexOf('actions.fetchWorktreeLineage()') + + expect(reposIndex).toBeGreaterThanOrEqual(0) + expect(scopeChainIndex).toBeGreaterThan(reposIndex) + expect(projectGroupsIndex).toBeGreaterThan(scopeChainIndex) + expect(folderWorkspacesIndex).toBeGreaterThan(projectGroupsIndex) + expect(promiseAllIndex).toBeGreaterThan(scopeChainIndex) + expect(awaitedScopeChainIndex).toBeGreaterThan(promiseAllIndex) + expect(worktreesIndex).toBeGreaterThan(promiseAllIndex) + expect(lineageIndex).toBeGreaterThan(promiseAllIndex) + }) + it('waits for first-window startup services before terminal reconnect', () => { const source = readFileSync(join(process.cwd(), 'src/renderer/src/App.tsx'), 'utf8') const reconnectIndex = source.indexOf('await actions.reconnectPersistedTerminals') diff --git a/src/renderer/src/store/slices/worktrees.test.ts b/src/renderer/src/store/slices/worktrees.test.ts index ce3ba3176b5..58dfa5cb7e2 100644 --- a/src/renderer/src/store/slices/worktrees.test.ts +++ b/src/renderer/src/store/slices/worktrees.test.ts @@ -91,6 +91,7 @@ const mockApi = { globalThis.window = { api: mockApi } import { + WORKTREE_REFRESH_CONCURRENCY, createWorktreeSlice, getHostedReviewLinkMutationGenerationForTests, getHostedReviewLinkWorktreeAliasCountForTests, @@ -5648,7 +5649,7 @@ describe('fetchAllWorktrees hydration-time purge (design §4.4)', () => { it('bounds concurrent repo scans during hydration-time refresh', async () => { const store = createTestStore() - const repos = Array.from({ length: 7 }, (_, index) => ({ + const repos = Array.from({ length: WORKTREE_REFRESH_CONCURRENCY + 2 }, (_, index) => ({ id: `repo-${index}`, path: `/repos/${index}`, displayName: `repo-${index}`, @@ -5670,14 +5671,14 @@ describe('fetchAllWorktrees hydration-time purge (design §4.4)', () => { await store.getState().fetchAllWorktrees() - expect(maxActiveScans).toBeLessThanOrEqual(5) + expect(maxActiveScans).toBeLessThanOrEqual(WORKTREE_REFRESH_CONCURRENCY) expect(mockApi.worktrees.list).toHaveBeenCalledTimes(repos.length) expect(store.getState().hasHydratedWorktreePurge).toBe(true) }) it('bounds concurrent repo scans after the hydration purge has run', async () => { const store = createTestStore() - const repos = Array.from({ length: 7 }, (_, index) => ({ + const repos = Array.from({ length: WORKTREE_REFRESH_CONCURRENCY + 2 }, (_, index) => ({ id: `repo-${index}`, path: `/repos/${index}`, displayName: `repo-${index}`, @@ -5702,7 +5703,7 @@ describe('fetchAllWorktrees hydration-time purge (design §4.4)', () => { await store.getState().fetchAllWorktrees() - expect(maxActiveScans).toBeLessThanOrEqual(5) + expect(maxActiveScans).toBeLessThanOrEqual(WORKTREE_REFRESH_CONCURRENCY) expect(mockApi.worktrees.list).toHaveBeenCalledTimes(repos.length) expect(store.getState().hasHydratedWorktreePurge).toBe(true) }) diff --git a/src/renderer/src/store/slices/worktrees.ts b/src/renderer/src/store/slices/worktrees.ts index 2ddc8100aca..15d44711ac5 100644 --- a/src/renderer/src/store/slices/worktrees.ts +++ b/src/renderer/src/store/slices/worktrees.ts @@ -72,7 +72,10 @@ const REMOTE_WORKTREE_LIST_PARITY_LIMIT = 10_000 const ACTIVE_WORKTREE_TERMINAL_PREP_DELAY_MS = 300 const ACTIVE_WORKTREE_TERMINAL_PREP_INPUT_QUIET_MS = 450 const ACTIVE_WORKTREE_TERMINAL_PREP_IDLE_TIMEOUT_MS = 180 -const WORKTREE_REFRESH_CONCURRENCY = 5 +// Why: each repo's `git worktree list` is an independent main-process child, so +// a higher ceiling cuts startup scan batches (#7225) while staying bounded so +// one UI moment can't launch every git probe at once. +export const WORKTREE_REFRESH_CONCURRENCY = 8 const pendingActivationTerminalPrepCancels = new Map void>() const detachedHeadAutoDerivedDisplayNames = new Map() const folderWorkspaceWorktreeCache = new WeakMap() From dd1133c873e5170f5b91caaa340bac03d08d6ece Mon Sep 17 00:00:00 2001 From: Loong <16333958+loong95@users.noreply.github.com> Date: Sat, 4 Jul 2026 16:52:41 +0800 Subject: [PATCH 10/45] chore: optimize and refine Chinese (zh-CN) translation (#7178) Refines 291 Simplified Chinese (zh-CN) strings for terminology consistency and clarity. Rebased onto main with full en/es/ja/ko key + interpolation parity; fixed placeholder parity on 4 keys and 9 reviewed translation-quality issues. Co-authored-by: linyunlong --- src/renderer/src/i18n/locales/zh.json | 582 +++++++++++++------------- 1 file changed, 291 insertions(+), 291 deletions(-) diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index 5970640488e..18fd3bae5fc 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -46,7 +46,7 @@ "showStatusBar": "显示状态栏", "showTasksButton": "显示任务按钮", "showAutomationsButton": "显示自动化按钮", - "showMobileButton": "显示 Orca手机端 按钮", + "showMobileButton": "显示 Orca 手机端 按钮", "showTitlebarAppName": "显示标题栏应用名称", "view": "查看", "reload": "重新加载", @@ -57,7 +57,7 @@ "openWorktreePalette": "打开工作树面板", "window": "窗口", "help": "帮助", - "paste": "Paste" + "paste": "粘贴" }, "tray": { "openOrca": "打开 Orca", @@ -94,8 +94,8 @@ "1468601e7b": "工作区列表出现错误。", "bdc71dddc9": "活动工作区保持打开状态。重试列表或切换视图。", "c1cf0b0e4a": "折叠窗格", - "8504ddf267": "应用仍在运行。请重试 shell,或通过菜单报告崩溃详情。", - "df1d56bf87": "工作区外壳遇到错误。", + "8504ddf267": "应用仍在运行。请重试工作区界面,或通过菜单报告崩溃详情。", + "df1d56bf87": "工作区界面遇到错误。", "9e0b441a91": "切换右侧边栏", "e81217c1b7": "隐藏应用名称", "5096cbbc86": "Orca", @@ -448,7 +448,7 @@ "action": { "plan": { "3f0ea9aa0d": "无法构建智能体启动命令。", - "46f1a2c9bd": "命令输入为空。", + "46f1a2c9bd": "提示词输入为空。", "8eb541cc83": "此工作区主机上未检测到所选智能体。", "b96e091fc9": "选定的智能体在“设置”中被禁用。", "a7ac8717c7": "在开始之前选择一个智能体。" @@ -457,7 +457,7 @@ }, "generation": { "plan": { - "dc480d5897": "命令输入为空。" + "dc480d5897": "提示词输入为空。" } } } @@ -537,10 +537,10 @@ "wslDefault": "WSL 默认" }, "sidebarWorktreeActivation": { - "wakeEphemeralVmFailed": "Failed to wake ephemeral VM workspace" + "wakeEphemeralVmFailed": "唤醒临时虚拟机工作区失败" }, "ephemeralVmWorkspaceTarget": { - "projectRootRegistrationFailed": "Failed to register the recipe-created project root on the runtime." + "projectRootRegistrationFailed": "在运行时上注册环境模板创建的项目根目录失败" } }, "hooks": { @@ -559,8 +559,8 @@ "chooseOrAddProjectBeforeWorkspace": "创建工作区前,请选择或添加项目。", "folderWorkspaceCreateFailedTitle": "文件夹工作区创建失败", "folderWorkspaceCreateFailedMessage": "无法创建文件夹工作区。请查看上方错误详情,然后重试。", - "setupAgentStartupPolicySaveFailed": "Failed to save setup startup behavior.", - "5f3d2c8a1b": "Failed to resolve MR base." + "setupAgentStartupPolicySaveFailed": "保存设置启动行为失败", + "5f3d2c8a1b": "解析 MR 基础分支失败" }, "useGlobalFileDrop": { "38c9f034ff": "无法上传删除的文件。", @@ -625,7 +625,7 @@ "a9fb10afca": "终端", "5235c215ca": "选择在“任务”页面和侧栏中显示的任务提供者。", "85f4fd7710": "任务来源", - "ab4b21b58e": "分支命名、基础引用、归因和 Git AI Author。", + "ab4b21b58e": "分支命名、基础引用、署名和 Git AI Author。", "09607cb0fe": "Git 和源代码控制", "33a5e1d597": "连接 GitHub、GitLab、Linear 和源托管服务。", "2b043783ef": "集成", @@ -648,11 +648,11 @@ "f70ac54d38": "AI 提供商账户", "4121f7a0a2": "管理 AI 智能体、设置默认值并自定义命令。", "b49abbd2f7": "智能体", - "dev": "Dev Tools", - "devDescription": "Dev-only tools for exercising UI states.", + "dev": "开发工具", + "devDescription": "仅用于调试 UI 状态的开发工具。", "devBadge": "Dev", - "devSearchNotificationPlayground": "Notification playground", - "devSearchNotificationPlaygroundDescription": "Trigger representative toast and notification UI states.", + "devSearchNotificationPlayground": "通知测试面板", + "devSearchNotificationPlaygroundDescription": "触发有代表性的 toast 和通知 UI 状态。", "devSearchKeywordDev": "dev", "devSearchKeywordToast": "toast", "devSearchKeywordSonner": "sonner", @@ -801,7 +801,7 @@ "d1fa2cf888": "没有PR负责人 SHA 无法发表评论。", "829674460a": "Diff 不可用,因为 PR commit SHA 丢失。", "af924014f8": "已查看", - "2d89a38d9d": "{{value0}} {{value1}} 如图所示", + "2d89a38d9d": "{{value0}} {{value1}} 为已查看", "70e84e3d0b": "没有匹配的评审人。", "1ffce94a8b": "其他人", "c2b21818e1": "建议", @@ -864,22 +864,22 @@ "5c1c973855": "移除评审人", "commentTooLarge": "评论过长,无法安全提交。", "timeline": { - "completed": "as completed", - "notPlanned": "as not planned", - "someone": "someone", - "assigned": "assigned", - "unassigned": "unassigned", - "mentioned": "mentioned this", - "in": "in", - "closed": "closed this", - "reopened": "reopened this", - "moved": "moved this", - "from": "from", - "to": "to", - "activity": "Activity", - "noActivity": "No activity yet." + "completed": "已完成", + "notPlanned": "未计划", + "someone": "某人", + "assigned": "已分配", + "unassigned": "已取消分配", + "mentioned": "提到了这个", + "in": "在", + "closed": "关闭了这个", + "reopened": "重新打开了这个", + "moved": "移动了这个", + "from": "从", + "to": "到", + "activity": "活动", + "noActivity": "暂无活动。" }, - "checkActionRequiredHint": "This check needs a manual action on GitHub (for example, approving the workflow run) before merging is unblocked." + "checkActionRequiredHint": "此检查需要在 GitHub 上执行手动操作(例如批准工作流运行)后才能解除合并阻止。" }, "GitLabItemDialog": { "65e784c1f1": "重新打开", @@ -1170,7 +1170,7 @@ "ephemeralVm": "工作区专属环境", "chooseRunTarget": "选择目标", "noRunTargets": "此项目还没有可用的运行目标。", - "perWorkspaceEnvHint": "根据配方按需创建环境", + "perWorkspaceEnvHint": "通过环境模板按需创建环境", "branchName": "分支名称", "branchNamePlaceholder": "feature/my-branch" }, @@ -1219,7 +1219,7 @@ "3912daf310": "此PR尚未报告检查。", "45877f5089": "未找到检查", "85e62c5266": "已启动 AI 智能体处理失败的检查。", - "ddfd42f460": "选择智能体并在启动前编辑完整的命令输入。", + "ddfd42f460": "启动智能体前请查看提示词。", "a053bdd082": "用 AI 修复失败的检查", "1b14d0a69c": "在 GitHub 中打开", "1550675e5f": "此检查没有可用的内联输出。", @@ -1288,7 +1288,7 @@ "d8c3ba91c4": "没有PR负责人 SHA 无法发表评论。", "74660bd80b": "Diff 不可用,因为 PR commit SHA 丢失。", "2e528e1c2d": "已查看", - "ff84e1f54c": "{{value0}} {{value1}} 如图所示", + "ff84e1f54c": "{{value0}} {{value1}} 为已查看", "5ad00c7a0e": "没有匹配的评审人。", "2760fa29a4": "其他人", "828f045847": "建议", @@ -1354,11 +1354,11 @@ "71a3c0f9d2": "启动工作区" }, "QuickOpen": { - "1dbd3f59ff": "手机", + "1dbd3f59ff": "移动", "73b2c581f1": "关闭", - "95fccbae88": "Esc键", - "61b1c871a6": "进行中", - "250e5b2dfb": "进入", + "95fccbae88": "Esc", + "61b1c871a6": "打开", + "250e5b2dfb": "回车", "74e2e1b3e4": "没有匹配的文件。", "722a21e1a8": "正在加载文件...", "1cb6ef47b7": "转到文件...", @@ -1460,7 +1460,7 @@ "6244a02f46": "在 Linear 中打开", "606a85c774": "进行中", "5e8061b088": "从 {{value0}} {{value1}} 开始工作区", - "592a55611b": "尝试选择更多的团队或者刷新;团队搜索器适用于当前获取的议题集。", + "592a55611b": "尝试选择更多的团队或者刷新;团队筛选条件适用于当前获取的议题集。", "618107fab3": "没有获取到与所选团队匹配的议题", "903c7af49f": "未找到 Linear 议题", "5ed38a49e5": "查看下面的工作区错误,然后刷新。", @@ -1645,10 +1645,10 @@ "e224d76876": "MR", "bbec4717ee": "mr", "d6d08c1650": "选择一个项目以查看 GitLab 工作项。", - "f294c500ef": "没有 GitLab 工作与此搜索器匹配。", - "cd7dc432a3": "没有 GitLab MR 与此搜索器匹配。", + "f294c500ef": "没有 GitLab 工作与此筛选条件匹配。", + "cd7dc432a3": "没有 GitLab MR 与此筛选条件匹配。", "171b7739d8": "mrs", - "a9f256ecea": "没有与此搜索器匹配的 GitLab 议题。", + "a9f256ecea": "没有与此筛选条件匹配的 GitLab 议题。", "2007e14d95": "GitLab", "456b8512da": "合并请求", "03da966159": "选择一个项目,以便我们可以向 GitLab 进行身份验证。", @@ -1771,7 +1771,7 @@ "f65d992a11": "进入", "c5081f2814": "当前工作树", "52404f8096": "当前选项卡", - "739bda980c": "主工作树", + "739bda980c": "主要", "556e7232ca": "当前", "684e8d7bc2": "收集您最近的工作树和打开的选项卡。", "ff908adfe9": "加载跳跃目标", @@ -1833,7 +1833,7 @@ "820fd21663": "此PR已关闭", "4f976d3450": "已关闭", "62eb8d39da": "此PR已合并", - "83ecdbb4a6": "合并", + "83ecdbb4a6": "已合并", "331ebe1170": "将此PR添加到 GitHub 合并队列", "b169f943e1": "准备好后合并", "62703b1dc4": "为此PR启用了 GitHub 自动合并", @@ -1877,7 +1877,7 @@ "8d669084f6": "受限", "6efdc0d920": "草稿", "d0d0e13a5a": "PR", - "af5d8c912a": "限制物品", + "af5d8c912a": "受限条目", "bb7ebc11e3": "添加编号", "9cb1a0c984": "添加文字", "2e26a06c70": "添加标签", @@ -1922,10 +1922,10 @@ "c3b81ddea2": "DRAFT_ISSUE" }, "ProjectViewList": { - "989f81dc2a": "专栏", + "989f81dc2a": "列", "f949f5b2b7": "配置列", "eddfc7a794": "按 {{value0}} 排序", - "4f57d2e0b1": "没有项目与此视图的搜索器匹配。" + "4f57d2e0b1": "没有项目与此视图的筛选条件匹配。" }, "ProjectViewWrapper": { "463f1205c0": "加载项目视图", @@ -1935,7 +1935,7 @@ "55de4fb57a": "{{value0}}。 {{value1}} 在 {{value2}} 提交功能请求。", "2edf5e7e77": "{{value0}} — Orca 尚不支持 {{value1}} 项目视图。在 {{value2}} 提交功能请求。", "7245c3d7ac": "清除搜索", - "c5bc7ec007": "查看搜索器:{{value0}}", + "c5bc7ec007": "视图筛选条件:{{value0}}", "840c268665": "添加仓库", "dffa899f36": "取消", "7037c8f5f1": "存储库不在 Orca 中", @@ -2012,8 +2012,8 @@ "9d0f2eda6d": "标签", "01f3f3d161": "作者", "13b3ac0a84": "状态", - "79c54552f7": "搜索器", - "8a2ffbf9b3": "删除 {{value0}} 搜索器", + "79c54552f7": "筛选条件", + "8a2ffbf9b3": "删除 {{value0}} 筛选条件", "19bb6f115f": "reviewed-by" }, "PRFilterPickers": { @@ -2028,7 +2028,7 @@ "de26e2eb06": "无标签", "458ea3602b": "没有作者", "b69fa4fa20": "返回", - "30ebb6ca44": "清除所有搜索器", + "30ebb6ca44": "清除所有筛选条件", "8177eda37e": "筛选", "ea3416d646": "负责人", "b1d9fdea08": "标签", @@ -2058,11 +2058,11 @@ "d12d3d6f33": "刷新 GitHub API 预算", "d5e5de9070": "Orca 通过 GitHub CLI 使用 REST、搜索和 GraphQL。", "58c5f88216": "GitHub API 预算", - "6da1858354": "左·重置于", + "6da1858354": "剩余 · 重置于", "f42790d150": "的", "01f7323e58": "GraphQL API", "1daf0f22a9": "GraphQL", - "1f2f28a4de": "搜索接口", + "1f2f28a4de": "搜索 API", "c377a4f06a": "搜索", "c392c749a6": "REST API", "bb227706a6": "REST", @@ -2203,13 +2203,13 @@ "sound": { "options": { "e38b0a2e68": "嘟", - "0acd3d384e": "唠叨", + "0acd3d384e": "咔嗒", "79919c832d": "丁", "2b44847d8d": "布洛普", "020826ef17": "声纳", "588c90487d": "光点", "1e4b81d892": "扑通", - "86af8d938c": "奉", + "86af8d938c": "咚", "80f7cc95b3": "二音", "017abebfa6": "系统默认值" } @@ -2226,7 +2226,7 @@ "767951265d": "创建工作树时出现问题。", "vmProvisioningTitle": "正在预配虚拟机", "cancelProvisioning": "取消", - "vmProvisioningLogEmpty": "正在等待配方输出…" + "vmProvisioningLogEmpty": "正在等待环境模板输出…" } } }, @@ -2373,7 +2373,7 @@ "ca414324ee": "命令文本", "dc921c17ee": "提示", "5b3f634a55": "添加快捷命令", - "f9b184fc16": "编辑快速命令", + "f9b184fc16": "编辑快捷命令", "6751598542": "编辑" }, "TerminalQuickCommandDialogFooter": { @@ -2445,9 +2445,9 @@ "98bccf4fa2": "向下拆分终端", "20e565d865": "向右拆分终端", "8a7ddb8b8a": "分叉智能体会话...", - "0a82b0608c": "添加快速命令...", - "9528a65ef8": "没有快速命令", - "3ce594a4a0": "全球的", + "0a82b0608c": "添加快捷命令...", + "9528a65ef8": "没有快捷命令", + "3ce594a4a0": "全局的", "ec85df5914": "快捷命令", "0a917b591a": "粘贴", "f3eeb1de13": "复制", @@ -2455,7 +2455,7 @@ "openLinkDefaultBrowser": "在默认浏览器中打开", "925f49f210": "展开窗格", "df766809e0": "折叠窗格", - "cff67afad1": "Copy Context" + "cff67afad1": "复制上下文" }, "TerminalErrorToast": { "e4aa243f8c": "重新启动守护进程", @@ -2588,8 +2588,8 @@ "localWorkspacesOnly": "从历史恢复仅支持本地工作区。", "openLocalWorkspace": "恢复会话前请先打开一个本地工作区。", "sessionQueued": "会话已排队", - "runtimeWorkspacesUnsupported": "Resume from history is not available in runtime-hosted workspaces.", - "openSupportedWorkspace": "Open a local or SSH workspace before resuming a session.", + "runtimeWorkspacesUnsupported": "在远程运行时托管的工作区中无法从历史记录恢复会话。", + "openSupportedWorkspace": "在恢复会话之前,请先打开一个本地或 SSH 工作区。", "localSessionSshWorkspaceUnsupported": "此会话的历史记录存储在本机上,因此无法在 SSH 工作区中恢复。请改为将其拖放到本地工作区。" }, "TabGroupDropOverlay": { @@ -2698,13 +2698,13 @@ "a2c7a33831": "命令", "20bbd75896": "无命令", "b82e237a4b": "更快捷的命令", - "85482c57bc": "运行快速命令", - "b775303755": "运行快速命令:{{value0}}", + "85482c57bc": "运行快捷命令", + "b775303755": "运行快捷命令:{{value0}}", "196593b6a9": "删除 {{value0}}", "15529ede69": "编辑 {{value0}}", - "1d411fb6a5": "为此仓库保存快速命令", + "1d411fb6a5": "为此仓库保存快捷命令", "8f1e971966": "添加快捷命令", - "3220e2da27": "该快速命令将从您保存的列表中删除。", + "3220e2da27": "该快捷命令将从您保存的列表中删除。", "e8e1a52edb": "删除“{{value0}}”?", "37e1bb90ce": "运行:{{value0}}", "77ac113df0": "开始 {{value0}}: {{value1}}", @@ -2852,7 +2852,7 @@ "16bc3c998a": "删除工作区 {{value0}}", "0f9e50eb07": "其他", "d406915b78": "渲染器", - "81cd37af99": "主要的", + "81cd37af99": "主进程", "fa6d36758d": "终止会话 {{value0}}", "b8f4a2c1d0e3": "{{value0}} 个孤立项", "c7e3b1a0d9f2": "终止 {{value0}} 个孤立终端", @@ -3022,7 +3022,7 @@ "ec7b076a75": "保留:git 未检查", "7ab8d7e2d7": "保留:更改的文件", "7f7895514e": "保持:活跃", - "2b501ee391": "保留:主要", + "2b501ee391": "保留:主工作树", "39801484e0": "失败的", "33653dbac2": "正在删除", "52b629eb84": "已更新", @@ -3123,7 +3123,7 @@ "7e76c84153": "最近的会话", "32176e1d44": "轮次", "02a046792e": "会话 •", - "f97435845c": "主要项目:", + "f97435845c": "用量最高的项目:", "7dc9e5613b": "按项目", "c3fdbc5474": "常用模型:", "0f394c24e3": "按模型", @@ -3177,7 +3177,7 @@ "0cb0983c07": "最近的会话", "79a69522a5": "事件", "bf1bf2f674": "会话 •", - "829ee743f2": "主要项目:", + "829ee743f2": "用量最高的项目:", "b98718aaab": "按项目", "95d2d89285": "常用模型:", "5a0d1d69cd": "按模型", @@ -3238,7 +3238,7 @@ "bed558df0b": "刷新 OpenCode 使用情况", "b5ed5c9fd0": "范围", "40d283c837": "范围", - "01583b30aa": "搜索器", + "01583b30aa": "筛选条件", "230d6de108": "OpenCode 使用选项", "bea80ceae0": "OpenCode 使用情况跟踪", "b8b3522436": "读取本地 OpenCode 使用日志以显示令牌、模型和会话统计信息。", @@ -3437,7 +3437,7 @@ "9963dff6d3": "没有找到描述。", "995fde8337": "无法显示技能文件", "ab5b777350": "检查本地主目录、存储库、捆绑和插件技能文件夹。", - "08a321a984": "调整搜索或搜索器。", + "08a321a984": "调整搜索或筛选条件。", "4acd6d68ec": "未找到本地技能", "6a62a0168c": "没有匹配项", "cd7893fbc1": "正在扫描技能", @@ -3738,7 +3738,7 @@ }, "SidebarFilter": { "e3b3898218": "添加项目", - "92a23e6d07": "重置搜索器", + "92a23e6d07": "重置筛选条件", "81ded53722": "SSH", "b9e8802e73": "没有匹配的项目", "779b7ba05d": "清除", @@ -3747,9 +3747,9 @@ "e5cb32a898": "隐藏默认分支", "638a2d221d": "隐藏休眠项", "f506a1262a": "搜索工作区", - "75405270ed": "编辑搜索器({{value0}} 活动)", + "75405270ed": "编辑筛选条件({{value0}} 个已启用)", "489d1c8c9f": "搜索项目...", - "ee240a39eb": "编辑搜索器", + "ee240a39eb": "编辑筛选条件", "automationCreated": "隐藏自动化创建的工作区" }, "SidebarHeader": { @@ -3777,7 +3777,7 @@ "SidebarRepositoryFilterSection": { "d3a9c4cea1": "清除", "7679f0c268": "项目", - "f10ca29601": "删除 {{value0}} 搜索器", + "f10ca29601": "删除 {{value0}} 筛选条件", "2656053db4": "SSH", "83a820fa71": "筛选项目...", "5a273fbfce": "添加项目...", @@ -3812,18 +3812,18 @@ "SidebarWorkspaceFilterSection": { "c3fa13dc2e": "隐藏默认分支", "ed1611b65b": "隐藏休眠项", - "82594419ba": "搜索器", + "82594419ba": "筛选条件", "automationCreated": "隐藏自动化创建的工作区" }, "SidebarWorkspaceOptionsMenu": { "95c9754653": "智能体活动布局", - "3d4b9c4997": "徘徊", + "3d4b9c4997": "悬停", "ba87080fb7": "显示属性", "320b675c9a": "卡片布局", "newCardDisplay": { "title": "卡片显示" }, - "09faabd875": "项目订单", + "09faabd875": "项目排序", "7bada3b1ab": "排序方式", "dc0bb670bc": "分组依据", "9919ae1082": "工作区选项", @@ -3926,9 +3926,9 @@ "a88c92d0e3": "{{value0}}/{{value1}} 已存在。", "6f09f58541": "删除工作区", "0777de5970": "主工作树(原始克隆目录)", - "0f33af979b": "部分结帐。这些路径之外的文件不在磁盘上。", - "4f964d5e8c": "疏", - "7d517f82e2": "主工作树", + "0f33af979b": "部分检出。这些路径之外的文件不在磁盘上。", + "4f964d5e8c": "稀疏", + "7d517f82e2": "主要", "4eba2ea99e": "自动命名失败。点击查看详情。", "74522ee457": "重命名失败", "02e19349f4": "自动重命名失败:查看错误", @@ -3939,7 +3939,7 @@ "8cb634cda6": "显示子工作区", "01f45d3d8a": "侧边栏", "93aebe4529": "文件夹", - "0d224eff10": "主要工作树", + "0d224eff10": "主工作树", "ca74db7550": "通过 SSH 远程项目", "021538e1d1": "SSH 已断开连接", "runtimeHostDisconnected": "服务器已断开连接", @@ -4035,7 +4035,7 @@ "changeParentWorkspace": "更改父工作树...", "setParentWorkspace": "设置父工作树...", "workspaceSection": "工作区", - "primaryDeleteDisabled": "主要工作树无法删除,请改为删除项目。", + "primaryDeleteDisabled": "主工作树不能删除。请改为移除项目。", "deleteWorktree": "删除工作树" }, "WorktreeList": { @@ -4043,7 +4043,7 @@ "bc1460beb3": "更新侧栏中显示的组名称。", "13757c053c": "新项目组", "f9dc6cc5d3": "重命名项目组", - "370c6a55dd": "清除搜索器", + "370c6a55dd": "清除筛选条件", "b7acbf038b": "未找到工作区", "0c6ee14f23": "孩子", "5fc9d1891b": "正在删除...", @@ -4236,10 +4236,10 @@ "6c1efa2cf8": "评审中", "6b8285b8dd": "完成", "93ac840dcb": "已阻塞", - "2c19d1db33": "Play", + "2c19d1db33": "播放", "111db162bf": "已暂停", "642da473f2": "警报", - "6380517b10": "Flag", + "6380517b10": "标记", "251c817bdd": "定时器", "409528031f": "评审", "5f9ca31a84": "等待中", @@ -4250,14 +4250,14 @@ "1a9383112b": "Conductor 进度", "caebe3c10f": "Conductor 评审", "895f381714": "Conductor 完成", - "caabd5ca85": "Zinc", - "7adb43ecf0": "Rose", - "ddf25b6262": "Emerald", - "7cebab6d4a": "Amber", - "1b81da243a": "Violet", - "6437a8c253": "Sky", - "fc3b92756c": "Blue", - "52e3c6e2a4": "Neutral" + "caabd5ca85": "锌", + "7adb43ecf0": "玫瑰", + "ddf25b6262": "翡翠", + "7cebab6d4a": "琥珀", + "1b81da243a": "紫罗兰", + "6437a8c253": "天空", + "fc3b92756c": "蓝色", + "52e3c6e2a4": "中性" } }, "worktree": { @@ -4486,7 +4486,7 @@ "saving": "正在保存...", "save": "保存", "label": "标签", - "sshLabelPlaceholder": "Dev box", + "sshLabelPlaceholder": "开发机", "sshHost": "主机或别名", "sshHostPlaceholder": "deploy@server:22", "username": "用户名", @@ -4495,7 +4495,7 @@ "identityFile": "身份文件", "identityFilePlaceholder": "~/.ssh/id_ed25519(可选)", "serverName": "服务器名称", - "serverNamePlaceholder": "Dev box", + "serverNamePlaceholder": "开发机", "pairingCode": "配对码", "pairingCodePlaceholder": "orca://pair?code=...", "pairingHelpPrefix": "在服务器上运行", @@ -4602,7 +4602,7 @@ "b15ce90870": "{{value0}} -> {{value1}}。在继续旧会话之前重新启动实时 Claude 终端。", "f921d32606": "Claude 账户已更新。", "5bf8764953": "Codex 账户更新失败。", - "9baf45d071": "此设备", + "9baf45d071": "当前设备", "2358ac71d2": "WSL 默认值", "ad47a33f72": "正在加载 WSL", "8619f9afa9": "WSL", @@ -4710,9 +4710,9 @@ "3057983501": "未分配", "0cd9b8228f": "选择 Dock 和窗口切换器中显示的应用程序图标。", "ca1590d42f": "应用程序图标", - "61d842eca0": "在侧边栏中显示 Orca手机端 快捷方式。它仍然可以从 Toolbox 中获取。", - "9da1020447": "显示 Orca手机端 按钮", - "5db6ba961f": "在左侧边栏顶部显示 Orca手机端 按钮。", + "61d842eca0": "在侧边栏中显示 Orca 手机端 快捷方式。它仍然可以从工具箱中获取。", + "9da1020447": "显示 Orca 手机端 按钮", + "5db6ba961f": "在左侧边栏顶部显示 Orca 手机端 按钮。", "fa882a3e6b": "在左侧边栏顶部显示“自动化”按钮。", "511f270ebb": "显示自动化按钮", "661942ab7f": "在左侧边栏顶部显示“任务”按钮。", @@ -4734,7 +4734,7 @@ "f687711a9b": "缩放整个应用程序界面。使用", "5e6d7aba8d": "用户界面缩放", "622e1c3465": "缩放整个应用程序界面。", - "fd89b5487c": "光", + "fd89b5487c": "浅色", "7d26ccabe8": "深色", "fb0e0b4453": "系统", "932ff1fbff": "主题", @@ -4762,12 +4762,12 @@ "windowSidebarSummary": "侧边栏、状态栏和文件浏览器", "statusBarCount": "显示 {{value0}} 个指示器。", "gitIgnoredGlossary": "与 .gitignore 匹配的文件。", - "statusBarDescription": "Choose which indicators appear in the status bar." + "statusBarDescription": "选择状态栏中显示哪些指示器。" }, "AutoRenameBranchFromWorkSetting": { "1626524572": "Nautilus", "0de9fda203": "放弃", - "c71770c455": "{基本提示}", + "c71770c455": "{basePrompt}", "f19a56498d": ";您的分支前缀设置仍然适用。", "800edb1e54": "修复登录流程", "5d569f5199": "。 Orca 仅生成最后一段,例如", @@ -4776,7 +4776,7 @@ "9c9b54e4ea": "内置分支名称提示", "69bf4830c2": "包括 Orca 的", "9241b59bf5": "使用", - "a869d0edd8": "分支名称命令模板", + "a869d0edd8": "分支名称提示词模板", "e784ea62dc": "高级", "d9b65054ef": ")到总结任务的简短名称。只有 Orca 命名的分支才会被重命名,并且在推送后不会被重命名。", "12ea4a408d": "当智能体开始在新工作区中工作时,Orca 会重命名其自动生成的分支(例如", @@ -4786,8 +4786,8 @@ "cfd82406dd": "保存中...", "40e7be7850": "已保存", "7c7e34a66d": "未保存的更改", - "a4fa380b67": "{助理消息}", - "2ee2779c05": "{第一提示}" + "a4fa380b67": "{assistantMessage}", + "2ee2779c05": "{firstPrompt}" }, "AutoRenameBranchPromptEditor": { "63121132c0": "放弃", @@ -4797,7 +4797,7 @@ "af2d9a2cc6": "。 Orca 仅生成最后一段,例如", "182d419b97": "内置分支名称提示", "2f5dc661fe": "附加到 Orca 的", - "7d6176f506": "迅速的", + "7d6176f506": "提示词", "5968112152": "保存", "54ac229ad4": "保存中...", "af0831a590": "已保存", @@ -4808,9 +4808,9 @@ "d166ff883d": "当前", "a4a9372eb2": "搜索分支...", "7db7fb87e5": "按名称搜索分支...", - "773a5687a3": "使用主要", + "773a5687a3": "跟随主分支", "ade9a5bb03": ") 来确定结果范围。", - "b468f46726": "上游/主要", + "b468f46726": "upstream/main", "80f7c82303": ")或完整的参考(例如", "915ad97875": "上游", "a5c16712c1": "已检测多个遥控器。输入远程名称(例如", @@ -4933,7 +4933,7 @@ "15eaad0d31": "命令路径:", "5dae812f50": "刷新", "52e640f3a0": "刷新 CLI 状态", - "38edbb5721": "外壳命令", + "38edbb5721": "Shell 命令", "6930feda9e": "从终端使用 Orca 打开应用程序、管理工作树并与 Orca 终端交互。", "c5c0f2641d": "Orca CLI", "d77352f2df": "无法从 PATH 中删除 `{{value0}}`。", @@ -4982,11 +4982,11 @@ "e001734396": "除非在编辑器中更改,否则将托管评审创建为草稿。", "6ba48f07a4": "默认草稿", "15b60d54b2": "例如ollama 运行 llama3.1 {提示}", - "3f1b26cc91": "将命令输入作为参数传递;否则 Orca 将其通过标准输入进行传输。", - "4f722a5f53": "由选择自定义命令的 commit 消息、PR和分支名称配方使用。使用", + "3f1b26cc91": "将提示词输入作为参数传递;否则 Orca 会通过标准输入传递。", + "4f722a5f53": "由选择自定义命令的 commit 消息、PR 和分支名称预设使用。使用", "47e45cbd5a": "自定义命令", - "1ef29f8c29": "当文本配方使用自定义命令时,命令行 Orca 运行。", - "2339a89104": "添加 AI 按钮,用于使用该操作的命令模板运行选定的智能体。", + "1ef29f8c29": "文本生成预设使用自定义命令时,Orca 运行的命令行。", + "2339a89104": "添加 AI 按钮,用于使用该操作的提示词模板运行选定的智能体。", "d5b45a3628": "显示源代码控制 AI 操作", "7bcad2b200": "为源代码管理的 commit、PR、分支命名和修复操作添加操作方案。", "d54c64163d": "已启用", @@ -5001,10 +5001,10 @@ "02bab6542c": "PR", "fdee745b87": "合并请求", "b388463881": "PR", - "19e10a12bb": "主持评论", - "b8b6fd55b4": "{迅速的}", + "19e10a12bb": "托管评审", + "b8b6fd55b4": "{prompt}", "fc1a525fa5": "占位符", - "a69e1fe91a": "迅速的", + "a69e1fe91a": "提示词", "1df7d71313": "二进制", "407d28bde6": "命令行", "54038660e0": "命令", @@ -5131,8 +5131,8 @@ "5f02e6fb21": "在富文本编辑器模式下显示本地 Markdown 评审笔记控件。", "51161d1647": "编辑文件时显示小地图概述。", "6690b1ffb9": "小地图", - "5a1ea6eaa2": "隐", - "73a09aad63": "如图所示", + "5a1ea6eaa2": "隐藏", + "73a09aad63": "显示", "1de48ad940": "默认差异文件树", "1b87897af9": "打开组合差异视图时显示或隐藏文件树。", "12cbc0d0d6": "并排", @@ -5163,8 +5163,8 @@ "0adfce9fa7": "支持http、https、socks、socks4 和socks5 URL。", "476f302aca": "http://proxy.example.com:8080", "1e214e265a": "留空以使用系统代理设置和继承的代理环境变量。", - "f00daf6324": "HTTPAgent", - "823e0f15b1": "Orca 网络请求和本地终端子项的智能体 URL。", + "f00daf6324": "HTTP 代理", + "823e0f15b1": "用于 Orca 网络请求和本地终端子进程的代理 URL。", "d93c7cd531": "配置应用程序级网络路由。", "c46cdbbd4e": "网络", "configureProxy": "配置代理" @@ -5251,10 +5251,10 @@ "1f744a72f4": "配置" }, "GitPane": { - "d2eede4c54": "将 Orca 归因添加到 commits、PR 和议题。", - "e02ea23a32": "Orca 归因", + "d2eede4c54": "将 Orca 署名添加到 commits、PR 和议题。", + "e02ea23a32": "Orca 署名", "e71ce09c42": "orca", - "b9b5771bb1": "归因", + "b9b5771bb1": "署名", "b5f534717a": "合着的", "9838c921ed": "共同作者", "afada55042": "议题", @@ -5271,7 +5271,7 @@ "612a440e57": "GitHub API 预算", "2cde9044a8": "graphql", "36e3de3619": "通过与陈旧的历史进行比较。如果该分支有未 commits 的更改或仅限本地 commits,Orca 会跳过更新。", - "d072a12995": "git diff 主要...HEAD", + "d072a12995": "git diff main...HEAD", "db3a127eb1": "。这使得命令像", "3ae3de8898": "master", "5bf885be48": "或者", @@ -5508,7 +5508,7 @@ "668016be7a": "在您的计算机和手机上。", "1dc87a7fbc": "Tailscale", "51d29927eb": "安装", - "9fc5d203ff": "Orca手机端 直接连接到此计算机。要在远离同一本地网络的地方使用它,请将您的计算机和手机置于同一私有覆盖网络上,然后使用所选的网络地址生成二维码。", + "9fc5d203ff": "Orca 手机端 直接连接到此计算机。要在远离同一本地网络的地方使用它,请将您的计算机和手机置于同一私有覆盖网络上,然后使用所选的网络地址生成二维码。", "39fad211d9": "使用尾网连接 Wi-Fi 外部", "a9db5d771d": "刷新网络接口", "b2c384cfd6": "没有找到接口", @@ -5518,7 +5518,7 @@ "1e64659126": "再生" }, "MobilePane": { - "dd3cd78d04": "使用 Orca手机端 扫描", + "dd3cd78d04": "使用 Orca 手机端 扫描", "35100bca5d": "当您在手机上使用终端时,Orca 会将其缩小以适合您的手机屏幕。当您关闭应用程序或切换离开时,这可以控制它是保持手机大小(因此交互式 CLI 工具不会回流)还是将大小调整回桌面。您始终可以在横幅上使用“恢复此终端”或“恢复所有终端”来手动调整大小。", "ee56f1c7e4": "当您离开手机应用程序时", "3939fd062c": "撤销设备会立即断开连接。", @@ -5547,9 +5547,9 @@ "c8491c17ef": "通过扫描二维码从手机控制 Orca。 Beta/早期预览 - 预计会出现错误和重大更改。从以下位置获取 iOS 应用程序", "e7a3ae8c4e": "移动端", "174f4a3c6d": "通过手机控制终端和智能体。", - "1de96ec8a6": "显示 Orca手机端 按钮", - "682293cadf": "在左侧边栏顶部显示 Orca手机端 按钮。", - "d4f2b65f30": "在边栏中显示 Orca手机端 快捷入口。" + "1de96ec8a6": "显示 Orca 手机端 按钮", + "682293cadf": "在左侧边栏顶部显示 Orca 手机端 按钮。", + "d4f2b65f30": "在边栏中显示 Orca 手机端 快捷入口。" }, "NotificationsPane": { "906b4afebf": "发送测试通知", @@ -5689,12 +5689,12 @@ "QuickCommandsPane": { "8764c6e9e4": "删除 {{value0}}", "7d90fd5299": "编辑 {{value0}}", - "8c877dec41": "全球的", + "8c877dec41": "全局的", "c6b155911b": "所有命令", "5aacc8f7dc": "添加命令", - "c36912efd5": "从选项卡栏中的“快速命令”按钮运行它们,或者在任何终端内右键单击。", + "c36912efd5": "从选项卡栏中的“快捷命令”按钮运行它们,或者在任何终端内右键单击。", "f91b649324": "保存的命令", - "3d9dc558e8": "该快速命令将从您保存的列表中删除。", + "3d9dc558e8": "该快捷命令将从您保存的列表中删除。", "3edf3deaf8": "删除“{{value0}}”?", "9fcfc29519": "插入", "9b3e338d62": "进入", @@ -5703,7 +5703,7 @@ "7784912ed6": "仓库", "2bb9e38e93": "无题", "3eb9897ab0": "所选范围内没有命令。", - "38d61927e6": "没有保存快速命令。", + "38d61927e6": "没有保存快捷命令。", "44923dd982": "destructive", "ec1ed99e70": "删除", "d1d0976320": "没有任何" @@ -5768,7 +5768,7 @@ "8d6c56bff8": "运行两者", "0fa21e19ec": "工作区的名称,通常基于分支名称。", "54c73d88d0": "正在创建的工作树的路径。安装命令从此目录运行。", - "30952c4aa4": "主仓库结帐的路径。对于将共享文件(例如 .env)复制到工作树中非常有用。", + "30952c4aa4": "主仓库检出目录的路径。对于将共享文件(例如 .env)复制到工作树中非常有用。", "9b821fa19d": "# e.g. echo \"Cleaning up $ORCA_WORKSPACE_NAME\"", "6f90ebe3fd": "在归档或删除工作树之前运行。", "a3fc966677": "# e.g. pnpm install cp \"$ORCA_ROOT_PATH/.env\" \"$ORCA_WORKTREE_PATH/.env\"", @@ -5893,7 +5893,7 @@ "projectRuntimeDescription": "选择此项目运行在 Windows 还是 WSL。" }, "RepositorySourceControlAiActionRows": { - "548a6e1281": "命令模板", + "548a6e1281": "提示词模板", "7a3a8e431d": "CLI 参数", "2b2f38652b": "自定义命令", "0ffb081b3a": "使用默认智能体", @@ -6102,7 +6102,7 @@ "3de4bbb841": "终端", "dd72ed437a": "选择在“任务”页面和侧栏中显示的任务提供者。", "11faa2f7dd": "任务来源", - "cfa34f4465": "分支命名、基础引用、归因和 Git AI Author。", + "cfa34f4465": "分支命名、基础引用、署名和 Git AI Author。", "70100f94c7": "Git 和源代码控制", "b07041697f": "连接 GitHub、GitLab、Linear 和源托管服务。", "c9ca101a3b": "集成", @@ -6135,8 +6135,8 @@ "23c6874fdf": "AI 能力", "2309068a6f": "destructive", "65358016ea": "放弃", - "dev": "Dev Tools", - "devDescription": "Dev-only tools for exercising UI states." + "dev": "开发工具", + "devDescription": "仅用于调试 UI 状态的开发工具。" }, "SettingsFormControls": { "42a4d15a30": "没有匹配的字体。", @@ -6170,14 +6170,14 @@ }, "ShortcutFilterRail": { "28b63545bf": "状态", - "8a1e78c14b": "快捷状态搜索器", + "8a1e78c14b": "快捷键状态筛选条件", "df8466f3fc": "清除快捷方式搜索", "f733c4b89f": "搜索命令或按键", "02dc7d4251": "搜索快捷键", "1d5634ba31": "{{value0}} 快捷方式" }, "ShortcutRowsList": { - "4ce3cd24d9": "没有与这些搜索器匹配的快捷键。" + "4ce3cd24d9": "没有与这些筛选条件匹配的快捷键。" }, "ShortcutTerminalPolicyControl": { "0762983d13": "终端优先", @@ -6203,13 +6203,13 @@ "SourceControlAiActionRecipeDefaults": { "2576299196": "参数", "b3914ecbbc": "放弃", - "fb09da4345": "命令模板", + "fb09da4345": "提示词模板", "2cb4bb7e5d": "CLI 参数", "0740d30915": "自定义命令", "ee0e5c2a48": "使用默认智能体", "bf84dea6af": "仅当您希望 Orca 注入上下文时才使用变量。将智能体保留为默认值以遵循您的正常智能体偏好。", "a79c567194": "操作方案", - "cf01d41bce": "每个源代码管理 AI 按钮使用的智能体、CLI 参数和命令模板。", + "cf01d41bce": "每个源代码管理 AI 按钮使用的智能体、CLI 参数和提示词模板。", "a9359c8aa9": "未知错误", "b5f46664d3": "无法保存源代码控制 AI 操作默认值:{{value0}}", "d18d665e12": "保存", @@ -6338,9 +6338,9 @@ "92f80edbfd": "远程终端持久化", "feae1d1e69": "可选。相当于 ProxyJump / ssh -J。", "11bcb4507a": "bastion.example.com", - "b2ab248ded": "跳跃主机", + "b2ab248ded": "跳板主机", "3b01ca44a0": "可选。用于隧道(例如 Cloudflare Access、ProxyCommand)。", - "f42d844544": "例如cloudflared 访问 ssh --主机名 %h", + "f42d844544": "例如 cloudflared access ssh --hostname %h", "c7d0e18ecb": "代理命令", "cb91f6375c": "可选。默认使用 SSH 智能体。", "d6a5f2ee5c": "~/.ssh/id_ed25519(SSH 智能体留空)", @@ -6494,11 +6494,11 @@ "f61ac77f16": "git bash", "0f1b8669e6": "命令提示符", "eb7fc4d98a": "PowerShell", - "27e301f22c": "默认外壳", - "09bf02de9a": "打开新的终端窗口时使用的 shell。对新的终端生效。", - "bd68f3170d": "为 Windows 上的新终端窗口选择默认 shell。", - "a55eee649f": "Windows 上新终端窗口的默认 shell。", - "87e678a8af": "Windows外壳", + "27e301f22c": "默认 Shell", + "09bf02de9a": "打开新的终端窗口时使用的 Shell。对新的终端生效。", + "bd68f3170d": "为 Windows 上的新终端窗口选择默认 Shell。", + "a55eee649f": "Windows 上新终端窗口的默认 Shell。", + "87e678a8af": "Windows Shell", "05efc0bada": "true", "348246b06f": "false", "5936387ddd": "自动", @@ -6541,7 +6541,7 @@ "cbe56a0f79": "控制深色模式下窗格之间的分割分隔线。", "b739d2abfe": "深色分隔线颜色", "7add204bd5": "选择深色模式下使用的终端主题。", - "9499ad1dc4": "黑暗主题", + "9499ad1dc4": "深色主题", "f012172e21": "选择深色模式下用于终端窗格的主题。", "catalog_title": "终端主题", "catalog_description": "为深色和浅色模式选择终端主题和分隔线颜色。", @@ -6620,7 +6620,7 @@ "c9e1fdf42f": "光标", "da64e8f4c1": "终端背景颜色", "cc1b2ffeb2": "背景", - "026a0b8013": "主要文字颜色", + "026a0b8013": "主文本颜色", "79f6bfb76e": "前景", "cf37ff69f6": "根据", "8abdab9f7c": "立即重启", @@ -6670,9 +6670,9 @@ "ab40b8a5f1": "没有匹配项。继续输入以添加自定义路径。", "4cd2a4c077": "输入路径(例如 .env 或 node_modules)...", "241325302c": "添加路径", - "7ff265071d": "创建新工作树时,此处列出的每个路径会在可能时通过 macOS 上的 APFS clone-copy 复制;否则,会从主签出创建符号链接。", + "7ff265071d": "创建新工作树时,此处列出的每个路径会在可能时通过 macOS 上的 APFS clone-copy 复制;否则,会从主检出目录创建符号链接。", "4755f120b6": "工作树共享路径", - "b07ef5a8b6": "从主签出放入新创建工作树的路径。", + "b07ef5a8b6": "从主检出目录放入新建工作树的路径。", "d72ba8dc68": "{{value0}} 路径", "9ea912d811": "1 条路径" }, @@ -6848,8 +6848,8 @@ "5bff6a2ef0": "侧边栏", "5e5b8878bf": "手机", "74618577c7": "移动端", - "682293cadf": "在左侧边栏顶部显示 Orca手机端 按钮。", - "1de96ec8a6": "显示 Orca手机端 按钮", + "682293cadf": "在左侧边栏顶部显示 Orca 手机端 按钮。", + "1de96ec8a6": "显示 Orca 手机端 按钮", "4c920ab2d1": "日程", "58f4e22fa2": "自动化", "b186f3cefb": "自动化", @@ -6906,7 +6906,7 @@ "c690a15849": "资源", "81ef5abc2f": "在状态栏中显示 CPU、内存、终端会话和工作区磁盘使用情况。", "7cf005b29f": "资源管理器", - "fe192b060e": "主持人", + "fe192b060e": "主机", "f4997e0f8a": "联系", "a278406ed5": "偏僻的", "6ecad74eb3": "SSH", @@ -6971,8 +6971,8 @@ "50139297e6": "内置提示", "502aa57681": "指示", "40d21f2efc": "提示词", - "672387fb77": "生成分支名称时使用的智能体命令模板。", - "722551c5b3": "分支名称命令模板", + "672387fb77": "生成分支名称时使用的智能体提示词模板。", + "722551c5b3": "分支名称提示词模板", "f41833025e": "产生", "ed677944cc": "工作树", "3ef3cbe98c": "智能体", @@ -7105,7 +7105,7 @@ "57c851a68c": "CLI", "61117e57f3": "参数", "0f29331fed": "论点", - "18b6d38835": "每个源代码管理 AI 按钮使用的智能体、CLI 参数和命令模板。", + "18b6d38835": "每个源代码管理 AI 按钮使用的智能体、CLI 参数和提示词模板。", "3c4e5e5938": "操作方案", "ee14a9e9f7": "已启用", "82109d627d": "源代码控制", @@ -7258,7 +7258,7 @@ "884e5e6132": "markdown", "49db74a92d": "浏览器", "6410fe83d8": "终端", - "2b5efa55c9": "全球的", + "2b5efa55c9": "全局的", "ebeedb2f6a": "快捷终端", "6f183fa1b9": "浮动码头", "a08e482f6d": "浮动工作区", @@ -7369,10 +7369,10 @@ "8436ff6f8e": "代理绕过规则", "e55d62dfa4": "启动板", "9da6c875e5": "码头", - "b9096a44cf": "https_Agent", - "8f03d44672": "http_Agent", - "e3b1d42f95": "Orca 网络请求和本地终端子项的智能体 URL。", - "c29f23ab57": "HTTPAgent", + "b9096a44cf": "https_proxy", + "8f03d44672": "http_proxy", + "e3b1d42f95": "用于 Orca 网络请求和本地终端子进程的代理 URL。", + "c29f23ab57": "HTTP 代理", "6c2ce8457c": "文件浏览器", "c9d9636f24": "发现者", "68d03d9980": "VSCode", @@ -7414,15 +7414,15 @@ "git": { "search": { "61eab13403": "orca", - "1b93c1143c": "归因", + "1b93c1143c": "署名", "8461c908ae": "合着的", "61f9f5d1fc": "共同作者", "af0a144bfb": "议题", "6bdea421bb": "PR", "16f53f7323": "gh", "d088806071": "GitHub", - "118c23484b": "将 Orca 归因添加到 commits、PR 和议题。", - "bc7d9f69ce": "Orca 归因", + "118c23484b": "将 Orca 署名添加到 commits、PR 和议题。", + "bc7d9f69ce": "Orca 署名", "40f9b815fd": "API预算", "b7e52124c7": "速率限制", "ead733645f": "光亮的", @@ -7436,12 +7436,12 @@ "0c75583ca9": "安全地", "bae91effdd": "新基线", "de06e9d105": "基本参考", - "ab0e22c9f6": "刷新本地主", - "d9f70d51a0": "陈旧的主要", + "ab0e22c9f6": "刷新本地 main", + "d9f70d51a0": "过期的 main", "0849b571fe": "最新", - "c41e345153": "后面主要", + "c41e345153": "落后于 main", "6ee3cfff02": "git 差异", - "564942ffc5": "起源/主要", + "564942ffc5": "origin/main", "28192e3a63": "master", "e3e9adde59": "main", "0e993bf00f": "当您创建工作区时,Orca 会刷新远程库并安全地快进匹配的本地分支,例如 main 或 master。这使得像 git diff main...HEAD 这样的命令无法与过时的历史记录进行比较。如果该分支有未 commits 的更改或仅限本地 commits,Orca 会跳过更新。", @@ -7772,7 +7772,7 @@ "1c5bdcd0f2": "存储库", "89d2a9ad9f": "仓库", "f58b92a48f": "项目", - "8bf43c2dad": "全球的", + "8bf43c2dad": "全局的", "a26ecdb77b": "片段", "d07d130849": "捷径", "0073cf8ce9": "终端", @@ -7837,7 +7837,7 @@ "f1c53f2820": "工作树", "7e228fc439": "符号链接", "c06adcf136": "符号链接", - "ed885e589f": "从主签出放入新创建工作树的路径。", + "ed885e589f": "从主检出目录放入新建工作树的路径。", "01b3377ebc": "工作树共享路径", "fff8834983": "提示词", "fa3131f223": "模型", @@ -7953,7 +7953,7 @@ "7f251a45a8": "从 ~/.ssh/config 导入主机。", "41a3127094": "从 SSH 配置导入", "f9493b80c0": "服务器", - "8fb1cc87cc": "主持人", + "8fb1cc87cc": "主机", "09395490af": "目标", "00d1fda01a": "新的", "f7b6383aec": "添加", @@ -8119,7 +8119,7 @@ "9c32726f47": "控制深色模式下窗格之间的分割分隔线。", "8987db7ff2": "深色分隔线颜色", "13f6310dd3": "选择深色模式下使用的终端主题。", - "ec07ce9b02": "黑暗主题", + "ec07ce9b02": "深色主题", "f036794286": "活跃", "846a7a1204": "窗格", "d1fa00a9cb": "徘徊", @@ -8232,9 +8232,9 @@ "591912177b": "gitbash", "12519edb5d": "命令提示符", "6cd20b9e64": "指令", - "7c7056940a": "壳", - "713c4a2f92": "为 Windows 上的新终端窗格选择默认 shell。", - "13715f9d23": "默认外壳" + "7c7056940a": "shell", + "713c4a2f92": "为 Windows 上的新终端窗格选择默认 Shell。", + "13715f9d23": "默认 Shell" } } }, @@ -8279,8 +8279,8 @@ "fixChecks": "从失败的托管评审检查中启动智能体。", "resolveConflicts": "启动用于解决本地或托管评审合并冲突的智能体。", "customCommand": "自定义命令", - "supportedAgents": "此配方支持的智能体:{{value0}}。", - "unsupportedSavedAgent": "{{value0}} 无法运行此文本生成配方。请在下方选择一个支持的智能体。", + "supportedAgents": "此预设支持的智能体:{{value0}}。", + "unsupportedSavedAgent": "{{value0}} 无法运行此文本生成预设。请在下方选择一个支持的智能体。", "resolveComments": "从选中的未解决 PR 或 MR 评论启动智能体。" } } @@ -8385,7 +8385,7 @@ "integration": { "cards": { "c90f2ef419": "重新检查", - "dd3529015d": "Disconnect {{value0}}", + "dd3529015d": "断开 {{value0}}", "8b2408a8e5": "此运行时已连接 Jira。如果已连接的站点列表看起来过期,请重新检查。", "8c20e76308": "每个已连接的 Jira 站点都由活动运行时存储一个 token。", "c24e56c532": "测试", @@ -8395,14 +8395,14 @@ "60996beda6": "添加 Jira 站点", "7ca5ffffdb": "从 Jira Cloud 议题浏览、创建并开始工作。", "a1093a06c7": "在显示设置操作前正在检查 Jira 访问权限。", - "9fa04a032e": "{{value0}} site{{value1}} connected", + "9fa04a032e": "{{value0}} 个站点{{value1}} 已连接", "cef18762a2": "用 Linear 设置里的个人 API 密钥添加访问权限。完全访问密钥可看到密钥所有者能访问的所有团队。", "6224fe9d34": "每个已连接的 Linear 工作区都有一个由活动运行时存储的密钥。全权限密钥可覆盖密钥所有者可访问的所有团队;受限密钥可随时更换。", "1a12e33fe5": "添加 Linear 访问", "622c224082": "添加工作区访问权限", "eae4a9f16b": "添加 Linear 访问以浏览和链接议题。", "fe9231215b": "在显示设置操作前正在检查 Linear 访问权限。", - "e1f5e6424c": "{{value0}} workspace{{value1}} connected", + "e1f5e6424c": "{{value0}} 个工作区{{value1}} 已连接", "disconnect_all": "断开连接", "account_scope_prefix": "账户范围" } @@ -8425,7 +8425,7 @@ "0613928cb3": "此运行时暂不支持 Gitea 状态。", "05863d2599": "通过 Gitea REST API 获取 pull request 和 commit 状态。", "52f75876be": "已检测仓库的 pull request 和 commit 状态", - "0b5242f8a2": "{{value0}} · Pull requests and commit statuses", + "0b5242f8a2": "{{value0}} · 拉取请求和提交状态", "40f678df73": "Azure DevOps 凭据已配置但无法通过身份验证。请检查 token、API 基础 URL 和仓库权限;如果环境变量已更改,请重启 Orca。", "7bd345e3f6": "仅当 Orca 无法从 git remote 推导出 API 基础 URL 时。", "186a6689df": "ORCA_AZURE_DEVOPS_API_BASE_URL", @@ -8437,7 +8437,7 @@ "f3f47dc7de": "此运行时暂不支持 Azure DevOps 状态。", "0eb50d5593": "通过 Azure DevOps REST API token 获取 pull request 和构建状态。", "54636c65d4": "已检测 Azure Repos 的 pull request 和构建状态", - "ea204f5e03": "{{value0}} · Pull requests and build statuses", + "ea204f5e03": "{{value0}} · 拉取请求和构建状态", "6154b02093": "Bitbucket 凭据已配置但无法通过身份验证。请检查 token 和仓库权限;如果环境变量已更改,请重启 Orca。", "e63fe8f627": "ORCA_BITBUCKET_ACCESS_TOKEN", "19416c874c": "ORCA_BITBUCKET_API_TOKEN", @@ -8456,7 +8456,7 @@ "checkingTitle": "正在检查计算机控制访问权限。", "checkingDescription": "Orca 正在检查计算机控制助手的 macOS 隐私权限。", "unavailableTitle": "计算机控制不可用。", - "unavailableDescription": "Computer Use permissions are unavailable because {{value0}}.", + "unavailableDescription": "Computer Use 权限不可用,因为 {{value0}}。", "readyTitle": "计算机控制已就绪。", "readyDescription": "当你要求时,智能体可以检查并操作应用窗口。", "permissionsTitle": "完成设置以使用本地应用。" @@ -8473,7 +8473,7 @@ "6f7a8b9cad": "使用相对路径(例如 .orca/worktrees)可让每个项目使用各自的位置,使用绝对路径则所有项目共用一个文件夹。" }, "ProviderHostScopeControl": { - "scope_label": "{{value0}}: {{value1}}", + "scope_label": "{{value0}}:{{value1}}", "change_host": "打开远程服务器" }, "providerAccountScope": { @@ -8536,7 +8536,7 @@ }, "ProjectWindowsRuntimeSetting": { "projectRuntime": "项目运行时", - "defaultRuntime": "Default ({{value0}})", + "defaultRuntime": "默认 ({{value0}})", "windows": "Windows", "wsl": "WSL", "selectDistro": "选择发行版", @@ -8619,7 +8619,7 @@ "b94ff260e6": "下载 Android Studio", "18925b082d": "定位 SDK 文件夹", "8c52684db8": "清除", - "76eb88b88e": "iOS Simulator (Xcode)", + "76eb88b88e": "iOS 模拟器 (Xcode)", "c6f3ea4f12": "已就绪", "e4f14b50d7": "请安装 Xcode 并添加 iOS 模拟器运行时。", "63fe73a1ea": "无法更新 Android SDK 文件夹。" @@ -8628,34 +8628,34 @@ "advanced": "高级" }, "DevToolsPane": { - "nativeActionLayoutCheck": "Native action layout check", - "secondary": "Secondary", - "secondaryActionClicked": "Secondary action clicked", - "primaryAction": "Primary action", - "primaryActionClicked": "Primary action clicked", - "branchHasChanges": "branch has changes", - "viewChangesClicked": "View Changes clicked", - "forceDeleteClicked": "Force Delete clicked", - "devOnlyCallback": "Dev-only callback.", - "infoToast": "Info toast", - "infoToastDescription": "Long informational copy with no explicit action.", - "localCanaryBehind": "Local canary is behind origin/canary", - "successToast": "Success toast", - "successToastDescription": "Short confirmation state.", - "settingsSaved": "Settings saved", - "shortSuccessCopy": "Short success copy.", - "errorToast": "Error toast", - "errorToastDescription": "Long error copy without recovery actions.", - "failedToSyncWorkspaceMetadata": "Failed to sync workspace metadata", - "nativeActionToast": "Native action toast", - "nativeActionToastDescription": "Uses Sonner action and cancel slots.", - "deleteFailureToast": "Delete failure toast", - "deleteFailureToastDescription": "Custom footer with View and Force Delete.", - "behindBaseRefToast": "Behind base ref toast", - "behindBaseRefToastDescription": "Persistent prompt with an inline Settings link and footer action.", - "notificationPlayground": "Notification playground", - "notificationPlaygroundDescription": "Dev-only triggers for checking toast layout, recovery actions, and long-copy wrapping.", - "devOnly": "Dev only" + "nativeActionLayoutCheck": "原生操作布局检查", + "secondary": "次要", + "secondaryActionClicked": "次要操作已点击", + "primaryAction": "主操作", + "primaryActionClicked": "主操作已点击", + "branchHasChanges": "分支有更改", + "viewChangesClicked": "查看更改已点击", + "forceDeleteClicked": "强制删除已点击", + "devOnlyCallback": "仅开发回调。", + "infoToast": "信息提示", + "infoToastDescription": "无明确操作的长信息文案。", + "localCanaryBehind": "本地 canary 落后于 origin/canary", + "successToast": "成功提示", + "successToastDescription": "简短的确认状态。", + "settingsSaved": "设置已保存", + "shortSuccessCopy": "简短的成功文案。", + "errorToast": "错误提示", + "errorToastDescription": "无恢复操作的详细错误文案。", + "failedToSyncWorkspaceMetadata": "同步工作区元数据失败", + "nativeActionToast": "原生操作提示", + "nativeActionToastDescription": "使用 Sonner 的操作和取消插槽。", + "deleteFailureToast": "删除失败提示", + "deleteFailureToastDescription": "包含查看和强制删除的自定义底部区域。", + "behindBaseRefToast": "落后于基础分支提示", + "behindBaseRefToastDescription": "带有内联设置链接和底部操作按钮的持久提示。", + "notificationPlayground": "通知测试面板", + "notificationPlaygroundDescription": "用于检查提示布局、恢复操作和长文案换行的开发触发器。", + "devOnly": "仅开发" }, "EphemeralVmRecipeRow": { "useInWorkspace": "在工作区中使用" @@ -8674,7 +8674,7 @@ "copiedCleanupPayload": "已复制清理载荷。", "copyCleanupFailed": "无法复制清理命令。", "title": "临时 VM 运行时", - "description": "由配方创建的运行时归工作区所有。崩溃、创建失败或手动恢复后,请清理过期条目。", + "description": "由环境模板创建的运行时归工作区所有。崩溃、创建失败或手动恢复后,请清理过期条目。", "refresh": "刷新临时 VM 运行时", "loading": "正在检查临时 VM 运行时…", "empty": "没有需要清理的临时 VM 运行时。", @@ -8685,26 +8685,26 @@ "ephemeralVms": { "search": { "title": "工作区专属环境", - "description": "了解仓库自带的配方如何为每个工作区提供按需创建、可丢弃的独立环境。" + "description": "了解仓库自带的环境模板如何为每个工作区提供按需创建、可丢弃的独立环境。" } }, "EphemeralVmsPane": { - "loadError": "无法加载配方。", + "loadError": "无法加载环境模板。", "copyError": "无法复制提示词。", "skillTitle": "工作区专属环境技能", - "skillDescription": "设置、构建、认证并验证仓库自带的环境配方。", + "skillDescription": "设置、构建、认证并验证仓库自带的环境模板。", "whatTitle": "此技能会与你一起完成", - "whatScaffold": "为你的提供商编写配方和脚本,可通过 Orca 服务器或 SSH 连接。", + "whatScaffold": "为你的提供商编写环境模板和脚本,可通过 Orca 服务器或 SSH 连接。", "whatBuild": "构建可复用的基础镜像,并让智能体完成登录。", "whatValidate": "验证环境,以便你可以在其上创建工作区。", "promptHint": "在任意工作区中,可以这样询问智能体:", "copy": "复制", "copied": "已复制", - "recipes": "配方", - "recipesHelp": "智能体添加到 orca.yaml 的配方会显示在这里,可用于启动工作区。", - "refresh": "刷新临时 VM 配方", - "checking": "正在检查配方...", - "none": "还没有找到配方。" + "recipes": "环境模板", + "recipesHelp": "智能体添加到 orca.yaml 的环境模板会显示在这里,可用于启动工作区。", + "refresh": "刷新临时 VM 环境模板", + "checking": "正在检查环境模板...", + "none": "还没有找到环境模板。" }, "ephemeralVmsExperimentalSetting": { "description": "显示仓库自带按需环境的设置控件和工作区运行目标。", @@ -8729,7 +8729,7 @@ "b5dd73a105": "选择一个工作区以查看检查", "a4ef4e0832": "未选择工作区", "5594400d73": "没有需要修复的失败检查。", - "abf59262fb": "在启动智能体之前查看并编辑完整的命令输入。", + "abf59262fb": "启动智能体前请查看并编辑完整提示词。", "4ede779461": "使用 AI 解决评审冲突", "3b203c62f8": "这将从 PR 中永久删除该评论。", "ea9b649ce3": "删除评论?", @@ -8766,7 +8766,7 @@ "02b2ce911f": "说明(可选)", "1cd53359db": "描述", "68314b4369": "标题", - "694550a610": "主要的", + "694550a610": "main", "0fad57a14c": "搜索远程分支或输入分支名称。", "8584ccb43c": "基础分支", "6f5f1962b6": "源分支", @@ -8849,10 +8849,10 @@ "d0fb0f4bf2": "刷新 commits", "9f7535d22b": "引用是指向该确切 commit 的分支或标记名称。它们仅出现在 Git 具有 commit 的命名引用的地方。", "9289ba0cb9": "什么是参考文献?", - "d836037d02": "Commits", + "d836037d02": "提交", "8232c8b2f2": "打开 commit {{value0}}: {{value1}}", "9a8b85882d": "加载中", - "62e685d5ec": "idle", + "62e685d5ec": "空闲", "111e1d0db4": "错误", "e5e81e59a6": "调整 commits 大小", "6d1e0a7c3b": "无法加载提交文件" @@ -9008,7 +9008,7 @@ "37a81f29ad": "生成 commit 消息。单击停止。", "b94112eb9e": "Commit 消息", "0d0a8359d3": "信息", - "15b7f210d7": "选择智能体并在启动前编辑完整的命令输入。", + "15b7f210d7": "启动智能体前请查看并编辑完整提示词。", "054ead86b1": "使用 AI 修复 Commit 失败", "9e5ccd00aa": "Commit 失败上下文不可用", "f0a2dc9e46": "自定义启动...", @@ -9022,7 +9022,7 @@ "4f76c0a9de": "基础分支必须与 head 分支不同。", "c5e4175139": "更多 {{value0}} 和远程操作", "78ddfd0bb4": "创建为草稿", - "e64a632456": "主要的", + "e64a632456": "main", "6055949c50": "{{value0}} 基础分支", "1f7119f604": "根据", "9484270f45": "正在生成标题和描述...", @@ -9036,11 +9036,11 @@ "b355e740b2": "停止生成 {{value0}} 详细信息", "527e130b6f": "停止生成", "e1970d327d": "新建 {{value0}}", - "f4c766f1ca": "为此运行选择智能体和命令模板。", + "f4c766f1ca": "为此运行选择智能体和提示词模板。", "1a6a6e0bc5": "生成托管评论详细信息", "6b122529d4": "生成 Commit 消息", "e48caaf0dd": "已启动 AI 智能体处理冲突。", - "901140f47d": "在启动智能体之前查看并编辑完整的命令输入。", + "901140f47d": "启动智能体前请查看并编辑完整提示词。", "19652ddd76": "用 AI 解决冲突", "c9ad22888e": "选择此存储库的分支比较目标。", "574d2f4413": "清晰的笔记", @@ -9187,7 +9187,7 @@ "SourceControlAgentActionDialogForm": { "1bc0bdbb5e": "启动:", "7ec6abbf2a": "重置", - "f4f3c9ca4a": "命令模板", + "f4f3c9ca4a": "提示词模板", "fe119187bb": "——模范十四行诗", "bc8dc39f4b": "CLI 参数", "b99c33cec5": "设置", @@ -9197,15 +9197,15 @@ "1d47db9bf0": "没有启用智能体", "c7ff8cef11": "检测剂...", "013c9ac04a": "保存范围", - "1bb611240f": "Use {basePrompt} for Orca's default prompt.", + "1bb611240f": "使用 {basePrompt} 作为 Orca 的默认提示词。", "23280cbab1": "此模板不包含 {basePrompt},因此智能体不会收到 Orca 的默认提示词。", "5421a96acb": "保存并启动智能体", "6cefcdfba1": "稍后可以在源代码管理 AI 设置中更改。", "c29f9cf266": "保存此提示,下次不再显示此评审", - "d8f40128ee": "{basePrompt} is Orca's default prompt.", + "d8f40128ee": "{basePrompt} 是 Orca 的默认提示词。", "ea4788705e": "取消", - "b0da3a4d3e": "启动配方已保存", - "bff4795a6d": "更改智能体、参数或提示词模板以更新已保存的配方。", + "b0da3a4d3e": "启动预设已保存", + "bff4795a6d": "更改智能体、参数或提示词模板以更新已保存的预设。", "5c75b24735": "自定义 Orca 启动智能体前发送给它的内容。" }, "SourceControlTextGenerationDialog": { @@ -9216,8 +9216,8 @@ }, "SourceControlTextGenerationDialogForm": { "25fcd8e49a": "保存默认值", - "d91b0a189d": "保存食谱", - "1f6fcfb6cf": "命令模板", + "d91b0a189d": "保存预设", + "1f6fcfb6cf": "提示词模板", "551ffd111b": "——模范十四行诗", "4eab815004": "CLI 参数", "914c8f6ac2": "自定义命令", @@ -9308,7 +9308,7 @@ "1e53e45072": "已复制", "084c516efb": "复制命令", "5dc3af25c0": "选择评论", - "d7a2f9c401": "Send unresolved {{value0}} comments", + "d7a2f9c401": "发送未解决的 {{value0}} 评论", "d91f2a6c39": "发送 {{value0}} 条已排队评论给 AI", "a6de3e5a20": "清除已排队评论", "49ea0937e4": "将评论添加到解决列表", @@ -9317,18 +9317,18 @@ "f8a2c91d04": "加入智能体队列", "b4e8a1c902": "已加入队列", "7c1f0a2b11": "打开", - "e8b4c1a903": "Resolved · {{value0}}", - "c3a8e5d710": "Needs review · {{value0}}", + "e8b4c1a903": "已解决 · {{value0}}", + "c3a8e5d710": "需审核 · {{value0}}", "a7f0c7e8d1": "加入队列", "8a621a2c4f": "已分组", "b13f85d75c": "时间线", "f5cf324efa": "评论显示选项", "5e6e5a13fa": "查看", - "actionRequiredHint": "This check needs a manual action on GitHub (for example, approving the workflow run) before merging is unblocked." + "actionRequiredHint": "此检查需要在 GitHub 上执行手动操作(例如批准工作流运行)后才能解除合并阻止。" }, "empty": { "state": { - "3322603418": "Pull request status unavailable", + "3322603418": "PR 状态不可用", "5b0cfae9a5": "创建 {{value0}} 以开始检查和评审。", "13e1c7d5ed": "未找到 {{value0}}", "d372072df1": "GitHub 刷新因当前速率限制预算而暂停", @@ -9344,7 +9344,7 @@ "41252bc53f": "分支未发布", "05e4aec17b": "{{value0}} 检查将在操作完成后可用", "d77c513c1e": "{{value0}} 正在进行中", - "b597440265": "Refresh GitHub status for this branch to load checks and review." + "b597440265": "刷新此分支的 GitHub 状态以加载检查和审核。" } } } @@ -9454,13 +9454,13 @@ "9e779995dd": "创建 {{value0}}", "226b85a3a7": "fetch", "323bb614aa": "Commit 并同步", - "2b8e6595fd": "Commit", + "2b8e6595fd": "提交", "04d709801d": "从远程获取但不合并" } }, "primary": { "action": { - "ed93b4f14f": "Commit", + "ed93b4f14f": "提交", "946a8a05ea": "为此分支创建 {{value0}}", "e7ffa46946": "创建 {{value0}}", "95550cff15": "推", @@ -9480,8 +9480,8 @@ "2d8f185fbc": "在提交部分暂存的文件之前暂存所有更改", "a6457b46a7": "提交前解决冲突", "484f45c439": "{{value0}} 正在进行中...", - "6f7a8b9c0d": "Remote operation in progress…", - "7f8a9b0c1d": "Remote operation in progress — try again once it finishes", + "6f7a8b9c0d": "远程操作进行中…", + "7f8a9b0c1d": "远程操作进行中——请等它完成后重试", "74fc171e99": "强制推送正在进行中...", "16aee3a5c1": "正在进行中……", "e61b0d7a3c": "请先检出分支再发布 commits。", @@ -9491,18 +9491,18 @@ "d37e68f61d": "正在准备分支以供评审…", "c72e5e65d1": "准备此分支并创建 {{value0}}", "b8e4f2a901": "等待远程操作完成。", - "c9f3a1b802": "Resolve conflicts before creating a {{value0}}.", - "d2a8c4e703": "No changes on this branch to include in a {{value0}}.", - "e3b9d5f814": "Cannot create a {{value0}} from the default branch.", - "f4c0e6a925": "Commit changes before creating a {{value0}}.", - "a5d1f7b036": "Publish commits before creating a {{value0}}.", - "b6e2a8c147": "Push commits before creating a {{value0}}.", - "c7f3b9d258": "Sync this branch before creating a {{value0}}.", - "d8a4c0e369": "Authenticate before creating a {{value0}}.", - "e9b5d1f470": "Check out a branch before creating a {{value0}}.", - "f0c6e2a581": "This branch is not ready for a {{value0}} yet.", - "8f9a0b1c2d": "Nothing to commit. Branch is up to date.", - "h3i4j5k607": "Checking whether this branch can create a {{value0}}…" + "c9f3a1b802": "在创建 {{value0}} 之前解决冲突。", + "d2a8c4e703": "此分支上没有需要包含在 {{value0}} 中的更改。", + "e3b9d5f814": "无法从默认分支创建 {{value0}}。", + "f4c0e6a925": "在创建 {{value0}} 之前提交更改。", + "a5d1f7b036": "在创建 {{value0}} 之前发布提交。", + "b6e2a8c147": "在创建 {{value0}} 之前推送提交。", + "c7f3b9d258": "在创建 {{value0}} 之前同步此分支。", + "d8a4c0e369": "在创建 {{value0}} 之前进行身份验证。", + "e9b5d1f470": "在创建 {{value0}} 之前检出一个分支。", + "f0c6e2a581": "此分支尚未准备好创建 {{value0}}。", + "8f9a0b1c2d": "没有可提交的内容。分支已是最新状态。", + "h3i4j5k607": "正在检查此分支是否可以创建 {{value0}}…" } } } @@ -9571,11 +9571,11 @@ "logPath": "日志路径", "agents": "智能体", "sessionsShownCompact": "已显示 {{value0}} 个", - "originalPaneUnavailable": "Original pane is no longer available.", - "worktreeUnavailable": "Worktree is no longer available.", - "runtimeBrowseLocalHistory": "Runtime-hosted workspaces can browse local history. Resume actions are available in local and SSH workspaces.", - "runtimeWorkspacesUnsupported": "Resume from history is not available in runtime-hosted workspaces.", - "openSupportedWorkspace": "Open a local or SSH workspace before resuming a session.", + "originalPaneUnavailable": "原始面板不再可用。", + "worktreeUnavailable": "工作树不再可用。", + "runtimeBrowseLocalHistory": "远程运行时托管的工作区可以浏览本地历史。恢复操作在本地和 SSH 工作区中可用。", + "runtimeWorkspacesUnsupported": "在远程运行时托管的工作区中无法从历史记录恢复会话。", + "openSupportedWorkspace": "在恢复会话之前,请先打开一个本地或 SSH 工作区。", "localSessionSshWorkspaceUnsupported": "此会话的历史记录存储在本机上,因此无法在 SSH 工作区中恢复。请改为打开一个本地工作区。" }, "AiVaultPanelControls": { @@ -10069,7 +10069,7 @@ "dfd2aa9d5d": "网络", "2f077ef4eb": ",然后扫码。", "3aa7bb2d8b": "配对桌面", - "d1495e5e64": "打开 Orca手机端,点击", + "d1495e5e64": "打开 Orca 手机端,点击", "901c98bb93": "配对这个", "3960f5c339": "第 2 步(共 2 步)", "3241f3c26a": "安装二维码", @@ -10077,15 +10077,15 @@ "aa97420ba4": "复制安装链接", "ac1eb64952": "安卓", "711e6f4b47": "iOS系统", - "e75647ace0": "使用手机扫描二维码或打开安装链接以获取 Orca手机端。", + "e75647ace0": "使用手机扫描二维码或打开安装链接以获取 Orca 手机端。", "0d9b33299e": "获取应用程序。", "92ddfdfa1f": "第 1 步(共 2 步)", "ff48d9d520": "与另一台设备配对", "f9cbf4bb53": "撤销设备", "34f878d04f": "撤销 {{value0}}", "94829abdb1": "配对", - "266c18c105": "打开 Orca手机端 从上次中断的地方继续,或配对其他设备。", - "5410d55d79": "Orca手机端", + "266c18c105": "打开 Orca 手机端 从上次中断的地方继续,或配对其他设备。", + "5410d55d79": "Orca 手机端", "10d27b4cba": "开始使用", "da1d5e5ed0": "可用于", "ec0607bf66": "支持的手机平台", @@ -10119,9 +10119,9 @@ "9883b58693": "关闭 Orca 手机", "fb5f28330e": "在侧边栏中显示", "c669abcf8f": "从侧边栏隐藏", - "e1c7b4a92d": "Configure in Settings > Mobile.", - "a4f8c2d91e": "Remove Orca手机端 from left sidebar", - "b7e3d1c84a": "Show Orca手机端 in left sidebar", + "e1c7b4a92d": "在设置 > 移动端中配置。", + "a4f8c2d91e": "从左侧边栏隐藏 Orca 手机端", + "b7e3d1c84a": "在左侧边栏显示 Orca 手机端", "f3d8e5b71a": "Adds the shortcut back to the left sidebar." }, "PhoneCarousel": { @@ -10507,7 +10507,7 @@ "3774b80eae": "描述", "07da9245cc": "PR标题", "54a093c52d": "标题", - "3b9b96d6a6": "主要的", + "3b9b96d6a6": "main", "ce7d5d3a18": "基础分支", "e4473d438f": "用 AI 生成", "c30cd930ff": "创建PR", @@ -10542,7 +10542,7 @@ "000106adfe": "claude", "7d9f1d5f7d": "(0.8秒)", "944199e54a": "› 购物车总更新", - "623881d72e": "结帐规格", + "623881d72e": "checkout.spec.ts", "5c5cbd783f": "(1.2秒)", "3261c6853b": "› 可以登录", "defe550fe2": "登录规范", @@ -10898,8 +10898,8 @@ "1f9f4def45": "复制代码" }, "CombinedDiffFileTree": { - "f984289373": "没有文件与当前搜索器匹配。", - "eafe1aeb53": "重置搜索器", + "f984289373": "没有文件与当前筛选条件匹配。", + "eafe1aeb53": "重置筛选条件", "be119cb9d1": "已查看的文件", "c00020f081": "文件扩展名", "cd0e0ed79e": "搜索差异文件", @@ -11414,7 +11414,7 @@ "07eccfa397": "此检查没有可用详情。", "a916648574": "打开详情", "834cb3f23d": "用 AI 修复", - "c8f1a2d4e7": "启动前请选择智能体并编辑完整命令输入。", + "c8f1a2d4e7": "启动前请选择智能体并编辑完整提示词。", "b3e7f9a1c2": "检查修复上下文不可用", "d5a8c2f1b9": "启动默认 AI 智能体来修复此检查", "e2b4d7c8a1": "为此检查选择智能体", @@ -11587,7 +11587,7 @@ "removeWorktree": "删除工作树", "trashWorktree": "垃圾工作树", "addQuickCommand": "添加快捷命令", - "newQuickCommand": "新的快速命令" + "newQuickCommand": "新的快捷命令" } } }, @@ -11775,7 +11775,7 @@ "cadb7b0bc9": "无效的" }, "AutomationDetail": { - "007c8ad874": "迅速的", + "007c8ad874": "提示词", "a1d52c2189": "使用范围", "449fc83bf7": "令牌", "401f40ae79": "预计。花费", @@ -11814,7 +11814,7 @@ "a4ac8fcc62": "/目标", "827b25a81e": "支持技能、文件路径和内置命令,例如", "6d778190b7": "每周运行依赖项审计并总结有风险的更改。", - "058c23cb3f": "迅速的", + "058c23cb3f": "提示词", "c4b19094c2": "日程", "e46c1aa9ad": "创建", "a9d9dccf77": "保存", @@ -12009,7 +12009,7 @@ "872d032d05": "跑步" }, "HermesCronOutputView": { - "e27c716b43": "迅速的", + "e27c716b43": "提示词", "4557213074": "回复", "05affc68e3": "错误", "88d48157fc": "默认" @@ -12069,8 +12069,8 @@ }, "AutomationSetupDecisionField": { "5a7863909c": "为每个新工作区运行设置", - "18f000ad4e": "Advanced", - "874b72195b": "When this automation creates a workspace, prepare it the same way creating a worktree by hand does — run the project's setup and open its terminal tabs." + "18f000ad4e": "高级", + "874b72195b": "当此自动化创建工作区时,使用与手动创建工作树相同的方式准备——运行项目的设置并打开其终端标签页。" } }, "agent": { @@ -12095,7 +12095,7 @@ "8de7c5beaa": "终端不可用", "866083500b": "拖动以调整大小", "443690186e": "调整活动线程列表的大小", - "7cd632006b": "没有智能体活动与这些搜索器匹配。", + "7cd632006b": "没有智能体活动与这些筛选条件匹配。", "a2b4437bfb": "{{value0}} 活动", "023ff75afe": "标记全部已读", "f70e4bec47": "紧凑模式", From 7c10abc582af16e550d33ef8109fab1505acca85 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 4 Jul 2026 02:35:00 -0700 Subject: [PATCH 11/45] fix(ssh): make reconnection prompts non-blocking unless the project is focused (#7344) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The SSH reconnect dialog was auto-opened at render time for any active/restored worktree whose SSH target was disconnected, blocking the entire UI even when the user was working elsewhere. On app restart the previously-active worktree is restored without a click, so the modal stole focus app-wide over unrelated work. The disconnected state is now surfaced only through the non-blocking affordances that already existed — the worktree card chip, the status-bar Connect action, and the pane-scoped terminal reconnect overlay. The blocking dialog opens only on deliberate focus (clicking the worktree card), and even then it is suppressed when the active view is a terminal, since that pane already shows the in-context reconnect overlay and a second prompt would just duplicate it. Fixes #7314 --- ...WorktreeCard.ssh-reconnect-prompt.test.tsx | 10 +++--- .../src/components/sidebar/WorktreeCard.tsx | 33 ++++++++++--------- 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/src/renderer/src/components/sidebar/WorktreeCard.ssh-reconnect-prompt.test.tsx b/src/renderer/src/components/sidebar/WorktreeCard.ssh-reconnect-prompt.test.tsx index 9af104c6c18..4d0fdec107a 100644 --- a/src/renderer/src/components/sidebar/WorktreeCard.ssh-reconnect-prompt.test.tsx +++ b/src/renderer/src/components/sidebar/WorktreeCard.ssh-reconnect-prompt.test.tsx @@ -133,7 +133,7 @@ describe('WorktreeCard SSH reconnect prompt', () => { worktreeCardProperties = ['status'] }) - it('opens the reconnect dialog for an active disconnected SSH worktree during render', () => { + it('does not auto-open the blocking reconnect dialog for a restored active disconnected SSH worktree', () => { sshConnectionStates.set('ssh-target-1', { status: 'disconnected' }) sshTargetLabels.set('ssh-target-1', 'Remote target') @@ -141,9 +141,11 @@ describe('WorktreeCard SSH reconnect prompt', () => { ) - expect(markup).toContain('data-ssh-disconnected-dialog="open"') - expect(markup).toContain('data-ssh-status="disconnected"') - expect(markup).toContain('data-ssh-target-label="Remote target"') + // The dialog is blocking, so being the active/restored card must not steal + // focus app-wide; it only opens on deliberate click (see handleClick). + expect(markup).toContain('data-ssh-disconnected-dialog="closed"') + // The disconnected state is still discoverable via the non-blocking card chip. + expect(markup).toContain('SSH disconnected') }) it('marks a runtime-host worktree disconnected when its environment has no status', () => { diff --git a/src/renderer/src/components/sidebar/WorktreeCard.tsx b/src/renderer/src/components/sidebar/WorktreeCard.tsx index a823442d23d..003878d3acf 100644 --- a/src/renderer/src/components/sidebar/WorktreeCard.tsx +++ b/src/renderer/src/components/sidebar/WorktreeCard.tsx @@ -342,6 +342,13 @@ const WorktreeCard = React.memo(function WorktreeCard({ return state?.status ?? 'disconnected' }) const isSshDisconnected = sshStatus != null && sshStatus !== 'connected' + // Why: a terminal view already carries its own in-pane reconnect overlay, so + // the blocking dialog would just duplicate it there; reserve the dialog for + // views without an in-context prompt. Default to terminal (suppress) for the + // ambiguous case so we err toward non-blocking. + const activeViewIsTerminal = useAppStore( + (s) => (s.activeTabTypeByWorktree?.[worktree.id] ?? 'terminal') === 'terminal' + ) // Why: runtime ("Orca server") hosts get the same disconnected treatment as // SSH — when the host's runtime environment has no live status, its worktrees @@ -353,23 +360,14 @@ const WorktreeCard = React.memo(function WorktreeCard({ } return !s.runtimeStatusByEnvironmentId.get(parsed.environmentId)?.status }) + // Why: the reconnect dialog is blocking, so it is never auto-shown for a + // disconnected worktree just because it is the active/restored card — that + // would steal focus app-wide while the user works elsewhere. The card chip, + // status bar, and terminal overlay carry the non-blocking disconnected state; + // the dialog only opens on deliberate focus (see handleClick). const [showDisconnectedDialog, setShowDisconnectedDialog] = useState(false) - const sshDisconnectedPromptKey = isActive && isSshDisconnected ? worktree.id : null - const [lastSshDisconnectedPromptKey, setLastSshDisconnectedPromptKey] = useState( - null - ) const [titleRenaming, setTitleRenaming] = useState(false) const [showRenameErrorDialog, setShowRenameErrorDialog] = useState(false) - - // Why: on restart the previously-active worktree is auto-restored without a - // click, so the dialog never opens. Auto-show it for the active card when SSH - // is disconnected, but keep dismissals sticky until that prompt key changes. - if (sshDisconnectedPromptKey !== lastSshDisconnectedPromptKey) { - setLastSshDisconnectedPromptKey(sshDisconnectedPromptKey) - if (sshDisconnectedPromptKey) { - setShowDisconnectedDialog(true) - } - } // Why: read the target label from the store (populated during hydration in // useIpcEvents.ts) instead of calling listTargets IPC per card instance. const sshTargetLabel = useAppStore((s) => @@ -862,7 +860,11 @@ const WorktreeCard = React.memo(function WorktreeCard({ }) onImmediateActivate?.(worktree.id, activationRowKey) void activateWorktreeFromSidebar(worktree.id) - if (isSshDisconnected) { + // Why: clicking the card is a deliberate focus of this project, so the + // blocking reconnect prompt is appropriate here (unlike auto-restore) — + // but skip it when a terminal is active, since that pane already shows the + // in-context reconnect overlay and a second prompt would just duplicate it. + if (isSshDisconnected && !activeViewIsTerminal) { setShowDisconnectedDialog(true) } onActivate?.() @@ -875,6 +877,7 @@ const WorktreeCard = React.memo(function WorktreeCard({ isDeleting, activationRowKey, isSshDisconnected, + activeViewIsTerminal, onActivate, onImmediateActivate, onSelectionGesture From 533cafdfa9d7a0500d53bbf8538d7e4b5d889cec Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 4 Jul 2026 02:40:37 -0700 Subject: [PATCH 12/45] Fix cross-worktree chat leak: key native-chat transcript cache by resolved file path (#7341) --- .../native-chat/transcript-read-cache.test.ts | 59 +++++++++++++++++++ src/main/native-chat/transcript-read-cache.ts | 27 +++++---- 2 files changed, 75 insertions(+), 11 deletions(-) diff --git a/src/main/native-chat/transcript-read-cache.test.ts b/src/main/native-chat/transcript-read-cache.test.ts index 6243138327e..df5b310fcef 100644 --- a/src/main/native-chat/transcript-read-cache.test.ts +++ b/src/main/native-chat/transcript-read-cache.test.ts @@ -17,6 +17,7 @@ vi.mock('./transcript-reader', async (importOriginal) => { } }) +import { isTextBlock } from '../../shared/native-chat-types' import { clearNativeChatTranscriptCache, readNativeChatTranscriptCached @@ -89,4 +90,62 @@ describe('readNativeChatTranscriptCached', () => { const result = await readNativeChatTranscriptCached('claude', 'absent') expect('error' in result && result.error).toBeTruthy() }) + + // Why: two worktrees can present the SAME (agent, sessionId) via different + // transcript files — e.g. the same session resumed into a second worktree, + // which writes a new transcript file. Keying the cache by sessionId let one + // worktree's cached parse be served to the other whenever their file mtimes + // coincided, leaking A's chat transcript into C's panel (#7326). + it('never serves one file’s parse for a different file that shares a sessionId', async () => { + const root = await mkdtemp(join(tmpdir(), 'orca-native-chat-cache-xwt-')) + tempRoots.push(root) + const fileA = join(root, 'worktree-a.jsonl') + const fileC = join(root, 'worktree-c.jsonl') + await writeFile( + fileA, + jsonLines([ + { + type: 'user', + uuid: 'a0', + timestamp: '2026-06-01T10:00:00.000Z', + message: { role: 'user', content: 'from-worktree-A' } + } + ]) + ) + await writeFile( + fileC, + jsonLines([ + { + type: 'user', + uuid: 'c0', + timestamp: '2026-06-01T10:00:00.000Z', + message: { role: 'user', content: 'from-worktree-C' } + } + ]) + ) + // Force IDENTICAL mtimes so a sessionId-only key's mtime guard cannot rescue + // the collision — this is the intermittent, activity-driven case. + const when = new Date('2026-06-01T10:00:00.000Z') + await utimes(fileA, when, when) + await utimes(fileC, when, when) + + const readText = (result: Awaited>): string => + 'messages' in result + ? result.messages + .flatMap((message) => message.blocks) + .filter(isTextBlock) + .map((block) => block.text) + .join(' ') + : '' + + // Same sessionId, different transcript files (worktree A resumed into C). + const a = await readNativeChatTranscriptCached('claude', 'shared-session', fileA) + const c = await readNativeChatTranscriptCached('claude', 'shared-session', fileC) + + expect(readText(a)).toContain('from-worktree-A') + expect(readText(c)).toContain('from-worktree-C') + expect(readText(c)).not.toContain('from-worktree-A') + // Distinct files must not share a cached parse object. + expect(c).not.toBe(a) + }) }) diff --git a/src/main/native-chat/transcript-read-cache.ts b/src/main/native-chat/transcript-read-cache.ts index 89738b15cb4..b62a1a1142a 100644 --- a/src/main/native-chat/transcript-read-cache.ts +++ b/src/main/native-chat/transcript-read-cache.ts @@ -4,13 +4,18 @@ import { resolveSessionFilePath } from './session-file-resolver' import { readNativeChatTranscript, type ReadTranscriptResult } from './transcript-reader' // Why: both the desktop IPC handler and the runtime RPC handler read the same -// host-filesystem transcript, so a single process-global cache keyed by -// agent:sessionId maximizes the hit rate across desktop + every paired -// web/mobile client. Keying by connection instead would defeat the multi-client -// case this feature targets and multiply memory by the connection count. -// The cache stores ONE canonical, unwindowed parse; windowing and per-surface -// truncation stay in the callers so the same parse is reused across all `limit` -// values and every client kind. +// host-filesystem transcript, so a single process-global cache keyed by the +// RESOLVED transcript file path maximizes the hit rate across desktop + every +// paired web/mobile client (all clients of one session resolve the same path +// against this runtime's home). Keying by connection instead would defeat the +// multi-client case this feature targets and multiply memory by the connection +// count. The key is the resolved file path, NOT `agent:sessionId`: two panes can +// share one sessionId yet resolve to DIFFERENT files (the same session resumed +// into a second worktree, which writes a new transcript file), and a +// sessionId-only key let one worktree's cached parse be served to another when +// their file mtimes momentarily coincided (#7326). The cache stores ONE +// canonical, unwindowed parse; windowing and per-surface truncation stay in the +// callers so the same parse is reused across all `limit` values and every client kind. type CachedTranscript = { result: ReadTranscriptResult @@ -40,8 +45,8 @@ function setCached(key: string, value: CachedTranscript): void { } } -function cacheKey(agent: AgentType, sessionId: string): string { - return `${agent}:${sessionId}` +function cacheKey(agent: AgentType, filePath: string): string { + return `${agent}:${filePath}` } async function fileMtimeMs(filePath: string): Promise { @@ -68,7 +73,7 @@ export async function readNativeChatTranscriptCached( return { error: `No transcript found for ${agent} session ${sessionId}` } } - const key = cacheKey(agent, sessionId) + const key = cacheKey(agent, filePath) const mtimeMs = await fileMtimeMs(filePath) const cached = cache.get(key) if (cached && Number.isFinite(mtimeMs) && cached.mtimeMs === mtimeMs) { @@ -84,7 +89,7 @@ export async function readNativeChatTranscriptCached( return result } -/** Test-only: drop the per-session transcript cache between runs. */ +/** Test-only: drop the transcript parse cache between runs. */ export function clearNativeChatTranscriptCache(): void { cache.clear() } From b5359ca7e381ae15b0d08a030d3ea465da2ac043 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 4 Jul 2026 02:54:20 -0700 Subject: [PATCH 13/45] Seed WSL Codex runtime config through the fresh-mirror preparation (#7343) * Seed WSL Codex runtime config through the fresh-mirror preparation The WSL runtime home seed copied config.toml verbatim, so relative path-valued settings (model_instructions_file etc.) resolved against the runtime home inside the distro and aborted Codex config load with 'os error 2' -- the same failure class #7157 fixed for host runtime and managed account homes. Seed now applies the shared fresh-mirror preparation (deprecated codex_hooks normalization + relative-path rewrite anchored to the Linux-side source home + system hook-trust strip). Also fixes CRLF configs skipping deprecated codex_hooks normalization: the feature-section header regexes did not tolerate the trailing \r. Co-authored-by: Orca * Extract prepareWslRuntimeSeedConfig and pin UNC->Linux anchor in tests The e2e seed test's mocked WSL home is a plain local path, so the parseWslUncPath linuxPath branch was never exercised. Extract the seed preparation as a pure exported function and cover both UNC spellings (wsl.localhost and wsl$) with hardcoded literals. Co-authored-by: Orca --------- Co-authored-by: Orca --- .../runtime-home-service.test.ts | 80 +++++++++++++++++++ .../codex-accounts/runtime-home-service.ts | 23 +++++- src/main/codex/codex-config-mirror.test.ts | 34 +++++++- src/main/codex/codex-config-mirror.ts | 33 +++++--- 4 files changed, 158 insertions(+), 12 deletions(-) diff --git a/src/main/codex-accounts/runtime-home-service.test.ts b/src/main/codex-accounts/runtime-home-service.test.ts index 87e3a0ea90d..51908be3253 100644 --- a/src/main/codex-accounts/runtime-home-service.test.ts +++ b/src/main/codex-accounts/runtime-home-service.test.ts @@ -1324,6 +1324,86 @@ describe('CodexRuntimeHomeService', () => { } }) + it('seeds the WSL runtime config with rewritten paths and no system hook trust', async () => { + const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform') + Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' }) + const wslHome = join(testState.userDataDir, 'wsl-home') + vi.doMock('../wsl', () => ({ + getDefaultWslDistro: () => 'Ubuntu', + getWslHome: () => wslHome + })) + const systemCodexHomePath = join(wslHome, '.codex') + mkdirSync(systemCodexHomePath, { recursive: true }) + writeFileSync( + join(systemCodexHomePath, 'config.toml'), + [ + 'model_instructions_file = "instructions.md"', + '', + '[hooks.state."system-hooks:stop:0:0"]', + 'enabled = true', + '', + '[projects."/home/alice/repo"]', + 'trust_level = "trusted"', + '' + ].join('\n'), + 'utf-8' + ) + const store = createStore(createSettings()) + + try { + const { CodexRuntimeHomeService } = await import('./runtime-home-service') + const service = new CodexRuntimeHomeService(store as never) + const wslRuntimeHomePath = join( + wslHome, + '.local', + 'share', + 'orca', + 'codex-runtime-home', + 'home' + ) + + expect(service.prepareForCodexLaunch({ runtime: 'wsl', wslDistro: 'Ubuntu' })).toBe( + wslRuntimeHomePath + ) + const runtimeConfigPath = join(wslRuntimeHomePath, 'config.toml') + const runtimeConfig = readFileSync(runtimeConfigPath, 'utf-8') + expect(runtimeConfig).toContain( + `model_instructions_file = '${join(systemCodexHomePath, 'instructions.md')}'` + ) + expect(runtimeConfig).toContain('[projects."/home/alice/repo"]') + expect(runtimeConfig).not.toContain('[hooks.state.') + + // Why: WSL runtime configs are seeded once; Codex writes trust into them + // afterwards, so a relaunch must not clobber the seeded file. + writeFileSync(runtimeConfigPath, `${runtimeConfig}\n[projects."/tmp/x"]\n`, 'utf-8') + service.prepareForCodexLaunch({ runtime: 'wsl', wslDistro: 'Ubuntu' }) + expect(readFileSync(runtimeConfigPath, 'utf-8')).toContain('[projects."/tmp/x"]') + } finally { + if (originalPlatform) { + Object.defineProperty(process, 'platform', originalPlatform) + } + } + }) + + it('anchors WSL seed rewrites to the Linux-side home parsed from the UNC source', async () => { + const { prepareWslRuntimeSeedConfig } = await import('./runtime-home-service') + + // Why: real UNC sources cannot back live fs operations in tests, so pin + // the UNC -> Linux-side anchor translation on the extracted seed function. + expect( + prepareWslRuntimeSeedConfig( + 'model_instructions_file = "instructions.md"\n', + '\\\\wsl.localhost\\Ubuntu\\home\\alice\\.codex' + ) + ).toContain("model_instructions_file = '/home/alice/.codex/instructions.md'") + expect( + prepareWslRuntimeSeedConfig( + 'model_instructions_file = "instructions.md"\n', + '\\\\wsl$\\Ubuntu\\home\\alice\\.codex' + ) + ).toContain("model_instructions_file = '/home/alice/.codex/instructions.md'") + }) + it('switches WSL accounts by rewriting one stable WSL runtime home', async () => { const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform') Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' }) diff --git a/src/main/codex-accounts/runtime-home-service.ts b/src/main/codex-accounts/runtime-home-service.ts index 12d119a8510..4d5d8e4c645 100644 --- a/src/main/codex-accounts/runtime-home-service.ts +++ b/src/main/codex-accounts/runtime-home-service.ts @@ -40,7 +40,10 @@ import { syncSystemCodexResourcesIntoManagedHome } from '../codex/codex-home-paths' import { startSystemCodexSessionBridgeInBackground } from '../codex/codex-session-bridge' -import { syncSystemConfigIntoManagedCodexHome } from '../codex/codex-config-mirror' +import { + prepareSystemConfigForFreshRuntimeMirror, + syncSystemConfigIntoManagedCodexHome +} from '../codex/codex-config-mirror' import { parseWslUncPath } from '../../shared/wsl-paths' import { getWslSelectionKey, @@ -705,7 +708,10 @@ export class CodexRuntimeHomeService { for (const homePath of candidateHomes) { const configPath = join(homePath, 'config.toml') if (existsSync(configPath)) { - copyFileSync(configPath, runtimeConfigPath) + writeFileAtomically( + runtimeConfigPath, + prepareWslRuntimeSeedConfig(readFileSync(configPath, 'utf-8'), homePath) + ) return } } @@ -1562,3 +1568,16 @@ export class CodexRuntimeHomeService { rmSync(this.getSystemDefaultSnapshotPath(), { force: true }) } } + +// Why: the seed config is read over UNC but consumed by Codex inside WSL, so +// relative path-valued settings must anchor to the Linux-side source home; a +// verbatim copy breaks Codex config load (os error 2). +export function prepareWslRuntimeSeedConfig( + configContents: string, + sourceHomePath: string +): string { + return prepareSystemConfigForFreshRuntimeMirror( + configContents, + parseWslUncPath(sourceHomePath)?.linuxPath ?? sourceHomePath + ) +} diff --git a/src/main/codex/codex-config-mirror.test.ts b/src/main/codex/codex-config-mirror.test.ts index ff3ad9ba3bd..416ce9af78b 100644 --- a/src/main/codex/codex-config-mirror.test.ts +++ b/src/main/codex/codex-config-mirror.test.ts @@ -23,7 +23,10 @@ vi.mock('node:os', async () => { } }) -import { syncSystemConfigIntoManagedCodexHome } from './codex-config-mirror' +import { + prepareSystemConfigForFreshRuntimeMirror, + syncSystemConfigIntoManagedCodexHome +} from './codex-config-mirror' let fakeHomeDir: string let userDataDir: string @@ -397,3 +400,32 @@ describe('syncSystemConfigIntoManagedCodexHome', () => { expect(existsSync(getRuntimeConfigPath())).toBe(false) }) }) + +describe('prepareSystemConfigForFreshRuntimeMirror', () => { + it('rewrites relative paths against a Linux-side home and strips hook trust', () => { + const prepared = prepareSystemConfigForFreshRuntimeMirror( + [ + 'model_instructions_file = "instructions.md"', + '', + '[features]', + 'codex_hooks = true', + '', + '[hooks.state."system-hooks:stop:0:0"]', + 'enabled = true', + '', + '[projects."/home/alice/repo"]', + 'trust_level = "trusted"', + '' + ].join('\r\n'), + '/home/alice/.codex' + ) + + // Why: WSL configs are consumed inside the distro, so rewrites must use + // posix join semantics regardless of the host platform. + expect(prepared).toContain("model_instructions_file = '/home/alice/.codex/instructions.md'") + expect(prepared).toContain('hooks = true') + expect(prepared).not.toContain('codex_hooks') + expect(prepared).toContain('[projects."/home/alice/repo"]') + expect(prepared).not.toContain('[hooks.state."system-hooks:stop:0:0"]') + }) +}) diff --git a/src/main/codex/codex-config-mirror.ts b/src/main/codex/codex-config-mirror.ts index ff9f72c55fe..ce9208e9b05 100644 --- a/src/main/codex/codex-config-mirror.ts +++ b/src/main/codex/codex-config-mirror.ts @@ -35,17 +35,19 @@ function syncSystemConfigIntoManagedCodexHomeUnsafe(): void { return } - const systemConfig = prepareSystemConfigForRuntimeMirror( - systemConfigExists ? readFileSync(systemConfigPath, 'utf-8') : '', - dirname(systemConfigPath) - ) + const rawSystemConfig = systemConfigExists ? readFileSync(systemConfigPath, 'utf-8') : '' if (!runtimeConfigExists) { - // Why: trust blocks reference a hooks.json path, so system-home hook trust - // entries are not valid in Orca's runtime CODEX_HOME until install remaps them. - writeFileAtomically(runtimeConfigPath, stripRuntimeOwnedTomlSections(systemConfig)) + writeFileAtomically( + runtimeConfigPath, + prepareSystemConfigForFreshRuntimeMirror(rawSystemConfig, dirname(systemConfigPath)) + ) return } + const systemConfig = prepareSystemConfigForRuntimeMirror( + rawSystemConfig, + dirname(systemConfigPath) + ) const runtimeConfig = readFileSync(runtimeConfigPath, 'utf-8') const mergedConfig = mergeSystemCodexConfigIntoRuntime(runtimeConfig, systemConfig) if (mergedConfig !== runtimeConfig) { @@ -60,6 +62,17 @@ function prepareSystemConfigForRuntimeMirror(config: string, systemConfigDir: st ) } +// Why: trust blocks reference a hooks.json path, so system-home hook trust +// entries are not valid in a fresh runtime CODEX_HOME until install remaps +// them. Also seeds WSL runtime homes, where systemConfigDir must be the +// Linux-side ~/.codex the config resolves against inside the distro. +export function prepareSystemConfigForFreshRuntimeMirror( + config: string, + systemConfigDir: string +): string { + return stripRuntimeOwnedTomlSections(prepareSystemConfigForRuntimeMirror(config, systemConfigDir)) +} + function normalizeDeprecatedCodexHookFeatureFlag(config: string): string { if (!config.includes('codex_hooks')) { return config @@ -71,7 +84,9 @@ function normalizeDeprecatedCodexHookFeatureFlag(config: string): string { for (let index = 0; index <= lines.length; index += 1) { const line = lines[index] - const isHeader = line === undefined || /^[ \t]*\[[^\]]+\][ \t]*(?:#.*)?$/.test(line) + // Why: CRLF configs keep a trailing \r after the split, so header anchors + // must tolerate it or Windows-shaped configs skip normalization entirely. + const isHeader = line === undefined || /^[ \t]*\[[^\]]+\][ \t]*(?:#.*)?\r?$/.test(line) if (!isHeader) { continue } @@ -80,7 +95,7 @@ function normalizeDeprecatedCodexHookFeatureFlag(config: string): string { featureSections.push({ start: featureStart, end: index }) featureStart = null } - if (line !== undefined && /^[ \t]*\[features\][ \t]*(?:#.*)?$/.test(line)) { + if (line !== undefined && /^[ \t]*\[features\][ \t]*(?:#.*)?\r?$/.test(line)) { featureStart = index } } From db7046721f1db702b672abe1e10d8e7553dd282e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 4 Jul 2026 12:42:10 +0000 Subject: [PATCH 14/45] Update README downloads badge --- docs/assets/readme-downloads.svg | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/assets/readme-downloads.svg b/docs/assets/readme-downloads.svg index ff7c9fd7520..f5d3d95b9d0 100644 --- a/docs/assets/readme-downloads.svg +++ b/docs/assets/readme-downloads.svg @@ -1,5 +1,5 @@ - - downloads: 3.4m + + downloads: 3.5m @@ -15,7 +15,7 @@ downloads downloads - 3.4m - 3.4m + 3.5m + 3.5m From c6bfd86d4ef8c20d47152151756af12b6b89291c Mon Sep 17 00:00:00 2001 From: Erik Date: Sat, 4 Jul 2026 16:46:50 +0200 Subject: [PATCH 15/45] fix(terminal): pass Ctrl+Arrow to PSReadLine on local Windows ConPTY (#6763) * fix(terminal): pass Ctrl+Arrow to PSReadLine on local Windows ConPTY On local Windows shells (PowerShell/cmd via PSReadLine) Ctrl+Left/Right printed a stray "b"/"f" instead of moving by word. The terminal shortcut policy translated Ctrl+Arrow to \eb/\ef (readline's Alt+b/Alt+f word-nav bytes), but PSReadLine binds Escape to RevertLine and has no Alt+b/Alt+f binding, so it consumed the ESC and self-inserted the bare letter. PSReadLine already binds Ctrl+Left/Right to word-nav natively, so the translation was breaking a working default. Gate the translation so genuine local native Windows ConPTY panes fall through to xterm's native \e[1;5D/\e[1;5C, while Linux and remote/WSL/SSH readline shells keep the \eb/\ef translation. Reuses isLocalNativeWindowsConpty (the same execution-host heuristic as the Kitty-keyboard fix) so SSH/WSL panes on a Windows client are correctly excluded. Adds policy coverage for the ConPTY-passthrough and SSH/WSL-still-translates paths. Co-Authored-By: Claude Opus 4.8 (1M context) * fix(terminal): use live session for Windows Ctrl+Arrow Co-authored-by: Orca --------- Co-authored-by: Claude Opus 4.8 (1M context) Co-authored-by: Jinwoo-H Co-authored-by: Orca --- .../components/terminal-pane/TerminalPane.tsx | 1 + .../terminal-pane/keyboard-handlers.ts | 31 ++++- .../terminal-ctrl-arrow-conpty.test.ts | 120 ++++++++++++++++++ .../terminal-ctrl-arrow-conpty.ts | 89 +++++++++++++ .../terminal-shortcut-ctrl-arrow.test.ts | 65 +++++++++- .../terminal-pane/terminal-shortcut-policy.ts | 28 +++- .../windows-pty-compatibility.test.ts | 12 ++ 7 files changed, 333 insertions(+), 13 deletions(-) create mode 100644 src/renderer/src/components/terminal-pane/terminal-ctrl-arrow-conpty.test.ts create mode 100644 src/renderer/src/components/terminal-pane/terminal-ctrl-arrow-conpty.ts diff --git a/src/renderer/src/components/terminal-pane/TerminalPane.tsx b/src/renderer/src/components/terminal-pane/TerminalPane.tsx index 60771da63fd..4db3c5035a5 100644 --- a/src/renderer/src/components/terminal-pane/TerminalPane.tsx +++ b/src/renderer/src/components/terminal-pane/TerminalPane.tsx @@ -1688,6 +1688,7 @@ export default function TerminalPane({ useTerminalKeyboardShortcuts({ tabId, + worktreeId, isActive, keyboardScopeRef: containerRef, managerRef, diff --git a/src/renderer/src/components/terminal-pane/keyboard-handlers.ts b/src/renderer/src/components/terminal-pane/keyboard-handlers.ts index 35cd9f82fcd..ed78a5d6500 100644 --- a/src/renderer/src/components/terminal-pane/keyboard-handlers.ts +++ b/src/renderer/src/components/terminal-pane/keyboard-handlers.ts @@ -22,6 +22,7 @@ import { recordCreatedTerminalPaneSplit } from './terminal-pane-split-completion import { splitTerminalPaneWithInheritedCwd } from './terminal-pane-split-with-inherited-cwd' import { useAppStore } from '@/store' import { recordTerminalUserInputForLeaf } from './terminal-input-activity' +import { isLocalWindowsConptyPaneForCtrlArrow } from './terminal-ctrl-arrow-conpty' import { markTerminalFollowOutput, markTerminalPinnedViewport, @@ -137,6 +138,7 @@ export function matchFileSearchShortcut( type KeyboardHandlersDeps = { tabId: string + worktreeId: string isActive: boolean keyboardScopeRef: React.RefObject managerRef: React.RefObject @@ -170,6 +172,7 @@ type KeyboardHandlersDeps = { */ export function useTerminalKeyboardShortcuts({ tabId, + worktreeId, isActive, keyboardScopeRef, managerRef, @@ -268,13 +271,36 @@ export function useTerminalKeyboardShortcuts({ return } + // Why: the active pane's live PTY session decides whether Ctrl+Arrow should + // pass through as native \e[1;5C/\e[1;5D or be translated to \eb/\ef. + // Resolved lazily so session/runtime lookups stay off other keystrokes. + const isLocalWindowsConptyPane = (): boolean => { + const activePane = manager.getActivePane() ?? manager.getPanes()[0] + if (!activePane) { + return false + } + const storeState = useAppStore.getState() + return isLocalWindowsConptyPaneForCtrlArrow({ + isWindows, + userAgent: navigator.userAgent, + state: storeState, + worktreeId, + tabId, + paneId: activePane.id, + paneCwd: paneCwdRef.current, + fallbackCwd, + transport: paneTransportsRef.current.get(activePane.id) ?? null + }) + } + const action = resolveTerminalShortcutAction( e, isMac, macOptionAsAltRef.current, optionKeyLocation, isWindows, - keybindings + keybindings, + isLocalWindowsConptyPane ) if (!action) { return @@ -513,7 +539,8 @@ export function useTerminalKeyboardShortcuts({ macOptionAsAltRef, keybindings, terminalShortcutPolicy, - tabId + tabId, + worktreeId ]) } diff --git a/src/renderer/src/components/terminal-pane/terminal-ctrl-arrow-conpty.test.ts b/src/renderer/src/components/terminal-pane/terminal-ctrl-arrow-conpty.test.ts new file mode 100644 index 00000000000..8c784bdf3cf --- /dev/null +++ b/src/renderer/src/components/terminal-pane/terminal-ctrl-arrow-conpty.test.ts @@ -0,0 +1,120 @@ +import { describe, expect, it } from 'vitest' +import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../../shared/constants' +import { + isLocalWindowsConptyPaneForCtrlArrow, + type TerminalCtrlArrowConptyState +} from './terminal-ctrl-arrow-conpty' +import type { PtyTransport } from './pty-transport-types' + +const WINDOWS_UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)' + +function state( + overrides: Partial = {} +): TerminalCtrlArrowConptyState { + return { + repos: [], + worktreesByRepo: {}, + folderWorkspaces: [], + projectGroups: [], + settings: null, + tabsByWorktree: {}, + ...overrides + } +} + +function localTransport( + metadata: { cwd?: string; shellOverride?: string } = {} +): Pick { + return { + getPtyId: () => 'pty-1', + getConnectionId: () => null, + getLocalSessionMetadata: () => metadata + } +} + +function remoteRuntimeTransport(): Pick< + PtyTransport, + 'getPtyId' | 'getConnectionId' | 'getLocalSessionMetadata' +> { + return { + getPtyId: () => 'remote:env-1@@terminal-1', + getConnectionId: () => null, + getLocalSessionMetadata: () => null + } +} + +describe('isLocalWindowsConptyPaneForCtrlArrow', () => { + it('uses live local session metadata for synthetic floating terminals with no repo record', () => { + expect( + isLocalWindowsConptyPaneForCtrlArrow({ + isWindows: true, + userAgent: WINDOWS_UA, + state: state({ + tabsByWorktree: { + [FLOATING_TERMINAL_WORKTREE_ID]: [{ id: 'tab-1', shellOverride: 'powershell.exe' }] + } + }), + worktreeId: FLOATING_TERMINAL_WORKTREE_ID, + tabId: 'tab-1', + paneId: 1, + paneCwd: new Map(), + fallbackCwd: 'C:\\Users\\me', + transport: localTransport({ cwd: 'C:\\Users\\me', shellOverride: 'powershell.exe' }) + }) + ).toBe(true) + }) + + it('keeps an already-launched local PowerShell pane local after store ownership changes', () => { + const worktreeId = 'repo-1::C:\\repo' + expect( + isLocalWindowsConptyPaneForCtrlArrow({ + isWindows: true, + userAgent: WINDOWS_UA, + state: state({ + settings: { activeRuntimeEnvironmentId: 'env-1' }, + tabsByWorktree: { + [worktreeId]: [{ id: 'tab-1', shellOverride: 'wsl.exe' }] + } + }), + worktreeId, + tabId: 'tab-1', + paneId: 1, + paneCwd: new Map([[1, { cwd: '\\\\wsl.localhost\\Ubuntu\\home\\me', confirmed: true }]]), + fallbackCwd: '\\\\wsl.localhost\\Ubuntu\\home\\me', + transport: localTransport({ cwd: 'C:\\repo', shellOverride: 'powershell.exe' }) + }) + ).toBe(true) + }) + + it('does not treat a live remote-runtime terminal as local ConPTY', () => { + expect( + isLocalWindowsConptyPaneForCtrlArrow({ + isWindows: true, + userAgent: WINDOWS_UA, + state: state(), + worktreeId: 'repo-1::/remote/repo', + tabId: 'tab-1', + paneId: 1, + paneCwd: new Map([[1, { cwd: '/remote/repo', confirmed: true }]]), + fallbackCwd: '/remote/repo', + transport: remoteRuntimeTransport() + }) + ).toBe(false) + }) + + it('stays conservative for unresolved repo-backed panes before a live session exists', () => { + expect( + isLocalWindowsConptyPaneForCtrlArrow({ + isWindows: true, + userAgent: WINDOWS_UA, + state: state(), + worktreeId: 'missing-repo::/home/me/repo', + tabId: 'tab-1', + paneId: 1, + paneCwd: new Map([[1, { cwd: '/home/me/repo', confirmed: true }]]), + fallbackCwd: '/home/me/repo', + transport: null + }) + ).toBe(false) + }) +}) diff --git a/src/renderer/src/components/terminal-pane/terminal-ctrl-arrow-conpty.ts b/src/renderer/src/components/terminal-pane/terminal-ctrl-arrow-conpty.ts new file mode 100644 index 00000000000..d2bc848886b --- /dev/null +++ b/src/renderer/src/components/terminal-pane/terminal-ctrl-arrow-conpty.ts @@ -0,0 +1,89 @@ +import type { ExecutionHostId } from '../../../../shared/execution-host' +import { LOCAL_EXECUTION_HOST_ID } from '../../../../shared/execution-host' +import { getConnectionIdFromState } from '@/lib/connection-context' +import { + getExecutionHostIdForWorktree, + type WorktreeRuntimeOwnerState +} from '@/lib/worktree-runtime-owner' +import { isLocalNativeWindowsConpty } from '@/lib/pane-manager/windows-pty-compatibility' +import type { PaneCwdMap } from './resolve-split-cwd' +import type { PtyTransport } from './pty-transport-types' + +const REMOTE_RUNTIME_PTY_ID_PREFIX = 'remote:' + +type TerminalTabShellState = { + tabsByWorktree: Record< + string, + readonly { id: string; shellOverride?: string | null }[] | undefined + > +} + +export type TerminalCtrlArrowConptyState = Parameters[0] & + WorktreeRuntimeOwnerState & + TerminalTabShellState + +type TerminalCtrlArrowConptyTransport = Pick< + PtyTransport, + 'getPtyId' | 'getConnectionId' | 'getLocalSessionMetadata' +> + +type TerminalCtrlArrowConptyArgs = { + isWindows: boolean + userAgent: string + state: TerminalCtrlArrowConptyState + worktreeId: string + tabId: string + paneId: number + paneCwd: PaneCwdMap + fallbackCwd: string + transport: TerminalCtrlArrowConptyTransport | null +} + +function isRemoteRuntimePtyId(ptyId: string): boolean { + return ptyId.startsWith(REMOTE_RUNTIME_PTY_ID_PREFIX) +} + +export function isLocalWindowsConptyPaneForCtrlArrow({ + isWindows, + userAgent, + state, + worktreeId, + tabId, + paneId, + paneCwd, + fallbackCwd, + transport +}: TerminalCtrlArrowConptyArgs): boolean { + if (!isWindows) { + return false + } + + const ptyId = transport?.getPtyId() ?? null + if (ptyId !== null && isRemoteRuntimePtyId(ptyId)) { + return false + } + + const sessionMetadata = transport?.getLocalSessionMetadata?.() + const hasLiveLocalSession = + ptyId !== null && sessionMetadata !== null && sessionMetadata !== undefined + const transportConnectionId = transport?.getConnectionId?.() + const connectionId = hasLiveLocalSession + ? null + : transportConnectionId === undefined + ? getConnectionIdFromState(state, worktreeId) + : transportConnectionId + const tabShellOverride = state.tabsByWorktree[worktreeId]?.find( + (candidate) => candidate.id === tabId + )?.shellOverride + const executionHostId: ExecutionHostId = hasLiveLocalSession + ? LOCAL_EXECUTION_HOST_ID + : getExecutionHostIdForWorktree(state, worktreeId) + + return isLocalNativeWindowsConpty({ + userAgent, + connectionId, + cwd: sessionMetadata?.cwd ?? paneCwd.get(paneId)?.cwd ?? fallbackCwd, + shellOverride: sessionMetadata?.shellOverride ?? tabShellOverride, + executionHostId + }) +} diff --git a/src/renderer/src/components/terminal-pane/terminal-shortcut-ctrl-arrow.test.ts b/src/renderer/src/components/terminal-pane/terminal-shortcut-ctrl-arrow.test.ts index 7e7559db5c0..af6750bee39 100644 --- a/src/renderer/src/components/terminal-pane/terminal-shortcut-ctrl-arrow.test.ts +++ b/src/renderer/src/components/terminal-pane/terminal-shortcut-ctrl-arrow.test.ts @@ -18,10 +18,9 @@ function event(overrides: Partial): TerminalShortcutEvent } describe('non-mac Ctrl+Left/Right word-nav', () => { - // Windows Terminal, GNOME Terminal, and Konsole all bind Ctrl+←/→ to - // word-nav. xterm.js emits \e[1;5D / \e[1;5C which default readline doesn't - // map, so translate to \eb / \ef (same bytes as our Alt+Arrow rule). - it('translates Ctrl+←/→ on Windows/Linux to readline \\eb / \\ef', () => { + // Linux and remote/WSL readline shells don't bind the \e[1;5D / \e[1;5C that + // xterm.js emits, so translate to \eb / \ef (same bytes as our Alt+Arrow rule). + it('translates Ctrl+←/→ on Linux to readline \\eb / \\ef', () => { expect( resolveTerminalShortcutAction( event({ key: 'ArrowLeft', code: 'ArrowLeft', ctrlKey: true }), @@ -36,6 +35,64 @@ describe('non-mac Ctrl+Left/Right word-nav', () => { ).toEqual({ type: 'sendInput', data: '\x1bf' }) }) + // Local Windows ConPTY shells (PowerShell/cmd via PSReadLine) already bind + // Ctrl+←/→ to word-nav and self-insert a stray "b"/"f" when fed \eb/\ef + // (Escape→RevertLine + self-insert), so the policy must defer to xterm's + // native \e[1;5D / \e[1;5C there. Signalled via the isLocalWindowsConptyPane + // getter (7th arg). + it('does NOT translate Ctrl+←/→ for a local Windows ConPTY pane', () => { + expect( + resolveTerminalShortcutAction( + event({ key: 'ArrowLeft', code: 'ArrowLeft', ctrlKey: true }), + false, + undefined, + undefined, + true, + undefined, + () => true + ) + ).toBeNull() + expect( + resolveTerminalShortcutAction( + event({ key: 'ArrowRight', code: 'ArrowRight', ctrlKey: true }), + false, + undefined, + undefined, + true, + undefined, + () => true + ) + ).toBeNull() + }) + + // A Windows client SSH'd into Linux (or running WSL) is NOT a local native + // Windows ConPTY, so the getter returns false and the readline translation + // must still apply — otherwise word-nav silently breaks on the remote shell. + it('still translates Ctrl+←/→ on Windows when the pane is not local ConPTY (SSH/WSL)', () => { + expect( + resolveTerminalShortcutAction( + event({ key: 'ArrowLeft', code: 'ArrowLeft', ctrlKey: true }), + false, + undefined, + undefined, + true, + undefined, + () => false + ) + ).toEqual({ type: 'sendInput', data: '\x1bb' }) + expect( + resolveTerminalShortcutAction( + event({ key: 'ArrowRight', code: 'ArrowRight', ctrlKey: true }), + false, + undefined, + undefined, + true, + undefined, + () => false + ) + ).toEqual({ type: 'sendInput', data: '\x1bf' }) + }) + it('does not translate Ctrl+Arrow on macOS (reserved by OS)', () => { // Mac uses Cmd+Arrow for line-nav and Option+Arrow for word-nav. // Ctrl+Arrow is the macOS Mission Control / Spaces chord. diff --git a/src/renderer/src/components/terminal-pane/terminal-shortcut-policy.ts b/src/renderer/src/components/terminal-pane/terminal-shortcut-policy.ts index d90b6e5ea62..35cc6c00ffe 100644 --- a/src/renderer/src/components/terminal-pane/terminal-shortcut-policy.ts +++ b/src/renderer/src/components/terminal-pane/terminal-shortcut-policy.ts @@ -53,7 +53,12 @@ export function resolveTerminalShortcutAction( macOptionAsAlt: MacOptionAsAlt = 'false', optionKeyLocation: number = 0, isWindows: boolean = false, - keybindings?: KeybindingOverrides + keybindings?: KeybindingOverrides, + // Why: lazily reports whether the active pane is a local native Windows + // ConPTY (PowerShell/cmd via PSReadLine). Only consulted for the Ctrl+Arrow + // word-nav rule below, so the execution-host lookup it performs stays off the + // hot path for every other keystroke. + isLocalWindowsConptyPane?: () => boolean ): TerminalShortcutAction | null { const platform: NodeJS.Platform = isMac ? 'darwin' : isWindows ? 'win32' : 'linux' if (!event.repeat) { @@ -206,12 +211,21 @@ export function resolveTerminalShortcutAction( !event.shiftKey && (event.key === 'ArrowLeft' || event.key === 'ArrowRight') ) { - // Why: Windows Terminal, GNOME Terminal, and Konsole all bind Ctrl+←/→ for - // word navigation on Linux/Windows — but xterm.js emits \e[1;5D / \e[1;5C, - // which default readline (bash, zsh) does not bind to backward-word / - // forward-word. Translate to \eb / \ef (same bytes as our Alt+Arrow rule) - // so Ctrl+←/→ works for word-nav matching user expectations on those - // platforms without requiring a custom inputrc. + // Why: local Windows ConPTY shells (PowerShell/cmd via PSReadLine) already + // bind Ctrl+←/→ to word-nav, and they treat \eb/\ef (Alt+b/f) as + // Escape→RevertLine followed by a self-inserted "b"/"f" — so the translation + // below prints a stray letter instead of moving the cursor (issue: Ctrl+→ + // types "b"/"f" in PowerShell). Defer to xterm's native \e[1;5D / \e[1;5C + // there. Remote/WSL panes on a Windows client run readline and still need + // the translation, so this is gated on a genuine local native ConPTY, not + // merely on the client being Windows. + if (isLocalWindowsConptyPane?.()) { + return null + } + // Why: default readline (bash, zsh) does not bind the \e[1;5D / \e[1;5C that + // xterm.js emits for Ctrl+←/→, so Linux and remote/WSL shells need the + // translation to \eb / \ef (same bytes as our Alt+Arrow rule) for word-nav + // to work without a custom inputrc. // // Mac-gated: Ctrl+Arrow on macOS is reserved for Mission Control / Spaces // navigation at the OS level and should never reach the app. diff --git a/src/renderer/src/lib/pane-manager/windows-pty-compatibility.test.ts b/src/renderer/src/lib/pane-manager/windows-pty-compatibility.test.ts index 8e32f7d7246..bfd67604707 100644 --- a/src/renderer/src/lib/pane-manager/windows-pty-compatibility.test.ts +++ b/src/renderer/src/lib/pane-manager/windows-pty-compatibility.test.ts @@ -229,6 +229,18 @@ describe('isLocalNativeWindowsConpty', () => { ).toBe(false) }) + it('does NOT treat unresolved connection ownership as native ConPTY', () => { + expect( + isLocalNativeWindowsConpty({ + userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)', + connectionId: undefined, + cwd: '/home/me/repo', + shellOverride: null, + executionHostId: 'local' + }) + ).toBe(false) + }) + it('stays false on a local host when the pane is not a native Windows PTY', () => { // Non-Windows client: the raw heuristic is false, so the gate result is false // regardless of the local execution host. From ccfd6727c9b9a24b8ee03c38174b6c73b81d33b5 Mon Sep 17 00:00:00 2001 From: mehmet turac Date: Sat, 4 Jul 2026 18:17:55 +0300 Subject: [PATCH 16/45] fix(browser): route focused guest zoom shortcuts to page zoom (#6744) Route focused browser guest zoom shortcuts to the browser page zoom path, including native Electron zoom-command fallback and wheel/native dedupe. --- src/main/browser/browser-guest-ui.test.ts | 102 ++++++++++++++++++++-- src/main/browser/browser-guest-ui.ts | 74 ++++++++++++++-- src/main/window/createMainWindow.ts | 34 +------- src/shared/window-shortcut-policy.ts | 32 +++++++ 4 files changed, 195 insertions(+), 47 deletions(-) diff --git a/src/main/browser/browser-guest-ui.test.ts b/src/main/browser/browser-guest-ui.test.ts index 28b7a8b2421..76a1b1c7953 100644 --- a/src/main/browser/browser-guest-ui.test.ts +++ b/src/main/browser/browser-guest-ui.test.ts @@ -433,6 +433,26 @@ describe('setupGuestShortcutForwarding', () => { return preventDefault } + function triggerZoomChanged(direction: 'in' | 'out' | 'reset'): ReturnType { + const handler = guestOnMock.mock.calls.find((call) => call[0] === 'zoom-changed')?.[1] as + | ((event: Electron.Event, direction: 'in' | 'out' | 'reset') => void) + | undefined + expect(handler).toBeTypeOf('function') + const preventDefault = vi.fn() + handler!({ preventDefault } as unknown as Electron.Event, direction) + return preventDefault + } + + function triggerBeforeMouse(mouse: Electron.MouseInputEvent): ReturnType { + const handler = guestOnMock.mock.calls.find((call) => call[0] === 'before-mouse-event')?.[1] as + | ((event: Electron.Event, mouse: Electron.MouseInputEvent) => void) + | undefined + expect(handler).toBeTypeOf('function') + const preventDefault = vi.fn() + handler!({ preventDefault } as unknown as Electron.Event, mouse) + return preventDefault + } + function triggerGuestBlur(): void { const handler = guestOnMock.mock.calls.find((call) => call[0] === 'blur')?.[1] as | (() => void) @@ -488,7 +508,7 @@ describe('setupGuestShortcutForwarding', () => { } }) - it('forwards app zoom shortcuts from focused guest pages', () => { + it('forwards browser page zoom shortcuts from focused guest pages', () => { setupGuestShortcutForwarding({ browserTabId, guest: makeGuest(), @@ -516,12 +536,12 @@ describe('setupGuestShortcutForwarding', () => { expect(numpadSubtractPreventDefault).toHaveBeenCalledTimes(1) expect(resetPreventDefault).toHaveBeenCalledTimes(1) expect(repeatPreventDefault).toHaveBeenCalledTimes(1) - expect(rendererSendMock).toHaveBeenNthCalledWith(1, 'terminal:zoom', 'in') - expect(rendererSendMock).toHaveBeenNthCalledWith(2, 'terminal:zoom', 'in') - expect(rendererSendMock).toHaveBeenNthCalledWith(3, 'terminal:zoom', 'out') - expect(rendererSendMock).toHaveBeenNthCalledWith(4, 'terminal:zoom', 'out') - expect(rendererSendMock).toHaveBeenNthCalledWith(5, 'terminal:zoom', 'reset') - expect(rendererSendMock).toHaveBeenNthCalledWith(6, 'terminal:zoom', 'in') + expect(rendererSendMock).toHaveBeenNthCalledWith(1, 'ui:zoomBrowserPage', 'in') + expect(rendererSendMock).toHaveBeenNthCalledWith(2, 'ui:zoomBrowserPage', 'in') + expect(rendererSendMock).toHaveBeenNthCalledWith(3, 'ui:zoomBrowserPage', 'out') + expect(rendererSendMock).toHaveBeenNthCalledWith(4, 'ui:zoomBrowserPage', 'out') + expect(rendererSendMock).toHaveBeenNthCalledWith(5, 'ui:zoomBrowserPage', 'reset') + expect(rendererSendMock).toHaveBeenNthCalledWith(6, 'ui:zoomBrowserPage', 'in') }) it('forwards browser history shortcuts from focused guest pages', () => { @@ -600,7 +620,73 @@ describe('setupGuestShortcutForwarding', () => { expect(defaultPreventDefault).not.toHaveBeenCalled() expect(customPreventDefault).toHaveBeenCalledTimes(1) - expect(rendererSendMock).toHaveBeenCalledWith('terminal:zoom', 'in') + expect(rendererSendMock).toHaveBeenCalledWith('ui:zoomBrowserPage', 'in') + }) + + it('forwards native guest zoom commands to browser page zoom when default zoom keys are bound', () => { + setupGuestShortcutForwarding({ + browserTabId, + guest: makeGuest(), + resolveRenderer: () => makeRenderer() + }) + + const zoomOutPreventDefault = triggerZoomChanged('out') + const zoomInPreventDefault = triggerZoomChanged('in') + const resetPreventDefault = triggerZoomChanged('reset') + + expect(zoomOutPreventDefault).toHaveBeenCalledTimes(1) + expect(zoomInPreventDefault).toHaveBeenCalledTimes(1) + expect(resetPreventDefault).not.toHaveBeenCalled() + expect(rendererSendMock).toHaveBeenNthCalledWith(1, 'ui:zoomBrowserPage', 'out') + expect(rendererSendMock).toHaveBeenNthCalledWith(2, 'ui:zoomBrowserPage', 'in') + }) + + it('does not double-forward ctrl wheel when Electron also emits a native zoom command', () => { + const guest = makeGuest() + + setupGuestMouseWheelZoomForwarding({ + browserTabId, + guest, + resolveRenderer: () => makeRenderer() + }) + setupGuestShortcutForwarding({ + browserTabId, + guest, + resolveRenderer: () => makeRenderer() + }) + + const wheelPreventDefault = triggerBeforeMouse({ + type: 'mouseWheel', + x: 0, + y: 0, + deltaY: -120, + modifiers: ['ctrl'] + } as Electron.MouseWheelInputEvent) + const zoomCommandPreventDefault = triggerZoomChanged('in') + + expect(wheelPreventDefault).toHaveBeenCalledTimes(1) + expect(zoomCommandPreventDefault).toHaveBeenCalledTimes(1) + expect(rendererSendMock).toHaveBeenCalledTimes(1) + expect(rendererSendMock).toHaveBeenCalledWith('ui:zoomBrowserPage', 'in') + }) + + it('ignores native guest zoom commands when matching default zoom keys are unbound', () => { + setupGuestShortcutForwarding({ + browserTabId, + guest: makeGuest(), + resolveRenderer: () => makeRenderer(), + getKeybindings: () => ({ + 'zoom.in': ['Mod+Alt+Z'], + 'zoom.out': [] + }) + }) + + const zoomOutPreventDefault = triggerZoomChanged('out') + const zoomInPreventDefault = triggerZoomChanged('in') + + expect(zoomOutPreventDefault).not.toHaveBeenCalled() + expect(zoomInPreventDefault).not.toHaveBeenCalled() + expect(rendererSendMock).not.toHaveBeenCalled() }) it('forwards double-tap window shortcuts from focused guest pages', () => { diff --git a/src/main/browser/browser-guest-ui.ts b/src/main/browser/browser-guest-ui.ts index 469803cc85f..ecfdae2e62b 100644 --- a/src/main/browser/browser-guest-ui.ts +++ b/src/main/browser/browser-guest-ui.ts @@ -11,6 +11,7 @@ import { import { isRecentTabSwitcherCommitRelease, matchesRecentTabSwitcherChord, + nativeZoomCommandMatchesKeybindings, resolveWindowShortcutAction, type WindowShortcutInput } from '../../shared/window-shortcut-policy' @@ -29,6 +30,38 @@ type IsMobileEmulatorEnabled = () => boolean const CONTROL_MODIFIERS = new Set(['control', 'ctrl']) const MAC_COMMAND_MODIFIERS = new Set(['meta', 'command', 'cmd']) const WHEEL_ZOOM_BLOCKING_MODIFIERS = new Set(['alt', 'shift']) +const GUEST_WHEEL_ZOOM_DEDUPE_MS = 250 + +type GuestWheelZoomDirection = Exclude + +const recentGuestWheelZoomByGuest = new WeakMap< + Electron.WebContents, + { direction: GuestWheelZoomDirection; at: number } +>() + +function markGuestWheelZoom(guest: Electron.WebContents, direction: GuestWheelZoomDirection): void { + recentGuestWheelZoomByGuest.set(guest, { direction, at: Date.now() }) +} + +function consumeRecentGuestWheelZoom( + guest: Electron.WebContents, + direction: GuestWheelZoomDirection +): boolean { + const recent = recentGuestWheelZoomByGuest.get(guest) + if (!recent) { + return false + } + const elapsed = Date.now() - recent.at + if (elapsed < 0 || elapsed > GUEST_WHEEL_ZOOM_DEDUPE_MS) { + recentGuestWheelZoomByGuest.delete(guest) + return false + } + if (recent.direction !== direction) { + return false + } + recentGuestWheelZoomByGuest.delete(guest) + return true +} function hasModifier(mouse: Electron.MouseInputEvent, modifiers: ReadonlySet): boolean { return mouse.modifiers?.some((modifier) => modifiers.has(modifier)) ?? false @@ -37,7 +70,7 @@ function hasModifier(mouse: Electron.MouseInputEvent, modifiers: ReadonlySet doubleTapDetector.reset() type GuestShortcutInput = WindowShortcutInput & { isAutoRepeat?: boolean } + const forwardBrowserPageZoom = ( + event: Electron.Event, + direction: BrowserPageZoomDirection + ): void => { + event.preventDefault() + const renderer = resolveRenderer(browserTabId) + renderer?.send('ui:zoomBrowserPage', direction) + } + const forwardShortcutInput = ( event: Electron.Event, input: GuestShortcutInput, @@ -284,11 +326,9 @@ export function setupGuestShortcutForwarding(args: { ): boolean => { const keybindings = getKeybindings?.() if (action?.type === 'zoom') { - // Why: keyboard zoom is Orca chrome zoom. Focused guests bypass the - // main-window shortcut path, so forward to the shared renderer zoom router. - event.preventDefault() - const renderer = resolveRenderer(browserTabId) - renderer?.send('terminal:zoom', action.direction) + // Why: focused browser guests own page zoom, but their key events never + // reach the renderer-owned webview ref that can apply Orca's page zoom. + forwardBrowserPageZoom(event, action.direction) return true } if (input.isAutoRepeat) { @@ -521,11 +561,32 @@ export function setupGuestShortcutForwarding(args: { forwardShortcutInput(event, input, action) } + const zoomCommandHandler = ( + event: Electron.Event, + zoomDirection: 'in' | 'out' | 'reset' + ): void => { + if (zoomDirection !== 'in' && zoomDirection !== 'out') { + return + } + // Why: some keyboard layouts/platforms turn Ctrl/Cmd +/- into Electron's + // native zoom command before `before-input-event` reaches the guest. + if (consumeRecentGuestWheelZoom(guest, zoomDirection)) { + event.preventDefault() + return + } + if (!nativeZoomCommandMatchesKeybindings(zoomDirection, process.platform, getKeybindings?.())) { + return + } + forwardBrowserPageZoom(event, zoomDirection) + } + guest.on('before-input-event', handler) + guest.on('zoom-changed', zoomCommandHandler) guest.on('blur', resetDoubleTapDetector) return () => { try { guest.off('before-input-event', handler) + guest.off('zoom-changed', zoomCommandHandler) guest.off('blur', resetDoubleTapDetector) } catch { // Why: best-effort — guest may already be destroyed during teardown. @@ -547,6 +608,7 @@ export function setupGuestMouseWheelZoomForwarding(args: { // Why: wheel input over a focused webview does not reach renderer DOM // handlers, so consume it here and forward to the existing page-zoom path. event.preventDefault() + markGuestWheelZoom(guest, direction) resolveRenderer(browserTabId)?.send('ui:zoomBrowserPage', direction) } diff --git a/src/main/window/createMainWindow.ts b/src/main/window/createMainWindow.ts index 0d6a89be6c0..44b246e8980 100644 --- a/src/main/window/createMainWindow.ts +++ b/src/main/window/createMainWindow.ts @@ -30,6 +30,7 @@ import { import { getWindowShortcutActionId, matchesRecentTabSwitcherChord, + nativeZoomCommandMatchesKeybindings, resolveWindowShortcutAction, windowShortcutActionCapturesTerminal, type WindowShortcutAction @@ -39,7 +40,6 @@ import { toModifierDoubleTapEvent } from '../../shared/modifier-double-tap-detector' import { - keybindingMatchesAction, normalizeTerminalShortcutPolicy, type KeybindingMatchOptions, type KeybindingOverrides @@ -66,38 +66,6 @@ function forceRepaint(window: BrowserWindow): void { }, 32) } -function nativeZoomCommandMatchesKeybindings( - direction: 'in' | 'out', - platform: NodeJS.Platform, - keybindings?: KeybindingOverrides, - options: KeybindingMatchOptions = {} -): boolean { - const primary = - platform === 'darwin' ? { meta: true, control: false } : { meta: false, control: true } - const actionId = direction === 'in' ? 'zoom.in' : 'zoom.out' - const candidates = - direction === 'in' - ? [ - { key: '=', code: 'Equal', shift: false }, - { key: '+', code: 'Equal', shift: true }, - { key: 'Add', code: 'NumpadAdd', shift: false } - ] - : [ - { key: '-', code: 'Minus', shift: false }, - { key: 'Subtract', code: 'NumpadSubtract', shift: false } - ] - - return candidates.some((candidate) => - keybindingMatchesAction( - actionId, - { ...primary, alt: false, ...candidate }, - platform, - keybindings, - options - ) - ) -} - function isMacAppPasteInput(input: Electron.Input): boolean { return ( process.platform === 'darwin' && diff --git a/src/shared/window-shortcut-policy.ts b/src/shared/window-shortcut-policy.ts index e56c0f596e3..0099c7e88a0 100644 --- a/src/shared/window-shortcut-policy.ts +++ b/src/shared/window-shortcut-policy.ts @@ -133,6 +133,38 @@ function actionMatches( return keybindingMatchesAction(actionId, input, platform, keybindings, options) } +export function nativeZoomCommandMatchesKeybindings( + direction: 'in' | 'out', + platform: NodeJS.Platform, + keybindings?: KeybindingOverrides, + options: KeybindingMatchOptions = {} +): boolean { + const primary = + platform === 'darwin' ? { meta: true, control: false } : { meta: false, control: true } + const actionId = direction === 'in' ? 'zoom.in' : 'zoom.out' + const candidates = + direction === 'in' + ? [ + { key: '=', code: 'Equal', shift: false }, + { key: '+', code: 'Equal', shift: true }, + { key: 'Add', code: 'NumpadAdd', shift: false } + ] + : [ + { key: '-', code: 'Minus', shift: false }, + { key: 'Subtract', code: 'NumpadSubtract', shift: false } + ] + + return candidates.some((candidate) => + keybindingMatchesAction( + actionId, + { ...primary, alt: false, ...candidate }, + platform, + keybindings, + options + ) + ) +} + export function resolveWindowShortcutAction( input: WindowShortcutInput, platform: NodeJS.Platform, From dc1c1e9fddae80464af997cd5a692281abd7cdbd Mon Sep 17 00:00:00 2001 From: rubin Date: Sat, 4 Jul 2026 23:20:58 +0800 Subject: [PATCH 17/45] Honor agent settings for bare CLI terminal launches (#6750) Bare repo-backed terminal creates now reuse Settings agent defaults for exact agent commands while preserving raw commands, disabled agents, and already-managed launches. Validation: - PR Checks / verify passed - Local runtime tests, typecheck, lint, CLI build, and Electron CLI/runtime smoke passed --- src/main/runtime/orca-runtime.test.ts | 219 ++++++++++++++- src/main/runtime/orca-runtime.ts | 251 +++++++++++++----- .../startup/desktop-startup-ordering.test.ts | 5 +- 3 files changed, 401 insertions(+), 74 deletions(-) diff --git a/src/main/runtime/orca-runtime.test.ts b/src/main/runtime/orca-runtime.test.ts index 0b410939aba..a005e79a814 100644 --- a/src/main/runtime/orca-runtime.test.ts +++ b/src/main/runtime/orca-runtime.test.ts @@ -176,6 +176,9 @@ const { listGitHubAssignableUsersMock, detectInstalledAgentsWithShellPathHydrationMock, detectRemoteAgentsMock, + markCodexProjectTrustedMock, + markCopilotFolderTrustedMock, + markCursorWorkspaceTrustedMock, listGitLabMergeRequestsMock, listGitLabWorkItemsMock, listGitLabIssuesMock, @@ -267,6 +270,9 @@ const { listGitHubAssignableUsersMock: vi.fn(), detectInstalledAgentsWithShellPathHydrationMock: vi.fn(), detectRemoteAgentsMock: vi.fn(), + markCodexProjectTrustedMock: vi.fn(), + markCopilotFolderTrustedMock: vi.fn(), + markCursorWorkspaceTrustedMock: vi.fn(), listGitLabMergeRequestsMock: vi.fn(), listGitLabWorkItemsMock: vi.fn(), listGitLabIssuesMock: vi.fn(), @@ -333,6 +339,12 @@ vi.mock('../ipc/preflight', () => ({ detectRemoteAgents: detectRemoteAgentsMock })) +vi.mock('../agent-trust-presets', () => ({ + markCodexProjectTrusted: markCodexProjectTrustedMock, + markCopilotFolderTrusted: markCopilotFolderTrustedMock, + markCursorWorkspaceTrusted: markCursorWorkspaceTrustedMock +})) + vi.mock('../hooks', () => ({ buildPosixRunnerScript: (script: string) => `#!/usr/bin/env bash\nset -e\n${script}\n`, buildWindowsRunnerScript: (script: string) => `@echo off\r\n${script}\r\n`, @@ -641,6 +653,9 @@ function resetRuntimeTestMocks(): void { detectInstalledAgentsWithShellPathHydrationMock.mockResolvedValue([]) detectRemoteAgentsMock.mockReset() detectRemoteAgentsMock.mockResolvedValue([]) + markCodexProjectTrustedMock.mockReset() + markCopilotFolderTrustedMock.mockReset() + markCursorWorkspaceTrustedMock.mockReset() listGitLabMergeRequestsMock.mockReset() listGitLabMergeRequestsMock.mockResolvedValue({ items: [] }) listGitLabWorkItemsMock.mockReset() @@ -7028,6 +7043,204 @@ describe('OrcaRuntimeService', () => { }) }) + it('applies Settings agent defaults to bare agent command terminal creates', async () => { + const spawn = vi.fn().mockResolvedValue({ id: 'pty-bg' }) + const runtimeStore = { + ...store, + getSettings: () => ({ + ...store.getSettings(), + disabledTuiAgents: [], + agentCmdOverrides: {}, + agentDefaultArgs: { codex: '--dangerously-bypass-approvals-and-sandbox' }, + agentDefaultEnv: { codex: { CODEX_PROFILE: 'captured' } } + }) + } + const runtime = new OrcaRuntimeService(runtimeStore) + runtime.setPtyController({ + spawn, + write: () => true, + kill: () => true, + getForegroundProcess: async () => null + }) + + await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { + command: 'codex', + title: 'worker' + }) + + const spawnCall = spawn.mock.calls[0]?.[0] as + | { command?: string; env?: Record } + | undefined + expect(spawnCall?.command).toBe("codex '--dangerously-bypass-approvals-and-sandbox'") + expect(spawnCall?.env).toMatchObject({ + CODEX_PROFILE: 'captured', + ORCA_WORKTREE_ID: TEST_WORKTREE_ID + }) + expect(spawnCall?.env?.ORCA_AGENT_LAUNCH_TOKEN).toMatch(UUID_RE) + expect(markCodexProjectTrustedMock).toHaveBeenCalledWith(TEST_WORKTREE_PATH) + expect(markCodexProjectTrustedMock.mock.invocationCallOrder[0]).toBeLessThan( + spawn.mock.invocationCallOrder[0]! + ) + }) + + it('matches canonical bare agent commands when a command override is configured', async () => { + const spawn = vi.fn().mockResolvedValue({ id: 'pty-bg' }) + const runtimeStore = { + ...store, + getSettings: () => ({ + ...store.getSettings(), + disabledTuiAgents: [], + agentCmdOverrides: { codex: 'codex --profile work' }, + agentDefaultArgs: { codex: '--dangerously-bypass-approvals-and-sandbox' }, + agentDefaultEnv: {} + }) + } + const runtime = new OrcaRuntimeService(runtimeStore) + runtime.setPtyController({ + spawn, + write: () => true, + kill: () => true, + getForegroundProcess: async () => null + }) + + await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { + command: 'codex' + }) + + const spawnCall = spawn.mock.calls[0]?.[0] as { command?: string } | undefined + expect(spawnCall?.command).toBe( + "codex --profile work '--dangerously-bypass-approvals-and-sandbox'" + ) + }) + + it('keeps non-bare agent command terminal creates unchanged', async () => { + const spawn = vi.fn().mockResolvedValue({ id: 'pty-bg' }) + const runtimeStore = { + ...store, + getSettings: () => ({ + ...store.getSettings(), + disabledTuiAgents: [], + agentCmdOverrides: {}, + agentDefaultArgs: { codex: '--dangerously-bypass-approvals-and-sandbox' }, + agentDefaultEnv: {} + }) + } + const runtime = new OrcaRuntimeService(runtimeStore) + runtime.setPtyController({ + spawn, + write: () => true, + kill: () => true, + getForegroundProcess: async () => null + }) + + await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { + command: 'codex exec summarize' + }) + + const spawnCall = spawn.mock.calls[0]?.[0] as + | { command?: string; env?: Record } + | undefined + expect(spawnCall?.command).toBe('codex exec summarize') + expect(spawnCall?.env?.ORCA_AGENT_LAUNCH_TOKEN).toBeUndefined() + }) + + it('keeps disabled bare agent command terminal creates unchanged', async () => { + const spawn = vi.fn().mockResolvedValue({ id: 'pty-bg' }) + const runtimeStore = { + ...store, + getSettings: () => ({ + ...store.getSettings(), + disabledTuiAgents: ['codex' as const], + agentCmdOverrides: {}, + agentDefaultArgs: { codex: '--dangerously-bypass-approvals-and-sandbox' }, + agentDefaultEnv: {} + }) + } + const runtime = new OrcaRuntimeService(runtimeStore) + runtime.setPtyController({ + spawn, + write: () => true, + kill: () => true, + getForegroundProcess: async () => null + }) + + await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { + command: 'codex' + }) + + const spawnCall = spawn.mock.calls[0]?.[0] as + | { command?: string; env?: Record } + | undefined + expect(spawnCall?.command).toBe('codex') + expect(spawnCall?.env?.ORCA_AGENT_LAUNCH_TOKEN).toBeUndefined() + }) + + it('sends Settings agent defaults through renderer-backed bare agent terminal creates', async () => { + const runtimeStore = { + ...store, + getSettings: () => ({ + ...store.getSettings(), + disabledTuiAgents: [], + agentCmdOverrides: {}, + agentDefaultArgs: { codex: '--dangerously-bypass-approvals-and-sandbox' }, + agentDefaultEnv: { codex: { CODEX_PROFILE: 'captured' } } + }) + } + const runtime = new OrcaRuntimeService(runtimeStore) + + const webContents = { send: vi.fn() } + webContents.send.mockImplementation((_channel: string, payload: { requestId: string }) => { + runtime.syncWindowGraph(1, { + tabs: [], + leaves: [ + { + tabId: 'tab-renderer', + worktreeId: TEST_WORKTREE_ID, + leafId: 'pane:1', + paneRuntimeId: 1, + ptyId: 'pty-renderer', + paneTitle: null + } + ] + }) + ipcMain.emit( + 'terminal:tabCreateReply', + { sender: webContents }, + { requestId: payload.requestId, tabId: 'tab-renderer', title: 'Codex' } + ) + }) + runtime.attachWindow(1) + runtime.syncWindowGraph(1, { tabs: [], leaves: [] }) + electronMocks.BrowserWindow.fromId.mockReturnValue({ + isDestroyed: () => false, + webContents + }) + + await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { + command: 'codex', + rendererBacked: true + }) + + expect(webContents.send).toHaveBeenCalledWith( + 'terminal:requestTabCreate', + expect.objectContaining({ + worktreeId: TEST_WORKTREE_ID, + command: "codex '--dangerously-bypass-approvals-and-sandbox'", + env: { CODEX_PROFILE: 'captured' }, + launchAgent: 'codex', + launchConfig: { + agentCommand: "codex '--dangerously-bypass-approvals-and-sandbox'", + agentArgs: '--dangerously-bypass-approvals-and-sandbox', + agentEnv: { CODEX_PROFILE: 'captured' } + } + }) + ) + expect(markCodexProjectTrustedMock).toHaveBeenCalledWith(TEST_WORKTREE_PATH) + expect(markCodexProjectTrustedMock.mock.invocationCallOrder[0]).toBeLessThan( + webContents.send.mock.invocationCallOrder[0]! + ) + }) + it('injects runtime hook receiver env into terminal sessions', async () => { const spawn = vi.fn().mockResolvedValue({ id: 'pty-hooked' }) const runtime = new OrcaRuntimeService(store, undefined, { @@ -7243,7 +7456,7 @@ describe('OrcaRuntimeService', () => { expect(compoundClaude.env?.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS).toBeUndefined() expect(compoundClaude.env?.TMUX).toBeUndefined() - expect(normalAgent.command).toBe('codex') + expect(normalAgent.command).toBe("codex '--dangerously-bypass-approvals-and-sandbox'") expect(normalAgent.env?.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS).toBeUndefined() expect(normalAgent.env?.TMUX).toBeUndefined() }) @@ -7602,7 +7815,7 @@ describe('OrcaRuntimeService', () => { }) expect(spawn).toHaveBeenCalledWith( expect.objectContaining({ - command: 'codex', + command: "codex '--dangerously-bypass-approvals-and-sandbox'", cwd: TEST_WORKTREE_PATH, worktreeId: TEST_WORKTREE_ID }) @@ -7671,7 +7884,7 @@ describe('OrcaRuntimeService', () => { expect.objectContaining({ requestId: expect.any(String), worktreeId: TEST_WORKTREE_ID, - command: 'codex', + command: "codex '--dangerously-bypass-approvals-and-sandbox'", title: 'Renderer Terminal' }) ) diff --git a/src/main/runtime/orca-runtime.ts b/src/main/runtime/orca-runtime.ts index cc3e26e590c..21623519884 100644 --- a/src/main/runtime/orca-runtime.ts +++ b/src/main/runtime/orca-runtime.ts @@ -186,7 +186,11 @@ import { resolveTuiAgentLaunchArgs, resolveTuiAgentLaunchEnv } from '../../shared/tui-agent-launch-defaults' -import { isTuiAgent, TUI_AGENT_CONFIG } from '../../shared/tui-agent-config' +import { + getTuiAgentLaunchCommand, + isTuiAgent, + TUI_AGENT_CONFIG +} from '../../shared/tui-agent-config' import { createDraftPasteReadyScanner } from '../../shared/draft-paste-ready-scanner' import { detectInstalledAgentsWithShellPathHydration, detectRemoteAgents } from '../ipc/preflight' import { @@ -944,6 +948,32 @@ type RuntimePtyWorktreeRecord = { waitBlockedAt: number | null } +type TerminalCreateOptions = { + command?: string + claudeAgentTeamsSourceCommand?: string + cwd?: string + env?: Record + launchConfig?: WorktreeStartupLaunch['launchConfig'] + launchToken?: string + launchAgent?: TuiAgent + startupCommandDelivery?: WorktreeStartupLaunch['startupCommandDelivery'] + telemetry?: WorktreeStartupLaunch['telemetry'] + title?: string + focus?: boolean + rendererBacked?: boolean + activate?: boolean + presentation?: RuntimeTerminalPresentation + tabId?: string + leafId?: string + sessionId?: string + persistHostSessionBinding?: boolean + // Why: the headless mobile-session create publishes its own authoritative + // snapshot (with the correct target group) right after spawn. Skip the + // intermediate pty-backed publish so the new tab doesn't briefly flash in + // the wrong (active) group before the corrected snapshot lands. + deferMobileSessionPublish?: boolean +} + type PtyForegroundAgentRefresh = { promise: Promise startedAfterTitleObservation: number @@ -960,6 +990,44 @@ function copySleepingAgentLaunchConfig( } } +function normalizeAgentLaunchCommandForMatch(command: string): string { + return command.trim().replace(/\s+/g, ' ') +} + +function resolveBareAgentLaunchCommand(args: { + command: string | undefined + settings: { + agentCmdOverrides?: Partial> | null + disabledTuiAgents?: Iterable | null + } + platform: NodeJS.Platform + isRemote: boolean +}): TuiAgent | null { + const command = args.command ? normalizeAgentLaunchCommandForMatch(args.command) : '' + if (!command) { + return null + } + + const cmdOverrides = args.settings.agentCmdOverrides ?? {} + for (const agent of Object.keys(TUI_AGENT_CONFIG) as TuiAgent[]) { + if (!isTuiAgentEnabled(agent, args.settings.disabledTuiAgents)) { + continue + } + const override = cmdOverrides[agent]?.trim() + const defaultLaunchCommand = getTuiAgentLaunchCommand(TUI_AGENT_CONFIG[agent], args.platform, { + isRemote: args.isRemote + }) + const launchCommands = override ? [defaultLaunchCommand, override] : [defaultLaunchCommand] + if ( + launchCommands.some((candidate) => command === normalizeAgentLaunchCommandForMatch(candidate)) + ) { + return agent + } + } + + return null +} + function inferCapturedClaudeAgentTeamsMode( launchConfig: SleepingAgentLaunchConfig | undefined, command: string | undefined, @@ -15445,33 +15513,71 @@ export class OrcaRuntimeService { return { handle, tabId: leaf.tabId, title } } + private async resolveAgentTerminalCreateOptions( + workspace: TerminalWorkspaceLaunchScope, + opts: TerminalCreateOptions + ): Promise { + // Why: raw shell commands like `codex exec` must remain user-authored shell. + // Only unmanaged, repo-backed, bare agent launches get Settings defaults. + if ( + !opts.command || + opts.env || + opts.launchConfig || + opts.launchAgent || + opts.startupCommandDelivery || + opts.claudeAgentTeamsSourceCommand || + !workspace.repo || + !this.store + ) { + return opts + } + + const settings = this.store.getSettings() + const platform = this.getAgentLaunchPlatformForWorkspace(workspace) + const isRemote = repoIsRemote(workspace.repo) + const agent = resolveBareAgentLaunchCommand({ + command: opts.command, + settings, + platform, + isRemote + }) + if (!agent) { + return opts + } + + const startupPlan = buildAgentStartupPlan({ + agent, + prompt: '', + cmdOverrides: settings.agentCmdOverrides ?? {}, + agentArgs: resolveTuiAgentLaunchArgs(agent, settings.agentDefaultArgs), + agentEnv: resolveTuiAgentLaunchEnv(agent, settings.agentDefaultEnv), + platform, + isRemote, + allowEmptyPromptLaunch: true + }) + if (!startupPlan) { + return opts + } + + if (workspace.connectionId) { + await this.markRemoteWorkspaceTrustedForAgent(agent, workspace.connectionId, workspace.path) + } else { + this.markLocalWorkspaceTrustedForAgent(agent, workspace.path) + } + + return { + ...opts, + command: startupPlan.launchCommand, + ...(startupPlan.env ? { env: startupPlan.env } : {}), + launchConfig: startupPlan.launchConfig, + launchAgent: agent, + startupCommandDelivery: startupPlan.startupCommandDelivery + } + } + async createTerminal( worktreeSelector?: string, - opts: { - command?: string - claudeAgentTeamsSourceCommand?: string - cwd?: string - env?: Record - launchConfig?: WorktreeStartupLaunch['launchConfig'] - launchToken?: string - launchAgent?: TuiAgent - startupCommandDelivery?: WorktreeStartupLaunch['startupCommandDelivery'] - telemetry?: WorktreeStartupLaunch['telemetry'] - title?: string - focus?: boolean - rendererBacked?: boolean - activate?: boolean - presentation?: RuntimeTerminalPresentation - tabId?: string - leafId?: string - sessionId?: string - persistHostSessionBinding?: boolean - // Why: the headless mobile-session create publishes its own authoritative - // snapshot (with the correct target group) right after spawn. Skip the - // intermediate pty-backed publish so the new tab doesn't briefly flash in - // the wrong (active) group before the corrected snapshot lands. - deferMobileSessionPublish?: boolean - } = {} + opts: TerminalCreateOptions = {} ): Promise { const presentation = resolveTerminalPresentation(opts) const requiresRendererFocus = opts.presentation === 'focused' || opts.focus === true @@ -15494,7 +15600,9 @@ export class OrcaRuntimeService { throw new Error('runtime_unavailable') } const workspace = await this.resolveTerminalWorkspaceLaunchScope(worktreeSelector) - const cwd = this.resolveGuardedWorkspaceTerminalCwd(workspace, opts.cwd) ?? workspace.path + const launchOpts = await this.resolveAgentTerminalCreateOptions(workspace, opts) + const cwd = + this.resolveGuardedWorkspaceTerminalCwd(workspace, launchOpts.cwd) ?? workspace.path const preAllocatedHandle = this.createPreAllocatedTerminalHandle() // Why: mint tabId in main before spawn so paneKey is known at PTY env // build time. Hook-based agent status (Claude/Codex/Cursor/Gemini) keys @@ -15502,25 +15610,27 @@ export class OrcaRuntimeService { // hook payload arrives with an empty paneKey and the renderer cannot // attribute the event. Use a stable UUID leaf because hooks reject the // legacy numeric pane keys after the pane-id migration. - const hintedTabId = opts.tabId?.trim() + const hintedTabId = launchOpts.tabId?.trim() const canAdoptPaneIdentity = hintedTabId !== undefined && isValidHostTerminalTabId(hintedTabId) && - opts.leafId !== undefined && - isTerminalLeafId(opts.leafId) + launchOpts.leafId !== undefined && + isTerminalLeafId(launchOpts.leafId) const tabId = canAdoptPaneIdentity ? (hintedTabId as string) : randomUUID() - const leafId = canAdoptPaneIdentity ? (opts.leafId as string) : randomUUID() + const leafId = canAdoptPaneIdentity ? (launchOpts.leafId as string) : randomUUID() const paneKey = makePaneKey(tabId, leafId) - const launchToken = opts.launchConfig ? (opts.launchToken ?? randomUUID()) : undefined + const launchToken = launchOpts.launchConfig + ? (launchOpts.launchToken ?? randomUUID()) + : undefined const baseEnv = { - ...opts.env, + ...launchOpts.env, ...(launchToken ? { ORCA_AGENT_LAUNCH_TOKEN: launchToken } : {}) } const claudeAgentTeamsSourceCommand = - opts.claudeAgentTeamsSourceCommand?.trim() || opts.command?.trim() || undefined + launchOpts.claudeAgentTeamsSourceCommand?.trim() || launchOpts.command?.trim() || undefined const claudeAgentTeamsMode = this.store?.getSettings?.().claudeAgentTeamsMode const effectiveClaudeAgentTeamsMode = inferCapturedClaudeAgentTeamsMode( - opts.launchConfig, + launchOpts.launchConfig, claudeAgentTeamsSourceCommand, claudeAgentTeamsMode ) @@ -15545,25 +15655,25 @@ export class OrcaRuntimeService { const sequencedStartupCommand = agentTeamsPlan && claudeAgentTeamsSourceCommand && - opts.command && - claudeAgentTeamsSourceCommand !== opts.command + launchOpts.command && + claudeAgentTeamsSourceCommand !== launchOpts.command ? agentTeamsPlan.command : undefined const effectiveLaunchConfig = - opts.launchConfig && agentTeamsPlan + launchOpts.launchConfig && agentTeamsPlan ? { - ...opts.launchConfig, - agentCommand: opts.launchConfig.agentCommand + ...launchOpts.launchConfig, + agentCommand: launchOpts.launchConfig.agentCommand ? effectiveClaudeAgentTeamsMode === 'in-process' || process.platform === 'win32' - ? addClaudeTeammateModeInProcess(opts.launchConfig.agentCommand) - : addClaudeTeammateModeAuto(opts.launchConfig.agentCommand) + ? addClaudeTeammateModeInProcess(launchOpts.launchConfig.agentCommand) + : addClaudeTeammateModeAuto(launchOpts.launchConfig.agentCommand) : agentTeamsPlan.command, agentEnv: { - ...opts.launchConfig.agentEnv, + ...launchOpts.launchConfig.agentEnv, ...agentTeamsPlan.env } } - : opts.launchConfig + : launchOpts.launchConfig // Why: setup/agent sequencing wraps the PTY launch in a wait shell before // Claude Agent Teams runs. Preserve the direct Claude command separately // so the wrapper can exec the teammate-mode variant after setup completes. @@ -15583,29 +15693,31 @@ export class OrcaRuntimeService { cols: 120, rows: 40, cwd, - command: sequencedStartupCommand ? opts.command : (agentTeamsPlan?.command ?? opts.command), + command: sequencedStartupCommand + ? launchOpts.command + : (agentTeamsPlan?.command ?? launchOpts.command), commandDelivery: 'provider', - startupCommandDelivery: opts.startupCommandDelivery, + startupCommandDelivery: launchOpts.startupCommandDelivery, env, envToDelete: agentTeamsPlan?.envToDelete, - telemetry: opts.telemetry, + telemetry: launchOpts.telemetry, connectionId: workspace.connectionId, worktreeId: workspace.id, preAllocatedHandle, tabId, leafId, - ...(opts.sessionId ? { sessionId: opts.sessionId } : {}), - ...(opts.persistHostSessionBinding ? { persistHostSessionBinding: true } : {}) + ...(launchOpts.sessionId ? { sessionId: launchOpts.sessionId } : {}), + ...(launchOpts.persistHostSessionBinding ? { persistHostSessionBinding: true } : {}) }) this.registerPreAllocatedHandleForPty(result.id, preAllocatedHandle) this.registerPty(result.id, workspace.id, workspace.connectionId) const pty = this.getOrCreatePtyWorktreeRecord(result.id) if (pty) { - if (opts.title) { + if (launchOpts.title) { const observedAt = this.nextTitleObservationSequence() - pty.title = opts.title + pty.title = launchOpts.title pty.titleUpdatedAt = observedAt - this.setPtyManagementTitleFromObservedTitle(pty, opts.title, observedAt) + this.setPtyManagementTitleFromObservedTitle(pty, launchOpts.title, observedAt) } else { pty.title = null pty.titleUpdatedAt = null @@ -15616,14 +15728,14 @@ export class OrcaRuntimeService { ? copySleepingAgentLaunchConfig(effectiveLaunchConfig) : null pty.launchToken = launchToken ?? null - pty.launchAgent = opts.launchAgent ?? null + pty.launchAgent = launchOpts.launchAgent ?? null } const handle = pty ? this.issuePtyHandle(pty) : preAllocatedHandle - if (pty && opts.deferMobileSessionPublish !== true) { + if (pty && launchOpts.deferMobileSessionPublish !== true) { this.publishPtyBackedMobileSessionTerminal(workspace.id, pty, { tabId, leafId, - title: opts.title ?? null, + title: launchOpts.title ?? null, activate: presentation === 'focused', // Why: explicit background presentation may carry legacy activate // metadata from an already-owned renderer pane; don't select it on mobile. @@ -15641,11 +15753,11 @@ export class OrcaRuntimeService { // already baked into the PTY env — keeps paneKey hook attribution intact. await this.notifier.revealTerminalSession(workspace.id, { ptyId: result.id, - title: opts.title ?? null, + title: launchOpts.title ?? null, ...(cwd !== workspace.path ? { cwd } : {}), ...(effectiveLaunchConfig ? { launchConfig: effectiveLaunchConfig } : {}), ...(launchToken ? { launchToken } : {}), - ...(opts.launchAgent ? { launchAgent: opts.launchAgent } : {}), + ...(launchOpts.launchAgent ? { launchAgent: launchOpts.launchAgent } : {}), activate: presentation === 'focused', ...(presentation ? { presentation } : {}), tabId, @@ -15665,7 +15777,7 @@ export class OrcaRuntimeService { paneKey, ptyId: result.id, worktreeId: workspace.id, - title: opts.title ?? null, + title: launchOpts.title ?? null, surface, ...(warning ? { warning } : {}) } @@ -15678,8 +15790,13 @@ export class OrcaRuntimeService { const workspace = worktreeSelector ? await this.resolveTerminalWorkspaceLaunchScope(worktreeSelector) : null + const launchOpts = workspace + ? await this.resolveAgentTerminalCreateOptions(workspace, opts) + : opts const worktreeId = workspace?.id - const cwd = workspace ? this.resolveGuardedWorkspaceTerminalCwd(workspace, opts.cwd) : opts.cwd + const cwd = workspace + ? this.resolveGuardedWorkspaceTerminalCwd(workspace, launchOpts.cwd) + : launchOpts.cwd const requestId = randomUUID() // Why: terminal creation is a renderer-side Zustand store operation (like @@ -15703,21 +15820,21 @@ export class OrcaRuntimeService { if (r.error) { reject(new Error(r.error)) } else { - resolve({ tabId: r.tabId!, title: r.title ?? opts.title ?? '' }) + resolve({ tabId: r.tabId!, title: r.title ?? launchOpts.title ?? '' }) } } ipcMain.on('terminal:tabCreateReply', handler) win.webContents.send('terminal:requestTabCreate', { requestId, worktreeId, - command: opts.command, + command: launchOpts.command, cwd, - ...(opts.env ? { env: opts.env } : {}), - ...(opts.launchConfig ? { launchConfig: opts.launchConfig } : {}), - ...(opts.launchToken ? { launchToken: opts.launchToken } : {}), - ...(opts.launchAgent ? { launchAgent: opts.launchAgent } : {}), - startupCommandDelivery: opts.startupCommandDelivery, - title: opts.title, + ...(launchOpts.env ? { env: launchOpts.env } : {}), + ...(launchOpts.launchConfig ? { launchConfig: launchOpts.launchConfig } : {}), + ...(launchOpts.launchToken ? { launchToken: launchOpts.launchToken } : {}), + ...(launchOpts.launchAgent ? { launchAgent: launchOpts.launchAgent } : {}), + startupCommandDelivery: launchOpts.startupCommandDelivery, + title: launchOpts.title, activate: presentation === 'focused', ...(presentation ? { presentation } : {}) }) diff --git a/src/main/startup/desktop-startup-ordering.test.ts b/src/main/startup/desktop-startup-ordering.test.ts index 276242040dc..7939af31aae 100644 --- a/src/main/startup/desktop-startup-ordering.test.ts +++ b/src/main/startup/desktop-startup-ordering.test.ts @@ -41,10 +41,7 @@ describe('desktop startup ordering', () => { const runtimeRpcStart = source.indexOf('await runtimeRpc.start()', serveStart) const automationStart = source.indexOf('automations.start()', serveStart) const desktopSetWebContents = source.indexOf('automations.setWebContents(window.webContents)') - const desktopAutomationStart = source.indexOf( - 'automations.start()', - desktopSetWebContents + 1 - ) + const desktopAutomationStart = source.indexOf('automations.start()', desktopSetWebContents + 1) expect(serveStart).toBeGreaterThanOrEqual(0) expect(serveReady).toBeGreaterThan(serveStart) From d7d7067ae122ed99d33ce088a1a0e4fc18f954a2 Mon Sep 17 00:00:00 2001 From: Luis Sebastian Urrutia Fuentes Date: Sat, 4 Jul 2026 20:37:26 +0200 Subject: [PATCH 18/45] chore(i18n): refine Spanish locale copy (#7359) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore(i18n): refine Spanish locale copy Why: Improve the Spanish UI copy so developer-facing terms stay natural and technically accurate. Changes: - Keep core dev terms such as PR, worktree, branch, and runtime consistent. - Preserve locale key parity and placeholder compatibility with English. * chore(i18n): localize new Spanish strings * chore(i18n): use rama for branch in Spanish * chore(i18n): clarify Spanish repository copy * fix(i18n): correct Spanish locale defects found in review Post-review polish on the Spanish copy pass: - Restore dropped/altered meaning: 'repo color' (was 'en el repositorio'), 'uploaded' (was 'sincronizado'), 'is behind {ref}' (was '{ref} atrás', which rendered the ref name as a quantity). - Translate leftover English over prior correct strings: 'Review from'-> 'Revisión de', 'Agents'->'Agentes', 'workspace'->'espacio de trabajo', 'clone'->'clon', 'Skill de Agent'->'Skill de agente'. - Grammar/register: 'para commit'->'para hacer commit' (x3), 'desde issue'-> 'desde el issue' (x4), 'Cuando tanto...y'->'Cuando ambos...', unify tú in the sidebar-tint description, 'obligatorio'->'obligatorias' (agrees with approvals), 'desbloquear'->'descongelar' for 'unfreeze', de-Spanglish the local-folder-drop message. Co-authored-by: Orca --------- Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com> Co-authored-by: Orca --- src/renderer/src/i18n/locales/es.json | 7966 ++++++++++++------------- 1 file changed, 3983 insertions(+), 3983 deletions(-) diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json index dce7125c0c3..2dc3780a125 100644 --- a/src/renderer/src/i18n/locales/es.json +++ b/src/renderer/src/i18n/locales/es.json @@ -4,7 +4,7 @@ "rootTitle": "Orca tuvo un error de renderizado.", "rootDescription": "El shell de la aplicación no pudo terminar de renderizarse. Vuelva a intentar volver a montarlo o reinicie Orca si el error persiste.", "webTitle": "Orca web tuvo un error de renderizado.", - "webDescription": "Vuelva a intentar el cliente web o vuelva a conectarse al tiempo de ejecución emparejado." + "webDescription": "Vuelva a cargar el cliente web o vuelva a conectarse al servidor Orca vinculado." } }, "settings": { @@ -20,14 +20,14 @@ "spanish": "Español" }, "statusBar": { - "claudeToggleDescription": "Muestra el token de Claude y el uso de costos para el espacio de trabajo activo.", - "codexToggleDescription": "Muestra el token Codex y el uso de costos para el espacio de trabajo activo.", - "geminiToggleDescription": "Muestra el token de Gemini y el uso de costos para el espacio de trabajo activo.", - "opencodeGoToggleDescription": "Muestra el token de OpenCode Go y el uso de costos para el espacio de trabajo activo.", + "claudeToggleDescription": "Muestra el consumo de tokens y el costo de Claude para el espacio de trabajo activo.", + "codexToggleDescription": "Muestra el consumo de tokens y el costo de Codex para el espacio de trabajo activo.", + "geminiToggleDescription": "Muestra el consumo de tokens y el costo de Gemini para el espacio de trabajo activo.", + "opencodeGoToggleDescription": "Muestra el consumo de tokens y el costo de OpenCode Go para el espacio de trabajo activo.", "kimiToggleDescription": "Muestra el uso de la suscripción de Kimi para el espacio de trabajo activo.", "sshToggleDescription": "Muestra hosts SSH configurados y hosts remotos de Orca cuando haya alguno disponible.", - "resourceUsageToggleDescription": "Muestra el Administrador de recursos. Haga clic en él para ver la CPU, la memoria, las sesiones, los controles del demonio y los análisis del disco del espacio de trabajo.", - "portsToggleDescription": "Mostrar puertos del espacio de trabajo en vivo. Haga clic en él para puertos con ámbito de espacio de trabajo y oyentes externos." + "resourceUsageToggleDescription": "Muestra el Administrador de recursos. Haz clic para ver CPU, memoria, sesiones, controles del servicio en segundo plano y análisis de disco de los espacios de trabajo.", + "portsToggleDescription": "Muestra los puertos activos de los espacios de trabajo. Haz clic para ver los puertos de cada espacio de trabajo y los puertos externos." } } }, @@ -35,10 +35,10 @@ "checkForUpdates": "Buscar actualizaciones...", "settings": "Ajustes", "exploreOrca": "Explorar Orca", - "gettingStarted": "Empezando con Orca", + "gettingStarted": "Primeros pasos con Orca", "reportCrash": "Informar fallo...", "file": "Archivo", - "exit": "Salida", + "exit": "Salir", "edit": "Editar", "appearance": "Apariencia", "toggleLeftSidebar": "Alternar barra lateral izquierda", @@ -52,12 +52,12 @@ "reload": "Recargar", "forceReload": "Forzar recarga", "resetSize": "Restablecer tamaño", - "zoomIn": "Dar un golpe de zoom", + "zoomIn": "Acercar", "zoomOut": "Alejar", - "openWorktreePalette": "Abrir paleta de árbol de trabajo", + "openWorktreePalette": "Abrir paleta de worktrees", "window": "Ventana", "help": "Ayuda", - "paste": "Paste" + "paste": "Pegar" }, "tray": { "openOrca": "Abrir Orca", @@ -69,42 +69,42 @@ "worktreeJumpPalette": { "matchLabel": { "comment": "Comentario", - "issue": "Asunto", + "issue": "Issue", "port": "Puerto", "pr": "PR" } }, "auto": { "App": { - "221a95ba38": "Vuelva a intentar la incorporación o ciérrela y continúe en la aplicación.", - "f02d37278a": "La incorporación produjo un error.", - "acd66311dc": "Utilice el menú Ayuda después de volver a intentarlo si aún necesita diagnóstico.", - "722d03aa62": "El cuadro de diálogo del informe de fallos generó un error.", - "8a023cea1f": "Vuelva a intentar la barra de estado para volver a montar sus controles.", - "2e8ff36f94": "La barra de estado encontró un error.", - "7cbfbf622f": "Vuelva a intentar el espacio de trabajo flotante o ciérrelo y vuelva a abrirlo.", - "1b3024bcd6": "Se produjo un error en el espacio de trabajo flotante.", - "8d1e160ed1": "Vuelva a intentar la barra lateral o cambie de pestaña para recargar esta superficie.", - "ed6b168d00": "La barra lateral derecha encontró un error.", - "03a14f6b5b": "Vuelva a intentar la página o navegue a otra superficie de Orca.", + "221a95ba38": "Vuelve a intentar la configuración inicial o ciérrala y continúa en la aplicación.", + "f02d37278a": "La configuración inicial tuvo un error.", + "acd66311dc": "Si todavía necesitas diagnóstico después de reintentarlo, usa el menú Ayuda.", + "722d03aa62": "La ventana de informe de fallos tuvo un error.", + "8a023cea1f": "Vuelve a intentarlo para recargar los controles de la barra de estado.", + "2e8ff36f94": "La barra de estado tuvo un error.", + "7cbfbf622f": "Vuelve a intentarlo, o cierra y vuelve a abrir el espacio de trabajo flotante.", + "1b3024bcd6": "El espacio de trabajo flotante tuvo un error.", + "8d1e160ed1": "Vuelve a intentarlo o cambia de pestaña para recargar la barra lateral.", + "ed6b168d00": "La barra lateral derecha tuvo un error.", + "03a14f6b5b": "Vuelve a intentarlo con la página o navega a otra sección de Orca.", "b7a714db1e": "Esta página tuvo un error.", - "98d4ea2823": "Error al renderizar el terminal, el navegador o el editor en este espacio de trabajo. Vuelva a intentar montarlo.", - "5a9519aef0": "Se produjo un error en el banco de trabajo del espacio de trabajo.", - "cba0fafda5": "La página activa permanece abierta. Vuelva a intentar la lista o cambie de vista.", - "1468601e7b": "Se produjo un error en la lista de espacios de trabajo.", - "bdc71dddc9": "El espacio de trabajo activo permanece abierto. Vuelva a intentar la lista o cambie de vista.", + "98d4ea2823": "No se pudo mostrar el terminal, el navegador o el editor en este espacio de trabajo. Vuelve a intentarlo para recargarlo.", + "5a9519aef0": "El panel principal del espacio de trabajo tuvo un error.", + "cba0fafda5": "La página activa sigue abierta. Vuelve a intentarlo con la lista o cambia de vista.", + "1468601e7b": "La lista de espacios de trabajo tuvo un error.", + "bdc71dddc9": "El espacio de trabajo activo sigue abierto. Vuelve a intentarlo con la lista o cambia de vista.", "c1cf0b0e4a": "Contraer panel", - "8504ddf267": "La aplicación todavía se está ejecutando. Vuelva a intentar el shell o utilice el menú para informar los detalles del fallo.", - "df1d56bf87": "El shell del espacio de trabajo encontró un error.", + "8504ddf267": "La aplicación sigue ejecutándose. Vuelve a intentar cargar el espacio de trabajo o usa el menú para informar los detalles del fallo.", + "df1d56bf87": "La vista del espacio de trabajo tuvo un error.", "9e0b441a91": "Alternar barra lateral derecha", "e81217c1b7": "Ocultar nombre de la aplicación", "5096cbbc86": "Orca", "8b0b8eb54f": "Menú de aplicaciones", "caea5b51b9": "Reiniciar ahora", - "0a9e810705": "Los cambios no se guardarán hasta que se reinicie. Tus pestañas anteriores están seguras en el disco.", + "0a9e810705": "Hasta que reinicies, los cambios no se guardarán. Tus pestañas anteriores siguen guardadas en disco.", "12e77cf12b": "Error al restaurar la sesión", "332dbfa497": "Espacio de trabajo subido", - "e960d18540": "Cerca", + "e960d18540": "Cerrar", "c9d6f98459": "Maximizar", "66f0a552e5": "Restaurar", "bbb7f90669": "Minimizar", @@ -117,7 +117,7 @@ "ca6c6eece7": "habilidades", "1b9d9d065f": "ajustes", "c184e056de": "Alternar barra lateral derecha ({{value0}})", - "f7aa73e785": "Seguir adelante ({{value0}})", + "f7aa73e785": "Avanzar ({{value0}})", "cf9099fe98": "Avanzar", "fe21e8f6f5": "Volver ({{value0}})", "064bd07810": "Volver", @@ -127,34 +127,34 @@ "web": { "WebConnect": { "b411ec0069": "Conectar", - "2cf9e5a294": "Borrar servidor guardado", - "4a4c017be1": "Punto final:", + "2cf9e5a294": "Eliminar servidor guardado", + "4a4c017be1": "Dirección de conexión:", "27393856e4": "orca://pair?code=...", "7a566540de": "URL o código de emparejamiento", "cb4d287238": "Nombre del servidor", - "3affe7de3a": "Pegue una URL de emparejamiento de un servidor Orca al que este navegador pueda acceder.", - "e3bcd082ac": "Conéctate a Orca", - "mobileScopeRejected": "Este código QR concede acceso limitado (móvil). Para usar la app web completa, abre el enlace del navegador desde Ajustes → Servidores remotos de Orca → Comparte este servidor Orca → Nuevo enlace." + "3affe7de3a": "Pega una URL de emparejamiento de un servidor Orca accesible desde este navegador.", + "e3bcd082ac": "Conectar con Orca", + "mobileScopeRejected": "Este código QR solo da acceso limitado para móvil. Para usar la app web completa, abre el enlace para navegador desde Ajustes → Servidores remotos de Orca → Comparte este servidor Orca → Nuevo enlace." }, "web": { "preload": { "api": { - "31bfe8ae1a": "No disponible en el cliente web.", + "31bfe8ae1a": "No está disponible en el cliente web.", "67ec964791": "La importación de cookies no está disponible en el cliente web.", - "275a776357": "La extracción al pasar el mouse no está disponible en el cliente web.", - "8dfcb7a351": "Las capturas de pantalla de selección no están disponibles en el cliente web.", - "31bea294d5": "El modo Grab no está disponible en el cliente web.", - "b8a1618172": "La generación de detalles de solicitudes de extracción no está disponible en el cliente web.", - "e57c82d276": "La Commit del descubrimiento del modelo de mensaje no está disponible en el cliente web.", - "9fc90740b6": "La generación de mensajes de Commit no está disponible en el cliente web.", + "275a776357": "La extracción al pasar el cursor no está disponible en el cliente web.", + "8dfcb7a351": "Las capturas de pantalla de una selección no están disponibles en el cliente web.", + "31bea294d5": "El modo de captura no está disponible en el cliente web.", + "b8a1618172": "La generación de detalles de pull requests no está disponible en el cliente web.", + "e57c82d276": "La detección del modelo para mensajes de commit no está disponible en el cliente web.", + "9fc90740b6": "La generación de mensajes de commit no está disponible en el cliente web.", "52bee9d8a0": "Se ignoraron los atajos personalizados en conflicto: {{value0}}.", "32f15bdb0f": "Se ignoró la plataforma desconocida \"{{value0}}\".", - "0a69fcd8bc": "plataformas debe ser un objeto con secciones darwin, linux o win32.", - "10898045f3": "Se ignoró el acceso directo para \"{{value0}}\": utilice una matriz de cadenas.", - "36761d9604": "Se ignoró la acción de combinación de teclas desconocida \"{{value0}}\".", + "0a69fcd8bc": "platforms debe ser un objeto con secciones darwin, linux o win32.", + "10898045f3": "Se ignoró el atajo para \"{{value0}}\": usa un array de strings.", + "36761d9604": "Se ignoró la acción de atajo desconocida \"{{value0}}\".", "d2e43e426a": "{{value0}} debe ser un objeto.", - "fb290366b2": "No disponible en la web.", - "76122208ca": "Se ignoró el acceso directo para \"{{value0}}\": {{value1}}" + "fb290366b2": "No está disponible en la web.", + "76122208ca": "Se ignoró el atajo para \"{{value0}}\": {{value1}}" } }, "runtime": { @@ -171,30 +171,30 @@ "08fc23631d": "Navegador" }, "editor": { - "dcb521ed29": "Este archivo se encuentra en estado de conflicto, pero no hay ningún archivo de árbol de trabajo disponible para editar.", - "51f15c37d3": "No se puede abrir el directorio: {{value0}}", - "f2e00db373": "Archivo no encontrado: {{value0}}", - "checkRunDetailsUnavailable": "No hay detalles disponibles para esta comprobación.", - "checkRunDetailsLoadFailed": "No se pudieron cargar los detalles de la comprobación." + "dcb521ed29": "Este archivo está en conflicto, pero no hay una copia editable en el worktree.", + "51f15c37d3": "No se puede abrir la carpeta: {{value0}}", + "f2e00db373": "No se encontró el archivo: {{value0}}", + "checkRunDetailsUnavailable": "No hay detalles disponibles para este check.", + "checkRunDetailsLoadFailed": "No se pudieron cargar los detalles del check." }, "github": { - "f129c42773": "GitHub no devolvió el nuevo comentario.", - "683a21264b": "La fila no tiene propietario/repo/número.", - "83f9b126ad": "El tipo de problema solo se puede configurar en problemas.", - "f963485d37": "Fila no encontrada", - "a967f23983": "Vista del proyecto no cargada", - "87020f6605": "La fila no tiene propietario/repo/número: no se puede parchear el elemento subyacente", - "d49ef4b944": "No se pudo guardar la preferencia de fuente del problema" + "f129c42773": "GitHub no devolvió el comentario nuevo.", + "683a21264b": "La fila no tiene owner/repo/number.", + "83f9b126ad": "El campo Issue Type solo se puede configurar en issues.", + "f963485d37": "No se encontró la fila", + "a967f23983": "La vista del proyecto no está cargada", + "87020f6605": "La fila no tiene owner/repo/number; no se puede actualizar el elemento asociado", + "d49ef4b944": "No se pudo guardar la preferencia de origen de issues" }, "sparse": { "presets": { - "ef13e994e6": "Los ajustes preestablecidos deben cargarse antes de guardarlos.", - "6ed7d6010a": "No se pudo eliminar el ajuste preestablecido", - "ee434d7941": "Preestablecido eliminado", - "c96b770172": "No se pudo guardar el ajuste preestablecido", - "811be06b57": "No se pudo actualizar el ajuste preestablecido", - "0696d13e56": "Preajuste guardado", - "e10f097822": "Preestablecido actualizado" + "ef13e994e6": "Los presets deben cargarse antes de guardarlos.", + "6ed7d6010a": "No se pudo eliminar el preset", + "ee434d7941": "Preset eliminado", + "c96b770172": "No se pudo guardar el preset", + "811be06b57": "No se pudo actualizar el preset", + "0696d13e56": "Preset guardado", + "e10f097822": "Preset actualizado" } }, "store": { @@ -211,39 +211,39 @@ }, "worktrees": { "5a58e03a26": "Se eliminó \"{{value0}}\".", - "d1d78a7baa": "Git no pudo eliminar de forma segura la rama \"{{value0}}\"{{value1}}, por lo que Orca la mantuvo para evitar perder commits locales.", - "4e6496f3d2": "{{value0}} eliminado, rama mantenida", + "d1d78a7baa": "Git no pudo eliminar de forma segura la rama \"{{value0}}\"{{value1}}, así que Orca la mantuvo para evitar perder commits locales.", + "4e6496f3d2": "{{value0}} eliminado, rama conservada", "e50495aae6": "Forzar eliminación de rama", - "889487d8bb": "Despedir", - "f4503ca505": "Abra Configuración > Git e inténtelo de nuevo.", - "34a03a6565": "Mantenga {{value0}} actualizado", - "fa9299a66f": "Su nuevo árbol de trabajo está actualizado, pero el {{value0}} local está {{value1}} {{value2}} atrasado. Las diferencias de IA pueden pasar por alto las commits recientes.", - "14bc053a47": "El local {{value0}} no se actualizó", - "4a18052018": "El local {{value0}} está detrás de {{value1}}", - "903b51c2ed": "Espacio de trabajo creado a partir de {{value0}}, pero Orca no pudo adelantar el {{value1}} local porque {{value2}}", + "889487d8bb": "Descartar", + "f4503ca505": "Abre Ajustes > Git e inténtalo de nuevo.", + "34a03a6565": "Mantener {{value0}} actualizado", + "fa9299a66f": "Tu nuevo worktree está actualizado, pero el {{value0}} local está {{value1}} {{value2}} por detrás. Los diffs de IA pueden omitir commits recientes.", + "14bc053a47": "No se actualizó el {{value0}} local", + "4a18052018": "El {{value0}} local está {{value1}} por detrás", + "903b51c2ed": "Espacio de trabajo creado desde {{value0}}, pero Orca no pudo hacer fast-forward del {{value1}} local porque {{value2}}", "0216895fb5": "No se pudo eliminar la rama", - "19db0085fb": "Sucursal local eliminada", + "19db0085fb": "Rama local eliminada", "2b0afc7f14": "No se pudo mantener actualizado el {{value0}} local", - "670864ab52": "Mantener actualizado el {{value0}} local", + "670864ab52": "Manteniendo actualizado el {{value0}} local", "5366d13eec": "Espacio de trabajo eliminado, rama conservada", - "2e17f825d4": "Árbol de trabajo eliminado, rama conservada", - "78e08cd877": "Git no pudo eliminar de forma segura la rama \"{{value0}}\", por lo que Orca la conservó para evitar perder commits locales.", - "3b57982bf6": "Git no pudo eliminar de forma segura la rama \"{{value0}}\" después de eliminar el espacio de trabajo \"{{value1}}\", por lo 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 árbol de trabajo \"{{value1}}\", por lo que Orca la conservó para evitar perder commits locales.", + "2e17f825d4": "Worktree eliminado, rama conservada", + "78e08cd877": "Git no pudo eliminar de forma segura la rama \"{{value0}}\", así que Orca la conservó para evitar perder commits locales.", + "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." }, "repos": { - "2975400634": "Actualice el servidor Orca para abrir carpetas que no sean Git en este tiempo de ejecución.", + "2975400634": "Actualiza el servidor Orca para abrir carpetas que no sean Git en este host.", "b7e14472ae": "No se pudo agregar la carpeta", - "e649269645": "Utilice una ruta de servidor para agregar proyectos desde un tiempo de ejecución remoto.", + "e649269645": "Usa Agregar proyecto para ingresar una ruta en el host seleccionado.", "c6e022ddfc": "No se pudo agregar el proyecto", "90d129b48b": "Carpeta agregada", "8bb3ad7935": "Proyecto agregado", "a8e4b3af5b": "Proyecto ya agregado", "6d3318e813": "No se pudieron importar repositorios", - "3be0f7df04": "No se puede abrir la carpeta en el tiempo de ejecución seleccionado", - "15cf5319ec": "{{path}} fue verificado en {{hostName}}, pero ese host no informó una carpeta utilizable." + "3be0f7df04": "No se puede abrir la carpeta en el host seleccionado", + "15cf5319ec": "{{path}} se verificó en {{hostName}}, pero ese host no reportó ninguna carpeta utilizable." }, "settings": { "e12dab333b": "No se pudo cambiar de servidor", @@ -302,12 +302,12 @@ "skill": { "cli": { "prerequisite": { - "79371593b0": "Orca CLI aún no es visible en PATH", + "79371593b0": "Orca CLI aún no está visible en PATH", "e99d7dc36f": "El registro de Orca CLI necesita atención", "2db0bd7515": "El registro de Orca CLI no está disponible", "8d6eedf97e": "No se pudo registrar Orca CLI en PATH.", - "0f116999f1": "Reinicie su shell o agregue el directorio CLI de Orca a PATH antes de la instalación.", - "15cbedc3e3": "Instale la CLI de Orca antes de ejecutar la configuración de habilidades del agente." + "0f116999f1": "Reinicia tu shell o agrega el directorio de Orca CLI a PATH antes de la configuración.", + "15cbedc3e3": "Instala la CLI de Orca antes de configurar skills para agentes." } } } @@ -323,14 +323,14 @@ "checks": { "agent": { "launch": { - "027228a06b": "No se puede encontrar un espacio de trabajo para estas comprobaciones.", + "027228a06b": "No se encontró un espacio de trabajo para estos checks.", "fb6c294e85": "No se pudo generar el comando de inicio del agente.", - "03c1d61f83": "No se puede abrir el espacio de trabajo adjunto a estos controles.", + "03c1d61f83": "No se puede abrir el espacio de trabajo adjunto a estos checks.", "822bf52295": "No se puede resolver la plataforma de inicio del espacio de trabajo.", - "dfb4dd7c00": "No se puede encontrar el espacio de trabajo adjunto a estos controles.", - "9f00d7df0c": "El mensaje de corrección de comprobaciones está vacío. Actualice la configuración de AI de control de fuente.", + "dfb4dd7c00": "No se encontró el espacio de trabajo adjunto a estos checks.", + "9f00d7df0c": "El prompt de corrección de checks está vacío. Actualiza los ajustes de Source Control AI.", "2ebf794906": "No se detectó ningún agente de IA habilitado en este host de espacio de trabajo.", - "4c7f783a7a": "El agente de cheques guardados no está disponible en este host de espacio de trabajo." + "4c7f783a7a": "El agente guardado para checks no está disponible en este host de espacio de trabajo." } } } @@ -350,13 +350,13 @@ "new": { "tab": { "11cce5cc77": "No se pudo iniciar {{value0}} en una nueva terminal.", - "a5a1f7033f": "Su {{value0}} no fue enviado; péguelo una vez que el agente esté listo." + "a5a1f7033f": "No se envió tu {{value0}}; pégalo cuando el agente esté listo." } } }, "background": { "session": { - "4ca0651d56": "Su mensaje de automatización no fue enviado: abra el espacio de trabajo y péguelo." + "4ca0651d56": "No se envió tu prompt de automatización: abre el espacio de trabajo y pégalo." } } }, @@ -371,11 +371,11 @@ "item": { "direct": { "3de6371df3": "No se pudo generar el comando de inicio del agente.", - "67e103dd60": "Espacio de trabajo creado pero no se pudo activar.", + "67e103dd60": "Se creó el espacio de trabajo, pero no se pudo activar.", "19c7683acf": "El agente seleccionado no está disponible en el espacio de trabajo creado.", - "8bc45efdbc": "No se pudo resolver el jefe de PR.", + "8bc45efdbc": "No se pudo resolver el head del PR.", "agent": { - "ceeeb509b5": "El Agent tardó demasiado en comenzar. El espacio de trabajo está listo: pegue {{value0}} cuando el Agent esté inactivo." + "ceeeb509b5": "El agente tardó demasiado en iniciar. El espacio de trabajo está listo: pega {{value0}} cuando el agente esté inactivo." } } } @@ -385,7 +385,7 @@ "path": { "open": { "guard": { - "edc1908653": "No está disponible abrir rutas remotas en el sistema operativo local." + "edc1908653": "No se pueden abrir rutas remotas en el sistema operativo local." } } } @@ -403,7 +403,7 @@ "mobile": { "emulator": { "tab": { - "bf4f2a8a72": "No se pudo iniciar el emulador. Verifique la configuración del emulador de iOS o Android y pruebe con otro dispositivo." + "bf4f2a8a72": "No se pudo iniciar el emulador. Revisa la configuración del emulador de iOS o Android y prueba con otro dispositivo." } } } @@ -411,10 +411,10 @@ "orchestration": { "usage": { "examples": { - "f91fe27f2a": "Dividir un cambio grande en RP más pequeños", - "9e37a5b1b3": "Ejecutar trabajo independiente en paralelo.", - "bddc4c09b8": "Ejecute un flujo de trabajo por fases", - "ab0e9803b7": "Entregar a otro árbol de trabajo", + "f91fe27f2a": "Dividir un cambio grande en PRs más pequeños", + "9e37a5b1b3": "Ejecutar trabajo independiente en paralelo", + "bddc4c09b8": "Ejecutar un flujo de trabajo por fases", + "ab0e9803b7": "Entregar a otro worktree", "5e0d489fe1": "Entregar una tarea activa" } } @@ -422,9 +422,9 @@ "pr": { "comment": { "audience": { - "64deee36a9": "robots", + "64deee36a9": "Bots", "a7150a17bc": "Humanos", - "27ce73211c": "Todo", + "27ce73211c": "Todos", "empty": { "bot": "Sin comentarios de robots.", "human": "Sin comentarios humanos.", @@ -449,9 +449,9 @@ "plan": { "3f0ea9aa0d": "No se pudo generar el comando de inicio del agente.", "46f1a2c9bd": "La entrada del comando está vacía.", - "8eb541cc83": "El agente seleccionado no fue detectado en este host de espacio de trabajo.", - "b96e091fc9": "El agente seleccionado está deshabilitado en Configuración.", - "a7ac8717c7": "Elija un agente antes de comenzar." + "8eb541cc83": "No se detectó el agente seleccionado en este host de espacio de trabajo.", + "b96e091fc9": "El agente seleccionado está deshabilitado en Ajustes.", + "a7ac8717c7": "Elige un agente antes de comenzar." } } }, @@ -465,8 +465,8 @@ "sparse": { "preset": { "draft": { - "5915a0a1f6": "Utilice directorios relativos al repo, no raíz, rutas absolutas ni segmentos principales.", - "efc05d1820": "Agregue al menos un directorio." + "5915a0a1f6": "Usa directorios relativos al repositorio, no la raíz, rutas absolutas ni segmentos padre.", + "efc05d1820": "Agrega al menos un directorio." } } }, @@ -475,8 +475,8 @@ "capture": { "notification": { "b0536028c9": "Abrir atajos", - "141ad6c004": "Acceso directo a terminal manejado", - "0ab0cd001a": "tamaño-4 texto-silenciado-primer plano" + "141ad6c004": "Atajo de terminal gestionado", + "0ab0cd001a": "size-4 text-muted-foreground" } } } @@ -486,7 +486,7 @@ "error": { "format": { "37cf0bc991": "Orca no pudo resolver una referencia base utilizable para este espacio de trabajo.", - "64555d0014": "No se encontró ninguna sucursal base" + "64555d0014": "No se encontró ninguna rama base" } } } @@ -494,7 +494,7 @@ "worktree": { "palette": { "search": { - "9ccec2316b": "Asunto", + "9ccec2316b": "Issue", "ca40ffcbec": "PR", "0b01ff98d2": "Puerto", "7d732521ec": "Comentario" @@ -506,41 +506,41 @@ "missing": "Carpeta no encontrada", "notDirectory": "La ruta no es una carpeta", "ambiguousConnection": "No se puede determinar la conexión", - "unavailable": "No se puede comprobar la carpeta" + "unavailable": "No se puede verificar la carpeta" }, "description": { "missing": "Orca no puede encontrar {{path}}. Elimina y vuelve a importar este espacio de trabajo de carpeta.", "notDirectory": "{{path}} existe, pero no es una carpeta.", - "ambiguousConnection": "Orca no puede determinar qué conexión SSH pertenece a este ámbito de carpeta.", - "unavailable": "Orca no puede verificar esta carpeta ahora mismo. Revisa el runtime o la conexión SSH e inténtalo de nuevo." + "ambiguousConnection": "Orca no puede determinar a qué conexión SSH pertenece este alcance de carpeta.", + "unavailable": "Orca no puede verificar esta carpeta ahora mismo. Revisa el host o la conexión SSH e inténtalo de nuevo." }, "createError": { "title": { "missing": "Carpeta no encontrada", "notDirectory": "La ruta no es una carpeta", "ambiguousConnection": "No se puede determinar la conexión", - "unavailable": "No se puede comprobar la carpeta", + "unavailable": "No se puede verificar la carpeta", "generic": "No se pudo crear el espacio de trabajo de carpeta" }, "description": { "missing": "Orca no puede encontrar {{path}}. Elimina y vuelve a importar la carpeta.", "notDirectory": "{{path}} existe, pero no es una carpeta.", - "ambiguousConnection": "Orca no puede determinar qué conexión SSH pertenece a este ámbito de carpeta.", - "unavailable": "Orca no puede verificar esta carpeta ahora mismo. Revisa el runtime o la conexión SSH e inténtalo de nuevo." + "ambiguousConnection": "Orca no puede determinar a qué conexión SSH pertenece este alcance de carpeta.", + "unavailable": "Orca no puede verificar esta carpeta ahora mismo. Revisa el host o la conexión SSH e inténtalo de nuevo." } } }, "projectSkillRuntime": { - "wslUnavailable": "El runtime del proyecto necesita WSL antes de instalar esta habilidad.", - "distroRequired": "Selecciona una distribución WSL para este proyecto antes de instalar esta habilidad.", - "distroMissing": "La distribución WSL seleccionada no está disponible. Elige una disponible o cambia este proyecto a Windows.", - "wslDefault": "Predeterminado de WSL" + "wslUnavailable": "El entorno del proyecto necesita WSL antes de instalar esta skill.", + "distroRequired": "Selecciona una distro WSL para este proyecto antes de instalar esta skill.", + "distroMissing": "La distro WSL seleccionada no está disponible. Elige una disponible o cambia este proyecto a Windows.", + "wslDefault": "WSL predeterminado" }, "sidebarWorktreeActivation": { - "wakeEphemeralVmFailed": "Failed to wake ephemeral VM workspace" + "wakeEphemeralVmFailed": "No se pudo reactivar el espacio de trabajo de la VM efímera" }, "ephemeralVmWorkspaceTarget": { - "projectRootRegistrationFailed": "Failed to register the recipe-created project root on the runtime." + "projectRootRegistrationFailed": "No se pudo registrar en el host la raíz del proyecto creada por la receta." } }, "hooks": { @@ -551,23 +551,23 @@ "3ad7d77f57": "El espacio de trabajo de destino está en un host distinto al destino de ejecución de esta automatización." }, "useComposerState": { - "7eb3f44ff7": "El agente seleccionado está deshabilitado. Elija un agente habilitado antes de crear.", - "b2ead86962": "No se pudo resolver la base de PR.", + "7eb3f44ff7": "El agente seleccionado está deshabilitado. Elige un agente habilitado antes de crear.", + "b2ead86962": "No se pudo resolver la base del PR.", "a9ff236145": "Algunos archivos adjuntos no se pudieron cargar.", "3db83fc58a": "No hay ninguna ruta de proyecto remoto disponible para los archivos adjuntos.", "ba6cb77082": "No se pudo conectar al proyecto.", "chooseOrAddProjectBeforeWorkspace": "Elige o agrega un proyecto antes de crear un espacio de trabajo.", "folderWorkspaceCreateFailedTitle": "No se pudo crear el espacio de trabajo de carpeta", "folderWorkspaceCreateFailedMessage": "No se pudo crear el espacio de trabajo de carpeta. Revisa los detalles del error de arriba e inténtalo de nuevo.", - "setupAgentStartupPolicySaveFailed": "Failed to save setup startup behavior.", - "5f3d2c8a1b": "Failed to resolve MR base." + "setupAgentStartupPolicySaveFailed": "No se pudo guardar el comportamiento de inicio del setup.", + "5f3d2c8a1b": "No se pudo resolver la base del MR." }, "useGlobalFileDrop": { - "38c9f034ff": "No se pudieron cargar los archivos eliminados.", - "d720e2f855": "Algunos archivos caídos no se pudieron cargar.", - "245faa95b9": "No hay ninguna ruta de espacio de trabajo remoto disponible para los archivos descartados.", + "38c9f034ff": "No se pudieron cargar los archivos soltados.", + "d720e2f855": "Algunos archivos soltados no se pudieron cargar.", + "245faa95b9": "No hay ninguna ruta de espacio de trabajo remoto disponible para los archivos soltados.", "nativeDropTooManyPathsDescription": "Suelta {{value0}} archivos o menos a la vez.", - "nativeDropTooManyPaths": "La suelta contiene demasiados archivos.", + "nativeDropTooManyPaths": "Has soltado demasiados archivos.", "nativeDropPathsTooLargeDescription": "Suelta menos archivos o usa una lista de rutas más corta.", "nativeDropPathsTooLarge": "La lista de rutas soltadas es demasiado grande." }, @@ -575,32 +575,32 @@ "0e3cf53060": "Pestaña del navegador {{value0}} no encontrada", "2f6637fe6c": "La pestaña del navegador {{value0}} está fijada", "a8d2bf8e9e": "No hay ninguna pestaña activa del navegador para cerrar", - "291c8ed902": "Las pestañas del navegador no están disponibles mientras un tiempo de ejecución remoto está activo", - "f45fa2b03c": "Los perfiles del navegador no están disponibles mientras un tiempo de ejecución remoto está activo", - "f000b2ff76": "Ningún árbol de trabajo activo", - "56d3ec4203": "No se pudo crear el archivo de markdowns sin título.", + "291c8ed902": "Las pestañas del navegador no están disponibles mientras haya un host remoto activo", + "f45fa2b03c": "Los perfiles del navegador no están disponibles mientras haya un host remoto activo", + "f000b2ff76": "No hay ningún worktree activo", + "56d3ec4203": "No se pudo crear el archivo Markdown sin título.", "f6300deb8b": "Nueva pestaña del navegador", - "7a64b31991": "La creación de terminal local no está disponible mientras un tiempo de ejecución remoto está activo", - "60428567b4": "La revelación de terminal local no está disponible mientras un tiempo de ejecución remoto está activo", + "7a64b31991": "La creación de terminales locales no está disponible mientras haya un host remoto activo", + "60428567b4": "Mostrar terminales locales no está disponible mientras haya un host remoto activo", "f8aaf2bde3": "Espacio de trabajo subido", "2fe88c2e06": "La sincronización del espacio de trabajo remoto no está disponible", "2ec42e1c52": "Aún no hay espacio de trabajo remoto", - "88214a785b": "La sincronización del espacio de trabajo esperó a que se hidratara la sesión local y se agotó el tiempo de espera", + "88214a785b": "La sincronización del espacio de trabajo esperó a que la sesión local terminara de cargarse y agotó el tiempo de espera", "4f78ba5885": "Espacio de trabajo sincronizado" }, "useSettingsNavigationMetadata": { - "4a728cd56b": "Nuevas características que aún están tomando forma. Pruébalos.", + "4a728cd56b": "Nuevas funciones que aún están tomando forma. Pruébalas.", "225071c560": "Experimental", - "e338c507c1": "Configuraciones de compatibilidad de bajo nivel para solucionar problemas.", + "e338c507c1": "Ajustes de compatibilidad de bajo nivel para solucionar problemas.", "580a04cd81": "Avanzado", "8400cfe1c1": "Datos de uso anónimos y controles de telemetría.", "3618579df6": "Privacidad y telemetría", - "65ec7d1968": "Acceso a la privacidad de macOS para herramientas de desarrollo lanzadas en terminales.", + "65ec7d1968": "Permisos de privacidad de macOS para herramientas de desarrollo lanzadas desde terminales.", "d91ae31fbd": "Permisos de macOS", - "95a1886d94": "Controla terminales y agents desde tu teléfono.", + "95a1886d94": "Controla terminales y agentes desde tu teléfono.", "1cd25673df": "Móvil", "31e57d1c70": "Usa máquinas existentes por SSH para archivos, terminales, Git y espacios de trabajo.", - "94a5afe910": "Anfitriones SSH", + "94a5afe910": "Hosts SSH", "40d80bad8a": "Beta", "de0c2907a1": "Servidores remotos de Orca", "b351014180": "Estadísticas de Orca más análisis de uso de Claude, Codex y OpenCode.", @@ -613,9 +613,9 @@ "0c6ee88a5f": "Entrada y edición", "b11a5a48a2": "Tema, zoom, apariencia de la aplicación y del terminal, barras laterales y barra de estado.", "93d88d20bf": "Apariencia", - "2d0659f6f0": "Terminal global, navegador y pestañas de markdowns.", + "2d0659f6f0": "Terminal global, navegador y pestañas Markdown.", "65b19f5bde": "Espacio de trabajo flotante", - "3d65d3f1b9": "Configure la compatibilidad con el emulador móvil para Orca y agents de codificación.", + "3d65d3f1b9": "Configura la compatibilidad del emulador móvil para Orca y coding agents.", "1e761cff2b": "Emulador móvil", "e815fd01bd": "Página de inicio, enrutamiento de enlaces y cookies de sesión.", "8c197f74a1": "Navegador", @@ -623,33 +623,33 @@ "3fc3db144f": "Comandos rápidos", "c33bfd664c": "Shells, renderizador, sesiones y comportamiento del terminal.", "a9fb10afca": "Terminal", - "5235c215ca": "Elija qué proveedores de tareas aparecen en la página Tareas y en la barra lateral.", + "5235c215ca": "Elige qué proveedores de tareas aparecen en la página Tareas y en la barra lateral.", "85f4fd7710": "Fuentes de tareas", - "ab4b21b58e": "Nomenclatura de sucursales, referencias base, atribución y autor de Git AI.", - "09607cb0fe": "Git y control de código fuente", - "33a5e1d597": "Conecte GitHub, GitLab, Linear y servicios de alojamiento de origen.", + "ab4b21b58e": "Nombres de ramas, refs base, atribución y Git AI Author.", + "09607cb0fe": "Git y Source Control", + "33a5e1d597": "Conecta GitHub, GitLab, Linear y servicios de hosting de código.", "2b043783ef": "Integraciones", - "2cd4ea75da": "Valores predeterminados del espacio de trabajo, configuración y mantenimiento de aplicaciones.", + "2cd4ea75da": "Ajustes predeterminados del espacio de trabajo, configuración de la app y mantenimiento.", "13241992bd": "General", - "724c440e72": "empezando", + "724c440e72": "primeros pasos", "0505d0df29": "empezar con Orca", "ea0b1bc7b8": "guía de configuración", - "17005c73d4": "Abra la lista de verificación de incorporación para conocer los pasos de configuración y hitos.", - "ded9e9032f": "Lista de verificación de incorporación", - "5f32ac08f3": "Complete la lista de verificación de incorporación para los flujos de trabajo principales de Orca.", + "17005c73d4": "Abre la lista de configuración inicial para ver pasos de setup e hitos.", + "ded9e9032f": "Lista de configuración inicial", + "5f32ac08f3": "Completa la lista de configuración inicial para los flujos de trabajo principales de Orca.", "8ac3de82f5": "Dictado local de voz a texto con modelos en el dispositivo.", "6a50cdcd7c": "Voz", - "0059bd17f3": "Permita que los agents controlen cualquier aplicación en su computadora.", - "b35e92364b": "Uso de la computadora", - "cd50cec5d7": "Coordine múltiples agents de codificación a través de Orca.", + "0059bd17f3": "Permite que los agentes controlen cualquier app en tu computadora.", + "b35e92364b": "Computer Use", + "cd50cec5d7": "Coordina múltiples coding agents a través de Orca.", "58a868e8e4": "Orquestación", "7c79d3b7bf": "Opcional", "b1c2f8b0ac": "Cambio de cuenta opcional para Claude, Codex, Gemini y OpenCode Go.", "f70ac54d38": "Cuentas de proveedores de IA", - "4121f7a0a2": "Administre agents de IA, establezca un valor predeterminado y personalice comandos.", + "4121f7a0a2": "Administra agentes de IA, define uno predeterminado y personaliza comandos.", "b49abbd2f7": "Agentes", "dev": "Dev Tools", - "devDescription": "Dev-only tools for exercising UI states.", + "devDescription": "Herramientas solo para dev para probar estados de UI.", "devBadge": "Dev", "devSearchNotificationPlayground": "Notification playground", "devSearchNotificationPlaygroundDescription": "Trigger representative toast and notification UI states.", @@ -668,10 +668,10 @@ }, "components": { "CodexRestartChip": { - "9132779820": "Despedir", - "c72a5fb234": "Reanudar", + "9132779820": "Descartar", + "c72a5fb234": "Reiniciar", "9263e75f49": "Codex está usando la cuenta anterior.", - "a4c8e1b2f7": "Codex aún inició sesión como {{value0}}", + "a4c8e1b2f7": "Codex sigue conectado como {{value0}}", "d3e8a1f4b2": "Cuenta cambiada", "e9b2c7d1a5": "Reinicia esta sesión para usar {{value0}}. Contrae para seguir trabajando con la cuenta actual.", "f1a6c8e3b4": "Contraer" @@ -681,15 +681,15 @@ "94cc673726": "Entiendo", "fc5cc29955": "Optar por no participar", "d1deebb050": "Política de privacidad", - "958d2cc31b": "Los recuentos anónimos de las funciones que utiliza nos ayudan a priorizar qué crear. Sin contenidos de archivos, mensajes, salida de terminal ni nada que lo identifique. Cambie en cualquier momento en Configuración -> Privacidad y telemetría.", - "9784b4d7bc": "Ayúdanos a decidir qué construir a continuación", + "958d2cc31b": "Los recuentos anónimos de las funciones que usas nos ayudan a priorizar qué crear. Sin contenido de archivos, prompts, salida de terminal ni nada que te identifique. Puedes cambiarlo en cualquier momento en Ajustes -> Privacidad y telemetría.", + "9784b4d7bc": "Ayúdanos a decidir qué crear a continuación", "fcbee32f08": "Aviso de telemetría" }, "GitHubItemDialog": { - "3ab6ac0fc8": "Obtenga una vista previa y edite el problema de GitHub seleccionado o la solicitud de extracción.", + "3ab6ac0fc8": "Previsualiza y edita el issue o pull request de GitHub seleccionado.", "3cd5ae5b7b": "No se cambiaron archivos.", "999b5ad7d9": "Archivos", - "4bd1f5b055": "cheques", + "4bd1f5b055": "Checks", "e30a5470c9": "Conversación", "474c59b4b3": "Cerrar · Esc", "45af57999b": "Cerrar vista previa", @@ -698,22 +698,22 @@ "0caac1a18f": "Iniciar espacio de trabajo desde PR", "8223320f8d": "actualizado", "10ef1afb8e": "· actualizado", - "55962099bc": "abrió este problema", - "0ab4664a8b": "Iniciar espacio de trabajo desde el problema", + "55962099bc": "abrió este issue", + "0ab4664a8b": "Iniciar espacio de trabajo desde issue", "36182aa57f": "Iniciar nuevo espacio de trabajo", - "fe6ff12dc2": "Más acciones en el espacio de trabajo de problemas", - "726db41722": "Espacio de trabajo abierto", - "84855fedd0": "Abrir espacio de trabajo adjunto al problema", + "fe6ff12dc2": "Más acciones del espacio de trabajo de issue", + "726db41722": "Abrir espacio de trabajo", + "84855fedd0": "Abrir espacio de trabajo adjunto al issue", "b7bf31b8de": "No se pudo sincronizar el estado visto con GitHub.", - "c0253318d6": "No se puede sincronizar el estado visto para esta solicitud de extracción.", + "c0253318d6": "No se puede sincronizar el estado visto de este pull request.", "5fea151559": "No se pudo copiar el enlace de GitHub", "2e77dc2053": "Enlace de GitHub copiado", - "2ef631437e": "No se puede abrir el espacio de trabajo adjunto a este problema.", + "2ef631437e": "No se puede abrir el espacio de trabajo adjunto a este issue.", "bf43425540": "Comentario", "0a73f59e85": "Enviar comentario", "c5c117270e": "Añade un comentario…", "082515176a": "No se pudo agregar el comentario", - "c6f37a563d": "+ Cesionario", + "c6f37a563d": "+ Asignado", "f41ec96c13": "+ Etiqueta", "ab050dffec": "Cerrado", "dc1ca081a8": "Abierto", @@ -721,9 +721,9 @@ "886a64b081": "Ninguno todavía", "4ba0132f37": "Editar etiquetas", "217e55d87c": "Etiquetas", - "c67de9e2fe": "nadie asignado", + "c67de9e2fe": "Nadie asignado", "76adcf5fe2": "Editar asignados", - "83ac703dda": "Cesionarios", + "83ac703dda": "Asignados", "00ccdf9b5a": "Estado", "2aa9acdf34": "Editar etiquetas en GitHub", "d23bbb6416": "{{value0}} omitido", @@ -731,54 +731,54 @@ "b1ac991806": "{{value0}} fallando", "checksNeedActionChip": "{{value0}} requiere acción", "311d0cee55": "{{value0}} pasando", - "e52bed9264": "Aún no se han reportado controles", - "90020cc1f3": "Esta solicitud de extracción aún no tiene comprobaciones reportadas.", - "ecffebc251": "No se encontraron cheques", + "e52bed9264": "Aún no se reportaron checks", + "90020cc1f3": "Este pull request aún no tiene checks reportados.", + "ecffebc251": "No se encontraron checks", "5dddefdf58": "Abrir en GitHub", - "744197c84d": "No hay salida en línea disponible para esta verificación.", - "08d072664d": "Empleos", + "744197c84d": "No hay output inline disponible para este check.", + "08d072664d": "Jobs", "96d8f36798": "Anotaciones", - "485609c4f2": "controlar #", - "0f478f5efa": "Terminado", - "4812814bc8": "Comenzó", + "485609c4f2": "check #", + "0f478f5efa": "Completado", + "4812814bc8": "Iniciado", "9c3ba11a05": "Estado:", - "934d87ab96": "Cargando detalles del cheque...", - "71c11aff84": "Vuelva a ejecutar todas las comprobaciones", - "e31651a224": "Volver a ejecutar comprobaciones fallidas", - "1b56e28faa": "Repetición", - "f4b1292569": "Inicie el agente de IA predeterminado en estas comprobaciones", - "9a1004fc76": "Actualizar cheques", - "03e542fcfe": "No se pudo iniciar un agente de IA para los cheques rotos: {{value0}}", - "28986b3747": "Inició un agente de inteligencia artificial para los cheques rotos.", - "1690fd7f4a": "No hay cheques rotos que arreglar.", - "9e7c221b8d": "No se pudieron volver a ejecutar las comprobaciones", - "e463ec935f": "Verificar repeticiones solicitadas", - "ddafe851e1": "Se solicita volver a ejecutar el cheque", - "0bbdc673c1": "No se pudieron actualizar las comprobaciones", - "e7007aa1d8": "No se pueden actualizar las comprobaciones sin una ruta al repositorio.", + "934d87ab96": "Cargando detalles del check...", + "71c11aff84": "Volver a ejecutar todos los checks", + "e31651a224": "Volver a ejecutar checks fallidos", + "1b56e28faa": "Volver a ejecutar", + "f4b1292569": "Iniciar el agente de IA predeterminado en estos checks", + "9a1004fc76": "Actualizar checks", + "03e542fcfe": "No se pudo iniciar un agente de IA para los checks fallidos: {{value0}}", + "28986b3747": "Se inició un agente de IA para los checks fallidos.", + "1690fd7f4a": "No hay checks fallidos que arreglar.", + "9e7c221b8d": "No se pudieron volver a ejecutar los checks", + "e463ec935f": "Se solicitó volver a ejecutar los checks", + "ddafe851e1": "Se solicitó volver a ejecutar el check", + "0bbdc673c1": "No se pudieron actualizar los checks", + "e7007aa1d8": "No se pueden actualizar los checks sin una ruta al repositorio.", "675bc0d638": "Cancelar", "a18f669c7a": "{{value0}} {{value1}} reacción{{value2}}", - "53fe19aefc": "Abrir el cuadro de combinación de GitHub", - "a2495e4784": "Solicitud de extracción", - "ce360fc318": "No se pudo deshabilitar la combinación automática", - "825a8fb8cd": "No se pudo habilitar la combinación automática", - "4b390bd50d": "Fusión automática deshabilitada", - "a35ea5a0f6": "Combinación automática habilitada", - "aba792c8b3": "No se pudo fusionar la solicitud de extracción", - "dbe5e2448e": "Solicitud de extracción fusionada", - "a27ee5ca1a": "Esto actualizará la solicitud de extracción en GitHub.", + "53fe19aefc": "Abrir el cuadro de merge de GitHub", + "a2495e4784": "Pull request", + "ce360fc318": "No se pudo deshabilitar auto-merge", + "825a8fb8cd": "No se pudo habilitar auto-merge", + "4b390bd50d": "Auto-merge deshabilitado", + "a35ea5a0f6": "Auto-merge habilitado", + "aba792c8b3": "No se pudo hacer merge del pull request", + "dbe5e2448e": "Pull request mergeado", + "a27ee5ca1a": "Esto actualizará el pull request en GitHub.", "03d7216d62": "{{value0}} PR #{{value1}}?", "e9b7cb7d17": "No se pudo {{value0}} PR", - "bd3b4492a0": "Solicitud de extracción reabierta", - "9f88657c4e": "Solicitud de extracción cerrada", - "b6f1b7adbd": "Esto reabrirá la solicitud de extracción en GitHub.", - "de45fedf7b": "Esto cerrará la solicitud de extracción en GitHub.", + "bd3b4492a0": "Pull request reabierto", + "9f88657c4e": "Pull request cerrado", + "b6f1b7adbd": "Esto reabrirá el pull request en GitHub.", + "de45fedf7b": "Esto cerrará el pull request en GitHub.", "5a94f3d0e9": "Aún no hay comentarios.", "1506916c09": "Comentarios", - "9b9cb55994": "No se proporciona descripción.", + "9b9cb55994": "No se proporcionó descripción.", "52b20b56f7": "Descripción", "4d555d3796": "Editar descripción", - "9df4e74bdf": "Ahorrar", + "9df4e74bdf": "Guardar", "0ae387d8ca": "por", "228e2f59d3": "Resuelto", "a154ec5224": "Abrir comentario en GitHub", @@ -788,62 +788,62 @@ "745c9089ec": "No se puede responder sin una ruta al repositorio.", "58c73cb0d8": "No se pudo actualizar la descripción.", "5221548274": "Descripción actualizada.", - "06c06e58ba": "Mostrar más líneas a continuación", + "06c06e58ba": "Mostrar más líneas abajo", "307c98e8e3": "Mostrar {{value0}} más líneas arriba", "5664681624": "Mostrar más líneas arriba", - "b1574e8ac2": "Restablecer el contexto del código", + "b1574e8ac2": "Restablecer contexto de código", "d43736d09c": "Controles de contexto de código", - "bd7be7b1fd": "comentario l", + "bd7be7b1fd": "comentario L", "db61d76cd5": "Cargando contexto de código...", "f2d02cdf8c": "archivos vistos", "1257d1435d": "Mostrar árbol de archivos", "a341343303": "Se agregó el comentario de revisión.", - "d1fa2cf888": "No se puede comentar sin el jefe de PR SHA.", - "829674460a": "La diferencia no está disponible porque faltan los SHA de commit de PR.", + "d1fa2cf888": "No se puede comentar sin el SHA del head del PR.", + "829674460a": "El diff no está disponible porque faltan los SHA de commits del PR.", "af924014f8": "Visto", - "2d89a38d9d": "{{value0}} {{value1}} tal como se ve", + "2d89a38d9d": "Cambiar estado de visto de {{value1}} ({{value0}})", "70e84e3d0b": "No hay revisores que coincidan.", "1ffce94a8b": "Todos los demás", "c2b21818e1": "Sugerencias", "a98433e73d": "Cargando...", - "b0b7344684": "Solicite hasta 15 revisores", - "934add88b6": "Crítico", + "b0b7344684": "Solicita hasta 15 revisores", + "934add88b6": "Revisor", "bb42774171": "Escribe o elige un usuario", "36f9ac4a47": "No se solicitaron revisores.", "8b15a5e91c": "Eliminar revisor {{value0}}", "6a45771d47": "Cargando revisores", "dc8a092c57": "Revisores", - "e3243d9376": "Editados recientemente estos archivos", + "e3243d9376": "Editó estos archivos recientemente", "8c45901789": "Solicitar revisor {{value0}}", - "fedc09eeb9": "Revisor no solicitado {{value0}}", + "fedc09eeb9": "Cancelar solicitud de revisor {{value0}}", "73487fb975": "No se pudo eliminar al revisor", "2e69540652": "Revisores eliminados", "69515bff81": "Revisor eliminado", - "b4af16bf43": "No hay contexto de repo disponible para esta solicitud de extracción.", + "b4af16bf43": "No hay contexto de repositorio disponible para este pull request.", "c42d942b75": "No se pudo solicitar el revisor", "c016e4bac3": "Revisores solicitados", "ea985e657f": "Revisor solicitado", "12e761610e": "Puedes solicitar hasta 15 revisores", - "94ab23a9f9": "Introduzca un revisor", + "94ab23a9f9": "Ingresa un revisor", "3853476a97": "elemento de GitHub", - "68796dafa0": "pr", + "68796dafa0": "PR", "b1157c78ff": "hoja", "038b3d39b1": "copiado", - "04539beb48": "asunto", + "04539beb48": "issue", "773ff70035": "desconocido", - "3e544d966d": "Asunto", + "3e544d966d": "Issue", "88fd82474d": "página", "e517b4d641": "cerrado", "d0a05e73f5": "compacto", "7d42606f66": "Anotación", - "2511f44bb7": "Arreglar cheques rotos", - "9157d48ddb": "Arreglar cheques", - "06482d6190": "No se puede crear un espacio de trabajo de reparación automáticamente.", + "2511f44bb7": "Arreglar checks fallidos", + "9157d48ddb": "Arreglar checks", + "06482d6190": "No se pudo crear automáticamente un espacio de trabajo de reparación.", "f64dd90102": "Responder", - "5752c25aff": "Destino…", + "5752c25aff": "Publicando…", "ec5c4b3ab2": "Reabrir PR", - "21860b58d0": "Cerrar solicitud de extracción", - "5932578f51": "La fusión requiere un repo local registrado", + "21860b58d0": "Cerrar pull request", + "5932578f51": "El merge requiere un repositorio local registrado", "ce8a85d209": "por defecto", "924c2fe05e": "destructivo", "e2bf3e41a9": "comentario", @@ -853,56 +853,56 @@ "136542c9ba": ":L{{value0}}", "283699bc82": "No se pudo publicar la respuesta.", "d1c0dad471": "-L{{value0}}", - "31770bef03": "Juntos", + "31770bef03": "Lado a lado", "6e43a16435": "En línea", "d00a0a7f8f": "Contraer todo", "3c19ec3069": "Expandir todo", "b0b09778c8": "No se pudo agregar el comentario de revisión.", "16c1abe76c": "Marcar visto", "ba8e329d92": "Desmarcar visto", - "3f79ffc8b7": "Abra los detalles de PR para ver los revisores actuales.", + "3f79ffc8b7": "Abre los detalles del PR para ver los revisores actuales.", "5c1c973855": "Eliminar revisor", "commentTooLarge": "El comentario es demasiado grande para enviarlo de forma segura.", "timeline": { - "completed": "as completed", - "notPlanned": "as not planned", - "someone": "someone", - "assigned": "assigned", - "unassigned": "unassigned", - "mentioned": "mentioned this", - "in": "in", - "closed": "closed this", - "reopened": "reopened this", - "moved": "moved this", - "from": "from", - "to": "to", - "activity": "Activity", - "noActivity": "No activity yet." + "completed": "como completado", + "notPlanned": "como no planificado", + "someone": "alguien", + "assigned": "asignó a", + "unassigned": "quitó a", + "mentioned": "mencionó esto", + "in": "en", + "closed": "cerró esto", + "reopened": "reabrió esto", + "moved": "movió esto", + "from": "de", + "to": "a", + "activity": "Actividad", + "noActivity": "Aún no hay actividad." }, - "checkActionRequiredHint": "This check needs a manual action on GitHub (for example, approving the workflow run) before merging is unblocked." + "checkActionRequiredHint": "Este check requiere una acción manual en GitHub (por ejemplo, aprobar la ejecución del workflow) antes de desbloquear el merge." }, "GitLabItemDialog": { "65e784c1f1": "Reabrir", - "a199eb364b": "Cerca", - "16b3412570": "Unir", + "a199eb364b": "Cerrar", + "16b3412570": "Merge", "131865e231": "Crear espacio de trabajo", "f2e64d1c20": "Abrir en GitLab", - "84012fa8fb": "Comentario", + "84012fa8fb": "Comentar", "c08e1d5a57": "Comentar en {{value0}}{{value1}}…", - "f11e3e7675": "No hay tuberías para este MR.", + "f11e3e7675": "No hay ejecuciones de pipeline para este MR.", "808b1ca1ba": "No hay archivos modificados.", - "007423f585": "Contenido diferencial no disponible.", - "a7eb4f4916": "de", - "21f8dde18a": "comentario en línea", + "007423f585": "El contenido del diff no está disponible.", + "a7eb4f4916": "desde", + "21f8dde18a": "Comentario inline", "7a7204417f": "Línea", "ceb08a733d": "Archivo", "85a8170279": "Aún no hay comentarios.", "14423484db": "Sin descripción.", "da4174b00f": "Editar", - "93f79a3fc1": "Ahorrar", + "93f79a3fc1": "Guardar", "f72fad3b16": "Cancelar", "717b706849": "Cargando etiquetas", - "3c0b6ccca7": "error, backend", + "3c0b6ccca7": "bug, backend", "dde24ade55": "Etiquetas", "908d8d2a73": "Descripción", "89f3f19368": "Título", @@ -910,75 +910,75 @@ "05939e977d": "Agregar revisor", "474b50d988": "Sin revisores.", "1b19cdc510": "Eliminar revisor {{value0}}", - "cb55b0390f": "Administrar", + "cb55b0390f": "Gestionar", "4f9313984d": "Revisores", - "02cbe2de44": "Tubería", + "02cbe2de44": "Pipeline", "be3d291837": "Archivos", "c996e2962c": "Conversación", - "b3c156dd51": "Refrescar", + "b3c156dd51": "Actualizar", "9bfb4a24d7": "por", - "30c97083c2": "Detalle del elemento de trabajo de GitLab", - "e089f62594": "¡MR fusionado! {{value0}}", - "865ea2703e": "Reabierto MR!{{value0}}", - "9b11cd233f": "Cerrado SEÑOR !{{value0}}", - "60c13320c4": "Se agregó comentario en línea", - "ffdd9a78e1": "Las referencias de diferencias de MR no están disponibles para comentarios en línea.", + "30c97083c2": "Detalle del work item de GitLab", + "e089f62594": "MR !{{value0}} mergeado", + "865ea2703e": "MR !{{value0}} reabierto", + "9b11cd233f": "MR !{{value0}} cerrado", + "60c13320c4": "Comentario inline agregado", + "ffdd9a78e1": "Las diff refs del MR no están disponibles para comentarios inline.", "00d0d25825": "Se requieren archivo, línea y comentario.", - "ceaf7c30c7": "La identificación del revisor no está disponible para este usuario de GitLab.", - "f7cb495a12": "Reintentado {{value0}}", - "98718490e4": "Se requiere título de MR.", - "d600c2619a": "Cargando registro", - "028bde664e": "Esconder", - "2f9b27f838": "Registro de trabajo", - "032ae1312b": "Trabajo abierto en GitLab", - "fa3e042203": "Rever", + "ceaf7c30c7": "El ID del revisor no está disponible para este usuario de GitLab.", + "f7cb495a12": "Se reintentó {{value0}}", + "98718490e4": "El título del MR es obligatorio.", + "d600c2619a": "Cargando log", + "028bde664e": "Ocultar", + "2f9b27f838": "Log del job", + "032ae1312b": "Abrir job en GitLab", + "fa3e042203": "Reintentar", "f23ea85341": "resuelto", "4186685c78": "reabrir", - "cae2712a23": "cerca", - "881e522e04": "unir", - "6de8ce0cc6": "{{value0}} requerido", + "cae2712a23": "cerrar", + "881e522e04": "merge", + "6de8ce0cc6": "{{value0}} obligatorias", "22511537d2": "Aprobado", - "00f3bab87b": "de {{value0}} requerido", + "00f3bab87b": " de {{value0}} obligatorias", "11384f99aa": "número", "40c56b95e2": "{{value0}} aprobación{{value1}} restante", - "3a051b8ade": "Elemento de trabajo", - "32f8bef818": "Sin salida de registro.", + "3a051b8ade": "Work item", + "32f8bef818": "Sin output de log.", "4168eb2c51": "Resolver", "commentTooLarge": "El comentario es demasiado grande para enviarlo de forma segura." }, "JiraIssueWorkspace": { - "b0b92666c9": "Comentario", - "a585fd204e": "Añade un comentario de Jira...", + "b0b92666c9": "Comentar", + "a585fd204e": "Agregar un comentario de Jira...", "2441be6f9f": "Iniciar espacio de trabajo", "9178090e26": "Aún no hay comentarios.", - "5cd09beaf9": "Rever", + "5cd09beaf9": "Reintentar", "9a980b06b9": "Comentarios", - "c4889a47e4": "No se proporciona descripción.", - "0f3c07a901": "backend, error", + "c4889a47e4": "No se proporcionó descripción.", + "0f3c07a901": "backend, bug", "aee97b6913": "Etiquetas", "444865b4a8": "Título", - "0b6b5646ed": "No asignado", + "0b6b5646ed": "Sin asignar", "51bed73f88": "Sin prioridad", - "7a96985ca0": "Cerca", - "76513c7898": "Cerrar vista previa del problema de Jira", - "857bd2f88f": "Obtenga una vista previa, edite y comience a trabajar desde el número seleccionado.", - "0cc62bd690": "Copiar mensaje", - "80efa101c5": "Copiar el nombre de la sucursal sugerida", + "7a96985ca0": "Cerrar", + "76513c7898": "Cerrar vista previa del issue de Jira", + "857bd2f88f": "Previsualiza, edita y empieza a trabajar desde el issue seleccionado.", + "0cc62bd690": "Copiar prompt", + "80efa101c5": "Copiar nombre de rama sugerido", "38839801e8": "Copiar clave", "779bb91ee0": "Copiar URL", - "69da9a208c": "Abierto en Jira", + "69da9a208c": "Abrir en Jira", "fa132c8aed": "No se pudo agregar el comentario.", - "ea21952aa3": "No se pudo actualizar el problema de Jira.", + "ea21952aa3": "No se pudo actualizar el issue de Jira.", "6c41a9bcea": "No se pudo copiar {{value0}}", "2ff69a3545": "{{value0}} copiado", "666cfdd835": "Desconocido", "9ebee71962": "etiquetas", "def3d0e824": "título", - "b8e2079d96": "cesionario", - "54649eaeab": "+ Cesionario", + "b8e2079d96": "asignado", + "54649eaeab": "+ Asignado", "2a829a2f00": "prioridad", "693be070d0": "transición", - "ef21405c6d": "Problema con Jira", + "ef21405c6d": "Issue de Jira", "commentTooLarge": "El comentario es demasiado grande para enviarlo de forma segura." }, "Landing": { @@ -988,15 +988,15 @@ "6ca6ff404e": "ORCA", "520304a067": "logotipo de Orca", "ce44fad849": "Dependencias faltantes", - "c1cf168479": "Esconder", - "00cee697c1": "Ejecute \"gh auth login\" en una terminal para conectar su cuenta de GitHub.", + "c1cf168479": "Ocultar", + "00cee697c1": "Ejecuta \"gh auth login\" en una terminal para conectar tu cuenta de GitHub.", "9f96d018b7": "La CLI de GitHub no está autenticada", - "73e1ad4282": "Orca utiliza la CLI de GitHub (gh) para mostrar solicitudes de extracción, problemas y comprobaciones.", + "73e1ad4282": "Orca usa GitHub CLI (gh) para mostrar pull requests, issues y checks.", "5beaef5f9e": "La CLI de GitHub no está instalada", "b673e7cf1b": "Se requiere Git para proyectos Git, control de código fuente y gestión del espacio de trabajo.", - "e5b7296d9d": "git no está instalado", - "cd21242762": "Agregue un proyecto para comenzar.", - "9c00bd4adf": "Seleccione un espacio de trabajo de la barra lateral para comenzar.", + "e5b7296d9d": "Git no está instalado", + "cd21242762": "Agrega un proyecto para comenzar.", + "9c00bd4adf": "Selecciona un espacio de trabajo en la barra lateral para comenzar.", "16e9e3df89": "con estrella", "0d0ace8861": "Dar estrella en GitHub", "ec43b38ba7": "Con estrella en GitHub", @@ -1005,49 +1005,49 @@ }, "LinearIssueMarkdownDescriptionEditor": { "d9c47069ef": "Markdown", - "a7301a11f3": "ahorrar", + "a7301a11f3": "guardar", "632096eb1c": "Enlace", "340160f4e8": "Quitar enlace", "9eaf02ac01": "Cita", "e2a0267c8c": "Lista de verificación", - "d6b2f3d35b": "lista numerada", - "c82917e06e": "lista de viñetas", - "ad1869bd54": "código en línea", - "28fd951b83": "Huelga", - "5666b4493d": "Itálico", - "caa88f50d0": "Atrevido", - "dddaa7a0a6": "Título 2", - "e3f741d258": "Título 1", + "d6b2f3d35b": "Lista numerada", + "c82917e06e": "Lista con viñetas", + "ad1869bd54": "Código inline", + "28fd951b83": "Tachado", + "5666b4493d": "Cursiva", + "caa88f50d0": "Negrita", + "dddaa7a0a6": "Encabezado 2", + "e3f741d258": "Encabezado 1", "68a41d5665": "Texto del cuerpo", - "7c52151156": "Formato de descripción del problema", + "7c52151156": "Formato de descripción del issue", "5c16ec8f14": "URL del enlace", - "4f2fddc2b7": "No se proporciona descripción." + "4f2fddc2b7": "No se proporcionó descripción." }, "LinearIssueTextEditor": { "947ba2d6f4": "guardar", - "04d73b72dc": "Título del problema", + "04d73b72dc": "Título del issue", "e8ff595db3": "No se pudo actualizar {{value0}}", - "1e08a1ec80": "Se requiere título", + "1e08a1ec80": "El título es obligatorio", "00fa439dc7": "título", "75294b07d1": "descripción" }, "LinearIssueWorkspace": { - "ad5dec37b7": "Obtenga una vista previa, edite y comience a trabajar desde el número seleccionado.", - "c23e79e5c0": "Comportamiento", - "b0eac92d85": "Rever", - "fabbd3f974": "actualizó el problema ·", + "ad5dec37b7": "Previsualiza, edita y empieza a trabajar desde el issue seleccionado.", + "c23e79e5c0": "Acciones", + "b0eac92d85": "Reintentar", + "fabbd3f974": "actualizó el issue ·", "543970c87a": "Actividad", - "df4c86ed12": "Cerca", - "7a4997d8bb": "Cerrar vista previa del problema Linear", + "df4c86ed12": "Cerrar", + "7a4997d8bb": "Cerrar vista previa del issue de Linear", "e1e0a9bca9": "Iniciar espacio de trabajo", - "30a7f56c0a": "Iniciar espacio de trabajo desde el problema", + "30a7f56c0a": "Iniciar espacio de trabajo desde el issue", "30c1242f3a": "Copiar identificador", - "9e3c49beb8": "Copiar identificador de problema", + "9e3c49beb8": "Copiar identificador del issue", "9a9a884236": "Copiar URL", - "97c19a84f1": "Copiar URL Linear", - "f63ef94ea8": "Asuntos", - "f6c6381593": "Copiar mensaje", - "5d670ec8dc": "Copiar el nombre de la sucursal sugerida", + "97c19a84f1": "Copiar URL de Linear", + "f63ef94ea8": "Issues", + "f6c6381593": "Copiar prompt", + "5d670ec8dc": "Copiar nombre de rama sugerido", "937ba6ad9a": "Cargando proyectos", "db3f269d98": "Buscar proyectos", "b51276c8d6": "Proyecto", @@ -1055,30 +1055,30 @@ "f9d4ef9807": "Proyecto actualizado", "38b80780c2": "No se pudieron cargar los proyectos", "42589845bc": "Crear", - "c182e02de5": "Título del subtema", - "8c55d6696a": "Agregar subtemas", - "b25e453c9d": "No se pudo crear el subproblema", - "aeed19d003": "Creado {{value0}}", - "9a1317cdd3": "No se pudo cargar el subproblema", + "c182e02de5": "Título del sub-issue", + "8c55d6696a": "Agregar sub-issues", + "b25e453c9d": "No se pudo crear el sub-issue", + "aeed19d003": "Se creó {{value0}}", + "9a1317cdd3": "No se pudo cargar el sub-issue", "9bcbaa2737": "No se pudo copiar {{value0}}", "7835483c43": "{{value0}} copiado", - "61f424f8ca": "Cuestión Linear", + "61f424f8ca": "Issue de Linear", "ca8778c124": "Desconocido", "8a33c85e9c": "Alguien", "f5a6b38a14": "hoja", "65239a714b": "Linear", "af6e02c44a": "página", - "76ffd3c937": "Busque un proyecto para agregar.", + "76ffd3c937": "Busca un proyecto para agregar.", "c11b4e3cc2": "No se encontraron proyectos.", - "519c3587f3": "Añadir al proyecto" + "519c3587f3": "Agregar al proyecto" }, "LinearItemDrawer": { - "04008e6c46": "Iniciar espacio de trabajo desde el problema", + "04008e6c46": "Iniciar espacio de trabajo desde el issue", "a4fcc57522": "Aún no hay comentarios.", "fde849b2b6": "Comentarios", "9dc54172db": "Cerrar · Esc", - "0190b760c1": "Abierto en Linear", - "04a442f796": "Obtenga una vista previa y edite la edición Linear seleccionada.", + "0190b760c1": "Abrir en Linear", + "04a442f796": "Previsualiza y edita el issue de Linear seleccionado.", "d369841269": "Enviar comentario", "2fcff829a8": "Añade un comentario…", "2820f0f0f0": "Deja un comentario...", @@ -1088,31 +1088,31 @@ "23886c7eec": "Agregar etiqueta", "7f7b89b631": "Etiquetas: {{value0}}", "b2376d0179": "Cargando miembros", - "866316f22c": "No asignado", - "b5675b0694": "Ahorrar", - "ceeb8c6153": "Claro", - "fbb90300e2": "Presupuesto personalizado", + "866316f22c": "Sin asignar", + "b5675b0694": "Guardar", + "ceeb8c6153": "Limpiar", + "fbb90300e2": "Estimación personalizada", "780ea6ed89": "No se encontraron estados", - "59b6cd3706": "Estados de carga", + "59b6cd3706": "Cargando estados", "64bfffc4dd": "Etiquetas", "dd304de85a": "Propiedades", "0be31fef8e": "La estimación debe ser un número entero no negativo", "48e17e8cbd": "Desconocido", "858d0630da": "Cerrar vista previa", - "39883467f4": "Cuestión Linear", + "39883467f4": "Issue de Linear", "fda549766e": "{{value0}} para comentar", - "d71cd3003e": "+ Cesionario", + "d71cd3003e": "+ Asignado", "commentTooLarge": "El comentario es demasiado grande para enviarlo de forma segura." }, "NewWorkspaceComposerCard": { "cbb47ee0dc": "Solo disponible para proyectos Git locales.", - "d861de981b": "Pago escaso", - "090cfedeb4": "escribe una nota", + "d861de981b": "Sparse checkout", + "090cfedeb4": "Escribe una nota", "f8728aa4f9": "Nota", "0ee17638fe": "Nombre del espacio de trabajo", "2688050e4b": "Nombre", "f0470c7383": "Avanzado", - "ba64270bdb": "Configurar agents", + "ba64270bdb": "Configurar agentes", "ab63f25397": "Abrir configuración del agente", "01d1e8f601": "Agente", "0c5d6a479c": "[Opcional]", @@ -1122,9 +1122,9 @@ "969a8bff66": "Proyecto", "23bb365554": "orca.yaml", "a239038146": "Error de conexión SSH", - "9a70e4859e": "Elija si desea ejecutar la instalación antes de crear este espacio de trabajo.", - "803b7fe72f": "Comprobando la configuración de instalación...", - "92e34f0311": "configuraciones locales", + "9a70e4859e": "Elige si quieres ejecutar el setup antes de crear este espacio de trabajo.", + "803b7fe72f": "Verificando configuración de setup...", + "92e34f0311": "ajustes locales", "326a578923": "orca.yaml + local", "2132b670da": "ambos", "0e587e31fb": "yaml", @@ -1132,7 +1132,7 @@ "f660aa1454": "Conectando", "7711ad5122": "Comando de configuración local", "e5db1b0419": "Comando de configuración combinado", - "addProjectBeforeWorkspace": "Agregue un proyecto antes de crear un espacio de trabajo.", + "addProjectBeforeWorkspace": "Agrega un proyecto antes de crear un espacio de trabajo.", "connectProjectFirst": "Conecta este proyecto primero", "connectSourceLookup": "Conectar", "reconnectSourceLookup": "Volver a conectar", @@ -1160,22 +1160,22 @@ "importHostSetup": "Importar", "notePasteTooLarge": "El contenido pegado es demasiado grande para el campo de nota.", "reuseExistingBranch": "Reutilizar rama", - "reuseExistingBranchHint": "Hacer checkout de la rama existente en lugar de crear una nueva a partir de ella.", + "reuseExistingBranchHint": "Haz checkout de la rama existente en lugar de crear una nueva a partir de ella.", "createMultiple": "Crear más", - "waitForSetupBeforeAgent": "Espere a que se complete la configuración antes de iniciar el agent", - "waitForSetupBeforeAgentHelp": "Active esto cuando el programa de instalación instale dependencias, servidores MCP o archivos de configuración que el agent necesite durante el inicio.", - "destroyDisabled": "destruir discapacitados", - "destroyConfigured": "destruir configurado", - "noDestroyConfigured": "no destruir", + "waitForSetupBeforeAgent": "Esperar a que termine el setup antes de iniciar el agente", + "waitForSetupBeforeAgentHelp": "Activa esto cuando el setup instale dependencias, servidores MCP o archivos de configuración que el agente necesita al iniciar.", + "destroyDisabled": "destroy deshabilitado", + "destroyConfigured": "destroy configurado", + "noDestroyConfigured": "sin destroy", "ephemeralVm": "Entorno por espacio de trabajo", - "chooseRunTarget": "Elige objetivo", - "noRunTargets": "No hay objetivos de ejecución listos para este proyecto.", - "perWorkspaceEnvHint": "Proporcione un entorno bajo demanda a partir de una receta", + "chooseRunTarget": "Elegir destino", + "noRunTargets": "No hay destinos de ejecución listos para este proyecto.", + "perWorkspaceEnvHint": "Provisiona un entorno bajo demanda a partir de una receta", "branchName": "Nombre de rama", "branchNamePlaceholder": "feature/my-branch" }, "NewWorkspaceComposerModal": { - "createWorktree": "Crear árbol de trabajo", + "createWorktree": "Crear worktree", "createWorkspace": "Crear espacio de trabajo", "fa90f739a5": "Elija el proyecto, el nombre del espacio de trabajo y el agente antes de crear el espacio de trabajo." }, @@ -1184,13 +1184,13 @@ "3450247584": "Comentario", "6ad2c1ab9c": "No se cambiaron archivos.", "4d18310d55": "Archivos cambiados", - "94d95cf1f7": "cheques", + "94d95cf1f7": "Checks", "9e8d45700e": "Conversación", "e6996f4024": "· actualizado", - "00b7b82329": "rama principal", + "00b7b82329": "rama head", "e1f3641bfd": "de", "c44b70352b": "rama base", - "b0e80f083d": "quiere fusionarse en", + "b0e80f083d": "quiere hacer merge en", "8ecda455a0": "Abrir en GitHub", "1a2570e18e": "Iniciar nuevo espacio de trabajo", "57c13a5aa4": "Más acciones en el espacio de trabajo de PR", @@ -1198,15 +1198,15 @@ "a459866967": "Reanudar espacio de trabajo adjunto a PR", "347034903a": "Copiar enlace de GitHub", "5a01ca7253": "No se pudo sincronizar el estado visto con GitHub.", - "996a1897d2": "No se puede sincronizar el estado visto para esta solicitud de extracción.", + "996a1897d2": "No se puede sincronizar el estado visto de este pull request.", "e0b15c793f": "No se pudo copiar el enlace de GitHub", "992e799227": "Enlace de GitHub copiado", - "61bfc81ada": "No se puede abrir el espacio de trabajo adjunto a esta solicitud de extracción.", + "61bfc81ada": "No se puede abrir el espacio de trabajo adjunto a este pull request.", "161d91ef02": "Enviar comentario", "d2030fc8cd": "Añade un comentario…", "1208347ac0": "No se pudo agregar el comentario", - "61452f2143": "Iniciar espacio de trabajo desde el problema", - "14c9fc70ed": "+ Cesionario", + "61452f2143": "Iniciar espacio de trabajo desde el issue", + "14c9fc70ed": "+ Asignado", "bc215fea4d": "+ Etiqueta", "b936cc51a4": "Cerrado", "7b8f6bf6d8": "Abierto", @@ -1215,56 +1215,56 @@ "ae2a34c7b8": "{{value0}} fallando", "checksNeedActionChip": "{{value0}} requiere acción", "7c5035931a": "{{value0}} pasando", - "a18d01cda3": "Aún no se han reportado controles", - "3912daf310": "Esta solicitud de extracción aún no tiene comprobaciones reportadas.", - "45877f5089": "No se encontraron cheques", - "85e62c5266": "Inició un agente de inteligencia artificial para los cheques rotos.", - "ddfd42f460": "Revise el mensaje antes de iniciar un agente.", - "a053bdd082": "Arreglar cheques rotos con IA", + "a18d01cda3": "Aún no se reportaron checks", + "3912daf310": "Este pull request aún no tiene checks reportados.", + "45877f5089": "No se encontraron checks", + "85e62c5266": "Se inició un agente de IA para los checks fallidos.", + "ddfd42f460": "Revisa el prompt antes de iniciar un agente.", + "a053bdd082": "Arreglar checks fallidos con IA", "1b14d0a69c": "Abrir en GitHub", - "1550675e5f": "No hay salida en línea disponible para esta verificación.", - "7720c9c3f5": "Empleos", + "1550675e5f": "No hay output inline disponible para este check.", + "7720c9c3f5": "Jobs", "8432d17901": "Anotaciones", - "f01bf79a79": "controlar #", - "000f90afcf": "Terminado", - "76551b1161": "Comenzó", + "f01bf79a79": "check #", + "000f90afcf": "Completado", + "76551b1161": "Iniciado", "662bc2998d": "Estado:", - "d8e82b7f15": "Cargando detalles del cheque...", - "54cddd1858": "Vuelva a ejecutar todas las comprobaciones", - "68605516dd": "Volver a ejecutar comprobaciones fallidas", - "522d9353e1": "Repetición", - "0fa8b8faec": "Inicie el agente de IA predeterminado en estas comprobaciones", - "5d0f42766d": "Actualizar cheques", - "98583589c6": "No se pudo iniciar un agente de IA para los cheques rotos: {{value0}}", - "51c65c0265": "No hay cheques rotos que arreglar.", - "788a782bb0": "No se pudieron volver a ejecutar las comprobaciones", - "18f2af42ac": "Verificar repeticiones solicitadas", - "5963a6a852": "Se solicita volver a ejecutar el cheque", - "246b2c6456": "No se pudieron actualizar las comprobaciones", - "c057f2fcb0": "No se pueden actualizar las comprobaciones sin una ruta al repositorio.", + "d8e82b7f15": "Cargando detalles del check...", + "54cddd1858": "Volver a ejecutar todos los checks", + "68605516dd": "Volver a ejecutar checks fallidos", + "522d9353e1": "Volver a ejecutar", + "0fa8b8faec": "Iniciar el agente de IA predeterminado en estos checks", + "5d0f42766d": "Actualizar checks", + "98583589c6": "No se pudo iniciar un agente de IA para los checks fallidos: {{value0}}", + "51c65c0265": "No hay checks fallidos que arreglar.", + "788a782bb0": "No se pudieron volver a ejecutar los checks", + "18f2af42ac": "Se solicitó volver a ejecutar los checks", + "5963a6a852": "Se solicitó volver a ejecutar el check", + "246b2c6456": "No se pudieron actualizar los checks", + "c057f2fcb0": "No se pueden actualizar los checks sin una ruta al repositorio.", "6591b1fa82": "Cancelar", "42c36d9166": "{{value0}} {{value1}} reacción{{value2}}", - "7df8d5fc60": "Abrir el cuadro de combinación de GitHub", - "1939d0f663": "Solicitud de extracción", - "973ef2fac9": "No se pudo deshabilitar la combinación automática", - "d31f4b508c": "No se pudo habilitar la combinación automática", - "0f5821b035": "Fusión automática deshabilitada", - "5edbe7eefa": "Combinación automática habilitada", - "aae645d36d": "No se pudo fusionar la solicitud de extracción", - "c57873d721": "Solicitud de extracción fusionada", - "a63b3c159c": "Esto actualizará la solicitud de extracción en GitHub.", + "7df8d5fc60": "Abrir el cuadro de merge de GitHub", + "1939d0f663": "Pull request", + "973ef2fac9": "No se pudo deshabilitar auto-merge", + "d31f4b508c": "No se pudo habilitar auto-merge", + "0f5821b035": "Auto-merge deshabilitado", + "5edbe7eefa": "Auto-merge habilitado", + "aae645d36d": "No se pudo hacer merge del pull request", + "c57873d721": "Pull request mergeado", + "a63b3c159c": "Esto actualizará el pull request en GitHub.", "eec3706a6a": "{{value0}} PR #{{value1}}?", "b8c6cbb8c4": "No se pudo {{value0}} PR", - "710e47aa06": "Solicitud de extracción reabierta", - "7aa3b5f706": "Solicitud de extracción cerrada", - "3d77438c92": "Esto reabrirá la solicitud de extracción en GitHub.", - "5a65651096": "Esto cerrará la solicitud de extracción en GitHub.", + "710e47aa06": "Pull request reabierto", + "7aa3b5f706": "Pull request cerrado", + "3d77438c92": "Esto reabrirá el pull request en GitHub.", + "5a65651096": "Esto cerrará el pull request en GitHub.", "d2d589556c": "Aún no hay comentarios.", "3463d10a63": "Comentarios", - "c8ea6c7c4c": "No se proporciona descripción.", + "c8ea6c7c4c": "No se proporcionó descripción.", "778683ec84": "Descripción", "da9aaa8bcf": "Editar descripción", - "4a337ac05f": "Ahorrar", + "4a337ac05f": "Guardar", "169a93b29a": "actualizado", "3c891789f6": "por", "f4fe47c2bb": "Resuelto", @@ -1275,60 +1275,60 @@ "6885c619e7": "No se puede responder sin una ruta al repositorio.", "d94810f652": "No se pudo actualizar la descripción.", "9b4190dc98": "Descripción actualizada.", - "51ed0cf38b": "Mostrar más líneas a continuación", + "51ed0cf38b": "Mostrar más líneas abajo", "e295a78c11": "Mostrar {{value0}} más líneas arriba", "c9de94b07a": "Mostrar más líneas arriba", - "5f3e293517": "Restablecer el contexto del código", + "5f3e293517": "Restablecer contexto de código", "85d119be40": "Controles de contexto de código", - "791ddede19": "comentario l", + "791ddede19": "comentario L", "4b960e5978": "Cargando contexto de código...", "89e80af1c7": "archivos vistos", "319cf2d54b": "Mostrar árbol de archivos", "eff839f438": "Se agregó el comentario de revisión.", - "d8c3ba91c4": "No se puede comentar sin el jefe de PR SHA.", - "74660bd80b": "La diferencia no está disponible porque faltan los SHA de commit de PR.", + "d8c3ba91c4": "No se puede comentar sin el SHA del head del PR.", + "74660bd80b": "El diff no está disponible porque faltan los SHA de commits del PR.", "2e528e1c2d": "Visto", - "ff84e1f54c": "{{value0}} {{value1}} tal como se ve", + "ff84e1f54c": "Cambiar estado de visto de {{value1}} ({{value0}})", "5ad00c7a0e": "No hay revisores que coincidan.", "2760fa29a4": "Todos los demás", "828f045847": "Sugerencias", "57750f4a8c": "Cargando...", - "805cb72cd4": "Solicite hasta 15 revisores", - "a04c137bb7": "Crítico", + "805cb72cd4": "Solicita hasta 15 revisores", + "a04c137bb7": "Revisor", "3bde131f49": "Escribe o elige un usuario", "d10b6d5209": "No se solicitaron revisores.", "ae9a38fd4a": "Eliminar revisor {{value0}}", "acbd110867": "Cargando revisores", "00d3be6bcd": "Revisores", - "f4a4b3fd9f": "Editados recientemente estos archivos", + "f4a4b3fd9f": "Editó estos archivos recientemente", "41d275d3ec": "Solicitar revisor {{value0}}", - "36b514a457": "Revisor no solicitado {{value0}}", + "36b514a457": "Cancelar solicitud de revisor {{value0}}", "c798fa0ec7": "No se pudo eliminar al revisor", "1e6d089420": "Revisores eliminados", "2c1d93da43": "Revisor eliminado", - "1ae11c905c": "No hay contexto de repo disponible para esta solicitud de extracción.", + "1ae11c905c": "No hay contexto de repositorio disponible para este pull request.", "102d3d177f": "Revisores solicitados", "03282ff3b9": "Revisor solicitado", "8f369a6b6b": "Puedes solicitar hasta 15 revisores", - "dace0d1a9f": "Introduzca un revisor", + "dace0d1a9f": "Ingresa un revisor", "77d9388fb0": "desconocido", "c9e7094a7b": "Reanudar espacio de trabajo", "3b6886b2ee": "copiado", "b6bda618cf": "compacto", "35a0573f41": "Anotación", "61a8c69a33": "página", - "a4541fd3db": "Arreglar cheques rotos", - "c808db1dd1": "Arreglar cheques", - "c4c02ea23e": "No se puede crear un espacio de trabajo de reparación automáticamente.", + "a4541fd3db": "Arreglar checks fallidos", + "c808db1dd1": "Arreglar checks", + "c4c02ea23e": "No se pudo crear automáticamente un espacio de trabajo de reparación.", "f119e5f5ef": "Responder", - "894cfd884b": "Destino…", + "894cfd884b": "Publicando…", "9d5425918e": "Reabrir PR", - "96d013ed28": "Cerrar solicitud de extracción", + "96d013ed28": "Cerrar pull request", "d65f70786e": "cerrado", - "eca289e593": "La fusión requiere un repo local registrado", + "eca289e593": "El merge requiere un repositorio local registrado", "6568ae8ece": "por defecto", "19f19560d5": "destructivo", - "aae99c6c04": "pr", + "aae99c6c04": "PR", "e01e34f5fa": "comentario", "345b68254c": "hilo", "31a7b202f2": "Responder a @{{value0}}", @@ -1336,42 +1336,42 @@ "34b9f7c264": ":L{{value0}}", "5821aab360": "No se pudo publicar la respuesta.", "84fc40769a": "-L{{value0}}", - "1378d79e83": "Juntos", + "1378d79e83": "Lado a lado", "e5f4a24f78": "En línea", "dd94111c18": "Contraer todo", "eb722a5a8c": "Expandir todo", "19628e058d": "No se pudo agregar el comentario de revisión.", "50b8fb290f": "Marcar visto", "2b4fdb880c": "Desmarcar visto", - "56ec6eafb7": "Abra los detalles de PR para ver los revisores actuales.", + "56ec6eafb7": "Abre los detalles del PR para ver los revisores actuales.", "7f964a365a": "Eliminar revisor", "commentTooLarge": "El comentario es demasiado grande para enviarlo de forma segura.", - "8ff5ae8866": "Responsables", - "82c87eceb9": "Editar responsables", + "8ff5ae8866": "Asignados", + "82c87eceb9": "Editar asignados", "1ff5d979df": "Nadie asignado", - "checkActionRequiredHint": "Esta comprobación requiere una acción manual en GitHub (por ejemplo, aprobar la ejecución del flujo de trabajo) antes de poder desbloquear la fusión.", + "checkActionRequiredHint": "Este check requiere una acción manual en GitHub (por ejemplo, aprobar la ejecución del workflow) antes de desbloquear el merge.", "dd5d9a4f17": "actualizado {{value0}}", "71a3c0f9d2": "Iniciar espacio de trabajo" }, "QuickOpen": { "1dbd3f59ff": "Mover", - "73b2c581f1": "Cerca", - "95fccbae88": "ESC", - "61b1c871a6": "Abierto", - "250e5b2dfb": "Ingresar", + "73b2c581f1": "Cerrar", + "95fccbae88": "Esc", + "61b1c871a6": "Abrir", + "250e5b2dfb": "Enter", "74e2e1b3e4": "No hay archivos coincidentes.", "722a21e1a8": "Cargando archivos...", "1cb6ef47b7": "Ir al archivo...", "9e97f08d0f": "Buscar un archivo para abrir", - "ec31e058f7": "ir al archivo", + "ec31e058f7": "Ir al archivo", "73b44e7bde": "Copiar comando de instalación", - "1cf8561ab4": "en el control remoto para habilitar un listado rápido y compatible con gitignore:", + "1cf8561ab4": "en el remoto para habilitar un listado rápido y compatible con gitignore:", "5d80dc39bb": "ripgrep", "2ca749c15d": "Instalar", "4725b0e931": "El escaneo de apertura rápida es demasiado grande (", "b227d88520": "{{value0}} archivos encontrados", "995be8ea22": "Copiar", - "cf144856dc": "copiado" + "cf144856dc": "Copiado" }, "SelectedTextCopyMenu": { "9b40d7b018": "Copiar" @@ -1379,9 +1379,9 @@ "StarNagCard": { "92b0f9d921": "está autenticado y vuelve a intentarlo.", "cd8c34aac1": "gh", - "cf82170065": "No se pudo destacar el repo. Cerciorarse", + "cf82170065": "No se pudo dar estrella al repositorio. Asegúrate de que", "30c36231c1": "Orca es de código abierto. Si te ayudó hoy, una estrella en GitHub ayuda a otros desarrolladores a encontrarlo.", - "b5e685e4d9": "Despedir", + "b5e685e4d9": "Descartar", "5f6df21046": "¿Disfrutando de Orca?", "2d67b6c849": "Dar estrella en GitHub", "af3c9bbb37": "Dando estrella...", @@ -1395,30 +1395,30 @@ "TaskPage": { "513cddfa7a": "Verificando…", "ff69a30681": "Cancelar", - "2abe22ef76": "Su token se cifra a través del llavero del sistema operativo y se almacena localmente.", + "2abe22ef76": "Tu token se cifra con el llavero del sistema operativo y se almacena localmente.", "246c2b3dd3": "Configuración de la cuenta de Atlassian", "59c14d34a2": "Crear un token en", "b95623e93f": "Token API de Atlassian", "68df347677": "tu@ejemplo.com", "163df31e0e": "https://example.atlassian.net", - "33fc2bcb30": "Utilice la URL de un sitio de Jira Cloud, un correo electrónico de Atlassian y un token de API para explorar problemas.", + "33fc2bcb30": "Usa una URL de Jira Cloud, un correo de Atlassian y un token de API para explorar issues.", "60f806ce99": "Conectar el sitio de Jira", "8ff6fdc368": "Creando…", - "fc0d8a1fa4": "para presentar.", - "919a20dd5b": "Ingrese {{value0}}", + "fc0d8a1fa4": "para enviar.", + "919a20dd5b": "Ingresa {{value0}}", "56cdb413a2": "Valores separados por comas", - "1f0fce91e3": "Seleccione {{value0}}", + "1f0fce91e3": "Selecciona {{value0}}", "cbcdcbe244": "Cargando campos obligatorios de Jira...", "34d97ca682": "¿Qué está sucediendo?", "f161bf9ede": "Descripción (opcional)", "578f730c16": "Breve resumen", "16cba35bee": "Título", - "ae592fee62": "Tipo de problema", + "ae592fee62": "Tipo de issue", "7d63e2626e": "Cargando...", "93c57f15e5": "No se encontraron proyectos.", "cfb56a7868": "Buscar proyectos...", "00022ec0ba": "Proyecto", - "0c11ca0b6d": "Nuevo problema de Jira", + "0c11ca0b6d": "Nuevo issue de Jira", "d0ca4aa1d0": "Etiquetas", "1742eafc14": "Sin proyecto", "69591944e7": "Bajo", @@ -1428,166 +1428,166 @@ "713179dfdc": "Sin prioridad", "c8d5bec5f7": "Prioridad", "42a9160321": "No asignado", - "d2a876ca53": "Cesionario", + "d2a876ca53": "Asignado", "154b0fa623": "Estado", "9bc8aea407": "Añadir descripción...", - "d9151fd4e9": "Título del problema", + "d9151fd4e9": "Título del issue", "4f3cb99f41": "Cambiar de equipo", - "c11105dac5": "Nuevo número", + "c11105dac5": "Nuevo Issue", "1b59a07674": "Creando...", "cf72580c04": "Escribe una descripción, un resumen del proyecto o recopila ideas...", - "2ea1c701b6": "fecha objetivo", + "2ea1c701b6": "Fecha objetivo", "7da41c9225": "Objetivo", "09623359b9": "Fecha de inicio", "7d08e8be0f": "Comenzar", "af9e877f30": "Sin etiquetas", "d6cda23ef1": "Miembros", - "cfaadb6b22": "Sin pista", - "34da8ac06c": "Dirigir", + "cfaadb6b22": "Sin lead", + "34da8ac06c": "Lead", "579f98afcd": "Añade un breve resumen...", "ecbcc83140": "Nombre del proyecto", - "b6795e65fd": "Cerca", + "b6795e65fd": "Cerrar", "a98cbe7664": "Equipo", "02f67c0d09": "Nuevo proyecto", "bdebffcbfe": "Crea un proyecto Linear para el equipo seleccionado.", "1361275ec3": "Nuevo proyecto Linear", "7f3f7b4c18": "Descripción (opcional, markdown)", - "9f2b4c03a6": "Presentando", - "d3d0998b7d": "Nuevo problema de GitHub", - "d1e243795c": "asuntos", - "be8cf68d9f": "ver problemas", - "67662ade50": "problemas del proyecto", + "9f2b4c03a6": "Creando en", + "d3d0998b7d": "Nuevo issue de GitHub", + "d1e243795c": "issues", + "be8cf68d9f": "ver issues", + "67662ade50": "issues del proyecto", "6244a02f46": "Abrir en Linear", "606a85c774": "Abierto", "5e8061b088": "Iniciar espacio de trabajo desde {{value0}} {{value1}}", - "592a55611b": "Intente seleccionar más equipos o actualizar; Los filtros de equipo se aplican al conjunto de incidencias recuperadas actualmente.", - "618107fab3": "No hay problemas recuperados que coincidan con los equipos seleccionados.", - "903c7af49f": "No se encontraron problemas Lineares", - "5ed38a49e5": "Revise el error del espacio de trabajo a continuación y luego actualice.", - "cc8795e07c": "No se pueden cargar problemas Lineares", + "592a55611b": "Prueba a seleccionar más equipos o actualizar; los filtros de equipo se aplican al conjunto de issues obtenido actualmente.", + "618107fab3": "No hay issues obtenidos que coincidan con los equipos seleccionados.", + "903c7af49f": "No se encontraron issues de Linear", + "5ed38a49e5": "Revisa el error del espacio de trabajo abajo y luego actualiza.", + "cc8795e07c": "No se pueden cargar issues de Linear", "f362667d55": "Actualizado", - "b1eaa18ace": "Asunto", - "37e7ee311e": "Llave", + "b1eaa18ace": "Issue", + "37e7ee311e": "Clave", "b7bae28b6a": "mostrado", "a26a48252e": "Propiedades de visualización", - "5d2d835467": "Realizar pedidos", - "5659da12fc": "Agrupamiento", + "5d2d835467": "Orden", + "5659da12fc": "Agrupación", "9c57663908": "Vista", - "af377b13b1": "{{value0}} ver", - "d47248df4d": "Modo de vista Linear", + "af377b13b1": "Vista {{value0}}", + "d47248df4d": "Modo de vista de Linear", "f397d513e3": "Atrás", "b39fe6511d": "proyectos", "8675cd6188": "Linear", "733b8f2421": "Linear / Vistas", "bc06ed0fb0": "Volver a vistas", "3cb855080f": "vistas", - "b4e10f096e": "Dueño", + "b4e10f096e": "Owner", "a04fe7ba73": "Visibilidad", "0aa8525950": "Modelo", - "dfc0c79bd8": "Asuntos", + "dfc0c79bd8": "Issues", "8a07f21e76": "Salud", "851017590d": "Agregar acceso Linear", - "228b25028f": "Explore y comience a trabajar en los problemas Lineares asignados directamente desde aquí.", + "228b25028f": "Explora y empieza a trabajar en tus issues asignados de Linear directamente desde aquí.", "6d56559467": "Conecta tu cuenta Linear", - "eee68073b2": "Abierto en Jira", + "eee68073b2": "Abrir en Jira", "9497f2787c": "Iniciar espacio de trabajo", - "eba87f2edb": "No se encontraron problemas con Jira", - "63b2abd3aa": "Problemas con Jira", + "eba87f2edb": "No se encontraron issues de Jira", + "63b2abd3aa": "Issues de Jira", "e7115334aa": "Ocultar Jira", "83bce6be5c": "Conecta Jira", - "b518ae6307": "Explora, edita, crea y comienza a trabajar desde incidencias de Jira directamente desde aquí.", - "a150c59da7": "Conecte su sitio Jira", + "b518ae6307": "Explora, edita, crea y empieza a trabajar desde issues de Jira directamente aquí.", + "a150c59da7": "Conecta tu sitio de Jira", "bcdc1330b2": "Abrir en GitLab", "00b7ffb952": "Tipo / Estado", - "eb10c32872": "IDENTIFICACIÓN", - "e9b6955dcd": "Número {{value0}}", - "a0544fb653": "¡SEÑOR! {{value0}}", + "eb10c32872": "ID", + "e9b6955dcd": "Issue #{{value0}}", + "a0544fb653": "MR !{{value0}}", "8396825a14": "Acción", "c1d1600362": "Abrir en el navegador", "b6329379ca": "Iniciar nuevo espacio de trabajo", "054bf695cc": "Borrador", - "285bc21dc5": "Cambie la consulta o borrela.", + "285bc21dc5": "Cambia la consulta o bórrala.", "d0e3c8f933": "No hay trabajo de GitHub coincidente", "5b6b2af943": "Reintentando…", - "0c0de0fc0e": "No se pudieron cargar los problemas de", + "0c0de0fc0e": "No se pudieron cargar los issues de", "d1766fd62d": "los proyectos no pudieron cargarse", "7762f4b03a": "de", - "443f7dd928": "Unir", - "a7396b05c6": "cheques", + "443f7dd928": "Merge", + "a7396b05c6": "Checks", "f6fa3c97d0": "Revisores", - "8aba10579d": "Cesionarios", + "8aba10579d": "Asignados", "5eccb3c841": "Título / Contexto", "d4c2830063": "Actualizar elementos de trabajo de GitLab", - "c679af7ad9": "Actualizar mis todos", + "c679af7ad9": "Actualizar My Todos", "dfd72673e7": "No se pudo guardar la selección del proyecto.", "b797bdd7c3": "Borrar búsqueda", - "99c2755218": "Jira JQL, p.ej. proyecto = ABC AND statusCategory! = Listo", - "2ff9fd71fd": "Actualizar problemas de Jira", - "0b65d3fb2c": "Buscar proyectos Lineares...", - "eec0c5c079": "Buscar temas Lineares...", + "99c2755218": "Jira JQL, p. ej. project = ABC AND statusCategory != Done", + "2ff9fd71fd": "Actualizar issues de Jira", + "0b65d3fb2c": "Buscar proyectos de Linear...", + "eec0c5c079": "Buscar issues de Linear...", "8964184a8b": "Actualizar Linear", - "3feb524d42": "Nueva edición Linear", + "3feb524d42": "Nuevo issue de Linear", "0cbf7e5cf3": "Modo de tarea Linear", - "ff53631e6f": "Actualizar el trabajo de GitHub", - "6ffa6be99f": "Actualización del trabajo de GitHub", - "b15ceb409d": "Buscar problemas de GitHub...", - "eee4df4c66": "Buscar PR de GitHub...", + "ff53631e6f": "Actualizar trabajo de GitHub", + "6ffa6be99f": "Actualizando trabajo de GitHub", + "b15ceb409d": "Buscar issues de GitHub...", + "eee4df4c66": "Buscar PRs de GitHub...", "e592d99051": "Todos los sitios de Jira", "d09b7631b7": "No se pudo cambiar el sitio de Jira.", - "8029e2bd4d": "Seleccione un equipo lineal para abrir en Linear", + "8029e2bd4d": "Selecciona un equipo de Linear para abrirlo en Linear", "609532fae7": "No se pudo guardar la fuente de tarea predeterminada.", "4826fd1ad8": "Cerrar · Esc", "1a06219d5c": "Cerrar tareas", "3f594861a5": "No se pudo guardar la selección del equipo.", "d0d570b306": "No se pudo cambiar el espacio de trabajo Linear.", - "cb98f0350c": "Creado {{value0}}", - "1e1b2ad8f2": "Seleccione un equipo del espacio de trabajo del proyecto antes de presentar este problema.", + "cb98f0350c": "Se creó {{value0}}", + "1e1b2ad8f2": "Selecciona un equipo del espacio de trabajo del proyecto antes de crear este issue.", "3ca9b424a3": "No se pudo crear el proyecto.", - "3f9604efc7": "Problema abierto #{{value0}}", - "585dba2989": "No se puede abrir el espacio de trabajo adjunto a este problema.", - "534a9c6017": "No se puede abrir el espacio de trabajo adjunto a esta solicitud de extracción.", + "3f9604efc7": "Issue #{{value0}} abierto", + "585dba2989": "No se puede abrir el espacio de trabajo adjunto a este issue.", + "534a9c6017": "No se puede abrir el espacio de trabajo adjunto a este pull request.", "fe380f306c": "No se pudo guardar la vista de tareas predeterminada.", - "af2a8371de": "No se pudieron cargar los tipos de problemas de Jira.", + "af2a8371de": "No se pudieron cargar los tipos de issues de Jira.", "6775c05483": "No se pudo actualizar el estado Linear", "745ae567d4": "\"{{value0}}\" no está disponible para {{value1}}", "669e419d65": "A la vista Linear le falta el contexto del espacio de trabajo.", "cba2a2b7fb": "Al proyecto Linear le falta el contexto del espacio de trabajo.", - "f4374519ae": "Su fuente de problemas preferida (ascendente) ya no está configurada para {{value0}}. Usando origen.", + "f4374519ae": "Tu fuente de issues preferida (upstream) ya no está configurada para {{value0}}. Se usará origin.", "e9139db03f": "No se pudo ocultar {{value0}}.", - "b73717af92": "Próximo", + "b73717af92": "Siguiente", "0c8df28045": "Página siguiente", "ae859c816b": "Página {{value0}}", "cd171f3391": "...", "297a805b64": "Anterior", - "6cd6b3ae6a": "Pagina anterior", + "6cd6b3ae6a": "Página anterior", "e65757a338": "Paginación", - "37d60046e3": "Abrir el cuadro de combinación de GitHub", - "1a9ea003dc": "No se pudo deshabilitar la combinación automática", - "a3318684bc": "No se pudo habilitar la combinación automática", - "a5bf86defe": "Fusión automática deshabilitada", - "fed317634c": "Combinación automática habilitada", - "88f478cdef": "No se pudo fusionar la solicitud de extracción", - "a161925adc": "Solicitud de extracción fusionada", - "0506a78337": "Esto actualizará la solicitud de extracción en GitHub.", + "37d60046e3": "Abrir el cuadro de merge de GitHub", + "1a9ea003dc": "No se pudo deshabilitar auto-merge", + "a3318684bc": "No se pudo habilitar auto-merge", + "a5bf86defe": "Auto-merge deshabilitado", + "fed317634c": "Auto-merge habilitado", + "88f478cdef": "No se pudo hacer merge del pull request", + "a161925adc": "Pull request mergeado", + "0506a78337": "Esto actualizará el pull request en GitHub.", "844dc193c7": "{{value0}} PR #{{value1}}?", - "995dd6af9b": "Abrir cheques de PR", + "995dd6af9b": "Abrir checks del PR", "8a22eb3f7b": "No hay revisores que coincidan.", "67755a83a1": "Todos los demás", "3ace2e6bcf": "Sugerencias", "0eacf48491": "Cargando…", "0b9b04f4b5": "Escribe o elige un usuario", - "62c7bd789f": "Solicite hasta 15 revisores", - "5d4fd69a6a": "Activo recientemente en esta solicitud de extracción", + "62c7bd789f": "Solicita hasta 15 revisores", + "5d4fd69a6a": "Activo recientemente en este pull request", "ed1daeb49a": "No se pudo eliminar al revisor", "837bb901ec": "Revisores eliminados", "f9191d1714": "Revisor eliminado", "dc67f69962": "No se pudo solicitar el revisor", "8f06dbb9e5": "Revisor solicitado", "969e26577c": "Puedes solicitar hasta 15 revisores", - "d00571d9b1": "Introduzca un revisor", + "d00571d9b1": "Ingresa un revisor", "edf4bc4135": "No hay usuarios asignables.", - "53e002d895": "La emisión no tiene slug de repo.", - "7f94eb6395": "Asignar problema", + "53e002d895": "El issue no tiene slug de repositorio.", + "7f94eb6395": "Asignar issue", "bb63046423": "Asignado a {{value0}}", "ca63694b4c": "No se pudieron actualizar los asignados.", "b36f4bf9de": "Sin etiquetas.", @@ -1595,15 +1595,15 @@ "d09bf34db7": "Cerrado", "1c893195ac": "No se pudo actualizar el estado", "afc68824ff": "No se encontraron estados", - "cc13109b5d": "Estados de carga", + "cc13109b5d": "Cargando estados", "d45a910c4a": "Cambiar estado Linear de {{value0}}", "d8a517ad89": "Identificador", "50387522d7": "Sin agrupación", - "d747aed72f": "Junta", + "d747aed72f": "Board", "a6f7e93d7f": "Lista", "e78ec261ed": "Vistas", "727069bee5": "Proyectos", - "137e2a8a01": "PR", + "137e2a8a01": "PRs", "18451e99df": "Hecho", "4b6e40e42c": "Todo abierto", "bd9965df51": "Reportado", @@ -1613,79 +1613,79 @@ "acef77f7ca": "GitHub", "524f095d55": "Necesita revisión", "7698af5263": "Mío", - "94f0339621": "Asignado a mi", + "94f0339621": "Asignado a mí", "c2268a9982": "Todo", - "37a82eaaf8": "Fusionado", + "37a82eaaf8": "Mergeado", "887efe9140": "Conectar", "a70153f583": "conectando", "6459faa8b3": "error", - "e15ba2d2eb": "Crear problema", - "3b11c8e8fc": "formación", - "e178c0a953": "Elija un proyecto de Jira antes de crear el problema.", - "0f7b0d964a": "Crea un nuevo problema en {{value0}}.", + "e15ba2d2eb": "Crear issue", + "3b11c8e8fc": "array", + "e178c0a953": "Elige un proyecto de Jira antes de crear el issue.", + "0f7b0d964a": "Crea un nuevo issue en {{value0}}.", "eff9800d4b": "{{value0}} etiqueta{{value1}}", "d7f16d0e32": "Seleccionar equipo", "5301ca0f20": "Crear proyecto", "7719d8daa9": "{{value0}} miembro{{value1}}", "5af6f0ae5b": "Seleccionar equipo", - "cfb730b73e": "asunto", - "3659f9792a": "junta", - "d079be2dc8": "No hay problemas asignados. Intenta buscar algo.", - "2bdefbcac3": "Pruebe con una consulta de búsqueda diferente.", - "25ff84769a": "Ningún problema coincide con este contexto Linear.", + "cfb730b73e": "issue", + "3659f9792a": "board", + "d079be2dc8": "No hay issues asignados. Intenta buscar algo.", + "2bdefbcac3": "Prueba con otra consulta de búsqueda.", + "25ff84769a": "Ningún issue coincide con este contexto de Linear.", "cbce2bc9cd": "ninguno", "51411113df": "lista", - "60f68a2ef4": "Cuestiones Lineares", + "60f68a2ef4": "Issues de Linear", "b2007ba885": "proyecto", "6edf402e11": "descripción general", "9ae151b26b": "lineal", - "94d900518d": "Ningún problema coincide con el ajuste preestablecido seleccionado.", - "f51e254d35": "Pruebe con una consulta JQL diferente.", + "94d900518d": "Ningún issue coincide con el preset seleccionado.", + "f51e254d35": "Prueba con otra consulta JQL.", "4645a7814f": "jira", - "e224d76876": "SEÑOR", - "bbec4717ee": "señor", - "d6d08c1650": "Seleccione un proyecto para ver los elementos de trabajo de GitLab.", - "f294c500ef": "Ningún trabajo de GitLab coincide con este filtro.", + "e224d76876": "MR", + "bbec4717ee": "mr", + "d6d08c1650": "Selecciona un proyecto para ver los work items de GitLab.", + "f294c500ef": "Ningún work item de GitLab coincide con este filtro.", "cd7dc432a3": "Ningún GitLab MR coincide con este filtro.", - "171b7739d8": "señora", - "a9f256ecea": "Ningún problema de GitLab coincide con este filtro.", + "171b7739d8": "mrs", + "a9f256ecea": "Ningún issue de GitLab coincide con este filtro.", "2007e14d95": "gitlab", - "456b8512da": "Solicitud de fusión", - "03da966159": "Seleccione un proyecto para que podamos autenticarnos en GitLab.", - "d591aac6ae": "No hay todos pendientes. ¡Estáis todos atrapados!", + "456b8512da": "MergeRequest", + "03da966159": "Selecciona un proyecto para que podamos autenticarnos en GitLab.", + "d591aac6ae": "No hay todos pendientes. ¡Estás al día!", "33a4bd7f5c": "pendientes", "66ae7330f6": "Más acciones", "93d5f21fc1": "pr", - "e104fa3d3d": "Iniciar espacio de trabajo desde el problema", - "2193a99ec1": "Abrir espacio de trabajo adjunto al problema", + "e104fa3d3d": "Iniciar espacio de trabajo desde el issue", + "2193a99ec1": "Abrir espacio de trabajo adjunto al issue", "7deb9e59a5": "Más acciones de PR", "7753652524": "Reanudar", "e4b29c5bcf": "Iniciar espacio de trabajo desde PR", "67d881244c": "Reanudar espacio de trabajo adjunto a PR", "6430594b18": "autor desconocido", "7799ad9ab6": "borrador", - "0bfbf62f75": "Rever", + "0bfbf62f75": "Reintentar", "38139edb52": "github", - "31f81cc334": "Actualizando el trabajo de GitHub...", + "31f81cc334": "Actualizando trabajo de GitHub...", "3d93316bb0": "PRs", "937b29fa35": "elementos", - "bc46d8204e": "Seleccione un proyecto para abrir en GitHub", - "d1132848f8": "Seleccione un proyecto de GitHub para abrir en GitHub", + "bc46d8204e": "Selecciona un proyecto para abrirlo en GitHub", + "d1132848f8": "Selecciona un proyecto de GitHub para abrirlo en GitHub", "2af3ab5c58": "Selecciona un equipo para abrir en Linear", - "aec5feeb69": "No se pudo crear el problema de Jira.", - "7437e340b4": "No se pudo crear el problema.", - "9e03c17847": "Abra los detalles de PR para ver los revisores actuales.", + "aec5feeb69": "No se pudo crear el issue de Jira.", + "7437e340b4": "No se pudo crear el issue.", + "9e03c17847": "Abre los detalles del PR para ver los revisores actuales.", "3b7f34282f": "cerrado", "246bd64aed": "Abrir {{value0}} en Linear", "ff90d0abc7": "Iniciar espacio de trabajo desde {{value0}}", "fe28c9821f": "vista", - "8d1e17a3ef": "Abra {{value0}} en GitHub", + "8d1e17a3ef": "Abrir {{value0}} en GitHub", "4ac8ff2275": "Abrir {{value0}} en Jira", "40eaf2c27c": "Detalles", "closeAsCompleted": "Cerrar como completado", "closeAsCompletedDescription": "Hecho, cerrado, corregido, resuelto", "closeAsNotPlanned": "Cerrar como no planificado", - "closeAsNotPlannedDescription": "No se corregirá, no se puede reproducir, obsoleto", + "closeAsNotPlannedDescription": "No se arreglará, no se puede reproducir, obsoleto", "closeAsDuplicate": "Cerrar como duplicado", "closeAsDuplicateDescription": "Duplicado de otro issue en este repositorio", "duplicateIssueNumberPlaceholder": "Número de issue", @@ -1702,101 +1702,101 @@ "editReviewersWithCurrent": "Editar revisores: {{value0}}" }, "Terminal": { - "73768427cf": "Cerca", + "73768427cf": "Cerrar", "f82e9f02df": "Cancelar", "7958465754": "Hay terminales locales con procesos en ejecución. ¿Cerrar la ventana de todos modos?", "2fa9c69ff3": "¿Cerrar ventana?", - "cd51e28d8b": "Ahorrar", + "cd51e28d8b": "Guardar", "0037b21794": "No guardar", "21295c6b8c": "Cambios no guardados", "5c1d2a32bb": "Cargando editor...", - "f0600556b3": "No se pudo crear el archivo de markdowns sin título.", + "f0600556b3": "No se pudo crear el archivo Markdown sin título.", "37da0d736f": "Nueva pestaña del navegador", - "a2a279b32a": "Se agotó el tiempo de guardado o falló. Corregir errores antes de cerrar.", + "a2a279b32a": "El guardado agotó el tiempo de espera o falló. Corrige los errores antes de cerrar.", "46e08bc5c8": "Este archivo tiene cambios no guardados.", "61ed600d29": "\"{{value0}}\" tiene cambios no guardados. ¿Quieres guardar antes de cerrar?", "cdc9ac4b2d": "editor", - "e57db40c11": "No se pudo construir el comando de inicio para {{value0}}.", - "5b2c1a9e44": "No se detectó ninguna CLI de agente; instala una o elige un agente predeterminado en Configuración." + "e57db40c11": "No se pudo generar el comando de inicio para {{value0}}.", + "5b2c1a9e44": "No se detectó ninguna CLI de agente; instala una o elige un agente predeterminado en Ajustes." }, "TerminalSearch": { - "db234b7519": "Cerca", - "7cb40c04eb": "Próximo partido", - "0f3066256e": "Partido anterior", - "42e466b9f1": "expresión regular", + "db234b7519": "Cerrar", + "7cb40c04eb": "Siguiente coincidencia", + "0f3066256e": "Coincidencia anterior", + "42e466b9f1": "Regex", "90c61387d9": "Distingue mayúsculas y minúsculas", "e07012f26e": "Buscar..." }, "UpdateCard": { "68b235d264": "Reiniciar para actualizar", - "6714206e5a": "Se descarga Orca v{{value0}}. Reinicie cuando esté listo.", + "6714206e5a": "Orca v{{value0}} se descargó. Reinicia cuando estés listo.", "93794ea932": "Orca v{{value0}} se está descargando.", "8acbdd3961": "Minimizar a la barra de estado", "17412483da": "Listo para instalar", "47126bcf57": "Descargar manualmente", - "3553a8672f": "último error", - "90559b14e3": "Esto activa un conmutador de red Electron de todo el proceso después del reinicio. Úselo para VPN corporativas o servidores proxy que rechacen las descargas de actualizaciones HTTP/2.", + "3553a8672f": "Último error", + "90559b14e3": "Esto activa un switch de red de Electron para todo el proceso después de reiniciar. Úsalo para VPNs corporativas o proxies que rechazan descargas de actualizaciones por HTTP/2.", "6e45bfa2e0": "Descargando...", "558842597d": "Descargando actualización", "f58b5c57a6": "Nuevo:", "ec8fe71cfc": "Actualizar", "44324ef542": "Notas de la versión", - "fdd4a364fa": "Las sesiones no serán interrumpidas.", - "05ad78a6d1": "La Orca v{{value0}} está lista.", + "fdd4a364fa": "Las sesiones no se interrumpirán.", + "05ad78a6d1": "Orca v{{value0}} está lista.", "318d3b4bc7": "Descartar actualización", "9abc59f814": "Actualización disponible", - "aad383aecc": "Lea las notas de la versión completas", + "aad383aecc": "Lee las notas de la versión completas", "ccd8b0a793": "más desde tu última actualización", - "b1d867f4fb": "Las sesiones de su terminal no se interrumpirán durante la actualización.", + "b1d867f4fb": "Tus sesiones de terminal no se interrumpirán durante la actualización.", "09a55c39b5": "Instalando...", "ea2a41adbe": "Estás en la última versión.", "ba5ffc949c": "Buscando actualizaciones...", - "2c2d3e03ca": "Intentar otra vez", + "2c2d3e03ca": "Intentar de nuevo", "4cf109845a": "Error de actualización", - "6b0085010d": "Vuelva a comprobar", + "6b0085010d": "Volver a comprobar", "48565a32bc": "Reintentar descargar", "933c6fdf5b": "Habilitar y reiniciar", "1339b82cee": "Descarga HTTP/2 bloqueada", "7274ef6e59": "Descartar consejo", - "a726967bd3": "Despedir", + "a726967bd3": "Descartar", "d5253b54af": "error", - "7ffc08506e": "controlar", - "522df222b9": "hilandero" + "7ffc08506e": "check", + "522df222b9": "spinner" }, "WorktreeJumpPalette": { "ac037cfac2": "Mover", - "75499e01d9": "Cerca", - "66b5a67bee": "ESC", - "45def60329": "Abierto", - "f65d992a11": "Ingresar", - "c5081f2814": "Árbol de trabajo actual", + "75499e01d9": "Cerrar", + "66b5a67bee": "Esc", + "45def60329": "Abrir", + "f65d992a11": "Enter", + "c5081f2814": "Worktree actual", "52404f8096": "Pestaña actual", "739bda980c": "primario", "556e7232ca": "Actual", - "684e8d7bc2": "Recopilando sus árboles de trabajo recientes y pestañas abiertas.", + "684e8d7bc2": "Recopilando tus worktrees recientes y pestañas abiertas.", "ff908adfe9": "Cargando objetivos de salto", - "1ebe225fee": "Busque árboles de trabajo, configuraciones, pestañas y acciones...", - "4e4ff044d5": "Buscar árboles de trabajo, configuraciones, pestañas y acciones", + "1ebe225fee": "Busca worktrees, ajustes, pestañas y acciones...", + "4e4ff044d5": "Buscar worktrees, ajustes, pestañas y acciones", "4ee378034d": "Saltar a...", - "f7fda8d562": "Cree un árbol de trabajo o abra una pestaña en Orca para comenzar.", - "1628fd7dfa": "No hay árboles de trabajo, configuraciones, acciones ni pestañas abiertas activas", - "b781ae05e3": "Escriba para buscar árboles de trabajo, configuraciones, pestañas y acciones.", - "f60f8730be": "No hay otros árboles de trabajo a los que cambiar", - "c4afa68159": "Pruebe con un árbol de trabajo, ajuste, acción, título de pestaña, prompt de agente, URL, PR o puerto.", + "f7fda8d562": "Crea un worktree o abre una pestaña en Orca para comenzar.", + "1628fd7dfa": "No hay worktrees activos, ajustes, acciones ni pestañas abiertas", + "b781ae05e3": "Escribe para buscar worktrees, ajustes, pestañas y acciones.", + "f60f8730be": "No hay otros worktrees a los que cambiar", + "c4afa68159": "Prueba con un worktree, ajuste, acción, título de pestaña, prompt de agente, URL, PR o puerto.", "dbd9d87eec": "Ningún resultado coincide con tu búsqueda", "2c38630a01": "El espacio de trabajo ya no existe", "7726ce9970": "La pestaña del emulador móvil ya no existe", "d7d496a451": "La página del navegador ya no existe", "50a1d11d5b": "Pestañas abiertas", - "088d66d980": "Acciones y configuraciones", - "dabd819ca1": "Escriba para ver todos los árboles de trabajo {{value0}}", - "20af998bff": "{{value0}} artículos disponibles{{value1}}", + "088d66d980": "Acciones y ajustes", + "dabd819ca1": "Escribe para ver los {{value0}} worktrees", + "20af998bff": "{{value0}} elementos disponibles{{value1}}", "bb72c08e63": "{{value0}} resultados encontrados{{value1}}", "34c8fbb46e": "SSH remoto", "63c2be1914": "SSH desconectado", - "95be6587d3": "Crear árbol de trabajo \"{{value0}}\"", - "worktreesHeader": "árboles de trabajo", - "recentWorktreesHeader": "Árboles de trabajo recientes", + "95be6587d3": "Crear worktree \"{{value0}}\"", + "worktreesHeader": "Worktrees", + "recentWorktreesHeader": "Worktrees recientes", "settingsBadge": "Ajustes", "actionBadge": "Acción", "paletteHostBadge": "Host: {{value0}}", @@ -1809,39 +1809,39 @@ "pr": { "merge": { "state": { - "a80132573b": "GitHub todavía está calculando el estado de fusión de esta solicitud de extracción", - "f958920f3a": "De cheques", - "9bd983ce8f": "GitHub dice que este PR puede fusionarse, pero aún se están realizando comprobaciones", - "4e2507176b": "Cheques pendientes", - "1432ecff30": "GitHub dice que este PR puede fusionarse, pero algunas comprobaciones fallaron", - "87fa36ac83": "Los controles fallaron", - "1766eb46ba": "GitHub informa que esta solicitud de extracción está bloqueada", - "bf5e4c6c92": "Obstruido", - "c614e2660a": "Actualizar la rama antes de fusionar", - "039c072f94": "Detrás", - "b37d45bca9": "GitHub informa conflictos de fusión", + "a80132573b": "GitHub todavía está calculando el estado de merge de este pull request", + "f958920f3a": "Verificando", + "9bd983ce8f": "GitHub dice que este PR se puede mergear, pero los checks siguen ejecutándose", + "4e2507176b": "Checks pendientes", + "1432ecff30": "GitHub dice que este PR se puede mergear, pero algunos checks fallaron", + "87fa36ac83": "Checks fallidos", + "1766eb46ba": "GitHub informa que este pull request está bloqueado", + "bf5e4c6c92": "Bloqueado", + "c614e2660a": "Actualiza la rama antes de hacer merge", + "039c072f94": "Por detrás", + "b37d45bca9": "GitHub informa conflictos de merge", "7e8bbe3cd7": "Conflictos", - "09896aad26": "El estado de fusión no está disponible para este PR", - "bd4f27b50e": "Unir", - "35ec24bc43": "Esta rama base usa la cola de combinación de GitHub", - "b289646bcd": "Los informes de GitHub solicitaron cambios en esta solicitud de extracción", + "09896aad26": "El estado de merge no está disponible para este PR", + "bd4f27b50e": "Merge", + "35ec24bc43": "Esta rama base usa la merge queue de GitHub", + "b289646bcd": "GitHub informa cambios solicitados en este pull request", "c606463dc2": "Cambios solicitados", - "a20db875ed": "GitHub requiere aprobación de revisión antes de que esta solicitud de extracción pueda fusionarse", + "a20db875ed": "GitHub requiere aprobación de revisión antes de que este pull request pueda mergearse", "1f8eb81c0e": "Se requiere aprobación", - "f03028e055": "Esta solicitud de extracción aún es un borrador.", + "f03028e055": "Este pull request sigue en borrador", "ec8e2cebaa": "Borrador", - "820fd21663": "Esta solicitud de extracción está cerrada.", + "820fd21663": "Este pull request está cerrado", "4f976d3450": "Cerrado", - "62eb8d39da": "Esta solicitud de extracción ya está fusionada", - "83ecdbb4a6": "Fusionado", - "331ebe1170": "Agregue esta solicitud de extracción a la cola de combinación de GitHub", - "b169f943e1": "Fusionar cuando esté listo", - "62703b1dc4": "La fusión automática de GitHub está habilitada para esta solicitud de extracción", - "48d75ae118": "Deshabilitar la fusión automática", - "a5b66afb58": "Comprobaciones aprobadas", - "fbd4f57f0a": "Comprobaciones aprobadas. La elegibilidad de fusión se volverá a comprobar antes de fusionar.", - "4ab19a62ef": "Habilitar la fusión automática", - "8f6cb3772f": "Fusionar esta solicitud de extracción automáticamente cuando se cumplan los requisitos" + "62eb8d39da": "Este pull request ya está mergeado", + "83ecdbb4a6": "Mergeado", + "331ebe1170": "Agrega este pull request a la merge queue de GitHub", + "b169f943e1": "Mergear cuando esté listo", + "62703b1dc4": "Auto-merge de GitHub está habilitado para este pull request", + "48d75ae118": "Deshabilitar auto-merge", + "a5b66afb58": "Checks aprobados", + "fbd4f57f0a": "Checks aprobados. La elegibilidad de merge se volverá a verificar antes de hacer merge.", + "4ab19a62ef": "Habilitar auto-merge", + "8f6cb3772f": "Mergear este pull request automáticamente cuando se cumplan los requisitos" } } }, @@ -1854,30 +1854,30 @@ "baa006f9af": "Documentos", "3fefeebde4": "Copiar comando de actualización", "9c2da6353b": "Copiar comando de inicio de sesión", - "b436c586d1": "comando copiar", + "b436c586d1": "Copiar comando", "8a7f6bf5dc": "No se pudo copiar", "224c9d0ae8": "Copiado al portapapeles", - "891a7d4616": "Desarmado para este shell", - "fd17b3019f": "Desarmado (PowerShell, persistente)", - "ae43542893": "Encuentra dónde está configurado", - "df636f5886": "Compruebe si está configurado (PowerShell)" + "891a7d4616": "Unset para este shell", + "fd17b3019f": "Unset (PowerShell, persistente)", + "ae43542893": "Encontrar dónde está configurado", + "df636f5886": "Comprobar si está configurado (PowerShell)" }, "ProjectCell": { - "4b5b871da8": "No hay etiquetas en este repo.", + "4b5b871da8": "No hay etiquetas en este repositorio.", "2219e945ef": "Cargando…", - "54cac64427": "Row no tiene slug de repo.", + "54cac64427": "La fila no tiene slug de repositorio.", "8ae56a88a6": "Etiquetas", - "f7cdb78efb": "Cesionarios", - "ebde486e3c": "Claro", - "191905e20e": "Actual y próximo", - "e17bb96881": "Terminado", - "943b3dadc9": "Este repo no tiene tipos de problemas.", - "c7b059cf07": "Tipo de problema", - "c5f949e489": "Asunto", + "f7cdb78efb": "Asignados", + "ebde486e3c": "Limpiar", + "191905e20e": "Actual y próximos", + "e17bb96881": "Completado", + "943b3dadc9": "Este repositorio no tiene tipos de issue.", + "c7b059cf07": "Tipo de issue", + "c5f949e489": "Issue", "8d669084f6": "Restringido", "6efdc0d920": "Borrador", "d0d0e13a5a": "PR", - "af5d8c912a": "Artículo restringido", + "af5d8c912a": "Elemento restringido", "bb7ebc11e3": "Añadir número", "9cb1a0c984": "Agregar texto", "2e26a06c70": "Agregar etiqueta", @@ -1891,38 +1891,38 @@ }, "ProjectItemSlugDialog": { "e55a5c4e68": "Vista previa de la fila del proyecto.", - "4450efea9c": "elemento de GitHub" + "4450efea9c": "Elemento de GitHub" }, "ProjectPicker": { - "96739284c3": "Pegue la URL de un proyecto a continuación para llegar a los que faltan.", + "96739284c3": "Pega una URL de proyecto abajo para acceder a los que faltan.", "9b36829267": "No se encontraron vistas.", "72a05c04a6": "Cargando vistas…", "9bf55fa1e8": "Elige una vista", "a51b3337ab": "← Volver", - "8ab5447c64": "Alfiler", - "5009ffc2f3": "Quitar pasador", + "8ab5447c64": "Fijar", + "5009ffc2f3": "Quitar fijado", "fce99a24a7": "Agregar", - "5113ecc298": "Agregar por URL o propietario/número", + "5113ecc298": "Agregar por URL u owner/number", "7b6d39627e": "Cargando…", "b787682111": "Explorar todo", "ba0ab9a117": "Explorar todo (cargando…)", - "b3044b7a25": "Reciente", + "b3044b7a25": "Recientes", "707843206c": "Fijado", "f492e1b539": "Buscar proyectos", "44b2c6326b": "No se pudieron cargar las vistas: {{value0}}", "ab1a2c357d": "Hoja de ruta (no compatible)", - "d34ef9b554": "Tablero (sin soporte)", + "d34ef9b554": "Board (no compatible)", "43a88ae574": "BOARD_LAYOUT", - "1a2b8e512e": "Mesa", + "1a2b8e512e": "Tabla", "cafb908f34": "TABLE_LAYOUT" }, "ProjectRow": { - "75b5d816e3": "empezar a trabajar", + "75b5d816e3": "Empezar a trabajar", "e12be8b4d4": "Abrir en GitHub", - "c3b81ddea2": "BORRADOR_EDICIÓN" + "c3b81ddea2": "DRAFT_ISSUE" }, "ProjectViewList": { - "989f81dc2a": "columnas", + "989f81dc2a": "Columnas", "f949f5b2b7": "Configurar columnas", "eddfc7a794": "Ordenar por {{value0}}", "4f57d2e0b1": "Ningún elemento coincide con el filtro de esta vista." @@ -1930,22 +1930,22 @@ "ProjectViewWrapper": { "463f1205c0": "Cargando vista del proyecto", "23b87ba9f7": "Abrir en GitHub", - "4d2a77a119": "Solicitud de función de archivo", - "1bf8c01c8b": "Cambie a una vista de tabla para trabajar con este proyecto en Orca.", - "55de4fb57a": "{{value0}}. {{value1}} Presente una solicitud de función en {{value2}}.", - "2edf5e7e77": "{{value0}}: Orca aún no admite vistas del proyecto {{value1}}. Presente una solicitud de función en {{value2}}.", + "4d2a77a119": "Enviar feature request", + "1bf8c01c8b": "Cambia a una vista de tabla para trabajar con este proyecto en Orca.", + "55de4fb57a": "{{value0}}. {{value1}} Envía un feature request en {{value2}}.", + "2edf5e7e77": "{{value0}}: Orca aún no admite vistas de proyecto {{value1}}. Envía un feature request en {{value2}}.", "7245c3d7ac": "Borrar búsqueda", - "c5bc7ec007": "Ver filtro: {{value0}}", - "840c268665": "Agregar repo", + "c5bc7ec007": "Filtro de vista: {{value0}}", + "840c268665": "Agregar repositorio", "dffa899f36": "Cancelar", - "7037c8f5f1": "Repositorio no en Orca", - "512fc171d6": "Elija un proyecto para comenzar.", - "71fb69926c": "Refrescar", - "a8fa0d2bf5": "Refrescante", + "7037c8f5f1": "Repositorio no está en Orca", + "512fc171d6": "Elige un proyecto para comenzar.", + "71fb69926c": "Actualizar", + "a8fa0d2bf5": "Actualizando", "fd15491034": "Abrir vista en GitHub", - "22df63c393": "Los datos de la subemisión no están disponibles para su token.", - "067119985c": "Búsqueda de GitHub, p. cesionario:@yo es:abierto", - "1850fceac8": "{{value0}}/{{value1}} no se agrega a Orca. Agréguelo para comenzar a trabajar o ábralo en GitHub.", + "22df63c393": "Los datos de sub-issues no están disponibles para tu token.", + "067119985c": "Búsqueda de GitHub, p. ej. assignee:@me is:open", + "1850fceac8": "{{value0}}/{{value1}} no está agregado a Orca. Agrégalo para empezar a trabajar o ábrelo en GitHub.", "1aa7c952b9": "Vista del proyecto", "f352abf7c3": "La lista de repositorios se está actualizando.", "1ce21b8cff": "Este elemento está fuera de los repositorios seleccionados.", @@ -1955,15 +1955,15 @@ "dialog": { "AssigneesEditor": { "529fec247b": "Cargando…", - "98914e6b36": "Cesionarios:", + "98914e6b36": "Asignados:", "94a4e6e4fa": "ninguno" }, "Comments": { "fd5cccd138": "Comentario", "1c95937c8b": "Escribe un comentario…", "c0e576e96b": "Cancelar", - "c3e829b4d9": "Ahorrar", - "463d030ae4": "Borrar", + "c3e829b4d9": "Guardar", + "463d030ae4": "Eliminar", "8564f58542": "Editar", "5f104bf855": "Aún no hay comentarios.", "commentTooLarge": "El comentario es demasiado grande para enviarlo de forma segura." @@ -1977,11 +1977,11 @@ "598ad6a517": "Comentarios", "41169e41fb": "Añade una descripción…", "a91735d19f": "Cancelar", - "e64f6c3eff": "Ahorrar", + "e64f6c3eff": "Guardar", "e4ef8281e9": "Cargando…", - "ae98897edf": "Cerca", + "ae98897edf": "Cerrar", "69caf40ae8": "Abrir en GitHub", - "7c302f8174": "Intitulado" + "7c302f8174": "Sin título" } } } @@ -1990,7 +1990,7 @@ "015b4e607d": "Cancelar", "e3bd59143c": "Insertar", "f24783f470": "https://...", - "ec6310b731": "Utilice una URL de imagen http:// o https://.", + "ec6310b731": "Usa una URL de imagen http:// o https://.", "b7e4a1c902": "Pega, suelta o haz clic para agregar archivos", "8f1c2d4e6a": "No hay nada que previsualizar", "c91f0a2b14": "Escribir", @@ -1998,15 +1998,15 @@ "imageUrlTooLarge": "La URL de la imagen es demasiado grande." }, "IssueSourceSelector": { - "d6aeb2012b": "Mostrando problemas de", - "787c970baf": "Fuente del problema", - "643d7e9496": "río arriba", + "d6aeb2012b": "Mostrando issues de", + "787c970baf": "Fuente de issues", + "643d7e9496": "upstream", "51d1608920": "Origen", "cdc9bd64fa": "compacto", - "30b2c9df91": "Río arriba" + "30b2c9df91": "Upstream" }, "PRFilterDropdowns": { - "979be3cf6b": "Cesionario", + "979be3cf6b": "Asignado", "b27b7e526c": "Revisión de", "7f1ba66c3e": "Revisado por", "9d0f2eda6d": "Etiqueta", @@ -2014,11 +2014,11 @@ "13b3ac0a84": "Estado", "79c54552f7": "Filtros", "8a2ffbf9b3": "Quitar filtro {{value0}}", - "19bb6f115f": "revisado por" + "19bb6f115f": "reviewed-by" }, "PRFilterPickers": { - "fdf387297c": "Claro (", - "472c12ae03": "Claro", + "fdf387297c": "Limpiar (", + "472c12ae03": "Limpiar", "2d1f58eda6": "Usar" }, "PRFilterSections": { @@ -2027,24 +2027,24 @@ "94b42b0edf": "Revisión solicitada", "de26e2eb06": "Sin etiquetas", "458ea3602b": "Sin autores", - "b69fa4fa20": "Atrás", + "b69fa4fa20": "Volver", "30ebb6ca44": "Limpiar todos los filtros", "8177eda37e": "Filtrar", - "ea3416d646": "Cesionario", + "ea3416d646": "Asignado", "b1d9fdea08": "Etiqueta", "24754c44ad": "Autor", "764a0b4ce1": "Estado", - "f0cf6dd591": "apagado", - "1e9b5244f2": "en", + "f0cf6dd591": "desactivado", + "1e9b5244f2": "activado", "b930de7194": "Solo borrador", "e0002f1eba": "seleccionado", - "2b2f019091": "cualquier estado", + "2b2f019091": "Cualquier estado", "0fd3249e2e": "Cerrado", "d78b60b5c2": "Abierto", - "bd162b7d5a": "Fusionado", - "2e639b84fa": "crítico", + "bd162b7d5a": "Mergeado", + "2e639b84fa": "reviewer", "712c5abdbf": "etiqueta", - "4e50c7bc03": "cesionario", + "4e50c7bc03": "assignee", "7bf3a6e5ac": "autor", "66256a73b3": "estado", "3ce4d5e96e": "PRs" @@ -2058,14 +2058,14 @@ "d12d3d6f33": "Actualizar el presupuesto de la API de GitHub", "d5e5de9070": "Orca usa REST, Search y GraphQL a través de la CLI de GitHub.", "58c5f88216": "Presupuesto de la API de GitHub", - "6da1858354": "izquierda · se reinicia en", + "6da1858354": "restantes · se reinicia en", "f42790d150": "de", "01f7323e58": "API GraphQL", - "1daf0f22a9": "GrafoQL", + "1daf0f22a9": "GraphQL", "1f2f28a4de": "API de búsqueda", "c377a4f06a": "Buscar", - "c392c749a6": "API DESCANSO", - "bb227706a6": "DESCANSAR", + "c392c749a6": "API REST", + "bb227706a6": "REST", "budget_scope_prefix": "Ámbito del presupuesto" } } @@ -2089,9 +2089,9 @@ "commentTooLarge": "El comentario es demasiado grande para enviarlo de forma segura." }, "GitHubWorkItemAssigneePopoverContent": { - "cddd9b04a7": "Cargando responsables", + "cddd9b04a7": "Cargando asignados", "a00830d3f7": "No hay usuarios", - "4f8b6f2c1d": "Filtrar responsables..." + "4f8b6f2c1d": "Filtrar asignados..." }, "GitHubWorkItemLabelPopoverContent": { "2aa9acdf34": "Editar etiquetas en GitHub", @@ -2122,13 +2122,13 @@ "f8f704a019": "Cancelar", "e603ee9156": "Configuración de la API del espacio de trabajo", "dc7ccb0f7c": "Claves API personales", - "e3100b36b9": "Si las claves API de los miembros están bloqueadas, solicite a un administrador del espacio de trabajo que las permita desde la configuración de la API del espacio de trabajo.", - "d56d3629f4": "Prefiera el acceso completo cuando Orca debería mostrar todos los equipos a los que la cuenta puede acceder en ese espacio de trabajo. Las claves restringidas solo exponen los equipos permitidos y los equipos privados requieren que el propietario de la clave tenga acceso.", - "af52a6227f": "Cree una clave API personal desde Cuenta > Seguridad y acceso.", + "e3100b36b9": "Si las claves API de miembros están bloqueadas, pide a un administrador del workspace que las permita desde los ajustes de API del workspace.", + "d56d3629f4": "Prefiere acceso completo cuando Orca deba mostrar todos los equipos a los que la cuenta puede acceder en ese workspace. Las claves restringidas solo exponen equipos permitidos, y los equipos privados requieren que el owner de la clave tenga acceso.", + "af52a6227f": "Crea una clave API personal desde Account > Security & Access.", "edec49dfae": "lin_api_...", "7d498f653c": "Clave API personal", "57a66522c8": "conectando", - "c9889a09f8": "Utilice Linear para elegir el espacio de trabajo deseado antes de crear la clave.", + "c9889a09f8": "Usa Linear para elegir el workspace correcto antes de crear la clave.", "e689a4d0a6": "error" } } @@ -2142,13 +2142,13 @@ "view": { "surfaces": { "8bbecb2510": "Ninguno", - "e1fa97d21d": "Seleccione un proyecto para ver su descripción general.", + "e1fa97d21d": "Selecciona un proyecto para ver su resumen.", "1748d3b9af": "Etiquetas", "65bda65159": "Miembros", - "c5f79616c3": "equipos", + "c5f79616c3": "Equipos", "25a2196732": "Objetivo", - "3fb6473111": "Comenzar", - "111bef9aa8": "Dirigir", + "3fb6473111": "Inicio", + "111bef9aa8": "Lead", "3be47aed6f": "Prioridad", "f5ef24cf46": "Salud", "9ddb58edbd": "Estado", @@ -2156,17 +2156,17 @@ "c8db98b73b": "Recursos", "bb1405eff8": "Hitos", "5d99315fb8": "Planificación", - "3ad562bdf4": "problemas de alcance", + "3ad562bdf4": "issues con alcance", "563501f191": "Progreso", "bb5664d456": "Sin descripción del proyecto.", "7b147907dc": "Linear", - "a9785c7158": "Refrescar", - "ee3d2caabd": "Asuntos", + "a9785c7158": "Actualizar", + "ee3d2caabd": "Issues", "5f79bc76b0": "Volver a proyectos", "aac9a4afc6": "Abrir en Linear", - "7616c986c6": "Problemas abiertos {{value0}}", + "7616c986c6": "Abrir issues de {{value0}}", "93e1f6bfca": "Cargando", - "98730088a6": ". Busque o abra Linear para ver el conjunto completo.", + "98730088a6": ". Busca o abre Linear para ver el conjunto completo.", "06b887d622": "Mostrando primero", "2c4b1c2c08": "número", "f2cc1e0ff6": "Linear / Proyectos", @@ -2175,14 +2175,14 @@ "20b9d09b7d": "Desconocido", "f059181bd9": "Privado", "27d91cb1a6": "Compartido", - "9f0f51fd9e": "Cree o guarde vistas en Linear y luego actualice.", - "f4c79cff5f": "Revise el error del espacio de trabajo a continuación y luego actualice.", + "9f0f51fd9e": "Crea o guarda vistas en Linear y luego actualiza.", + "f4c79cff5f": "Revisa el error del espacio de trabajo abajo y luego actualiza.", "ef90b21366": "No se encontraron vistas", "c0a50f96a4": "No se pueden cargar vistas", "df4bd63c1d": "No asignado", - "30402d2c6e": "Intente buscar o actualizar.", - "a2f31c4cd6": "No se encontraron proyectos Lineares", - "c9b6e9f90d": "No se pueden cargar proyectos Lineares" + "30402d2c6e": "Intenta buscar o actualizar.", + "a2f31c4cd6": "No se encontraron proyectos de Linear", + "c9b6e9f90d": "No se pueden cargar proyectos de Linear" } } }, @@ -2190,12 +2190,12 @@ "selector": { "91c8871dad": "Agregar acceso al equipo", "7783361266": "Todos los equipos", - "e1ae6bebb0": "equipos", + "e1ae6bebb0": "Equipos", "a14ce4df2b": "Todos los espacios de trabajo", "05baa5ae90": "Espacio de trabajo", - "89f6580dbf": "Equipos de búsqueda...", - "b3488fad3c": "No se buscó ningún equipo. El acceso puede depender del alcance de la clave, la membresía de un equipo privado, los equipos archivados, los permisos o un error de recuperación.", - "405b33c378": "Ningún equipo encontrado coincide con su búsqueda." + "89f6580dbf": "Buscar equipos...", + "b3488fad3c": "No se obtuvo ningún equipo. El acceso puede depender del scope de la clave, membresía en equipos privados, equipos archivados, permisos o un error al obtener datos.", + "405b33c378": "Ningún equipo obtenido coincide con tu búsqueda." } } }, @@ -2203,14 +2203,14 @@ "sound": { "options": { "e38b0a2e68": "Bip", - "0acd3d384e": "Charla", - "79919c832d": "Timbre", + "0acd3d384e": "Clack", + "79919c832d": "Ding", "2b44847d8d": "Blop", "020826ef17": "Sonar", - "588c90487d": "Punto luminoso en un radar", - "1e4b81d892": "Golpear", - "86af8d938c": "bong", - "80f7cc95b3": "dos tonos", + "588c90487d": "Blip", + "1e4b81d892": "Thump", + "86af8d938c": "Bong", + "80f7cc95b3": "Dos tonos", "017abebfa6": "Valor predeterminado del sistema" } } @@ -2218,77 +2218,77 @@ "worktree": { "creation": { "WorktreeCreationPanel": { - "dabd226118": "Despedir", - "34dd5ee38b": "Rever", - "ed2a664f8b": "No se pudo crear el árbol de trabajo", - "a3346fc6ed": "Cancelar la creación del árbol de trabajo", + "dabd226118": "Descartar", + "34dd5ee38b": "Reintentar", + "ed2a664f8b": "No se pudo crear el worktree", + "a3346fc6ed": "Cancelar creación del worktree", "532aea14ce": "Cancelar", - "767951265d": "Algo salió mal al crear el árbol de trabajo.", - "vmProvisioningTitle": "Aprovisionamiento de máquina virtual", + "767951265d": "Algo salió mal al crear el worktree.", + "vmProvisioningTitle": "Provisionando VM", "cancelProvisioning": "Cancelar", - "vmProvisioningLogEmpty": "Esperando la salida de la receta..." + "vmProvisioningLogEmpty": "Esperando output de la receta..." } } }, "workspace": { "space": { "WorkspaceSpacePage": { - "8d0048e1cb": "Uso del disco del espacio de trabajo y almacenamiento recuperable del árbol de trabajo.", + "8d0048e1cb": "Uso de disco del espacio de trabajo y almacenamiento de worktrees recuperable.", "e8d6ba11ab": "Beta", "45f6302dbc": "Espacio", - "ecf72fdc3b": "Atrás" + "ecf72fdc3b": "Volver" } }, "cleanup": { "WorkspaceCleanupDialog": { "3828408538": "Quitar {{value0}}", "352f15d6fc": "Último activo", - "cbf2f664e2": "Borrar", + "cbf2f664e2": "Eliminar", "b6bae1eed1": "Cancelar", "592fbab446": "Ordenado por actividad más antigua", "dba753e94f": "eliminar", "38ca0b1400": "Esto elimina permanentemente sus archivos locales. No puedes deshacer esto.", "c4f4782c02": "No sugerido", "0a2e3c7cba": "Revisar", - "e97e4580c7": "Sucio", - "9623a5107d": "commits no enviadas", + "e97e4580c7": "Dirty", + "9623a5107d": "commits sin push", "e8b3741ff7": "ignorado", "a9957007eb": "Ignorar {{value0}}", "1bffc07ba7": "Ver {{value0}}", "bef0adef9b": "Rama", - "0b1766738a": "Repo", - "bbb1ab6a6f": "Seleccione {{value0}}", + "0b1766738a": "Repositorio", + "bbb1ab6a6f": "Selecciona {{value0}}", "d1094dd529": "Necesita revisión", "4b93a235d8": "sugerido", "f68d538c63": "No hay espacios de trabajo en este conjunto de limpieza.", "4719327c9c": "Se ignoran todas las sugerencias de limpieza.", - "a19040cd67": "Ningún espacio de trabajo inactivo coincide con los repos seleccionados.", + "a19040cd67": "Ningún espacio de trabajo inactivo coincide con los repositorios seleccionados.", "97c772c4fe": "No se encontraron espacios de trabajo inactivos en los repositorios marcados.", "d3eef9463d": "No hay espacios de trabajo inactivos para eliminar.", "aaee139eab": "Restaurar sugerencias ignoradas", "06cf78521e": "Seleccionar todo en {{value0}}", "73690b0031": "Deseleccionar todo en {{value0}}", - "b771c92598": "Eliminar seleccionado", + "b771c92598": "Eliminar seleccionados", "37ab28277e": "no sugerido", "1b18868569": "necesita revisión", "b299f201b9": "seguro para eliminar", "2b31bf68de": "inactivo", "ac5ba84cc1": "seleccionado", - "8b74d4ea6e": "Escaneando árboles de trabajo y estado de git, luego combinando señales de pestaña abierta, terminal, agente en vivo y disponibilidad remota antes de sugerir eliminaciones.", - "7eee951968": "Comprobar la seguridad del lugar de trabajo", - "191f0bc98e": "Cerca", - "7ae2ad30f4": "Refrescar", - "e0b5a4deaa": "Revise los espacios de trabajo inactivos antes de eliminar sus archivos locales y el estado de Orca.", + "8b74d4ea6e": "Escaneando worktrees y estado de git, luego combinando señales de pestañas abiertas, terminal, agente en vivo y disponibilidad remota antes de sugerir eliminaciones.", + "7eee951968": "Verificando seguridad del espacio de trabajo", + "191f0bc98e": "Cerrar", + "7ae2ad30f4": "Actualizar", + "e0b5a4deaa": "Revisa los espacios de trabajo inactivos antes de eliminar sus archivos locales y el estado de Orca.", "b2c1331844": "Eliminar espacios de trabajo inactivos", - "41d594d01e": "{{value0}} espacio de trabajo {{value1}} no se pudo eliminar", + "41d594d01e": "No se pudo eliminar {{value0}} espacio de trabajo{{value1}}", "0f00612b6d": "Se eliminó {{value0}} espacio de trabajo{{value1}}", "7f451a3e2c": "No se pudo ignorar la sugerencia de limpieza", - "662b8ec3f8": "Error en el análisis de limpieza del espacio de trabajo", + "662b8ec3f8": "Falló el escaneo de limpieza del espacio de trabajo", "bc43c37faf": "oculto", "0c6672f5e3": "Sugerencias de limpieza ignoradas", - "fc49f79434": "mezclado", - "2ddbd6fe8a": "comprobado", - "ee81adfcef": "Vista", + "fc49f79434": "mixto", + "2ddbd6fe8a": "marcado", + "ee81adfcef": "Ver", "4d0b72481c": "Ignorar", "9cc26c019d": "Eliminar", "0e2d235c63": "Escaneo del espacio de trabajo inactivo listo", @@ -2299,12 +2299,12 @@ "e94b1f8bb4": "Limpiar filtros", "efb3843e75": "Filtrar y ordenar espacios de trabajo", "93b7381d50": "Filtros", - "a615e24679": "Clasificar", - "4cc5b73efe": "Encontrar espacios de trabajo inactivos...", - "5bf2e88480": "{{value0}}/{{value1}} {{value2}} escaneado", + "a615e24679": "Ordenar", + "4cc5b73efe": "Buscando espacios de trabajo inactivos...", + "5bf2e88480": "{{value0}}/{{value1}} {{value2}} escaneados", "searchPlaceholder": "Buscar espacios de trabajo", "ageFilter": "Edad", - "reviewFilter": "Revisar", + "reviewFilter": "Revisión", "gitFilter": "Git", "contextFilter": "Contexto", "sortBy": "Ordenar por", @@ -2316,18 +2316,18 @@ "color": { "picker": { "ebcf6ba29e": "Color hexadecimal no válido.", - "faa855a582": "Maleficio", + "faa855a582": "Hex", "1cec618bcc": "{{value0}} selector" } }, "dialog": { - "f26c4baeda": "Cerca" + "f26c4baeda": "Cerrar" }, "repo": { "multi": { "combobox": { "286ce70256": "SSH", - "4471d4a1c0": "Ningún proyecto coincide con su búsqueda.", + "4471d4a1c0": "Ningún proyecto coincide con tu búsqueda.", "bfd8ce21c6": "Todos los proyectos", "a58a0cd100": "Buscar proyectos...", "65a3dae41d": "Sin proyectos" @@ -2335,14 +2335,14 @@ } }, "sheet": { - "1189e9fe0a": "Cerca" + "1189e9fe0a": "Cerrar" }, "team": { "multi": { "combobox": { "de83523bf9": "Ningún equipo coincide con tu búsqueda.", "301f2a796e": "Todos los equipos", - "18ec58881e": "Equipos de búsqueda..." + "18ec58881e": "Buscar equipos..." } } } @@ -2351,33 +2351,33 @@ "quick": { "commands": { "TerminalQuickCommandActionToggle": { - "b0d58e37ed": "Mensaje del Agent", - "b5ea4d64f6": "Comando terminal" + "b0d58e37ed": "Prompt de agente", + "b5ea4d64f6": "Comando de terminal" }, "TerminalQuickCommandAppendEnterSwitch": { - "e4e5fed3b3": "Alternar agregar Ingresar", - "c936c2d6d2": "Envíelo inmediatamente en lugar de solo insertar texto.", - "5fa607d807": "Agregar Ingresar" + "e4e5fed3b3": "Alternar agregar Enter", + "c936c2d6d2": "Enviar inmediatamente en lugar de solo insertar texto.", + "5fa607d807": "Agregar Enter" }, "TerminalQuickCommandDialog": { "925b8e0f6e": "Avanzado", - "97e96cc027": "/meta", - "e604bd40d6": "Admite habilidades, rutas de archivos y comandos integrados como", - "79af0c0841": "npm ejecutar desarrollador", - "577a342c7d": "Pídale al agente que investigue este espacio de trabajo.", - "026cfb232a": "No admite comandos rápidos", + "97e96cc027": "/goal", + "e604bd40d6": "Admite skills, rutas de archivos y comandos integrados como", + "79af0c0841": "npm run dev", + "577a342c7d": "Pide al agente que investigue este espacio de trabajo", + "026cfb232a": "No admite comandos de prompt", "346d409ab2": "Elige agente", "0adba8fa0c": "Agente", "ec8f081919": "Acción", - "ed04233b3e": "Guarde los comandos del terminal o las indicaciones del agente para un acceso rápido.", + "ed04233b3e": "Guarda comandos de terminal o prompts de agente para acceso rápido.", "ca414324ee": "Texto de comando", - "dc921c17ee": "Inmediato", + "dc921c17ee": "Prompt", "5b3f634a55": "Agregar comando rápido", "f9b184fc16": "Editar comando rápido", "6751598542": "editar" }, "TerminalQuickCommandDialogFooter": { - "2e2b958dfc": "Ahorrar", + "2e2b958dfc": "Guardar", "8dff838dea": "Guardar ({{value0}})", "28370f16b9": "Cancelar" }, @@ -2386,21 +2386,21 @@ "db17f1e41e": "Etiqueta" }, "TerminalQuickCommandScopeField": { - "2db6edede7": "Al guardar se mantiene el alcance del proyecto existente a menos que elija otro.", + "2db6edede7": "Al guardar se mantiene el alcance del proyecto existente a menos que elijas otro.", "2496523a6f": "Elige proyecto", "2264edd5d3": "Proyecto no en la lista", "3834d24243": "Proyecto", "b83efc79e2": "Global", "c25cf350ef": "Alcance", - "f0631e4999": "repo" + "f0631e4999": "repositorio" } } }, "pane": { "CloseTerminalDialog": { - "ebd2fa844d": "Cerca", + "ebd2fa844d": "Cerrar", "1d1a7a9c1f": "Cancelar", - "6b9a6975f8": "La terminal todavía tiene un proceso en ejecución. Si cierra la terminal, el proceso finalizará.", + "6b9a6975f8": "El terminal aún tiene un proceso en ejecución. Si cierras el terminal, el proceso se terminará.", "78b79d854d": "¿Cerrar terminal?", "stop_agent_title": "¿Detener este agente?", "stop_command_title": "¿Detener comando en ejecución?", @@ -2411,27 +2411,27 @@ "stop_command_confirm": "Detener y cerrar" }, "MobileDriverOverlay": { - "c6460cf584": "Devolver", - "c8f2e1a4b9": "Recuperar este terminal", - "c44659e09f": "Teléfono al mando", - "7cffad954c": "Colapsar", + "c6460cf584": "Recuperar control", + "c8f2e1a4b9": "Recuperar control de este terminal", + "c44659e09f": "Teléfono controlando", + "7cffad954c": "Contraer", "3eed73394f": "Tu teclado está en pausa", "faa367dc74": "Tu teléfono dejó esto en tamaño móvil", - "54f7d6f69d": "Recuperar todos los terminales", + "54f7d6f69d": "Recuperar control de todos los terminales", "b3d8e1f42a": "Restaurar este terminal", "e8c4f2a91b": "Restaurar todos los terminales", "f2a8b9c1d3": "Desde tu teléfono", - "c7e4a2b8f1": "Tu teléfono está al mando", - "d9f3c6e2a4": "El teclado del escritorio está en pausa. Recupera este terminal para escribir aquí, recupera todos los terminales que controla tu teléfono, o colapsa para seguir mirando.", + "c7e4a2b8f1": "Tu teléfono tiene el control", + "d9f3c6e2a4": "El teclado de escritorio está en pausa. Recupera este terminal para escribir aquí, recupera todos los terminales que controla tu teléfono o contrae para seguir observando.", "a6b1d8f3e2": "La sesión del teléfono terminó. Restaura el tamaño de escritorio para este terminal o para todos los terminales que tu teléfono dejó en tamaño móvil." }, "TerminalAgentSessionForkDialog": { "17fc841e59": "Copiar contexto", - "0c8a8629b1": "La bifurcación aparece como su propio espacio de trabajo, no como un hijo anidado. El nuevo agente recibe una transcripción encuadernada como borrador editable.", - "620461df22": "Bifurcación de nivel superior", - "619b5a35d2": "Cree una bifurcación del espacio de trabajo de nivel superior e inicie una nueva pestaña de agente con contexto capturado.", - "64e292e8e3": "Sesión de Agent de bifurcación", - "9d25de2920": "crear bifurcación", + "0c8a8629b1": "El fork aparece como su propio espacio de trabajo, no como un hijo anidado. El nuevo agente recibe una transcripción acotada como borrador editable.", + "620461df22": "Fork de nivel superior", + "619b5a35d2": "Crea un fork de espacio de trabajo de nivel superior e inicia una nueva pestaña de agente con el contexto capturado.", + "64e292e8e3": "Fork de sesión de agente", + "9d25de2920": "Crear fork", "2b10412cfc": "Creando..." }, "TerminalContextMenu": { @@ -2442,26 +2442,26 @@ "39809d152f": "Establecer título…", "clearPaneTitle": "Borrar título del panel", "06c2b0f043": "Igualar tamaños de paneles", - "98bccf4fa2": "Terminal dividido hacia abajo", - "20e565d865": "Terminal dividido a la derecha", - "8a7ddb8b8a": "Sesión de Agent de bifurcación...", + "98bccf4fa2": "Dividir terminal hacia abajo", + "20e565d865": "Dividir terminal a la derecha", + "8a7ddb8b8a": "Fork de sesión de agente...", "0a82b0608c": "Agregar comando rápido…", "9528a65ef8": "Sin comandos rápidos", "3ce594a4a0": "Global", "ec85df5914": "Comandos rápidos", - "0a917b591a": "Pasta", + "0a917b591a": "Pegar", "f3eeb1de13": "Copiar", "c2f0b72b8d": "Insertar", "openLinkDefaultBrowser": "Abrir en el navegador predeterminado", "925f49f210": "Expandir panel", "df766809e0": "Contraer panel", - "cff67afad1": "Copy Context" + "cff67afad1": "Copiar contexto" }, "TerminalErrorToast": { - "e4aa243f8c": "Reiniciar demonio", - "a7e2fd2699": "presentar un problema", - "5c8ce20be6": "Si esto persiste por favor", - "cc6d997c65": "Reinicie el demonio del terminal desde aquí para borrar el estado obsoleto del demonio." + "e4aa243f8c": "Reiniciar servicio", + "a7e2fd2699": "abre un issue", + "5c8ce20be6": "Si esto persiste, por favor", + "cc6d997c65": "Reinicia el servicio del terminal desde aquí para borrar el estado obsoleto." }, "TerminalPane": { "ac112e9036": "Quitar título", @@ -2470,19 +2470,19 @@ "7dbbfcbecc": "Título del panel" }, "TerminalSessionStateSaveFailureDialog": { - "6bee0c8f17": "Analizador de espacio en disco abierto", - "ae20d0ffc2": "Despedir", - "38c282a2c4": "El analizador se abre directamente desde aquí. También puede abrirlo más tarde desde el menú de la caja de herramientas inferior izquierda eligiendo Space Analyzer.", - "e2fcf07c0d": "Orca no pudo guardar esta sesión de terminal porque el almacenamiento local está lleno o no se puede escribir. Abra el analizador de espacio en disco para encontrar espacio de almacenamiento que pueda limpiar.", + "6bee0c8f17": "Abrir analizador de espacio en disco", + "ae20d0ffc2": "Descartar", + "38c282a2c4": "El analizador se abre directamente desde aquí. También puedes abrirlo más tarde desde el menú de herramientas inferior izquierdo eligiendo Space Analyzer.", + "e2fcf07c0d": "Orca no pudo guardar esta sesión de terminal porque el almacenamiento local está lleno o no permite escritura. Abre el analizador de espacio en disco para encontrar almacenamiento de espacios de trabajo que puedas limpiar.", "678c780a2c": "El espacio en disco no está disponible" }, "osc52": { "clipboard": { "blocked": { "toast": { - "97c98f1afe": "Abrir configuración", - "7cf51f74fd": "Habilite las escrituras en el portapapeles TUI en la configuración de Terminal para copiar desde SSH, tmux, Neovim o fzf.", - "89eaa3e80b": "Escritura en el portapapeles del terminal bloqueada" + "97c98f1afe": "Abrir ajustes", + "7cf51f74fd": "Habilita las escrituras al portapapeles TUI en los ajustes de Terminal para copiar desde SSH, tmux, Neovim o fzf.", + "89eaa3e80b": "Escritura al portapapeles del terminal bloqueada" } } } @@ -2490,8 +2490,8 @@ "stale": { "agent": { "row": { - "ad991ece5c": "El panel del Agent ya no está disponible.", - "090d607412": "fila-agente-obsoleto-{{value0}}" + "ad991ece5c": "El panel del agente ya no está disponible.", + "090d607412": "stale-agent-row-{{value0}}" } } }, @@ -2499,13 +2499,13 @@ "agent": { "session": { "fork": { - "2317900211": "No se pudo copiar el contexto de la bifurcación.", - "88e34d00eb": "Bifurcación de sesión de nivel superior abierta en un nuevo espacio de trabajo", - "fd3d12a1e1": "No se pudo crear el espacio de trabajo de bifurcación.", - "38e41edc6e": "Este espacio de trabajo no se puede bifurcar en un árbol de trabajo de git.", - "f867385bb5": "No se pudo encontrar el espacio de trabajo de origen para esta bifurcación.", - "046e8d853c": "No hay contexto de terminal para bifurcar", - "c00421d320": "Se copió el contexto de la bifurcación. Inicie un agente y péguelo para iniciar la bifurcación.", + "2317900211": "No se pudo copiar el contexto del fork.", + "88e34d00eb": "Fork de sesión de nivel superior abierto en un nuevo espacio de trabajo", + "fd3d12a1e1": "No se pudo crear el espacio de trabajo del fork.", + "38e41edc6e": "Este espacio de trabajo no puede hacer fork hacia un worktree de git.", + "f867385bb5": "No se pudo encontrar el espacio de trabajo de origen para este fork.", + "046e8d853c": "No hay contexto de terminal para hacer fork", + "c00421d320": "Se copió el contexto del fork. Inicia un agente y pégalo para iniciar el fork.", "f62b40e2c7": "No hay contexto de terminal para copiar", "373a3103e7": "Contexto copiado", "3fc568a49d": "No se pudo copiar el contexto." @@ -2514,16 +2514,16 @@ }, "drop": { "handler": { - "1e072f611e": "No se pudo cargar {{value0}} {{value1}}.", - "53f015fd85": "Se omitió el enlace simbólico {{value0}}{{value1}}.", - "29c031b49a": "Subiendo el archivo {{value0}}{{value1}} al tiempo de ejecución...", - "0c77693641": "Worktree no está listo; inténtalo de nuevo en un momento.", - "ce8248b835": "Ruta del árbol de trabajo no disponible.", - "internalTooManyPaths": "La operación contiene demasiadas rutas para pegarlas de forma segura en la terminal.", - "internalPathsTooLarge": "La lista de rutas es demasiado grande para pegarla de forma segura en la terminal.", + "1e072f611e": "No se pudo subir {{value0}} {{value1}}.", + "53f015fd85": "Se omitió {{value0}} symlink{{value1}}.", + "29c031b49a": "Subiendo {{value0}} archivo{{value1}} al host…", + "0c77693641": "El worktree no está listo; inténtalo de nuevo en un momento.", + "ce8248b835": "Ruta del worktree no disponible.", + "internalTooManyPaths": "El drop contiene demasiadas rutas para pegarlas de forma segura en el terminal.", + "internalPathsTooLarge": "La lista de rutas del drop es demasiado grande para pegarla de forma segura en el terminal.", "b4cf68e889": "Se omitió {{value0}} {{value1}}.", - "writeTimeout": "Se canceló la suelta de archivos: la terminal no aceptó la ruta antes del tiempo de seguridad.", - "writeRejected": "Se canceló la suelta de archivos: la terminal no pudo aceptar la ruta." + "writeTimeout": "Se canceló el drop de archivos: el terminal no aceptó la ruta antes del tiempo límite de seguridad.", + "writeRejected": "Se canceló el drop de archivos: el terminal no pudo aceptar la ruta." } } }, @@ -2571,7 +2571,7 @@ "TabGroupPanel": { "814fb04c43": "Cargando editor...", "f7d6ce445e": "Cerrar grupo", - "0db2081805": "Partir", + "0db2081805": "Dividir hacia arriba", "30137df7d0": "Dividir a la izquierda", "4df2a06d36": "Dividir hacia abajo", "ab1e2bff04": "Dividir a la derecha", @@ -2588,9 +2588,9 @@ "localWorkspacesOnly": "Reanudar desde el historial solo está disponible en espacios de trabajo locales.", "openLocalWorkspace": "Abre un espacio de trabajo local antes de reanudar una sesión.", "sessionQueued": "Sesión en cola", - "runtimeWorkspacesUnsupported": "Resume from history is not available in runtime-hosted workspaces.", - "openSupportedWorkspace": "Open a local or SSH workspace before resuming a session.", - "localSessionSshWorkspaceUnsupported": "El historial de esta sesión está almacenado en este equipo, por lo que no se puede reanudar en un espacio de trabajo SSH. En su lugar, suéltala en un espacio de trabajo local." + "runtimeWorkspacesUnsupported": "Reanudar desde el historial no está disponible en espacios de trabajo alojados en un host.", + "openSupportedWorkspace": "Abre un espacio de trabajo local o SSH antes de reanudar una sesión.", + "localSessionSshWorkspaceUnsupported": "El historial de esta sesión solo existe en este equipo. Para reanudarla, arrástrala a un workspace local. Los workspaces SSH no tienen acceso a ese historial." }, "TabGroupDropOverlay": { "paneColumnLabel": "Nueva división" @@ -2600,13 +2600,13 @@ "BrowserTab": { "6e0bc8f3a8": "Abrir en el navegador", "9dd880bd56": "Cerrar pestañas a la derecha", - "1611a1324b": "Cerca", + "1611a1324b": "Cerrar", "5d6e89891f": "Duplicar pestaña", "966feb9ad5": "Dividir a la derecha", "7e8106899f": "Dividir a la izquierda", "2186a8407c": "Dividir hacia abajo", - "96354ed249": "Partir", - "911542656f": "Pestaña Fijar", + "96354ed249": "Dividir hacia arriba", + "911542656f": "Fijar pestaña", "c5aaee8c39": "Desanclar pestaña" }, "EditorFileTab": { @@ -2615,24 +2615,24 @@ "EditorFileTabContextMenu": { "52ce4f4605": "Copiar ruta relativa", "5b85754786": "Copiar ruta", - "bfd5797ef4": "Abrir vista previa de Markdowns", + "bfd5797ef4": "Abrir vista previa de Markdown", "e5ff31ccaf": "Cerrar pestañas a la derecha", "ba1369dd24": "Cerrar todas las pestañas del editor", - "1ba8492c5b": "Cerca", - "68cc610e7f": "Rebautizar", + "1ba8492c5b": "Cerrar", + "68cc610e7f": "Renombrar", "f7c3d7d5af": "Dividir a la derecha", "e3ff145b98": "Dividir a la izquierda", "1d04b1630b": "Dividir hacia abajo", - "6b3efb106e": "Partir", - "fdd29eb669": "Pestaña Fijar", + "6b3efb106e": "Dividir hacia arriba", + "fdd29eb669": "Fijar pestaña", "8e9d603a09": "Desanclar pestaña" }, "QuickLaunchButton": { - "348a04c1ad": "Configuración del Agent...", - "ec2adf093e": "Inicie {{value0}} en una nueva terminal", + "348a04c1ad": "Ajustes de Agents…", + "ec2adf093e": "Iniciar {{value0}} en un nuevo terminal", "465e432ef1": "No se pudo generar el comando de inicio para {{value0}}.", - "e518f544b1": "No se detectaron agents", - "8dea9b5cdf": "No hay agents habilitados" + "e518f544b1": "No se detectaron agentes", + "8dea9b5cdf": "No hay agentes habilitados" }, "RecentTabSwitcher": { "329638ff6f": "Cambiar pestaña", @@ -2648,14 +2648,14 @@ "35e8892fd0": "Color de pestaña", "2f697b3c31": "Cambiar título", "c1ee099c7e": "Cerrar pestañas a la derecha", - "8d16f9cd30": "Cerrar Otros", - "89359a36f7": "Cerca", + "8d16f9cd30": "Cerrar otras", + "89359a36f7": "Cerrar", "21132389e9": "Dividir a la derecha", "0ce4bae39d": "Dividir a la izquierda", "af80ed83c1": "Dividir hacia abajo", - "591f9b12c1": "Partir", + "591f9b12c1": "Dividir hacia arriba", "7703990447": "Gris", - "845576bed1": "verde azulado", + "845576bed1": "Verde azulado", "be905e9b0a": "Verde", "69682e2ce4": "Amarillo", "a47629b3cf": "Naranja", @@ -2664,59 +2664,59 @@ "c2d8b0991f": "Púrpura", "cb3eadefd2": "Azul", "20baa43c05": "Ninguno", - "60f958ec75": "Pestaña Fijar", + "60f958ec75": "Fijar pestaña", "417722e9c2": "Desanclar pestaña", "splitTerminalRight": "Dividir terminal a la derecha", "splitTerminalDown": "Dividir terminal hacia abajo" }, "TabBar": { "b1a132357f": "Nueva pestaña", - "4f327c8b3d": "Abrir Markdowns...", - "3d5d6c960d": "Nueva Markdown", + "4f327c8b3d": "Abrir Markdown...", + "3d5d6c960d": "Nuevo Markdown", "fd2b42aaa3": "Nuevo emulador móvil", - "aea43b5748": "Abra la pestaña del emulador existente.", + "aea43b5748": "Abrir la pestaña del emulador existente.", "b426bb2615": "Ir al emulador móvil", - "4833fb2cbe": "Nueva pestaña del navegador", - "d364f3c8d4": "Nueva Terminal", - "7c1313d237": "Nueva terminal:", + "4833fb2cbe": "Nueva pestaña de navegador", + "d364f3c8d4": "Nuevo terminal", + "7c1313d237": "Nuevo terminal:", "d1afac112b": "WSL", - "efb33546ff": "Git bash", - "1a8af49530": "Mensaje CMD", + "efb33546ff": "Git Bash", + "1a8af49530": "CMD Prompt", "2148f65e04": "PowerShell", "ab589350e5": "No se pudo generar el comando de inicio para {{value0}}.", "7a9b4af2af": "Desplazar pestañas a la izquierda", "232e075b07": "Desplazar pestañas a la derecha" }, "TabBarCreateEntry": { - "d62d63b807": "crear archivo", + "d62d63b807": "Crear archivo", "25dc1cd653": "Abrir archivo", "7cdf8ee0c8": "Abrir URL", - "b27864279e": "Agente de lanzamiento", - "39676a184c": "Abra cualquier archivo, URL, agente, ..." + "b27864279e": "Iniciar agente", + "39676a184c": "Abrir cualquier archivo, URL, agente, ..." }, "TabBarQuickCommandsButton": { "a2c7a33831": "Comando", "20bbd75896": "Sin comandos", - "b82e237a4b": "Comandos más rápidos", + "b82e237a4b": "Más comandos rápidos", "85482c57bc": "Ejecutar comando rápido", "b775303755": "Ejecutar comando rápido: {{value0}}", "196593b6a9": "Quitar {{value0}}", "15529ede69": "Editar {{value0}}", - "1d411fb6a5": "Guarde un comando rápido para este repo", + "1d411fb6a5": "Guarda un comando rápido para este repositorio", "8f1e971966": "Agregar comando rápido", - "3220e2da27": "Este comando rápido se eliminará de su lista guardada.", + "3220e2da27": "Este comando rápido se eliminará de tu lista guardada.", "e8e1a52edb": "¿Eliminar \"{{value0}}\"?", "37e1bb90ce": "Ejecutar: {{value0}}", - "77ac113df0": "Inicio {{value0}}: {{value1}}", - "7b1c9d6ae1": "Correr", + "77ac113df0": "Iniciar {{value0}}: {{value1}}", + "7b1c9d6ae1": "Ejecutar", "c781f992e4": "destructivo", - "be8f0ff166": "Borrar", + "be8f0ff166": "Eliminar", "f3a8c2d1e7": "Buscar comandos rápidos...", "b4e7f9a2c1": "No hay comandos coincidentes" }, "shell": { "icons": { - "d4ceaa227c": "git", + "d4ceaa227c": "Git", "e9b2e70613": "WSL" } }, @@ -2726,33 +2726,33 @@ "classifier": { "42e6262ae9": "No hay acción disponible.", "097a982ee0": "Cargando archivos...", - "5a9c83c04b": "Abra cualquier archivo, URL, agente, ...", - "90eb94dc48": "Introduzca una URL http:// o https://.", - "5553b283ce": "Ingrese una URL o ruta de archivo.", + "5a9c83c04b": "Abrir cualquier archivo, URL, agente, ...", + "90eb94dc48": "Introduce una URL http:// o https://.", + "5553b283ce": "Introduce una URL o ruta de archivo.", "queryTooLarge": "El texto de búsqueda es demasiado grande." } }, "menu": { "options": { "5501c2fb7a": "terminal", - "9630dd5494": "caparazón", - "a094576900": "nueva terminal", - "4f23f4d01d": "nuevo caparazón", + "9630dd5494": "shell", + "a094576900": "nuevo terminal", + "4f23f4d01d": "nueva shell", "4f2a91e15b": "navegador", "6d0e6a4b7a": "nuevo navegador", "c87ad57785": "pestaña del navegador", "cce7ef1d2c": "web", "5f17fb9d0c": "markdown", - "44caaf7b36": "Maryland", - "fb50e3d874": "nueva markdown", + "44caaf7b36": "md", + "fb50e3d874": "nuevo markdown", "6d8b6b4117": "nuevo archivo", - "b330f72434": "marca", - "37ff3ddca1": "markdown abierta", + "b330f72434": "mark", + "37ff3ddca1": "abrir markdown", "164c394bab": "abrir archivo", "bbaf4f85a4": "emulador móvil", "3784b83bd4": "emulador", "a63847a742": "simulador", - "1baeb07c17": "simulador de ios", + "1baeb07c17": "simulador de iOS", "8a580f88cf": "iPhone", "7ecdc5ef08": "iPad", "14965cc123": "móvil" @@ -2777,20 +2777,20 @@ "bar": { "PetStatusSegment": { "3668339495": "Quitar {{value0}}", - "cd8c6c654c": "Configuración de mascotas…", - "ed176ad68f": "Importar paquete .codex-pet…", - "59b5955621": "Sube el tuyo…", + "cd8c6c654c": "Ajustes de mascota…", + "ed176ad68f": "Importar bundle .codex-pet…", + "59b5955621": "Subir el tuyo…", "0608ad02a2": "Elige mascota", "b75484a01a": "Tamaño de mascota", "c6aa805b1b": "píxeles", "2f7bbaa457": "Tamaño", "34c25dfe9c": "Mascota", "aec479308a": "Menú de mascotas", - "cef0ab4636": "No se pudo importar el paquete de mascotas", - "2021d4f6db": "La importación de paquetes de mascotas necesita reiniciar completamente la aplicación (no solo recargarla).", + "cef0ab4636": "No se pudo importar el bundle de mascota", + "2021d4f6db": "Importar un bundle de mascota requiere reiniciar la app por completo (no solo recargarla).", "f395c9a685": "No se pudo importar el archivo", - "e6234bcc17": "La carga de mascotas personalizada necesita reiniciar la aplicación por completo (no solo recargarla).", - "6d0a8cd179": "mostrar mascota", + "e6234bcc17": "Subir una mascota personalizada requiere reiniciar la app por completo (no solo recargarla).", + "6d0a8cd179": "Mostrar mascota", "1fbc51cc77": "Ocultar mascota" }, "PortsStatusSegment": { @@ -2812,52 +2812,52 @@ }, "ResourceUsageStatusSegment": { "946d9f94d0": "Cancelar", - "67c4ecda49": "Forzar el cierre de esta terminal. Cualquier trabajo no guardado en el panel se perderá. Esto no se puede deshacer.", - "4bb076fa89": "Matar", - "996295bff2": "terminal huérfana", + "67c4ecda49": "Fuerza el cierre de este terminal. Cualquier trabajo sin guardar en el panel se perderá. Esto no se puede deshacer.", + "4bb076fa89": "Terminar", + "996295bff2": "terminal huérfano", "92924a14e3": "Revisar espacios de trabajo inactivos ({{value0}})", - "27a74f91f0": "Nada funcionando ahora mismo", + "27a74f91f0": "No hay nada ejecutándose ahora mismo", "1b24a32d3a": "Memoria", - "298f4be7f2": "UPC", + "298f4be7f2": "CPU", "2aa2de6cb9": "Nombre", "30ff2c3c31": "{{value0}} huérfano", - "6449a95c78": "En qué cantidad de RAM física de esta máquina se encuentran los procesos rastreados por Orca.", + "6449a95c78": "Cuánta RAM física de esta máquina están usando los procesos que Orca rastrea.", "e7ccce7e87": "de RAM del sistema", - "9e2525c89f": "Memoria residente en poder de Orca más los procesos bajo las terminales de cada árbol de trabajo.", - "1fedf94eae": "Carga de CPU combinada. Los valores superiores al 100% significan que más de un núcleo está funcionando a la vez.", + "9e2525c89f": "Memoria residente de Orca más los procesos bajo los terminales de cada worktree.", + "1fedf94eae": "Carga de CPU combinada. Los valores superiores al 100% significan que más de un núcleo está trabajando a la vez.", "e7cf14ec78": "Sesiones de terminal no disponibles. La lista puede estar obsoleta.", - "93b0de3c21": "Reanudar", - "f85af9cda6": "Las instantáneas de recursos y las sesiones de terminal no están disponibles.", - "f8e0d794b4": "Daemon no responde", - "bd19fd7a59": "Matar todas las sesiones", - "c9382662bb": "Reiniciar demonio", - "59f178fe11": "{{value0}}, demonio inalcanzable", + "93b0de3c21": "Reiniciar", + "f85af9cda6": "Las snapshots de recursos y las sesiones de terminal no están disponibles.", + "f8e0d794b4": "El servicio no responde", + "bd19fd7a59": "Terminar todas las sesiones", + "c9382662bb": "Reiniciar servicio", + "59f178fe11": "{{value0}}, servicio inaccesible", "21cacb16d1": "· remoto", - "73a3fd68a9": "Contraer repo", - "b12e31dfcb": "Expandir repo", + "73a3fd68a9": "Contraer repositorio", + "b12e31dfcb": "Expandir repositorio", "d659d71d2d": "Reanudar el espacio de trabajo {{value0}}", "bbcd9b7b85": "Contraer espacio de trabajo", - "c4a8968bdd": "Ampliar el espacio de trabajo", - "b10695d6ce": "Matar sesión", + "c4a8968bdd": "Expandir espacio de trabajo", + "b10695d6ce": "Terminar sesión", "288a4dd177": "Orca", - "53dd5560ae": "Colapso Orca", + "53dd5560ae": "Contraer Orca", "e419d27083": "Expandir Orca", - "41ae4fa725": "Asesinato…", + "41ae4fa725": "Terminando…", "138b99bd80": "esta sesión", "888dad8c55": "Cargando…", "6d9793d4bc": "Administrador de recursos - Terminales", - "ca95d077db": "demonio inalcanzable", + "ca95d077db": "servicio inaccesible", "a82253b458": "Eliminar espacio de trabajo.", "946724a70a": "El espacio de trabajo principal no se puede eliminar.", "16bc3c998a": "Eliminar espacio de trabajo {{value0}}", "0f9e50eb07": "Otro", - "d406915b78": "renderizador", - "81cd37af99": "Principal", - "fa6d36758d": "Matar sesión {{value0}}", + "d406915b78": "Renderer", + "81cd37af99": "Main", + "fa6d36758d": "Terminar sesión {{value0}}", "b8f4a2c1d0e3": "{{value0}} huérfanos", - "c7e3b1a0d9f2": "Mata la terminal huérfana {{value0}}", - "d8f4c2b1e0a3": "Mata terminales huérfanas {{value0}}", - "e9a5d3c2b1f0": "¿Matar a {{value0}}?" + "c7e3b1a0d9f2": "Terminar {{value0}} terminal huérfano", + "d8f4c2b1e0a3": "Terminar {{value0}} terminales huérfanos", + "e9a5d3c2b1f0": "¿Terminar {{value0}}?" }, "SshStatusSegment": { "3ad70e0365": "Administrar hosts remotos…", @@ -2872,8 +2872,8 @@ "3d0128b105": "conectado", "fd9a3c600e": "error", "fbb3f9f05e": "conflicto", - "95e4ff5b4b": "emprendedor", - "63a2b965f6": "tracción", + "95e4ff5b4b": "haciendo push", + "63a2b965f6": "haciendo pull", "remote_server": "Servidor remoto", "runtime_checking": "Comprobando", "runtime_online": "Conectado", @@ -2897,8 +2897,8 @@ "75ded02687": "Administrar cuentas…", "ff0fbe9311": "Activo", "7657e3db9c": "Cuenta del Codex", - "38b5647724": "Tiempo de ejecución de uso del Codex", - "ba55303942": "Abrir detalles del Codex y cambiar de cuenta", + "38b5647724": "Servicio de uso de Codex", + "ba55303942": "Abrir detalles de Codex y cambiar de cuenta", "4dff061aab": "·", "2483c60695": "···", "c35af53b73": "Iniciar sesión", @@ -2907,126 +2907,126 @@ "54e8d6bb2d": "% Fable", "d79c3362c4": "% 5h", "a79c64f87e": "Fable", - "8295903d17": "Reinicie los terminales Claude en vivo antes de continuar conversaciones antiguas después de cambiar.", + "8295903d17": "Reinicia los terminales Claude activos antes de continuar conversaciones antiguas después de cambiar.", "c98ea88392": "Ninguna otra cuenta", "9332ba8684": "Cambiar a", - "d450654fa2": "cuenta Claude", - "11e2354daf": "Tiempo de ejecución de uso de Claude", + "d450654fa2": "Cuenta de Claude", + "11e2354daf": "Servicio de uso de Claude", "3dd7ddfae1": "Abrir detalles de Claude y cambiar de cuenta", - "59c6e7b4e0": "Las sesiones visibles se reinician ahora. Otros reinician cuando su árbol de trabajo se activa.", - "c676918adc": "Valor predeterminado del sistema", - "3325d996cb": "Límites de frecuencia de actualización", + "59c6e7b4e0": "Las sesiones visibles se reinician ahora. Las demás se reinician cuando su worktree se activa.", + "c676918adc": "Predeterminado del sistema", + "3325d996cb": "Actualizar rate limits", "fda8146810": "Abrir detalles de uso de Kimi", "629251f4b6": "Abrir detalles de uso de OpenCode Go", "d2375976eb": "Abrir detalles de uso de Gemini", "3d79122c3f": "kimi", - "d7a0668acc": "código abierto-ir", + "d7a0668acc": "opencode-go", "68efc0345c": "gemini", "76b06d4da5": "claude", "a28a5dd9b1": "error", "cd9d7b40ff": "Reiniciar {{value0}} sesiones", "6cd6650b4c": "Reiniciar sesión", - "1446d0d8a0": "{{value0}} Las sesiones del Codex todavía están en la cuenta anterior.", - "605901a495": "1 sesión del Codex todavía está en la cuenta anterior", - "5e5f9f5160": "1 restablecimiento de límite disponible", - "5ecae9197c": "{{value0}} restablecimientos de límite disponibles", - "25d8bbde69": "Usando restablecimiento…", - "e159fc1fd7": "Restablecer ahora", + "1446d0d8a0": "{{value0}} sesiones de Codex siguen en la cuenta anterior.", + "605901a495": "1 sesión de Codex sigue en la cuenta anterior", + "5e5f9f5160": "1 reset de rate limit disponible", + "5ecae9197c": "{{value0}} resets de rate limit disponibles", + "25d8bbde69": "Aplicando reset…", + "e159fc1fd7": "Resetear ahora", "972a1ff497": "¿Restablecer límites de Codex?", - "6d1042aa6f": "Esto usa un crédito de restablecimiento de límite de Codex para la cuenta activa y restablece de inmediato las ventanas de uso aptas.", + "6d1042aa6f": "Esto usa un crédito de reset de rate limit de Codex para la cuenta activa y restablece de inmediato las ventanas de uso elegibles.", "f077f586db": "No volver a preguntar", "c0e972d726": "Cancelar" }, "StatusBarUsageEmptyCta": { "828c764a79": "Conectar una cuenta", "caa0f39811": "Soporta:", - "97957ad3a3": "Conecte las cuentas de su proveedor de IA para ver su uso en tiempo real y cambiar fácilmente entre cuentas.", + "97957ad3a3": "Conecta tus cuentas de proveedores de IA para ver su uso en tiempo real y cambiar fácilmente entre cuentas.", "9a542f46c7": "Ocultar de la barra de estado", - "84c3b15dca": "Límites de uso del Agent", + "84c3b15dca": "Límites de uso de Agents", "d663430cf9": "Configurar seguimiento de uso" }, "UpdateStatusSegment": { - "5cd13105a3": "La actualización falló. Haga clic para ampliar.", - "2201df6987": "Error en la actualización: haga clic para ver los detalles", + "5cd13105a3": "La actualización falló. Haz clic para expandir.", + "2201df6987": "La actualización falló — haz clic para ver detalles", "8533c12c3c": "La actualización falló", - "962404f68e": "Actualización lista para instalar. Haga clic para ampliar.", + "962404f68e": "Actualización lista para instalar. Haz clic para expandir.", "248ee5d8ef": "Orca v{{value0}} descargando… {{value1}}%", "57a29c3b0e": "Actualización lista", - "fd1d3b3a1d": "Descarga de actualizaciones, {{value0}} por ciento. Haga clic para ampliar.", + "fd1d3b3a1d": "Descargando actualización, {{value0}} por ciento. Haz clic para expandir.", "9d13213a56": "Orca v{{value0}} lista para instalar" }, "WorkspaceSpaceCompactPanel": { "a471aa9c24": "Actualizado", "9be86c46a0": "Liberable", - "f4d2651498": "escaneado", + "f4d2651498": "Escaneado", "6a5dc3c61a": "Revisar", "c361440dc0": "Beta", "8ff597593d": "Espacio", "0582df6d2e": "Escanear", - "f5e1a84d79": "Refrescar", + "f5e1a84d79": "Actualizar", "2af2174d6d": "Cancelar", - "5691353a21": "Parada", - "2837dc7c72": "cancelado", - "0583c806ac": "El uso del disco del espacio de trabajo no se analiza.", - "39786e3b73": "Escaneo de tamaños de espacios de trabajo.", + "5691353a21": "Deteniendo", + "2837dc7c72": "cancelando", + "0583c806ac": "No se ha escaneado el uso de disco del espacio de trabajo.", + "39786e3b73": "Escaneando tamaños de espacios de trabajo.", "bef4dc0457": "{{value0}} recuperable · {{value1}} no disponible", "3d8d47ce77": "{{value0}} · último resultado mantenido" }, "WorkspaceSpaceManagerPanel": { - "2965415393": "Error al forzar la eliminación", + "2965415393": "Falló la eliminación forzada", "e031e93219": "No hay espacios de trabajo coincidentes.", - "a02d84d2d2": "Escaneo de espacios de trabajo. Puedes salir de esta página.", + "a02d84d2d2": "Escaneando espacios de trabajo. Puedes salir de esta página.", "be37293b10": "Estado", "33aef3e9cc": "Tamaño", "81f14d9924": "Repositorio", "e4aebea158": "Espacio de trabajo", - "1d0f8300d1": "Seleccionar espacios de trabajo visibles y eliminables", - "697d60c456": "Borrar selección visible", + "1d0f8300d1": "Seleccionar espacios de trabajo visibles que se pueden eliminar", + "697d60c456": "Limpiar selección visible", "81aaf1de65": "Mostrar solo espacios de trabajo eliminables", "d7ac56452e": "Actividad", "243287ac60": "Nombre", "6f8f6a6b04": "Filtrar espacios de trabajo", - "5caccea440": "Eliminar seleccionado", - "e4a12c455b": "Claro", - "0cb1501ccf": "reclamable", + "5caccea440": "Eliminar seleccionados", + "e4a12c455b": "Limpiar", + "0cb1501ccf": "recuperable", "65402b7192": "seleccionado", "43171f3e60": "Espacios de trabajo", "83f1a0a932": "Reclamable", "09960d86bd": "escaneado", "1cc6cd4c0f": "espacios de trabajo", "02b27c2230": "espacio de trabajo", - "63efebe0e6": "{{value0}} {{value1}} eliminado del espacio.", + "63efebe0e6": "{{value0}} {{value1}} eliminado de Space.", "eee5240810": "Espacios de trabajo eliminados", "9afc97f9a3": "Espacio de trabajo eliminado", "792a214457": "Eliminar espacio de trabajo", - "a998501630": "Fuerza", - "9155381019": "Escaso", + "a998501630": "Forzar", + "9155381019": "Sparse", "f39d291997": "Seleccionar", "16988df079": "No se encontraron archivos.", "b25c2c1086": "elementos de nivel superior", "d3f9c69ddc": "Zoom", "ef890d31b9": "Todo", "c28643d3da": "Ir al espacio de trabajo", - "66870929fb": "Asunto", + "66870929fb": "Issue", "fb2069acb7": "Revisar", "b9b4a3a25d": "Rama", - "c432278ec7": "Búfers del editor", - "0bc756efaf": "cambios de git", + "c432278ec7": "Buffers del editor", + "0bc756efaf": "Cambios de git", "e9528a89b3": "Terminales", - "a8d9e0de79": "Agentes", + "a8d9e0de79": "Agents", "d384a4ce9f": "Eliminar decisión", - "7d7745bb8f": "puede eliminar", + "7d7745bb8f": "Se puede eliminar", "720870a18e": "Mantener: vinculado", "cbc343a7a8": "Mantener: en uso", "2055bc6a5a": "Mantener: ediciones no guardadas", - "ec7b076a75": "Mantener: git no marcado", + "ec7b076a75": "Mantener: git no verificado", "7ab8d7e2d7": "Mantener: archivos modificados", "7f7895514e": "Mantener: activo", "2b501ee391": "Mantener: principal", "39801484e0": "Fallido", "33653dbac2": "Eliminando", "52b629eb84": "Actualizado", - "e91dd2a9ae": "Ejecute un escaneo para inspeccionar los tamaños del espacio de trabajo.", + "e91dd2a9ae": "Ejecuta un escaneo para inspeccionar los tamaños de espacios de trabajo.", "61e25239da": "No hubo filas del espacio de trabajo disponibles en el escaneo.", "8194a4fb29": "El escaneo falló antes de que se recopilaran los tamaños del espacio de trabajo.", "b2f82ed5ae": "Eliminable", @@ -3037,12 +3037,12 @@ "1fce91d1b9": "Parada", "d254f04097": "cancelado", "265d956765": "{{value0}}. Puedes salir de esta página.", - "d595295d7d": "{{value0}} se puede recuperar de árboles de trabajo vinculados.", + "d595295d7d": "{{value0}} se puede recuperar de worktrees vinculados.", "34174bd83d": "{{value0}}. Puedes salir de esta página; el último resultado permanece visible.", "433bb7f595": "OK", "0ba046fbc5": "El escaneo falló.", "5c6d25720c": "Seleccione un espacio de trabajo para inspeccionar.", - "c5135e7e4a": "Escaneo de tamaños de espacios de trabajo. Puedes salir de esta página.", + "c5135e7e4a": "Escaneando tamaños de espacios de trabajo. Puedes salir de esta página.", "0990a63160": "Aún no hay tamaños de espacio de trabajo escaneados.", "977bdf9a36": "No hay elementos de nivel superior para mostrar.", "131662ac65": "{{value0}} abierto", @@ -3052,13 +3052,13 @@ "status": { "popover": { "rows": { - "a49ea79246": "Ir al árbol de trabajo", + "a49ea79246": "Ir al worktree", "f2b813345f": "Espacio de trabajo no disponible", "0e72c8d9fb": "Detener proceso", "536d48a5dc": "Copiar {{value0}}", "085f4f0334": "Abrir en el navegador", "e4a709548c": "No se pudieron actualizar los puertos", - "acdb6df590": "Proceso detenido el {{value0}}", + "acdb6df590": "Proceso detenido en {{value0}}", "480d8f2347": "Copiado {{value0}}", "b854ec9ff5": "No se pudo abrir el navegador" } @@ -3066,21 +3066,21 @@ } }, "tooltip": { - "cedb7b99e3": "% izquierda", + "cedb7b99e3": "% restante", "6d6df77f41": "No hay datos disponibles", "7f7f208060": "Mensual", - "252c096536": "Semanalmente", + "252c096536": "Semanal", "a79c64f87e": "Fable", "94038ad2fa": "Sesión", - "2c35eca8d4": "No se puede recuperar el uso", - "1292d4f2ee": "Indisponible", + "2c35eca8d4": "No se pudo obtener el uso", + "1292d4f2ee": "No disponible", "7567cd1c6b": "Uso no disponible", - "a9a318b7a3": "Error al actualizar: se muestran datos almacenados en caché", + "a9a318b7a3": "Error al actualizar — mostrando datos en caché", "7ad719c4bf": "Limitado", "e740f92596": "Error al actualizar", - "8418ec448d": "No se pudo actualizar el uso de {{value0}}. Es posible que las sesiones de agente sigan iniciadas.", - "45198c7d95": "1 restablecimiento de límite disponible", - "bce421cba3": "{{value0}} restablecimientos de límite disponibles", + "8418ec448d": "No se pudo actualizar el uso de {{value0}}. Es posible que las sesiones de agentes sigan iniciadas.", + "45198c7d95": "1 reset de rate limit disponible", + "bce421cba3": "{{value0}} resets de rate limit disponibles", "7ec6e030a0": "El siguiente vence ahora", "d1e442a9e5": "Vence ahora", "6cf9eaed10": "El siguiente vence en {{value0}}", @@ -3090,8 +3090,8 @@ "f8f0f9d8cc": "Problema de red", "bf2e739f18": "Inicio de sesión no disponible", "f8b8dbed85": "Uso no disponible", - "3d3c9c0c1f": "El uso de Claude se actualizará cuando la terminal Claude activa rote sus credenciales.", - "42fdd4da1d": "Se está actualizando el inicio de sesión de Claude. Las sesiones de agentes pueden seguir conectadas.", + "3d3c9c0c1f": "El uso de Claude se actualizará cuando el terminal Claude activo rote sus credenciales.", + "42fdd4da1d": "Se está actualizando el inicio de sesión de Claude. Las sesiones de agentes pueden seguir iniciadas.", "c06c1d215d": "No se pudo actualizar el uso de Claude porque falló la solicitud de red.", "cabdc2a9e0": "No se pudieron leer las credenciales de inicio de sesión de Claude.", "a7517cccb6": "El uso de Claude no está disponible ahora." @@ -3103,54 +3103,54 @@ }, "stats": { "ClaudeUsageDailyChart": { - "2a6360c7cb": "escritura en caché", - "61c58f8976": "lectura de caché", - "7d2efeff5e": "Producción", - "d7fb787e6b": "Aporte", - "a7902d3c1d": "fichas", + "2a6360c7cb": "Escritura de caché", + "61c58f8976": "Lectura de caché", + "7d2efeff5e": "Salida", + "d7fb787e6b": "Entrada", + "a7902d3c1d": "tokens", "059945f71d": "Totales de entrada, salida, lectura de caché y escritura de caché por día.", "c9f7cd30e9": "Uso diario" }, "ClaudeUsagePane": { "21ea00bfa8": "Caché", - "a8b7487ff7": "Producción", - "faf3444859": "Aporte", - "0f03975d59": "vueltas", + "a8b7487ff7": "Salida", + "faf3444859": "Entrada", + "0f03975d59": "turnos", "1afc25eb06": "Modelo", "c17bed0416": "Proyecto", "01476891c7": "Último activo", "abfc4a4943": "Tasa de reutilización de caché:", "7e76c84153": "Sesiones recientes", - "32176e1d44": "vueltas", + "32176e1d44": "turnos", "02a046792e": "sesiones •", "f97435845c": "Proyecto principal:", "7dc9e5613b": "Por proyecto", - "c3fdbc5474": "Modelo superior:", + "c3fdbc5474": "Modelo principal:", "0f394c24e3": "Por modelo", "51ae85fa00": "La tasa de reutilización de caché se calcula como tokens de lectura de caché / (tokens de entrada + tokens de lectura de caché).", - "b26d4ddb58": "Est. Costo equivalente a API", + "b26d4ddb58": "Costo est. equivalente a API", "0f3e696ca9": "Sesiones / Turnos", - "8cc23be4a3": "Turnos de lectura de caché cero", + "8cc23be4a3": "Turnos sin lectura de caché", "1634c4f404": "Tasa de reutilización de caché", - "b786fb4a70": "escritura en caché", - "268cf0af51": "lectura de caché", - "2b8a2f14aa": "Fichas de salida", - "ea71fae8fc": "Fichas de entrada", + "b786fb4a70": "Escritura de caché", + "268cf0af51": "Lectura de caché", + "2b8a2f14aa": "Tokens de salida", + "ea71fae8fc": "Tokens de entrada", "7dde9331fd": "Aún no se ha encontrado ningún uso local de Claude para este ámbito.", "424cd50412": "Habilitar análisis de uso de Claude", - "8d18bbb771": "Refrescar", - "c5b9b344d0": "Actualizar el uso de Claude", + "8d18bbb771": "Actualizar", + "c5b9b344d0": "Actualizar uso de Claude", "505be9aac4": "Rango", "f61cffb9c8": "Alcance", "dd29209b21": "Filtros", "e9bf9fce0e": "Opciones de uso de Claude", "6afacbee37": "Seguimiento de uso de Claude", - "0cb1a36d7d": "Lee los registros de uso locales de Claude para mostrar estadísticas de token, modelo y sesión.", + "0cb1a36d7d": "Lee los registros locales de uso de Claude para mostrar estadísticas de tokens, modelo y sesión.", "5ce4842c2c": "Todo el uso local de Claude", - "4f8368c272": "Solo árboles de trabajo Orca", + "4f8368c272": "Solo worktrees de Orca", "cfe2282ffa": "Desconocido", - "7765a4c3e1": "n / A", - "2d41fd45c6": "• Error del último análisis: {{value0}}", + "7765a4c3e1": "n/a", + "2d41fd45c6": " • Error del último escaneo: {{value0}}", "rangeLast7Days": "Últimos 7 días", "rangeLast30Days": "Últimos 30 días", "rangeLast90Days": "Últimos 90 días", @@ -3158,17 +3158,17 @@ }, "CodexUsageDailyChart": { "1e6f62d7e3": "Razonamiento", - "c646e1783c": "Entrada en caché", - "7b596a88b2": "Producción", - "99a91d3143": "Aporte", - "e4bdcf0071": "fichas", + "c646e1783c": "Entrada cacheada", + "7b596a88b2": "Salida", + "99a91d3143": "Entrada", + "e4bdcf0071": "tokens", "c756cda6a8": "Totales de entrada, entrada en caché, salida y razonamiento por día.", "609aa96e8b": "Uso diario" }, "CodexUsagePane": { "e0b988599d": "Total", - "bbd20344b8": "Producción", - "3acc582214": "Aporte", + "bbd20344b8": "Salida", + "3acc582214": "Entrada", "bd0822ca47": "Eventos", "c2478bcc3c": "Modelo", "1a65900aea": "Proyecto", @@ -3179,31 +3179,31 @@ "bf1bf2f674": "sesiones •", "829ee743f2": "Proyecto principal:", "b98718aaab": "Por proyecto", - "95d2d89285": "Modelo superior:", + "95d2d89285": "Modelo principal:", "5a0d1d69cd": "Por modelo", - "94ac1f1ee7": "Los tokens de razonamiento se muestran para mayor visibilidad, pero el costo se calcula a partir de entradas no almacenadas en caché, entradas almacenadas en caché y salidas únicamente.", - "1a18fbd56b": "Est. Costo equivalente a API", + "94ac1f1ee7": "Los tokens de razonamiento se muestran para dar visibilidad, pero el costo se calcula solo con entrada no cacheada, entrada cacheada y salida.", + "1a18fbd56b": "Costo est. equivalente a API", "907b31865f": "Sesiones / Eventos", "6e18146e9b": "Salida de razonamiento", - "a9ac0f423a": "Entrada en caché", - "5d8eba87bd": "Fichas de salida", - "e365eaa6fd": "Fichas de entrada", + "a9ac0f423a": "Entrada cacheada", + "5d8eba87bd": "Tokens de salida", + "e365eaa6fd": "Tokens de entrada", "4c865393b4": "Aún no se ha encontrado ningún uso del Codex local para este ámbito.", "f7c1affbd5": "Habilitar análisis de uso del Codex", - "3022cda443": "Refrescar", - "ec4d270e2c": "Actualizar el uso del Codex", + "3022cda443": "Actualizar", + "ec4d270e2c": "Actualizar uso de Codex", "89162e019b": "Rango", "6d68e8399a": "Alcance", "1af1a39b2f": "Filtros", "70b5b8581f": "Opciones de uso del Codex", "408210470c": "Seguimiento del uso del Codex", - "13badcd8f2": "Lee los registros de uso del Codex local para mostrar estadísticas de token, modelo y sesión.", + "13badcd8f2": "Lee los registros locales de uso de Codex para mostrar estadísticas de tokens, modelo y sesión.", "4fe8820098": "Todo el uso del Codex local", - "201766b754": "Solo árboles de trabajo Orca", + "201766b754": "Solo worktrees de Orca", "bf6cf2d4dd": "Desconocido", - "ae255c3dba": "n / A", + "ae255c3dba": "n/a", "247c93ca92": "• precios inferidos", - "8a6655f7a2": "• Error del último análisis: {{value0}}", + "8a6655f7a2": " • Error del último escaneo: {{value0}}", "rangeLast7Days": "Últimos 7 días", "rangeLast30Days": "Últimos 30 días", "rangeLast90Days": "Últimos 90 días", @@ -3211,8 +3211,8 @@ }, "OpenCodeUsagePane": { "349f7c3f5c": "Total", - "dfc4513657": "Producción", - "0f2f266c9d": "Aporte", + "dfc4513657": "Salida", + "0f2f266c9d": "Entrada", "d416f5cf92": "Eventos", "08c78441b7": "Modelo", "a4738de041": "Proyecto", @@ -3223,30 +3223,30 @@ "bc0cb89901": "sesiones •", "048ffe4d65": "Proyecto principal:", "0f0a1684bb": "Por proyecto", - "a15206a63a": "Modelo superior:", + "a15206a63a": "Modelo principal:", "040c044d39": "Por modelo", - "e5bb23d85e": "El costo proviene de la base de datos local OpenCode cuando el mensaje del asistente registró uno.", + "e5bb23d85e": "El costo proviene de la base de datos local de OpenCode cuando el mensaje del asistente registró uno.", "15c34d4b08": "Costo registrado", "7e9433469a": "Sesiones / Eventos", "5a65d68b77": "Salida de razonamiento", - "603504ee3b": "Entrada en caché", - "7aa4d8ce35": "Fichas de salida", - "d637a892ed": "Fichas de entrada", + "603504ee3b": "Entrada cacheada", + "7aa4d8ce35": "Tokens de salida", + "d637a892ed": "Tokens de entrada", "bb6363e08c": "Aún no se ha encontrado ningún uso de OpenCode local para este ámbito.", "f04131b3be": "Habilitar análisis de uso de OpenCode", - "603cd138dc": "Refrescar", - "bed558df0b": "Actualizar el uso de OpenCode", + "603cd138dc": "Actualizar", + "bed558df0b": "Actualizar uso de OpenCode", "b5ed5c9fd0": "Rango", "40d283c837": "Alcance", "01583b30aa": "Filtros", "230d6de108": "Opciones de uso de OpenCode", "bea80ceae0": "Seguimiento del uso de OpenCode", - "b8b3522436": "Lee registros de uso de OpenCode locales para mostrar estadísticas de token, modelo y sesión.", + "b8b3522436": "Lee los registros locales de uso de OpenCode para mostrar estadísticas de tokens, modelo y sesión.", "144a6050e9": "Todo el uso local de OpenCode", - "e04c58327c": "Solo árboles de trabajo Orca", + "e04c58327c": "Solo worktrees de Orca", "362231082f": "Desconocido", - "8095a63426": "n / A", - "6cc7782458": "• Error del último análisis: {{value0}}", + "8095a63426": "n/a", + "6cc7782458": " • Error del último escaneo: {{value0}}", "rangeLast7Days": "Últimos 7 días", "rangeLast30Days": "Últimos 30 días", "rangeLast90Days": "Últimos 90 días", @@ -3255,86 +3255,86 @@ "ShareUsageButton": { "7d6b25323d": "Compartir en X", "b295c1c75d": "Copiar imagen", - "bd82c76a70": "copiado", + "bd82c76a70": "Copiado", "bce08eccb9": "Compartir uso", "cecefa7c32": "Compartir" }, "ShareUsageCard": { "4a4c6c79a3": "sesiones ·", - "66c83284cf": "Fichas diarias", - "b760c0b622": "modelo superior", - "2d9eb39264": "fichas totales", - "beb6f24f37": "Est. costo", + "66c83284cf": "Tokens diarios", + "b760c0b622": "Modelo principal", + "2d9eb39264": "Tokens totales", + "beb6f24f37": "Costo est.", "da62578d9d": "Uso", "0eb31e79ee": "IDE de Orca", "960324e9b8": "eventos", - "6adac63cfe": "vueltas" + "6adac63cfe": "turnos" }, "StatsPane": { "42d3e0bdf7": "Proveedor de análisis de uso: {{value0}}", "c79f073d4c": "Análisis de uso", - "a58aba506f": "RP creados", - "1c96f433e2": "Tiempo que trabajaron los agents", - "9dbec9e675": "Agents generados", - "73ed07859c": "Inicie su primer agente para comenzar a rastrear", + "a58aba506f": "PRs creados", + "1c96f433e2": "Tiempo trabajado por agentes", + "9dbec9e675": "Agents iniciados", + "73ed07859c": "Inicia tu primer agente para empezar a rastrear", "1e696db2f6": "OpenCode", "7d26110cea": "Codex", "85457c02fe": "Claude", - "b2cf4310ce": "Descripción general", + "b2cf4310ce": "Resumen", "908c470587": "codex", "eb6a066185": "claude", - "eee19cfade": "descripción general" + "eee19cfade": "resumen" }, "UsageOverviewPane": { "22ed1b7669": "sesiones", "444585cb41": "con datos", - "ecb0cd8a4c": "activado -", + "ecb0cd8a4c": "habilitado -", "33f7b043d2": "Proveedores", - "60002bb22f": "Aún no se ha encontrado ningún uso local de Claude, Codex u OpenCode. La descripción general se completará después de que la siguiente sesión del agente escriba registros de tokens.", - "70f36452d4": "compartir caché", + "60002bb22f": "Aún no se encontró uso local de Claude, Codex u OpenCode. El resumen se completará después de que la siguiente sesión de agente escriba logs de tokens.", + "70f36452d4": "Porcentaje de caché", "327603fe8b": "Días activos", "0eaf937335": "Est. costo", - "3887b94ce5": "fichas totales", + "3887b94ce5": "Tokens totales", "2d13e57f72": "Habilitar OpenCode", "2f1ee2878b": "Habilitar Codex", "0ea0cae435": "Habilitar Claude", - "6c00c46815": "Permita que un proveedor escanee los registros de los agentes locales y cree el libro mayor de tokens combinado.", - "49405ccc8d": "Comenzar a rastrear tokens", - "ca6bc5fded": "Refrescar", - "e06d1baf5c": "Actualizar descripción general de uso", - "c760c481c5": "Descripción general de uso", - "55c910f4f1": "- los precios de algunos modelos no están disponibles" + "6c00c46815": "Habilita un proveedor para escanear logs locales de agentes y crear el registro combinado de tokens.", + "49405ccc8d": "Empezar a rastrear tokens", + "ca6bc5fded": "Actualizar", + "e06d1baf5c": "Actualizar resumen de uso", + "c760c481c5": "Resumen de uso", + "55c910f4f1": "- algunos precios de modelos no están disponibles" }, "share": { "card": { "utils": { "19f4b4dc75": "github.com/stablyai/orca", - "d864fc5f98": "producción", - "5d66fdd7c2": "aporte", + "d864fc5f98": "salida", + "5d66fdd7c2": "entrada", "7080aeaebb": "Razonamiento", "4ee864629a": "Entrada en caché", - "33d38e2177": "Producción", - "c2d7b23d57": "Aporte", - "9d166247ee": "escritura en caché", - "cc28cb965e": "lectura de caché" + "33d38e2177": "Salida", + "c2d7b23d57": "Entrada", + "9d166247ee": "Escritura de caché", + "cc28cb965e": "Lectura de caché" } } }, "stats": { "search": { "cb6a9f0334": "caché", - "eaf251e183": "fichas", - "6953af58e6": "código abierto", + "eaf251e183": "tokens", + "6953af58e6": "opencode", "b77826fca3": "codex", "e9dc37d889": "claude", "8efeae0b22": "seguimiento", "5acbe1fdf2": "tiempo", "ef8bbf7739": "PRs", - "ce8533f02e": "agentes", - "0bba8ca244": "estadística", + "ce8533f02e": "agents", + "0bba8ca244": "estadísticas", "0e2a0b6431": "uso", "372debfac0": "estadísticas", - "26bb901fcd": "Estadísticas de Orca más análisis de uso, tokens, caché, modelos y sesiones combinados de Claude, Codex y OpenCode.", + "26bb901fcd": "Estadísticas de Orca más analíticas de uso combinadas de Claude, Codex y OpenCode: tokens, caché, modelos y sesiones.", "cb2430ae6a": "Estadísticas y uso" } }, @@ -3347,32 +3347,32 @@ }, "sections": { "9564a3b21b": "sesiones -", - "32330a6e66": "{{value0}}: fichas {{value1}}", - "57d1448ef8": "Permitir", + "32330a6e66": "{{value0}}: {{value1}} tokens", + "57d1448ef8": "Habilitar", "f6df0d7d6d": "Más", "1dd166c920": "Menos", - "52d9221dc0": "Mapa de calor de actividad reciente de tokens", + "52d9221dc0": "Heatmap de actividad reciente de tokens", "c424eb3f8e": "Mejor:", "f28ff1f852": "Actividad reciente de tokens combinados de Claude, Codex y OpenCode.", "69e2b50427": "Intensidad diaria", - "3a795542fa": "Mezcla de tokens combinada", + "3a795542fa": "Mix combinado de tokens", "e65084cb4b": "razonamiento", - "3bc4a01b24": "Tokens combinados de entrada, salida y caché entre proveedores habilitados.", - "4ff104da47": "Mezcla de tokens", + "3bc4a01b24": "Tokens combinados de entrada, salida y caché en proveedores habilitados.", + "4ff104da47": "Mix de tokens", "0015facc1f": "Caché", - "7f270458af": "Producción", + "7f270458af": "Salida", "9365b14a4e": "Nueva entrada", "3de9bf87fc": "Aún no hay modelo", - "6762f6a682": "fichas", + "6762f6a682": "tokens", "a7f937fb29": "{{value0}} sesiones - {{value1}} {{value2}}", - "c8f3a2d1e0b4": "vueltas", + "c8f3a2d1e0b4": "turnos", "d9a4b3e2f1c5": "eventos" } } }, "UsageBreakdownSection": { "7765a4c3e1": "n/a", - "247c93ca92": "• precio inferido" + "247c93ca92": "• precios inferidos" }, "UsageSessionsTable": { "1afc25eb06": "Turnos", @@ -3389,27 +3389,27 @@ }, "sparse": { "SparseCheckoutPresetSelect": { - "c4ac80151d": "Nuevo preajuste", + "c4ac80151d": "Nuevo preset", "7c3275d307": "Editar {{value0}}", - "c7f9b3f0c1": "Apagado", - "8b12c0850a": "Ahorrar", + "c7f9b3f0c1": "Desactivado", + "8b12c0850a": "Guardar", "de8fce5854": "Cancelar", - "ddbcaef7be": "src/paquetes de renderizador/ui", + "ddbcaef7be": "src/renderer packages/ui", "0e9ad9c798": "Directorios", - "064c1e2d12": "Interfaz de usuario del renderizador", + "064c1e2d12": "UI del renderer", "b3a500c623": "Nombre", - "16223dde6a": "Cargar ajustes preestablecidos", - "a683a4bc8e": "Reintentar cargar ajustes preestablecidos", - "14952d451e": "directorios {{value0}}", + "16223dde6a": "Cargar presets", + "a683a4bc8e": "Reintentar cargar presets", + "14952d451e": "{{value0}} directorios", "e9283eb171": "1 directorio", - "69c020eddc": "Editar preajuste", + "69c020eddc": "Editar preset", "bd6cec2056": "nuevo" } }, "source": { "control": { "SourceControlActionVariableChips": { - "1b77798d5f": "variables", + "1b77798d5f": "Variables", "6b921a0ac2": "Ejemplo", "4bf6d88039": "(vacío)" } @@ -3417,30 +3417,30 @@ }, "skills": { "SkillsPage": { - "cb142070b4": "Refrescar", - "984405683f": "Complemento", - "4d177feabd": "incluido", + "cb142070b4": "Actualizar", + "984405683f": "Plugin", + "4d177feabd": "Incluido", "aa59462502": "Repositorio", - "571c5818c1": "Hogar", + "571c5818c1": "Inicio", "0bc1379f4c": "Todas las fuentes", - "38e0951c3a": "Habilidades del Agent", + "38e0951c3a": "Agent Skills", "fb6bf60b52": "Claude", "426be2aac6": "Codex", "39b6998ddb": "Todos los proveedores", - "a68dee6a32": "Habilidades de búsqueda", + "a68dee6a32": "Buscar skills", "e46e162e2e": "de", "b088e0785d": "Beta", - "f43ad6edf3": "Habilidades", - "7e828fb2c6": "Atrás", - "ea72d6185b": "No se pudieron escanear las habilidades locales", - "dc4c3328ee": "Revelar archivo", + "f43ad6edf3": "Skills", + "7e828fb2c6": "Volver", + "ea72d6185b": "No se pudieron escanear los skills locales", + "dc4c3328ee": "Mostrar archivo", "9963dff6d3": "No se encontró ninguna descripción.", - "995fde8337": "No se pudo revelar el archivo de habilidad", - "ab5b777350": "Se comprobaron las carpetas de habilidades locales de inicio, repositorio, paquetes y complementos.", + "995fde8337": "No se pudo mostrar el archivo del skill", + "ab5b777350": "Se comprobaron las carpetas de skills locales, del repositorio, incluidas y de plugins.", "08a321a984": "Ajusta la búsqueda o los filtros.", - "4acd6d68ec": "No se encontraron habilidades locales", + "4acd6d68ec": "No se encontraron skills locales", "6a62a0168c": "No hay coincidencias", - "cd7893fbc1": "Habilidades de escaneo", + "cd7893fbc1": "Escaneando skills", "35b9a724a0": "Disponible", "0c74e7ff34": "Local" } @@ -3451,13 +3451,13 @@ "ref": { "suggestion": { "toast": { - "670864ab52": "Mantener actualizado el {{value0}} local", - "84c62e4d7f": "No se pudo encender {{value0}}", - "442552c656": "Abra Configuración e inténtelo de nuevo.", - "f15fd80989": "Su nuevo árbol de trabajo está actualizado, pero el {{value0}} local está {{value1}} {{value2}} atrás, por lo que las diferencias de IA pueden compararse con el historial obsoleto. Deja que Orca lo mantenga actualizado automáticamente. Cambie esto en cualquier momento en", - "3d260e1a5d": "Configuración › {{value0}}", - "34a03a6565": "Mantenga {{value0}} actualizado", - "4a18052018": "El local {{value0}} está detrás de {{value1}}", + "670864ab52": "Manteniendo {{value0}} local actualizado", + "84c62e4d7f": "No se pudo activar {{value0}}", + "442552c656": "Abre Ajustes e inténtalo de nuevo.", + "f15fd80989": "Tu nuevo worktree está actualizado, pero {{value0}} local está {{value1}} {{value2}} atrás, así que los diffs de IA pueden compararse con historial obsoleto. Deja que Orca lo mantenga actualizado automáticamente. Puedes cambiar esto en cualquier momento en", + "3d260e1a5d": "Ajustes › {{value0}}", + "34a03a6565": "Mantener {{value0}} actualizado", + "4a18052018": "{{value0}} local está detrás de {{value1}}", "commit": "commit", "commits": "commits" } @@ -3468,8 +3468,8 @@ "AddProjectFromFolderDialog": { "7d1f51678c": "Agregar proyecto", "7726a16374": "Cancelar", - "046751dbfb": "Agregue esta carpeta como un proyecto Orca separado.", - "e643b30398": "Proyecto remoto agregado" + "046751dbfb": "Agrega esta carpeta como un proyecto de Orca separado.", + "e643b30398": "Proyecto agregado en host SSH" }, "AddRepoCreateStep": { "0ae45b8238": "mi-proyecto", @@ -3477,7 +3477,7 @@ "11fd2a7db8": "Repositorio Git", "5e97f0c4b9": "Proyecto creado", "2c12db1511": "Proyecto ya agregado", - "875dda0995": "Introduzca una ruta principal del servidor.", + "875dda0995": "Introduce una ruta padre del host.", "45b7c26034": "Crear proyecto", "85085d74d2": "Creando…", "c7b9f94456": "Crear un proyecto nuevo", @@ -3485,21 +3485,21 @@ "685b5eefe1": "{{kind}} en {{parent}}", "2a762f3b19": "Comprobando Git en este host...", "fe1e616c5b": "Git es necesario para crear un proyecto.", - "c234df77f7": "Elige o introduce una carpeta principal del servidor antes de crear.", + "c234df77f7": "Elige o introduce una carpeta padre del host antes de crear.", "3a13f6e88b": "ubicación no seleccionada", - "6ed14c0281": "carpeta del servidor no seleccionada", + "6ed14c0281": "carpeta del host no seleccionada", "ssh_parent_manual": "Introduce una ruta padre SSH." }, "AddRepoNestedImportStep": { - "496f68cf8c": "Escaneo de repositorios. Haga clic para detener.", - "a32bef9516": "Dejar de escanear", + "496f68cf8c": "Escaneando repositorios. Haz clic para detener.", + "a32bef9516": "Detener escaneo", "2f8298f3c3": "Detener escaneo", "5f857ba8e6": "en", - "4df0d08cc5": "Encontró", - "8db50afe1a": "Importar repositorios desde la carpeta", + "4df0d08cc5": "Encontrado", + "8db50afe1a": "Importar repositorios desde carpeta", "5b2e6fe3c8": "Importar por separado", "cf9d382ca1": "Importar", - "220dd32d83": "Exploración...", + "220dd32d83": "Escaneando...", "fb33359f69": "¿Agrupar estos repositorios?", "d75170194e": "Elige esto si estos proyectos van juntos: un monorepo o simplemente un conjunto de repositorios relacionados. Orca los agrupará y te permitirá trabajar desde la carpeta principal.", "39d51212cc": "Nombre del grupo", @@ -3507,55 +3507,55 @@ "a0bc4d1f8e": "Sí, importar como grupo", "8401a7a0d0": "1 repositorio", "d4f1df62ef": "{{value0}} repositorios", - "b4263a2ac4": "Se encontraron {{value0}} en {{value1}}.", - "24eda6c8b2": "Explorando... {{value0}}", + "b4263a2ac4": "Se encontró {{value0}} en {{value1}}.", + "24eda6c8b2": "Escaneando... {{value0}}", "6149d5203f": "No hay repositorios seleccionados. Abre la carpeta principal para usar el editor, la terminal y la búsqueda sin funciones de Git.", "e52454b7f6": "Abrir como carpeta" }, "AddRepoRemoteStep": { "5b205b5281": "Detener escaneo", "6680289908": "/inicio/usuario/proyecto", - "ef410aa881": "Ruta remota", - "0416bde073": "Agregar configuración", + "ef410aa881": "Ruta del host", + "0416bde073": "Agregar en Ajustes", "df6fbcf880": "No hay destinos SSH configurados.", - "44637f43bd": "Objetivo SSH", - "80557be85a": "Elija un destino SSH conectado e ingrese la ruta a un repositorio Git.", - "91b93a90a4": "Abrir proyecto remoto", - "007651bdf9": "Navegue hasta un directorio y haga clic en Seleccionar para elegirlo.", + "44637f43bd": "Destino SSH", + "80557be85a": "Elige un destino SSH conectado e introduce la ruta a un repositorio Git.", + "91b93a90a4": "Abrir proyecto en host SSH", + "007651bdf9": "Navega hasta un directorio y haz clic en Seleccionar para elegirlo.", "dd3ff65486": "Explorar el sistema de archivos remoto", - "36d427bb66": "Agregar proyecto remoto", - "35831a7312": "Añadiendo...", + "36d427bb66": "Agregar proyecto en host SSH", + "35831a7312": "Agregando...", "lockedDescription": "Introduce la ruta a un repositorio Git en {{value0}}.", "lockedDisconnected": "{{value0}} está desconectado.", "93e0221434": "Conectar" }, "AddRepoServerStartStep": { - "ae990c86a0": "Volver para agregar opciones", + "ae990c86a0": "Volver a opciones de agregado", "e1710bf831": "Abrir como carpeta", "8da4d1a5be": "Agregar proyecto Git", - "ac66a3ed2d": "Explorar el sistema de archivos del servidor", + "ac66a3ed2d": "Explorar el sistema de archivos del host", "92d25420a0": "/inicio/usuario/proyecto", - "867692f505": "Ruta del servidor", - "423b5d3d31": "Agregue un repositorio o carpeta de Git que ya exista en el servidor de ejecución seleccionado.", - "3d0c035483": "Proyecto de servidor abierto", - "438493f214": "O ingrese una ruta de servidor manualmente", - "6b9958492a": "¿Quieres importar muchos repos a la vez? Busque la carpeta principal.", - "d40d751517": "Nuevo repo o carpeta", + "867692f505": "Ruta del host", + "423b5d3d31": "Agrega un repositorio Git o carpeta que ya exista en el host seleccionado.", + "3d0c035483": "Abrir proyecto del host", + "438493f214": "O introduce una ruta del host manualmente", + "6b9958492a": "¿Quieres importar muchos repositorios a la vez? Busca la carpeta padre.", + "d40d751517": "Nuevo repositorio o carpeta", "a81ffa0a99": "Crear en el servidor", "a2ea37d549": "Repositorio remoto de Git", "47759c9491": "Clonar desde URL", "516187414c": "Proyecto o carpeta existente", - "0adf083af7": "Examinar servidor", - "8efa930eb5": "Agregue otro proyecto del servidor de ejecución seleccionado.", + "0adf083af7": "Explorar host", + "8efa930eb5": "Agrega otro proyecto del host seleccionado.", "39bd249b3a": "Agregar un proyecto", - "0f8aba944c": "Navegue hasta un directorio y haga clic en Seleccionar para elegirlo." + "0f8aba944c": "Navega hasta un directorio y haz clic en Seleccionar para elegirlo." }, "AddRepoStartSteps": { "87596c1446": "Otras formas de agregar", "acf895cb42": "Agrega un proyecto para comenzar con Orca.", "d13757911c": "Agregar un proyecto", - "d301db1c9a": "Escaneo de repositorios. Haga clic para detener.", - "69ea7f8dc4": "Dejar de escanear", + "d301db1c9a": "Escaneando repositorios. Haz clic para detener.", + "69ea7f8dc4": "Detener escaneo", "9906cae183": "Detener escaneo" }, "AddRepoStepIndicator": { @@ -3563,46 +3563,46 @@ }, "AddRepoSteps": { "569326d9cc": "Elige carpeta", - "a93ef169b5": "Explorar el sistema de archivos del servidor", + "a93ef169b5": "Explorar el sistema de archivos del host", "2ce3f6edf8": "/ruta/al/destino", "04a4c4e84a": "Ubicación del clon", "b698a4a29d": "https://github.com/user/repo.git", "3d4acbe693": "URL de Git", - "5b2ea674b1": "Ingrese la URL de Git y elija dónde clonarla.", + "5b2ea674b1": "Introduce la URL de Git y elige dónde clonarla.", "c05f88a31f": "Clonar desde URL", - "fe8e629fe3": "Navegue hasta un directorio y haga clic en Seleccionar para elegirlo.", - "df8b0e6c22": "Proyecto remoto agregado", + "fe8e629fe3": "Navega hasta un directorio y haz clic en Seleccionar para elegirlo.", + "df8b0e6c22": "Proyecto agregado en host SSH", "3e64e8a70d": "La conexión falló", - "32a7256d85": "Clon", - "69f5b5380d": "Clonación...", + "32a7256d85": "Clonar", + "69f5b5380d": "Clonando...", "cloneOnHostDescription": "Introduce la URL de Git y elige dónde clonarlo en {{value0}}.", "cloneParentFolder": "Carpeta padre" }, "AutoRenameFailedDialog": { - "aed1623b1e": "Cerca", - "eab8b45238": "Error de copia", - "a23b22d16f": "copiado", + "aed1623b1e": "Cerrar", + "eab8b45238": "Error al copiar", + "a23b22d16f": "Copiado", "74fc00776f": "Detalles del error", "3afcad0497": "desde el primer mensaje del agente.", - "ff62a18580": "Orca no pudo generar un nombre de sucursal para", - "ca3b225195": "Error en el nombre automático de sucursal" + "ff62a18580": "Orca no pudo generar un nombre de rama para", + "ca3b225195": "Error en el nombre automático de rama" }, "CreateProjectLocationField": { - "95548e33bf": "Elija la carpeta principal...", + "95548e33bf": "Elige carpeta padre...", "632b456b1b": "Cambiar", - "afaf54f245": "Cambiar carpeta principal", - "f520f83a97": "Explorar el sistema de archivos del servidor", - "2a20a603a3": "/inicio/usuario/proyectos", + "afaf54f245": "Cambiar carpeta padre", + "f520f83a97": "Explorar el sistema de archivos del host", + "2a20a603a3": "/home/user/projects", "134e37f711": "Ubicación", - "b589b77997": "Navegue hasta un directorio y haga clic en Seleccionar para elegirlo." + "b589b77997": "Navega hasta un directorio y haz clic en Seleccionar para elegirlo." }, "DeleteWorktreeDialog": { "ff2a74ac0e": "y", "91492c9ad6": "Eliminar", "4f6750ca7b": "No se pudo eliminar el espacio de trabajo", - "42e610d6cf": "Error al forzar la eliminación", - "5cc1a6701c": "Abrir configuración", - "2b56b35f53": "Puedes cambiar esto en Configuración.", + "42e610d6cf": "Falló la eliminación forzada", + "5cc1a6701c": "Abrir Ajustes", + "2b56b35f53": "Puedes cambiar esto en Ajustes.", "dd3a45bbbd": "Omitiremos esta confirmación la próxima vez.", "fc23c4cbdf": "Eliminar espacio de trabajo", "86f0ae1257": "Eliminar espacios de trabajo" @@ -3617,56 +3617,56 @@ "66798cc6a2": "Al eliminar este espacio de trabajo, también se elimina 1 espacio de trabajo secundario." }, "DeleteWorktreeSkipConfirmOption": { - "29aefb7e52": "no vuelvas a preguntar" + "29aefb7e52": "No volver a preguntar" }, "DeleteWorktreeWarningPanels": { "026738155a": "(el directorio de clonación original).", - "c4f96a6e18": "árbol de trabajo principal", + "c4f96a6e18": "worktree principal", "e3be9eba15": "Este es el" }, "ImportedWorktreesVisibilityLine": { - "b7a87dc32f": "Mostrar en la lista del árbol de trabajo", + "b7a87dc32f": "Mostrar en la lista de worktrees", "ad99f4eea9": "Mantener oculto", - "9f4f14e821": "Cambie esto más tarde desde el menú del proyecto.", + "9f4f14e821": "Cambia esto más tarde desde el menú del proyecto.", "b2bc47c080": "más ubicaciones", - "b47ba1a9d2": "{{value0}} vista previa", - "2251d41ebb": "Grupos de árboles de trabajo ocultos", - "f54f2bec5d": "{{value0}} árboles de trabajo ocultos para {{value1}}", + "b47ba1a9d2": "Vista previa de {{value0}}", + "2251d41ebb": "Grupos de worktrees ocultos", + "f54f2bec5d": "{{value0}} worktrees ocultos para {{value1}}", "5a9688802a": "Mostrar {{value0}} más", "294de4aeb2": "Mostrar menos" }, "NonGitFolderDialog": { "e52454b7f6": "Abrir como carpeta", "05b33a17a9": "Cancelar", - "8fba4b8cbb": "Esta carpeta no es un repositorio de Git. Tendrás el editor, la terminal y la búsqueda, pero las funciones basadas en Git no estarán disponibles.", - "c49fb13492": "No se pudo agregar la carpeta remota", + "8fba4b8cbb": "Esta carpeta no es un repositorio Git. Tendrás editor, terminal y búsqueda, pero las funciones basadas en Git no estarán disponibles.", + "c49fb13492": "No se pudo agregar la carpeta en este host", "9a766f33ac": "Esta ruta se verificó en el host SSH.", - "79fd02cf5f": "Esta ruta fue verificada el {{hostName}}.", - "8851b77327": "Esta ruta fue verificada localmente." + "79fd02cf5f": "Esta ruta se verificó en {{hostName}}.", + "8851b77327": "Esta ruta se verificó localmente." }, "OrcaYamlTrustDialog": { - "f3e2b868fb": "Ejecutar ganchos", - "43b7bec4cd": "no corras", + "f3e2b868fb": "Ejecutar hooks", + "43b7bec4cd": "No ejecutar", "c494b3ccb1": "en", "79afc6772b": "orca.yaml", - "531689199b": "Confía siempre", - "bf800b7e04": ". Solo corre si confías", - "831f2cd9f0": "se ejecuta en su máquina", - "aa3ffb33fb": "este repositorio", - "c55beddbf8": "cambiado desde la última vez que lo aprobó. Vuelva a revisar antes de que se ejecute", - "95bf974a1a": "Guión {{value0}}", + "531689199b": "Confiar siempre", + "bf800b7e04": ". Ejecútalo solo si confías en ", + "831f2cd9f0": "se ejecuta en tu máquina", + "aa3ffb33fb": "El archivo ", + "c55beddbf8": "cambió desde la última vez que lo aprobaste. Revísalo de nuevo antes de que se ejecute", + "95bf974a1a": "Script {{value0}}", "9e52effffd": "Nuevo script {{value0}}", "e4a51dc4b3": "¿Ejecutar {{value0}} desde {{value1}}?", "02b0ede5ad": "El {{value1}} de {{value0}} cambió: ¿ejecutar la nueva versión?" }, "PendingWorktreeRow": { - "af21e953d1": "Cancelar la creación del árbol de trabajo", + "af21e953d1": "Cancelar creación del worktree", "188f6922a0": "Cancelar" }, "ProjectGroupDeleteDialog": { "ca65b78f78": "Cancelar", "9be10d49ea": "y desagrupar sus proyectos.", - "69f5cb97d0": "Borrar", + "69f5cb97d0": "Eliminar", "591f330288": "Eliminar grupo de proyectos", "2c14ce677a": "Eliminando...", "0e0e6764af": "Proyectos contenidos", @@ -3681,37 +3681,37 @@ "ProjectGroupNameDialog": { "d99a034073": "Cancelar", "83dfbc5313": "Nombre del grupo", - "4a64e78822": "Ahorro..." + "4a64e78822": "Guardando..." }, "RemoteFileBrowser": { - "2300612806": "Escriba para filtrar o ingresar una ruta...", + "2300612806": "Escribe para filtrar o introducir una ruta...", "9e060f5815": "Seleccionar carpeta", "f8b1deb1a4": "Cancelar", - "51001182e3": "directorio vacio", - "971d85cc84": "Se abre como proyecto remoto · {{value0}}", + "51001182e3": "Directorio vacío", + "971d85cc84": "Se abre como proyecto en este host · {{value0}}", "00c4235c10": "No hay coincidencias para '{{value0}}'", "largeInputNoMatches": "No hay coincidencias para esta entrada larga" }, "RemoveFolderDialog": { - "4dc5b5065b": "Eliminar", + "4dc5b5065b": "Quitar", "d36883e046": "Cancelar", - "8c097ef04e": "de Orca. Todavía está en tu disco.", - "e62415c3d0": "Esto sólo elimina", - "b79b39d865": "Eliminar proyecto" + "8c097ef04e": "de Orca. Sigue estando en tu disco.", + "e62415c3d0": "Esto solo quita", + "b79b39d865": "Quitar proyecto" }, "ScrollToCurrentWorkspaceToolbarButton": { - "23989bb663": "Revelar un espacio de trabajo activo" + "23989bb663": "Mostrar espacio de trabajo activo" }, "SetupGuideSidebarEntry": { "b0a7bfc34c": "Ocultar de la barra lateral", - "88d402b71d": "Lista de verificación de incorporación" + "88d402b71d": "Checklist de onboarding" }, "SetupScriptPromptCard": { "ff1e819a11": "Agregar un script de configuración", "70715947fb": "El script de configuración no puede estar vacío", "888b83bf78": "No se pudo guardar el script de configuración", - "a49196d538": "Se ejecuta cuando Orca crea un nuevo árbol de trabajo.", - "d9f2db2738": "configuración del proyecto", + "a49196d538": "Se ejecuta cuando Orca crea un nuevo worktree.", + "d9f2db2738": "ajustes del proyecto", "a5bb8c5135": "Guardado en este", "dcaa645da5": "OK" }, @@ -3719,39 +3719,39 @@ "96a7f4198c": "Guardar configuración local", "3933401d28": "Configurar", "31b8b01a45": "Ajustes", - "4a98f907ae": "Rever", - "0a98169776": "Agregue un comando de configuración para ejecutar cuando Orca cree nuevos árboles de trabajo.", - "8349e3fa4c": ". Guárdelo para ejecutarlo en busca de nuevos árboles de trabajo.", - "b56d1322f7": "Encontré un comando de configuración en", - "aef6c0a213": "Guarde el comando detectado para ejecutarlo cada vez que Orca cree un árbol de trabajo.", - "660cdc17f8": "guiones de configuración. Agregue un comando local o cambie la fuente en Configuración.", + "4a98f907ae": "Reintentar", + "0a98169776": "Agrega un comando de setup para ejecutar cuando Orca cree nuevos worktrees.", + "8349e3fa4c": ". Guárdalo para ejecutarlo en nuevos worktrees.", + "b56d1322f7": "Se encontró un comando de setup en", + "aef6c0a213": "Guarda el comando detectado para ejecutarlo cada vez que Orca cree un worktree.", + "660cdc17f8": "scripts de setup. Agrega un comando local o cambia la fuente en Ajustes.", "8f6be51aa1": "orca.yaml", - "bb879db364": "Este repo ignora lo compartido", - "0155fb9ed3": "No se pudo verificar el script de configuración de este repo en este momento.", + "bb879db364": "Este repositorio ignora lo compartido", + "0155fb9ed3": "No se pudo verificar el script de configuración de este repositorio en este momento.", "eefa756190": "Configurar manualmente", - "ca4efcbc25": "Ahorrar", + "ca4efcbc25": "Guardar", "d02e6a42b1": "Detectado desde", - "fdbc6cb064": "Script de configuración detectado", - "7275f674cc": "Configuración detectada", - "822ff300ad": "Despedir", + "fdbc6cb064": "Script de setup detectado", + "7275f674cc": "Setup detectado", + "822ff300ad": "Descartar", "5bfd5c8779": "Descartar scripts de configuración" }, "SidebarFeedbackDialog": { "8bf619e4cf": "Cancelar", - "8de03e23c5": "Envíelo solo con sus comentarios escritos o conecte \"gh\" para incluir la identidad de GitHub.", - "d20439c560": "Comprobando la identidad de GitHub...", + "8de03e23c5": "Envíalo solo con tu feedback escrito o conecta `gh` para incluir tu identidad de GitHub.", + "d20439c560": "Comprobando identidad de GitHub...", "5b120b9634": "Enviar de forma anónima", "c9e5ea0791": "GitHub:", "d46ddd66fc": "¿Qué podríamos mejorar?", "3460258a54": "Sigue en X", - "26108d3699": "Únete a la Discord", - "d245c4ef6c": "Problemas de GitHub", - "9b33530b3d": "Otras formas de llegar a nosotros", - "a828fa4aee": "Comparta lo que funciona, lo que no funciona o lo que Orca debería hacer a continuación.", - "0eb643f07f": "Enviar comentarios", - "60b721e857": "No se pudieron enviar comentarios. Por favor inténtalo de nuevo.", - "7a46c228b8": "Gracias por los comentarios.", - "a2fd890d9e": "Por favor ingrese sus comentarios antes de enviarlos.", + "26108d3699": "Únete a Discord", + "d245c4ef6c": "Issues de GitHub", + "9b33530b3d": "Otras formas de contactarnos", + "a828fa4aee": "Comparte qué funciona, qué está roto o qué debería hacer Orca después.", + "0eb643f07f": "Enviar feedback", + "60b721e857": "No se pudo enviar el feedback. Inténtalo de nuevo.", + "7a46c228b8": "Gracias por el feedback.", + "a2fd890d9e": "Introduce feedback antes de enviarlo.", "f2e42e1307": "Enviar", "69969ba364": "Envío…" }, @@ -3760,11 +3760,11 @@ "92a23e6d07": "Restablecer filtros", "81ded53722": "SSH", "b9e8802e73": "Ningún proyecto coincide", - "779b7ba05d": "Claro", + "779b7ba05d": "Limpiar", "139877b384": "Seleccionar todo", "5f7085a077": "Proyectos", "e5cb32a898": "Ocultar rama predeterminada", - "638a2d221d": "Ocultar durmiendo", + "638a2d221d": "Ocultar en reposo", "f506a1262a": "Filtrar espacios de trabajo", "75405270ed": "Editar filtros ({{value0}} activo)", "489d1c8c9f": "Buscar proyectos...", @@ -3773,7 +3773,7 @@ }, "SidebarHeader": { "92154beb7e": "Nuevo espacio de trabajo", - "49f62c5665": "tablero del espacio de trabajo", + "49f62c5665": "Tablero de espacios de trabajo", "5c9c7c16aa": "Agregar un proyecto para crear espacios de trabajo", "ca6f729da2": "Nuevo espacio de trabajo ({{value0}})", "a30e34eb5c": "Cerrar tablero del espacio de trabajo", @@ -3781,25 +3781,25 @@ }, "SidebarNav": { "80611a8b10": "Buscar", - "0c3395fd32": "Buscar árboles de trabajo y pestañas del navegador", + "0c3395fd32": "Buscar worktrees y pestañas del navegador", "c86d83b5c3": "Nuevo", "1b5c41caee": "Orca Móvil", "9c95e1ce91": "Agentes", "f323383e9a": "Automatizaciones", "e7ad3c540d": "Abrir tareas de Jira", - "c39ab10000": "Abrir tareas Lineares", + "c39ab10000": "Abrir tareas de Linear", "196c1b5362": "Abrir tareas de GitLab", "0ccba862b8": "Abrir tareas de GitHub", "fee535205b": "Tareas", "d599269755": "Ocultar de la barra lateral" }, "SidebarRepositoryFilterSection": { - "d3a9c4cea1": "Claro", + "d3a9c4cea1": "Limpiar", "7679f0c268": "Proyectos", "f10ca29601": "Quitar filtro {{value0}}", "2656053db4": "SSH", "83a820fa71": "Filtrar proyectos...", - "5a273fbfce": "Añadir proyecto...", + "5a273fbfce": "Agregar proyecto...", "4815c70605": "Ningún proyecto coincide", "bbbc6e8e3b": "Ningún proyecto no seleccionado coincide" }, @@ -3808,29 +3808,29 @@ "29c56f30ee": "Buscar actualizaciones", "eb9884e55b": "Discord", "5687ab246a": "GitHub", - "5f83d86d92": "Registro de cambios", - "cdc87f897e": "Documentos", + "5f83d86d92": "Changelog", + "cdc87f897e": "Docs", "e565171a7c": "Atajos de teclado", "4cf5b868d7": "Enviar comentarios", "a428c25998": "Ajustes", "2991a0106c": "Ayuda", "4e8f5710d3": "No se pudo reiniciar Orca.", "5161eef55d": "Reiniciando Orca...", - "d396773ef0": "de cheques", - "f8a2c91d4e": "Hitos", - "b7e4d2a19c": "Introducción", + "d396773ef0": "comprobando", + "f8a2c91d4e": "Milestones", + "b7e4d2a19c": "Onboarding", "c4f8e1b72a": "X" }, "SidebarToolbar": { - "19e32d0e5f": "Abra el selector de carpetas para agregar un proyecto", + "19e32d0e5f": "Abrir el selector de carpetas para agregar un proyecto", "abc62b6328": "Agregar proyecto", - "87d0064026": "El tablero del espacio de trabajo se movió a la barra inferior", - "a30e34eb5c": "Cerrar tablero del espacio de trabajo", - "49f62c5665": "tablero del espacio de trabajo" + "87d0064026": "El tablero de espacios de trabajo se movió a la barra inferior", + "a30e34eb5c": "Cerrar tablero de espacios de trabajo", + "49f62c5665": "Tablero de espacios de trabajo" }, "SidebarWorkspaceFilterSection": { "c3fa13dc2e": "Ocultar rama predeterminada", - "ed1611b65b": "Ocultar durmiendo", + "ed1611b65b": "Ocultar en reposo", "82594419ba": "Filtros", "automationCreated": "Ocultar creados por automatizaciones" }, @@ -3851,19 +3851,19 @@ "b451c8b162": "Reciente", "6664282a7b": "Arrastra proyectos para organizarlos", "7b316bdd51": "Manual", - "7153d07485": "Arrastre los espacios de trabajo para organizarlos dentro de cada grupo.", + "7153d07485": "Arrastra los espacios de trabajo para organizarlos dentro de cada grupo.", "2170d553cf": "Proyecto", "b759bb87ee": "Agents que necesitan atención, luego actividad más reciente.", - "503462f2b4": "Actividad del Agent", + "503462f2b4": "Actividad de Agents", "3728165cdd": "Nombre", "2a81e07366": "Lista completa", "25105b28cb": "Compacto", - "d7084e8bc8": "Actividad del Agent", + "d7084e8bc8": "Actividad de Agents", "b64d8bcca0": "Puertos", "26c71e536c": "Notas", "b8dcc6f321": "Enlace PR/MR", - "ca4d3c522e": "Cuestión Linear", - "91dfc653e8": "billete de GitHub", + "ca4d3c522e": "Issue de Linear", + "91dfc653e8": "Ticket de GitHub", "bdd23b4e07": "Issues de GitHub", "44713a5d04": "Issues de Linear", "cc17bd443b": "Detallado", @@ -3871,7 +3871,7 @@ "e029a2d775": "Estado", "c2c7a45cda": "Ninguno", "680043342f": "compacto", - "c7591b6014": "repo", + "c7591b6014": "repositorio", "hosts": "Hosts", "allHostsDetail": "Mostrar todos los hosts", "configuredSshHost": "SSH configurado", @@ -3885,19 +3885,19 @@ "8d62c68b35": "Notas", "automation": "Automatización", "2d74665a56": "Puertos", - "65a9820bd1": "Estados del agente", + "65a9820bd1": "Estados de Agents", "219ebf1961": "Nombre de rama", "folderPathIdentity": "Rama / ruta de carpeta" }, "SshDisconnectedDialog": { "ca4a7892af": "Conectando...", - "89385db176": "Despedir", - "656368f3a2": "Error de reconexión", + "89385db176": "Descartar", + "656368f3a2": "Falló la reconexión", "376bed88e5": "La conexión al host remoto encontró un error.", "4afcca1d24": "Reconectar", "11552bf786": "SSH desconectado", "cb5938ae79": "Reconectando...", - "disconnected": "This remote repository is not connected.", + "disconnected": "Este host SSH no está conectado.", "reconnecting": "Reconectando con el host remoto...", "reconnectionFailed": "Falló la reconexión con el host remoto.", "authFailed": "Falló la autenticación con el host remoto." @@ -3910,13 +3910,13 @@ "cefae8983e": "Fijado" }, "WorkspaceKanbanDrawerHeader": { - "f369f5c5a3": "Cerca", - "e1a34450fc": "Organice espacios de trabajo por estado y abra tarjetas de espacios de trabajo.", + "f369f5c5a3": "Cerrar", + "e1a34450fc": "Organiza espacios de trabajo por estado y abre tarjetas de espacios de trabajo.", "81870af08f": "seleccionado", - "c6a77ab0f4": "tablero del espacio de trabajo" + "c6a77ab0f4": "Tablero de espacios de trabajo" }, "WorkspaceKanbanPinDropTarget": { - "c30151c5ee": "Vaya aquí para fijar sin cambiar el estado.", + "c30151c5ee": "Suelta aquí para fijar sin cambiar el estado.", "8fae2d0862": "Fijado" }, "WorkspaceKanbanSettingsMenu": { @@ -3925,17 +3925,17 @@ "b45b350eb0": "Mover {{value0}} a la izquierda", "8ce44af9a8": "Cambiar nombre {{value0}}", "395e541d5d": "Estados", - "34f03eb0de": "Configuración del tablero", - "26cbc92150": "Configuración del tablero del espacio de trabajo", - "87d24a0c2f": "Sincronizar el estado del tablero y de incidencias", - "4c2eaa78cc": "Al mover un espacio de trabajo vinculado, se actualiza el estado de su incidencia de Linear cuando existe un estado de flujo de trabajo coincidente." + "34f03eb0de": "Ajustes del tablero", + "26cbc92150": "Ajustes del tablero de espacios de trabajo", + "87d24a0c2f": "Sincronizar tablero y estado de issue", + "4c2eaa78cc": "Al mover un espacio de trabajo vinculado, se actualiza el estado de su issue de Linear cuando existe un estado de workflow coincidente." }, "WorkspaceKanbanStatusLane": { "8ad104642b": "Vacío", - "3611d1ae7f": "Cambiar el tamaño de las columnas del tablero del espacio de trabajo" + "3611d1ae7f": "Cambiar tamaño de columnas del tablero de espacios de trabajo" }, "WorkspaceStatusAppearancePopover": { - "514be2f569": "Establezca el color {{value0}} en {{value1}}", + "514be2f569": "Establecer color de {{value0}} en {{value1}}", "8be427206b": "Icono", "2ac106f6b2": "Color", "74b1413279": "Apariencia", @@ -3944,12 +3944,12 @@ "WorktreeCard": { "a88c92d0e3": "{{value0}}/{{value1}} ya existe.", "6f09f58541": "Eliminar espacio de trabajo", - "0777de5970": "Árbol de trabajo principal (directorio de clonación original)", - "0f33af979b": "Pago parcial. Los archivos fuera de estas rutas no están en el disco.", - "4f964d5e8c": "escaso", - "7d517f82e2": "primario", - "4eba2ea99e": "Falló el nombre automático. Haga clic para ver detalles.", - "74522ee457": "cambiar el nombre falló", + "0777de5970": "Worktree principal (directorio de clonación original)", + "0f33af979b": "Checkout parcial. Los archivos fuera de estas rutas no están en disco.", + "4f964d5e8c": "sparse", + "7d517f82e2": "principal", + "4eba2ea99e": "Falló el nombre automático. Haz clic para ver detalles.", + "74522ee457": "falló el cambio de nombre", "02e19349f4": "Error al cambiar el nombre automáticamente: ver error", "691ccfd622": "Eliminando…", "35ccfe2475": "Proyecto {{value0}}", @@ -3958,17 +3958,17 @@ "8cb634cda6": "Mostrar espacios de trabajo secundarios", "01f45d3d8a": "barra lateral", "93aebe4529": "Carpeta", - "0d224eff10": "árbol de trabajo primario", - "ca74db7550": "Proyecto remoto vía SSH", + "0d224eff10": "Worktree principal", + "ca74db7550": "Proyecto en host SSH", "021538e1d1": "SSH desconectado", "runtimeHostDisconnected": "Servidor desconectado", - "runtimeHostProject": "Proyecto en servidor Orca", + "runtimeHostProject": "Proyecto en servidor de Orca", "automationCreated": "Creado por automatización", "branchIdentity": "Rama", "branchFolderPathIdentity": "Rama o ruta de carpeta" }, "WorktreeCardAgents": { - "1b0a156717": "Agentes" + "1b0a156717": "Agents" }, "WorktreeCardReviewDetailSection": { "reviewHeader": "{{value0}} #{{value1}}", @@ -3982,15 +3982,15 @@ "dbe2d18972": "Más acciones de {{value0}}", "ae76907ca6": "Desvincular {{value0}}", "ad25c3ff05": "Ver en {{value0}}", - "2c67730e07": "Abierto en Orca", + "2c67730e07": "Abrir en Orca", "e42941631a": "Ver en Linear", "5e982e6128": "Linear {{value0}}", - "807b13b9ec": "Editar problema", + "807b13b9ec": "Editar issue", "b22f058067": "Ver en GitHub", - "e97d8f2876": "Número {{value0}}", + "e97d8f2876": "Issue #{{value0}}", "3ea2702e62": "Vinculado {{value0}} #{{value1}}", "b105fd3057": "Linear vinculado {{value0}}", - "3f2649eeb8": "Problema vinculado #{{value0}}", + "3f2649eeb8": "Issue vinculado #{{value0}}", "fe075cb851": "Notas del espacio de trabajo", "automationHeader": "Automatización", "openAutomation": "Abrir automatización", @@ -4011,19 +4011,19 @@ "fe188062a1": "Estado: Abierto", "2931b42b09": "Estado: Borrador {{value0}}", "e888362def": "Estado: Cerrado", - "f394b3e86e": "Estado: Fusionado", + "f394b3e86e": "Estado: merged", "af2b07bda5": "Estado: {{value0}}", - "29df45afa2": "SEÑOR" + "29df45afa2": "MR" }, "WorktreeCardPorts": { - "34f733dda2": "Ir al árbol de trabajo", + "34f733dda2": "Ir al worktree", "3240f320d7": "Puertos en vivo", "3e5f66564e": "Espacio de trabajo no disponible", "2f854442ff": "Detener proceso", "c8067a829a": "Copiar {{value0}}", "33bc7d7495": "Abrir en el navegador", "9950fe2d20": "No se pudieron actualizar los puertos", - "5d1a5d51bb": "Proceso detenido el {{value0}}", + "5d1a5d51bb": "Proceso detenido en {{value0}}", "c89f290e25": "Copiado {{value0}}", "d1113f4660": "No se pudo abrir el navegador", "fed49903c9": "{{value0}} en vivo {{value1}}" @@ -4034,57 +4034,57 @@ "e091caab15": "No se pudo encontrar el proyecto.", "439fa94d53": "Actualizar", "579b1a8e61": "Quitar del padre", - "8d9cd19d09": "Abrir árbol de trabajo padre", + "8d9cd19d09": "Abrir worktree padre", "d35dfeae58": "Quitar del grupo", "76865d827f": "Mover al grupo", "503ec0f8e6": "Nuevo grupo del proyecto", "3350101edb": "Copiar ruta", - "f4475537d8": "Borrar", - "f5ac91531d": "Eliminar proyecto de Orca", + "f4475537d8": "Eliminar", + "f5ac91531d": "Quitar proyecto de Orca", "b42391d8bf": "Eliminando…", - "0918b35e4f": "Cierre todos los paneles activos en este espacio de trabajo para liberar memoria y CPU.", - "7d190f7d2b": "Cierre todos los paneles activos en los espacios de trabajo seleccionados para liberar memoria y CPU.", + "0918b35e4f": "Cierra todos los paneles activos en este espacio de trabajo para liberar memoria y CPU.", + "7d190f7d2b": "Cierra todos los paneles activos en los espacios de trabajo seleccionados para liberar memoria y CPU.", "84cdbb7e30": "Mover al estado", "56cde9e8e6": "Mover estados a", "f50603c6b2": "Marcar como no leído", "8dacff1fe0": "Marcar como leído", - "3baa7d6507": "Alfiler", - "697d0f6e1b": "Desprender", + "3baa7d6507": "Fijar", + "697d0f6e1b": "Desanclar", "250de158fd": "Eliminar espacio de trabajo", - "changeParentWorkspace": "Cambiar árbol de trabajo padre...", - "setParentWorkspace": "Establecer árbol de trabajo padre...", + "changeParentWorkspace": "Cambiar worktree padre...", + "setParentWorkspace": "Establecer worktree padre...", "workspaceSection": "Espacio de trabajo", - "primaryDeleteDisabled": "El árbol de trabajo principal no se puede eliminar. Elimina el proyecto en su lugar.", - "deleteWorktree": "Eliminar árbol de trabajo" + "primaryDeleteDisabled": "El worktree principal no se puede eliminar. Quita el proyecto en su lugar.", + "deleteWorktree": "Eliminar worktree" }, "WorktreeList": { "d880ea0744": "Crea un grupo y mueve este proyecto a él.", - "bc1460beb3": "Actualice el nombre del grupo que se muestra en la barra lateral.", + "bc1460beb3": "Actualiza el nombre del grupo que se muestra en la barra lateral.", "13757c053c": "Nuevo grupo de proyecto", "f9dc6cc5d3": "Cambiar nombre del grupo de proyectos", - "370c6a55dd": "Borrar filtros", + "370c6a55dd": "Limpiar filtros", "b7acbf038b": "No se encontraron espacios de trabajo", - "0c6ee14f23": "niño", + "0c6ee14f23": "hijo", "5fc9d1891b": "Eliminando…", "c83968f87f": "Eliminar proyecto", "64e55f7f01": "Quitar del grupo", "4a08fb55f2": "Mover al grupo", "cbfd565f83": "Nuevo grupo del proyecto", "e82d3589a1": "Cambiar icono de proyecto", - "2cdffbc728": "Configuración del proyecto", + "2cdffbc728": "Ajustes del proyecto", "2ef41bf9a7": "Acciones del proyecto", "609633a9e6": "Acciones del proyecto para {{value0}}", "902115cdbe": "Eliminar grupo", "4d7b73658c": "Cambiar nombre de grupo", - "79465e9034": "Acciones grupales para {{value0}}", - "bfbedc547b": "árboles de trabajo", - "45fbfe0335": "rebautizar", + "79465e9034": "Acciones del grupo para {{value0}}", + "bfbedc547b": "Worktrees", + "45fbfe0335": "renombrar", "ebc5c7dcef": "Ocultar espacios de trabajo secundarios", "84a2238242": "Mostrar espacios de trabajo secundarios", - "045a8aed48": "niños", - "2ca6e29a3c": "repo", + "045a8aed48": "hijos", + "2ca6e29a3c": "repositorio", "bb85cd86ba": "Crear espacio de trabajo para {{value0}}", - "ebadb7eadb": "{{value0}} {{value1}} niño {{value2}}", + "ebadb7eadb": "{{value0}} {{value1}} hijo {{value2}}", "20bebf9c7f": "Mostrar {{value0}} espacio de trabajo secundario", "c1f4a31623": "Mostrar {{value0}} espacios de trabajo secundarios", "e97297cb75": "Ocultar {{value0}} espacio de trabajo secundario", @@ -4099,38 +4099,38 @@ "hostDisconnected": "Desconectado", "failedNestWorkspace": "No se pudo anidar el espacio de trabajo", "sidebarRowMissing": "El destino ya no existe", - "failedUnnestWorkspace": "Failed to unnest workspace" + "failedUnnestWorkspace": "No se pudo quitar la anidación del espacio de trabajo" }, "WorktreeMetaDialog": { "3db0a2a593": "Cancelar", - "b48c271d39": "para guardar, Mayús+Entrar para una nueva línea.", - "7f0be5e9a6": "Admite **markdowns**: negrita, listas, \"código\", enlaces. Presione Entrar o", - "030d484fc0": "Notas sobre este árbol de trabajo...", + "b48c271d39": "para guardar, Mayús+Enter para una nueva línea.", + "7f0be5e9a6": "Admite **markdown** — negrita, listas, `código`, enlaces. Pulsa Enter o", + "030d484fc0": "Notas sobre este worktree...", "9c1d1e9b71": "Comentario", - "5ae06f40fd": "Pegue una URL de solicitud de extracción o ingrese un número. Déjelo en blanco para eliminar el enlace.", + "5ae06f40fd": "Pega una URL de pull request o introduce un número. Déjalo en blanco para eliminar el enlace.", "077a4f7b5c": "PR # o URL de GitHub", "1b91db7e14": "GH PR", - "7c454be4c5": "Pegue la URL de un problema o ingrese un número. Déjelo en blanco para eliminar el enlace.", - "029ea5ec57": "Abrir problema de GitHub", - "741279e7b7": "Número de problema o URL de GitHub", - "645fa4a0fd": "Problema de GH", - "459ad7f650": "Solo cambia el nombre que se muestra en la barra lateral; la carpeta en el disco permanece igual. Déjelo en blanco para usar el nombre de la rama o la carpeta.", + "7c454be4c5": "Pega una URL de issue o introduce un número. Déjalo en blanco para eliminar el enlace.", + "029ea5ec57": "Abrir issue de GitHub", + "741279e7b7": "Issue # o URL de GitHub", + "645fa4a0fd": "GH Issue", + "459ad7f650": "Solo cambia el nombre que se muestra en la barra lateral — la carpeta en disco no cambia. Déjalo en blanco para usar el nombre de la rama o carpeta.", "7f21e0464f": "Nombre para mostrar personalizado...", "ad5e4e514f": "Nombre para mostrar", - "65770ad0f0": "Edite enlaces y notas de GitHub para este espacio de trabajo.", - "382fd11a3e": "Editar detalles del árbol de trabajo", - "2174f17011": "Ahorrar", - "61d6f612cf": "Ahorro..." + "65770ad0f0": "Edita enlaces y notas de GitHub para este espacio de trabajo.", + "382fd11a3e": "Editar detalles del worktree", + "2174f17011": "Guardar", + "61d6f612cf": "Guardando..." }, "WorktreeOpenInMenu": { - "1417fd8380": "Personalizar aplicaciones...", + "1417fd8380": "Personalizar apps...", "8009ab69a6": "Abrir en", - "bd0e8159f8": "Verifique el comando del editor o la configuración del administrador de archivos en esta máquina.", + "bd0e8159f8": "Verifica el comando del editor o la configuración del gestor de archivos en esta máquina.", "9a5381eb09": "No se pudo abrir la carpeta del espacio de trabajo.", - "0bed8727db": "Es posible que se haya movido o eliminado. Actualizar los espacios de trabajo o eliminarlos de Orca.", + "0bed8727db": "Puede que se haya movido o eliminado. Actualiza los espacios de trabajo o quítalo de Orca.", "3921d3d9a5": "No se encontró la carpeta del espacio de trabajo.", "f387af445b": "La ruta del espacio de trabajo no es una ruta local válida.", - "3ec372b664": "administrador de archivos" + "3ec372b664": "gestor de archivos" }, "WorktreeTitleInlineRename": { "2f42ae024f": "No leído:", @@ -4138,9 +4138,9 @@ "8df295a78d": "No se pudo cambiar el nombre del espacio de trabajo." }, "WorktreeVisibilityDialog": { - "83a5ba8dd1": "Árboles de trabajo que no son de Orca", + "83a5ba8dd1": "Worktrees que no son de Orca", "f1f71b9f02": "Importar", - "759371df43": "Esconder", + "759371df43": "Ocultar", "25ddf19920": "{{value0}} disponible para importar", "8372e4bbd9": "{{value0}} mostrado actualmente", "5d02a5647f": "Oculto de la barra lateral", @@ -4157,12 +4157,12 @@ "7edb8ebe24": "Clonar desde URL", "a6c20dca96": "Abrir un proyecto desde un destino SSH", "3d162cc76f": "Proyecto remoto", - "fb4fc5380e": "Proyecto local, repo de Git o carpeta con muchos repos", + "fb4fc5380e": "Proyecto local, repositorio de Git o carpeta con muchos repositorios", "2281fdc8c7": "Explorar carpeta", - "sshCreateUnavailable": "Not available for SSH hosts yet", - "sshBrowseTitle": "Open project on SSH host", - "sshBrowseDescription": "Existing Git repository or folder on this SSH host", - "runtimeBrowseDescription": "Existing Git repository or folder on this host" + "sshCreateUnavailable": "Aún no disponible para hosts SSH", + "sshBrowseTitle": "Abrir proyecto en host SSH", + "sshBrowseDescription": "Repositorio Git o carpeta existente en este host SSH", + "runtimeBrowseDescription": "Repositorio Git o carpeta existente en este host" } } } @@ -4171,18 +4171,18 @@ "delete": { "worktree": { "flow": { - "b81b4e40ca": "Actualice el espacio e inténtelo nuevamente si la lista de espacios de trabajo parece obsoleta.", - "7243145cd6": "No se han seleccionado espacios de trabajo eliminables", + "b81b4e40ca": "Actualiza el espacio e inténtalo de nuevo si la lista de espacios de trabajo parece obsoleta.", + "7243145cd6": "No se seleccionaron espacios de trabajo que se puedan eliminar", "ae57cbf6e4": "No se pudo eliminar el espacio de trabajo", - "7488ed8711": "Vista", + "7488ed8711": "Ver", "2b20ce87b3": "Forzar eliminación", - "4f3876c0f5": "Error al forzar la eliminación" + "4f3876c0f5": "Falló la eliminación forzada" }, "toast": { "1d0fa5c0a5": "No se pudo eliminar el espacio de trabajo {{value0}}", - "ead7b8ee15": "Ha cambiado archivos. Utilice Forzar eliminación para eliminarlo de todos modos.", - "905fc8efac": "Git ya eliminó este espacio de trabajo. Utilice Forzar eliminación para borrarlo de Orca.", - "0899ebdb28": "Git ya olvidó este espacio de trabajo, pero su directorio todavía está en el disco. Utilice Forzar eliminación para eliminar el directorio huérfano." + "ead7b8ee15": "Tiene archivos modificados. Usa Forzar eliminación para eliminarlo de todos modos.", + "905fc8efac": "Git ya eliminó este espacio de trabajo. Usa Forzar eliminación para borrarlo de Orca.", + "0899ebdb28": "Git ya olvidó este espacio de trabajo, pero su directorio todavía está en disco. Usa Forzar eliminación para eliminar el directorio huérfano." } } }, @@ -4200,9 +4200,9 @@ "header": { "create": { "state": { - "992cfbc44b": "Crear un nuevo árbol de trabajo para {{value0}}", - "3a70acd808": "Vuelva a conectar el destino SSH antes de crear espacios de trabajo para {{value0}}", - "6d022563a8": "Vuelva a conectar el destino SSH antes de crear espacios de trabajo", + "992cfbc44b": "Crear un nuevo worktree para {{value0}}", + "3a70acd808": "Reconecta el destino SSH antes de crear espacios de trabajo para {{value0}}", + "6d022563a8": "Reconecta el destino SSH antes de crear espacios de trabajo", "62e71f2d5d": "Crear espacio de trabajo para {{value0}}" } } @@ -4212,10 +4212,10 @@ "project": { "drop": { "669e12dd97": "Carpetas locales y repositorios Git", - "ffc769ca29": "Carpeta desplegable para agregar proyecto", - "740e8d0d46": "Utilice Agregar proyecto para rutas de servidor", - "e344666fb8": "Tiempo de ejecución del servidor activo", - "d0f8943f8b": "Preparando el flujo de adición del proyecto", + "ffc769ca29": "Suelta una carpeta para agregar el proyecto", + "740e8d0d46": "Usa Agregar proyecto para rutas de host", + "e344666fb8": "Servidor activo", + "d0f8943f8b": "Preparando el flujo para agregar proyecto", "18d3cf40e9": "Comprobando carpeta" } } @@ -4223,17 +4223,17 @@ "sleep": { "worktree": { "flow": { - "c460fecc4a": "No se pudieron dormir algunos espacios de trabajo", - "8bc3fc0671": "No se pudo dormir el espacio de trabajo" + "c460fecc4a": "No se pudieron poner en reposo algunos espacios de trabajo", + "8bc3fc0671": "No se pudo poner en reposo el espacio de trabajo" } } }, "useAddRepoCloneFlow": { "4d0013cc93": "Repositorio clonado", - "0dc4d1b657": "Introduzca una ruta de servidor para el destino del clon." + "0dc4d1b657": "Introduce una ruta del host para el destino del clon." }, "useAddRepoLocalFolderFlow": { - "7ab10e4974": "Utilice una ruta de servidor para agregar proyectos desde un tiempo de ejecución remoto.", + "7ab10e4974": "Usa una ruta del host para agregar proyectos desde un host remoto.", "skippedBatchFolders": "Se omitieron algunas carpetas", "skippedBatchFoldersDescription": "Agrega las carpetas omitidas de forma individual para revisarlas o confirmarlas." }, @@ -4243,32 +4243,32 @@ "1b33c5f090": "No se importaron repositorios" }, "useSidebarProjectDrop": { - "f34a286c0d": "No se pudo agregar la carpeta descartada.", + "f34a286c0d": "No se pudo agregar la carpeta soltada.", "451a4638db": "Suelta una carpeta para agregarla como proyecto.", - "5ccb56c7be": "Utilice Agregar proyecto para ingresar una ruta de servidor.", - "849ef13dc0": "Las ubicaciones de carpetas locales no están disponibles para los tiempos de ejecución del servidor.", - "c0315153d1": "Suelte una carpeta a la vez." + "5ccb56c7be": "Usa Agregar proyecto para introducir una ruta del host.", + "849ef13dc0": "No se pueden soltar carpetas locales en runtimes de servidor.", + "c0315153d1": "Suelta una carpeta a la vez." }, "workspace": { "status": { "cb387159f6": "En curso", "6c1efa2cf8": "En revisión", "6b8285b8dd": "Hecho", - "93ac840dcb": "Obstruido", - "2c19d1db33": "Jugar", + "93ac840dcb": "Bloqueado", + "2c19d1db33": "Play", "111db162bf": "En pausa", "642da473f2": "Alerta", "6380517b10": "Bandera", - "251c817bdd": "Minutero", - "409528031f": "Revisar", - "5f9ca31a84": "Espera", - "821d156f54": "discontinuo", + "251c817bdd": "Temporizador", + "409528031f": "Review", + "5f9ca31a84": "En espera", + "821d156f54": "Discontinuo", "226d1e7773": "Progreso", "a702bc08d4": "Punto", "b4a7101fe1": "Círculo", - "1a9383112b": "Progreso del director", - "caebe3c10f": "Revisión del director", - "895f381714": "Director hecho", + "1a9383112b": "Conductor Progress", + "caebe3c10f": "Conductor Review", + "895f381714": "Conductor Done", "caabd5ca85": "Zinc", "7adb43ecf0": "Rosa", "ddf25b6262": "Esmeralda", @@ -4283,16 +4283,16 @@ "card": { "compact": { "agents": { - "a128d7006b": "{{value0}} {{value1}} niño {{value2}}", - "289a1d2ca7": "Expanda {{value0}}. {{value1}}", + "a128d7006b": "{{value0}} {{value1}} hijo {{value2}}", + "289a1d2ca7": "Expandir {{value0}}. {{value1}}", "0c1debfe84": "Contraer {{value0}}" } } }, "list": { "groups": { - "0ed04075b8": "Todo", - "4aeefc5996": "Fijado", + "0ed04075b8": "Todos", + "4aeefc5996": "Fijados", "682ed5d551": "Cerrado", "7c2f009786": "En curso", "6798dc7c94": "En revisión", @@ -4301,52 +4301,52 @@ } }, "CacheTimer": { - "07729cc155": "venció" + "07729cc155": "expirado" }, "DeleteWorktreeDialogFooter": { "c0e972d726": "Cancelar", - "cf95e3b5bb": "Cerca" + "cf95e3b5bb": "Cerrar" }, "index": { "b826a98b6f": "ocupado" }, "LinearAgentSkillSetupPrompt": { - "missingCliAndSkill": "Faltan la CLI de Orca y la habilidad de Agent de Linear.", - "modalTitle": "Activar acceso a tickets de Linear", - "modalDescription": "Instala la habilidad de Linear desde una terminal.", - "modalPrompt": "Permite que los Agents lean y editen el ticket de Linear adjunto.", + "missingCliAndSkill": "Faltan la CLI de Orca y el skill de Linear para Agents.", + "modalTitle": "Habilitar acceso a issues de Linear", + "modalDescription": "Instala el skill de Linear desde una terminal.", + "modalPrompt": "Permite que los Agents lean y editen el issue de Linear adjunto.", "dontShowAgain": "No volver a mostrar", "notNow": "Ahora no", - "missingBoth": "Faltan la CLI de Orca y la habilidad de Agent de Linear.", + "missingBoth": "Faltan la CLI de Orca y el skill de Linear para Agents.", "missingCli": "Falta la CLI de Orca.", - "missingSkill": "Falta la habilidad de Agent de Linear.", - "title": "Configurar habilidad de Agent de Linear", - "remoteCopy": "Esto instala la configuración del host; los entornos de Agent remotos pueden necesitar una configuración separada.", - "hostCopy": "Instálala para traspasos de Agent del host desde trabajo Linear enlazado.", - "dismiss": "Descartar configuración de habilidad de Agent de Linear", + "missingSkill": "Falta el skill de Linear para Agents.", + "title": "Configurar skill de Linear para Agents", + "remoteCopy": "Esto instala el setup del host; los entornos de Agents remotos pueden necesitar setup propio.", + "hostCopy": "Instálalo para handoffs de Agents del host desde trabajo de Linear enlazado.", + "dismiss": "Descartar setup del skill de Linear para Agents", "setup": "Configurar", "recheck": "Volver a comprobar", - "panelTitle": "Habilidad de Agent de Linear", - "panelDescription": "Instala la habilidad de Agent del host para traspasos desde tareas Linear enlazadas.", - "terminalTitle": "Instalar habilidad de Agent de Linear", - "terminalAria": "Terminal de instalación de la habilidad de Agent de Linear", - "install": "Instalar CLI y habilidad", - "successTitle": "El acceso a tickets de Linear está listo", - "successDescription": "Los Agents ya pueden leer y actualizar tickets de Linear enlazados desde este espacio de trabajo.", - "successDescriptionWsl": "Los Agents de WSL ya pueden usar tickets de Linear enlazados desde este espacio de trabajo.", - "successDescriptionRemote": "Los Agents del host ya pueden usar tickets de Linear enlazados. Los entornos de Agent remotos aún pueden necesitar su propia configuración.", - "successStatus": "Acceso a tickets de Linear listo", + "panelTitle": "Skill de Linear para Agents", + "panelDescription": "Instala el skill de Agents del host para handoffs desde tareas de Linear enlazadas.", + "terminalTitle": "Instalar skill de Linear para Agents", + "terminalAria": "Terminal de instalación del skill de Linear para Agents", + "install": "Instalar CLI y skill", + "successTitle": "El acceso a issues de Linear está listo", + "successDescription": "Los Agents ya pueden leer y actualizar issues de Linear enlazados desde este espacio de trabajo.", + "successDescriptionWsl": "Los Agents de WSL ya pueden usar issues de Linear enlazados desde este espacio de trabajo.", + "successDescriptionRemote": "Los Agents del host ya pueden usar issues de Linear enlazados. Los entornos de Agents remotos aún pueden necesitar setup propio.", + "successStatus": "Acceso a issues de Linear listo", "done": "Listo", - "wslCopy": "Instálala para traspasos de Agent en WSL desde trabajo Linear enlazado.", + "wslCopy": "Instálalo para handoffs de Agents en WSL desde trabajo de Linear enlazado.", "wslLabel": "WSL predeterminado", - "toastMissingCliAndSkill": "Faltan la CLI de Orca y la habilidad de Linear", + "toastMissingCliAndSkill": "Faltan la CLI de Orca y el skill de Linear", "toastMissingCli": "Falta la CLI de Orca", - "toastMissingSkill": "Falta la habilidad de Linear", - "toastInstallCliAndSkillDescription": "Instala la CLI de Orca y la habilidad de Linear para permitir que tus Agents lean y editen tareas de Linear.", + "toastMissingSkill": "Falta el skill de Linear", + "toastInstallCliAndSkillDescription": "Instala la CLI de Orca y el skill de Linear para permitir que tus Agents lean y editen tareas de Linear.", "toastInstallCliDescription": "Instala la CLI de Orca para permitir que tus Agents lean y editen tareas de Linear.", - "toastInstallSkillDescription": "Instala la habilidad de Linear para permitir que tus Agents lean y editen tareas de Linear a través de la CLI de Orca.", - "toastRemoteDescription": "{{value0}} Los entornos de Agent remotos pueden necesitar su propia configuración.", - "toastWslDescription": "{{value0}} Esta configuración se ejecuta en el runtime de Agent WSL seleccionado." + "toastInstallSkillDescription": "Instala el skill de Linear para permitir que tus Agents lean y editen tareas de Linear a través de la CLI de Orca.", + "toastRemoteDescription": "{{value0}} Los entornos de Agents remotos pueden necesitar setup propio.", + "toastWslDescription": "{{value0}} Este setup se ejecuta en el entorno de Agent WSL seleccionado." }, "FolderWorkspaceComposerDialog": { "connectFailed": "No se pudo conectar al proyecto.", @@ -4366,111 +4366,111 @@ "AddRepoHostSelector": { "host": "Host", "local": "Local", - "runtime": "Server", + "runtime": "Servidor", "ssh": "SSH", "connecting": "Conectando", "connect": "Conectar", "addSshHost": "Agregar host SSH", "addRemoteServer": "Agregar servidor remoto", "addRemoteHost": "Agregar host remoto", - "addRemoteHostTooltip": "Elija SSH para una máquina en la que pueda iniciar sesión o servidor remoto para un servidor Orca.", - "addSshHostDetail": "Utilice una máquina existente a través de SSH.", - "addRemoteServerDetail": "Emparéjelo con Orca ejecutándose en otra computadora.", + "addRemoteHostTooltip": "Elige SSH para una máquina donde puedas iniciar sesión, o servidor remoto para un servidor de Orca.", + "addSshHostDetail": "Usa una máquina existente mediante SSH.", + "addRemoteServerDetail": "Empareja con Orca ejecutándose en otro equipo.", "addRemoteHostDetail": "Host SSH o servidor Orca" }, "sidebarHostOptions": { - "3e102f111c": "All hosts", + "3e102f111c": "Todos los hosts", "visibleHostsCount": "{{value0}} hosts" }, "SidebarHostScopeStrip": { "scopedTo": "{{value0}} visible", - "backToAll": "All hosts" + "backToAll": "Todos los hosts" }, "HostRemoveDialog": { - "1a2b3c4d5e": "Removed {{value0}}", - "2b3c4d5e6f": "Failed to remove host", - "3c4d5e6f7a": "Remove {{value0}}?", - "4d5e6f7a8b": "This opens the Orca servers settings where you can remove this server.", - "5e6f7a8b9c": "This removes the saved SSH host and its credentials from this computer. Remote files are not deleted.", + "1a2b3c4d5e": "Se quitó {{value0}}", + "2b3c4d5e6f": "No se pudo quitar el host", + "3c4d5e6f7a": "¿Quitar {{value0}}?", + "4d5e6f7a8b": "Esto abre los ajustes de servidores de Orca, donde puedes quitar este servidor.", + "5e6f7a8b9c": "Esto quita el host SSH guardado y sus credenciales de este equipo. Los archivos remotos no se eliminan.", "6f7a8b9c0d": "Cancelar", - "7a8b9c0d1e": "Open settings", - "8b9c0d1e2f": "Remove host" + "7a8b9c0d1e": "Abrir ajustes", + "8b9c0d1e2f": "Quitar host" }, "HostRenameDialog": { - "1a2b3c4d5e": "Rename host", - "2b3c4d5e6f": "This label is shown only on this computer. Leave it blank to use the default name.", - "3c4d5e6f7a": "Display name", - "4d5e6f7a8b": "Reset to default", + "1a2b3c4d5e": "Renombrar host", + "2b3c4d5e6f": "Esta etiqueta se muestra solo en este equipo. Déjala en blanco para usar el nombre predeterminado.", + "3c4d5e6f7a": "Nombre para mostrar", + "4d5e6f7a8b": "Restablecer predeterminado", "5e6f7a8b9c": "Cancelar", - "6f7a8b9c0d": "Save" + "6f7a8b9c0d": "Guardar" }, "HostSectionHeaderMenu": { - "5b8b4b6a01": "Update server required", - "9b3c1d2e44": "Update client required", - "2c29e2de68": "Connection failed", + "5b8b4b6a01": "Se requiere actualizar el servidor", + "9b3c1d2e44": "Se requiere actualizar el cliente", + "2c29e2de68": "Falló la conexión", "bf07aee59e": "No se pudo desconectar", - "7f1a2b3c4d": "{{value0}} is reachable", - "4f2c8a9b10": "Host actions for {{value0}}", - "6b7c8d9e10": "Host actions", - "8d1e2f3a4b": "Rename…", - "63f36455cc": "Reconnect", - "59b553e2aa": "Disconnect", - "2d3e4f5a6b": "Check connection", - "3c4d5e6f7a": "Manage host…", - "6e7f8a9b0c": "Remove host…" + "7f1a2b3c4d": "{{value0}} está accesible", + "4f2c8a9b10": "Acciones de host para {{value0}}", + "6b7c8d9e10": "Acciones de host", + "8d1e2f3a4b": "Renombrar…", + "63f36455cc": "Reconectar", + "59b553e2aa": "Desconectar", + "2d3e4f5a6b": "Verificar conexión", + "3c4d5e6f7a": "Administrar host…", + "6e7f8a9b0c": "Quitar host…" }, "WorkspaceKanbanDrawer": { - "1975a4e480": "Error al sincronizar el estado de la tarea", - "e02b0d92ff": "Sincronización del estado de la tarea omitida", - "c1d2e3f4a5": "No se pudo leer la incidencia {{value0}} de Linear.", - "d2e3f4a5b6": "No hay ningún estado de flujo de trabajo de Linear que coincida con {{value0}}.", - "e3f4a5b6c7": "Varios estados de flujo de trabajo de Linear coinciden con {{value0}}.", - "f4a5b6c7d8": "No se pudo actualizar la incidencia {{value0}} de Linear.", - "a5b6c7d8e9": "No se pudo sincronizar la incidencia {{value0}} de Linear.", + "1975a4e480": "Falló la sincronización del estado de la tarea", + "e02b0d92ff": "Se omitió la sincronización del estado de la tarea", + "c1d2e3f4a5": "No se pudo leer el issue {{value0}} de Linear.", + "d2e3f4a5b6": "No hay ningún estado de workflow de Linear que coincida con {{value0}}.", + "e3f4a5b6c7": "Varios estados de workflow de Linear coinciden con {{value0}}.", + "f4a5b6c7d8": "No se pudo actualizar el issue {{value0}} de Linear.", + "a5b6c7d8e9": "No se pudo sincronizar el issue {{value0}} de Linear.", "b6c7d8e9f0": "No se pudo completar la sincronización del estado de la tarea.", - "c7d8e9f0a1": "{{value0}} actualizadas", - "d8e9f0a1b2": "{{value0}} omitidas", - "e9f0a1b2c3": "{{value0}} fallidas" + "c7d8e9f0a1": "{{value0}} actualizado", + "d8e9f0a1b2": "{{value0}} omitido", + "e9f0a1b2c3": "{{value0}} falló" }, "WorktreeParentPickerPopover": { - "failedSetParent": "No se pudo establecer el árbol de trabajo padre", + "failedSetParent": "No se pudo establecer el worktree padre", "current": "Actual", - "searchPlaceholder": "Buscar árboles de trabajo...", - "empty": "No hay árboles de trabajo elegibles que coincidan." + "searchPlaceholder": "Buscar worktrees...", + "empty": "No hay worktrees elegibles que coincidan." }, "WorktreeCardStatusSlot": { "branchIdentity": "Rama" }, "NewExternalWorktreesInboxLine": { - "9f2d4c8b17": "Ocultar árboles de trabajo externos permanentemente para {{value0}}", - "d9f7b2a14c": "Contraer árboles de trabajo creados externamente para {{value0}}", - "e2c4a8d91f": "Expandir árboles de trabajo creados externamente para {{value0}}", - "7c4e9b2a81": "Nuevos árboles de trabajo creados externamente", - "5e1b8d3f62": "Ocultar árboles de trabajo externos permanentemente", - "4d7a1c9e53": "Estos árboles de trabajo se crearon fuera de Orca.", + "9f2d4c8b17": "Ocultar worktrees externos permanentemente para {{value0}}", + "d9f7b2a14c": "Contraer worktrees nuevos creados externamente para {{value0}}", + "e2c4a8d91f": "Expandir worktrees nuevos creados externamente para {{value0}}", + "7c4e9b2a81": "Nuevos worktrees creados externamente", + "5e1b8d3f62": "Ocultar worktrees externos permanentemente", + "4d7a1c9e53": "Estos worktrees se crearon fuera de Orca.", "8b3f2e1d74": "Importar", "1c9e7a4b28": "Mantener ocultos", "6f2d8c1e95": "Importar todo", "c3e8a1f4b2": "No volver a mostrar" }, "newExternalWorktreesInboxActions": { - "a11c2f6d89": "No se pudieron mantener ocultos los árboles de trabajo externos. Inténtalo de nuevo.", - "b7e4d1a062": "No se pudieron importar los árboles de trabajo externos. Inténtalo de nuevo.", - "c94f0b3a15": "No se pudieron ocultar permanentemente los árboles de trabajo externos. Inténtalo de nuevo." + "a11c2f6d89": "No se pudieron mantener ocultos los worktrees externos. Inténtalo de nuevo.", + "b7e4d1a062": "No se pudieron importar los worktrees externos. Inténtalo de nuevo.", + "c94f0b3a15": "No se pudieron ocultar permanentemente los worktrees externos. Inténtalo de nuevo." }, "SuppressExternalWorktreeInboxDialog": { - "a4c2d8f1b0": "¿Ocultar árboles de trabajo externos?", - "6e91b3c4d2": "Los árboles de trabajo externos ya no se mostrarán en la barra lateral ni en esta lista para {{value0}}, incluidos los que se creen más adelante.", + "a4c2d8f1b0": "¿Ocultar worktrees externos?", + "6e91b3c4d2": "Los worktrees externos ya no se mostrarán en la barra lateral ni en esta lista para {{value0}}, incluidos los que se creen más adelante.", "1f8a5d9e73": "Puedes volver a activarlo más tarde desde los ajustes del proyecto.", - "8c0b2e7a41": "Abrir ajustes de árboles de trabajo no pertenecientes a Orca", + "8c0b2e7a41": "Abrir ajustes de worktrees que no son de Orca", "5d1c9f0a82": "Cancelar", - "3b7e4a1c96": "Ocultar árboles de trabajo externos" + "3b7e4a1c96": "Ocultar worktrees externos" }, "useAddRepoHostSelection": { "connectionFailed": "La conexión SSH falló." }, "AddRemoteHostDialog": { - "sshHostRequired": "Se requiere un alias de configuración de host o SSH.", + "sshHostRequired": "Se requiere un host o alias de configuración SSH.", "sshPortInvalid": "El puerto debe estar entre 1 y 65535.", "sshRelayGraceInvalid": "El tiempo de espera del terminal debe estar entre 60 y {{value0}} segundos.", "sshSaved": "Se agregó el host SSH.", @@ -4480,14 +4480,14 @@ "serverSaveFailed": "No se pudo agregar el servidor remoto.", "serverTitle": "Agregar servidor remoto", "sshTitle": "Agregar host SSH", - "serverDescription": "Emparéjelo con Orca ejecutándose en otra computadora.", - "sshDescription": "Agregue una máquina persistente en la que pueda iniciar sesión a través de SSH.", + "serverDescription": "Empareja con Orca ejecutándose en otro equipo.", + "sshDescription": "Agrega una máquina persistente donde puedas iniciar sesión por SSH.", "cancel": "Cancelar", - "saving": "Ahorro...", - "save": "Ahorrar", + "saving": "Guardando...", + "save": "Guardar", "label": "Etiqueta", "sshLabelPlaceholder": "Dev box", - "sshHost": "Anfitrión o alias", + "sshHost": "Host o alias", "sshHostPlaceholder": "deploy@server:22", "username": "Nombre de usuario", "usernamePlaceholder": "deploy", @@ -4496,36 +4496,36 @@ "identityFilePlaceholder": "~/.ssh/id_ed25519 (opcional)", "serverName": "Nombre del servidor", "serverNamePlaceholder": "Dev box", - "pairingCode": "código de emparejamiento", + "pairingCode": "Código de emparejamiento", "pairingCodePlaceholder": "orca://pair?code=...", "pairingHelpPrefix": "Ejecuta", "pairingCommand": "orca serve --pairing-address ", "pairingHelpSuffix": "en el servidor y pega la URL de emparejamiento impresa.", "sshImportAlreadySynced": "~/.ssh/config ya está sincronizado.", - "sshImportSynced": "Sincronizado {{value0}} host{{value1}}.", + "sshImportSynced": "Se sincronizó {{value0}} host{{value1}}.", "sshImportFailed": "No se pudo importar la configuración SSH.", - "importing": "Importador...", + "importing": "Importando...", "importSshConfig": "Importar ~/.ssh/config", - "sshPersistenceDefault": "Los terminales remotos de este host seguirán activos hasta que los cierres o restablezcas el relé." + "sshPersistenceDefault": "Los terminales remotos en este host seguirán activos hasta que los cierres o restablezcas el relay." } }, "shared": { "useDaemonActions": { "01af244097": "Cancelar", - "28c8e53176": "Esto fuerza el cierre de todos los paneles de terminal en ejecución en todos los espacios de trabajo. Cualquier trabajo no guardado en esas sesiones se perderá. El demonio sigue ejecutándose y se pueden abrir nuevas terminales inmediatamente. Esto no se puede deshacer.", - "1bbea41a77": "¿Eliminar todas las sesiones de terminal?", - "01d6b7c64e": "Elimina todos los paneles de terminal en ejecución y reinicia el proceso del demonio. Los paneles muestran \"Proceso cerrado\" y se pueden volver a abrir inmediatamente. Se conservan las sesiones de protocolo heredado de una versión anterior de la aplicación. Esto no se puede deshacer.", - "922548bc66": "¿Reiniciar el demonio del terminal?", + "28c8e53176": "Esto fuerza el cierre de todos los paneles de terminal en ejecución en todos los espacios de trabajo. Cualquier trabajo sin guardar en esas sesiones se perderá. El servicio sigue ejecutándose y se pueden abrir nuevos terminales inmediatamente. Esto no se puede deshacer.", + "1bbea41a77": "¿Terminar todas las sesiones de terminal?", + "01d6b7c64e": "Termina todos los paneles de terminal en ejecución y reinicia el proceso del servicio. Los paneles muestran \"Process exited\" y se pueden volver a abrir inmediatamente. Se conservan las sesiones de protocolo heredado de una versión anterior de la app. Esto no se puede deshacer.", + "922548bc66": "¿Reiniciar el servicio del terminal?", "2b4efdc162": "No se pudieron finalizar las sesiones.", "d18f3005c2": "{{value0}} sesión{{value1}} se negó a salir.", "baad8cd651": "No hay sesiones en ejecución.", - "fe2ab66d45": "Eliminó {{value0}} de {{value1}} sesiones. {{value2}} se negó a salir.", + "fe2ab66d45": "Se terminaron {{value0}} de {{value1}} sesiones. {{value2}} se negó a salir.", "d762b41f41": "El reinicio falló.", - "b5954e12d3": "Error al reiniciar: verifique los registros.", - "0e9da1b98e": "Daemon se reinició.", - "d6372cc797": "Mató a {{value0}} sesión{{value1}}.", - "87412c2a68": "Sesión {{value0}} eliminada.", - "a2f040ac1c": "Mató {{value0}} sesiones.", + "b5954e12d3": "Error al reiniciar — revisa los logs.", + "0e9da1b98e": "Servicio reiniciado.", + "d6372cc797": "Se terminó {{value0}} sesión{{value1}}.", + "87412c2a68": "Se terminó {{value0}} sesión.", + "a2f040ac1c": "Se terminaron {{value0}} sesiones.", "63520148e2": "La sesión {{value0}} se negó a salir.", "cc0a26cb14": "{{value0}} sesiones se negaron a salir." } @@ -4533,10 +4533,10 @@ "setup": { "guide": { "SetupGuideModal": { - "3598a3ca0c": "Termine los flujos de trabajo principales que hacen que Orca sea útil para el trabajo de agentes paralelos.", - "48a9e5ef2d": "Empezando", - "28cf59fcb4": "Esto ocultará la lista de verificación de la barra lateral.", - "f3b5ffb2a6": "Ocultar lista de verificación de la barra lateral" + "3598a3ca0c": "Completa los flujos de trabajo principales que hacen útil a Orca para trabajar con agentes en paralelo.", + "48a9e5ef2d": "Primeros pasos", + "28cf59fcb4": "Esto ocultará el checklist de la barra lateral.", + "f3b5ffb2a6": "Ocultar checklist de la barra lateral" }, "SetupGuideProgressRing": { "dac3a4724a": "{{value0}} de {{value1}} pasos de configuración completos" @@ -4551,7 +4551,7 @@ "to": { "date": { "setting": { - "f8bda25f29": "Mantenga actualizado el principal local" + "f8bda25f29": "Mantener main local actualizado" } } } @@ -4562,88 +4562,88 @@ "AccountsPane": { "c2d2751587": "Eliminar cuenta", "dbb9626ed1": "Cancelar", - "854ebbcc45": "Orca eliminará la autenticación de Claude administrada para esta cuenta guardada. Si está actualmente activo, Orca recurre al inicio de sesión Claude predeterminado del sistema.", + "854ebbcc45": "Orca eliminará la autenticación de Claude gestionada para esta cuenta guardada. Si está activa actualmente, Orca volverá al login predeterminado de Claude del sistema.", "63843e37e2": "¿Eliminar la cuenta de Claude?", - "99c8f9e498": "Orca eliminará el hogar del Codex administrado para esta cuenta guardada. Si está actualmente activo, Orca recurre al inicio de sesión predeterminado del Codex del sistema.", - "0d47394635": "¿Eliminar cuenta del Codex?", + "99c8f9e498": "Orca eliminará el home de Codex gestionado para esta cuenta guardada. Si está activo actualmente, Orca volverá al login predeterminado de Codex del sistema.", + "0d47394635": "¿Eliminar cuenta de Codex?", "ae3b21eb6c": "opencode.ai/workspace/wrk__…/go", - "51c9104e13": "Encuentre esto en la URL después de iniciar sesión en opencode.ai (p. ej.", - "b398b834c9": "Claro", + "51c9104e13": "Encuéntralo en la URL después de iniciar sesión en opencode.ai (p. ej.", + "b398b834c9": "Limpiar", "a122332371": "wrk_… (dejar en blanco para búsqueda automática)", - "dbdb0b0bd8": "Anulación de ID del espacio de trabajo", - "d70a5287a4": "Anulación opcional del ID del espacio de trabajo si falla la búsqueda automática.", - "02cb127710": "ID del espacio de trabajo de OpenCode Go", - "7ce0e1907c": "). Encuéntrelo en DevTools → Red → cualquier solicitud de opencode.ai → Encabezado de cookies de su navegador. La autenticación OpenCode Go está basada en la web y se comparte entre terminales Windows y WSL.", - "8951c5309f": "autenticación=Fe26.2**…", + "dbdb0b0bd8": "Override de Workspace ID", + "d70a5287a4": "Override opcional de Workspace ID si falla la búsqueda automática.", + "02cb127710": "Workspace ID de OpenCode Go", + "7ce0e1907c": "). Encuéntralo en DevTools → Network → cualquier request de opencode.ai → encabezado Cookie. La autenticación de OpenCode Go está basada en web y se comparte entre terminales Windows y WSL.", + "8951c5309f": "auth=Fe26.2**…", "338820326a": ") o el encabezado completo de la cookie (p. ej.", "922b51e02d": "Fe26.2**…", - "0023cc336e": "Pegue el valor del token sin formato (p. ej.", + "0023cc336e": "Pega el valor raw del token (p. ej.", "a7e38affcd": "Fe26.2**… token o auth=Fe26.2**… encabezado", "67e3c33670": "Cookie de sesión de OpenCode Go", - "b2b1aa936d": "Pegue su cookie de sesión de opencode.ai para obtener el límite de velocidad.", + "b2b1aa936d": "Pega tu cookie de sesión de opencode.ai para obtener rate limits.", "36223200ac": "Cookie de sesión de OpenCode Go", - "ea631977b5": "Configure los ajustes del proveedor OpenCode Go.", + "ea631977b5": "Configura los ajustes del proveedor OpenCode Go.", "4ac10b4d08": "OpenCode Go", - "c2aee76420": "Extrae las credenciales de OAuth de su instalación local de CLI de Gemini para autenticarse con Google para {{value0}}. Esto utiliza credenciales emitidas para la aplicación Gemini CLI, no para Orca. Puede fallar si Google actualiza la CLI. Úselo bajo su propio riesgo.", - "96f3649526": "Utilice las credenciales de Gemini CLI (experimental)", - "d676c41fc6": "Extrae las credenciales de OAuth de su instalación local de CLI de Gemini para autenticarse con Google. Esto utiliza credenciales emitidas para la aplicación Gemini CLI, no para Orca. Puede fallar si Google actualiza la CLI. Úselo bajo su propio riesgo.", - "0c7f915b01": "Utilice las credenciales de la CLI de Gemini", - "973741a871": "Configure los ajustes del proveedor Gemini.", + "c2aee76420": "Extrae credenciales OAuth de tu instalación local de Gemini CLI para autenticarse con Google para {{value0}}. Usa credenciales emitidas para la app Gemini CLI, no para Orca. Puede romperse si Google actualiza la CLI. Úsalo bajo tu propio riesgo.", + "96f3649526": "Usar credenciales de Gemini CLI (experimental)", + "d676c41fc6": "Extrae credenciales OAuth de tu instalación local de Gemini CLI para autenticarse con Google. Usa credenciales emitidas para la app Gemini CLI, no para Orca. Puede romperse si Google actualiza la CLI. Úsalo bajo tu propio riesgo.", + "0c7f915b01": "Usar credenciales de Gemini CLI", + "973741a871": "Configura los ajustes del proveedor Gemini.", "0c64dc2a64": "Gemini", "db209ee572": "Eliminar", "8a0f870153": "Volver a autenticar", "3d245ef7d9": "Codex informó que este inicio de sesión no está actualizado", "589eba1eee": "Necesita volver a autenticarse", "e74831fb6b": "Activo", - "b4c9450319": "No hay cuentas gestionadas de Codex para {{value0}}. Orca utilizará el inicio de sesión de Codex predeterminado del sistema de ese entorno hasta que agregue uno aquí.", + "b4c9450319": "No hay cuentas de Codex gestionadas para {{value0}}. Orca usará el login predeterminado de Codex del sistema de ese entorno hasta que agregues una aquí.", "93c47b333a": "Necesita iniciar sesión", - "f2a265f8c7": "Valor predeterminado del sistema", + "f2a265f8c7": "Predeterminado del sistema", "b0e948a4f9": "Agregar cuenta", "c0a52abfc5": "Mostrando cuentas para {{value0}}. Las nuevas cuentas se agregan allí.", "94d351af4a": "Cuentas", - "d0d53b7eb0": "Administre qué cuenta Codex utiliza Orca para obtener el límite de tasa en vivo.", - "3180536c7a": "Cuentas del Codex", - "340d6f7a85": "Cada cuenta mantiene su propio contexto de inicio de sesión local en Orca. La autenticación de la cuenta permanece en este dispositivo.", - "cedfab35ab": "Opcional. Orca puede utilizar su inicio de sesión normal del Codex; agregue cuentas solo si desea un cambio rápido en Orca.", + "d0d53b7eb0": "Administra qué cuenta de Codex usa Orca para obtener rate limits en vivo.", + "3180536c7a": "Cuentas de Codex", + "340d6f7a85": "Cada cuenta mantiene su propio contexto de login local en Orca. La autenticación de la cuenta permanece en este dispositivo.", + "cedfab35ab": "Opcional. Orca puede usar tu login normal de Codex; agrega cuentas solo si quieres cambiar rápido en Orca.", "ef91cfa06b": "Codex", - "3fe7862418": "No hay cuentas administradas de Claude para {{value0}}. Orca utilizará el inicio de sesión de Claude predeterminado del sistema de ese entorno hasta que agregue uno aquí.", - "3455cf43fa": "Claude iniciar sesión.", - "fcc4093fc1": "Utilice su inicio de sesión actual del Codex {{value0}}.", - "79e484c3b2": "Conmutador de cuenta opcional para los archivos de autenticación compartidos de Claude.", - "8bbfd74556": "Cuentas Claude", - "72b36ea174": "Opcional. Orca puede utilizar su inicio de sesión normal de Claude; agregue cuentas solo si desea cambiar rápidamente sin mover sesiones de chat.", + "3fe7862418": "No hay cuentas de Claude gestionadas para {{value0}}. Orca usará el login predeterminado de Claude del sistema de ese entorno hasta que agregues una aquí.", + "3455cf43fa": "Login de Claude.", + "fcc4093fc1": "Usa tu login actual de Codex de {{value0}}.", + "79e484c3b2": "Switcher de cuenta opcional para los archivos de autenticación compartidos de Claude.", + "8bbfd74556": "Cuentas de Claude", + "72b36ea174": "Opcional. Orca puede usar tu login normal de Claude; agrega cuentas solo si quieres cambiar rápido sin mover sesiones de chat.", "26ef4b55be": "Claude", "2743cdc0af": "Error al actualizar la cuenta de Claude.", - "b15ce90870": "{{value0}} -> {{value1}}. Reinicie los terminales Claude en vivo antes de continuar con sesiones anteriores.", + "b15ce90870": "{{value0}} -> {{value1}}. Reinicia los terminales Claude activos antes de continuar sesiones antiguas.", "f921d32606": "Cuenta Claude actualizada.", "5bf8764953": "Error al actualizar la cuenta del Codex.", - "9baf45d071": "este dispositivo", + "9baf45d071": "Este dispositivo", "2358ac71d2": "WSL predeterminado", "ad47a33f72": "Cargando WSL", "8619f9afa9": "WSL", "46cf7e7495": "Ubicación de la cuenta", - "0b4591ff93": "Elija qué entorno local inspeccionar y dónde se agregan las nuevas cuentas administradas de Claude y Codex.", + "0b4591ff93": "Elige qué entorno local inspeccionar y dónde se agregan nuevas cuentas gestionadas de Claude y Codex.", "0c67a2a1aa": "WSL no está disponible en esta máquina.", - "2cd197025c": "Elija si las cuentas de proveedor se inspeccionan y agregan en {{value0}} o WSL.", + "2cd197025c": "Elige si las cuentas de proveedor se inspeccionan y agregan en {{value0}} o WSL.", "f54b4fbd71": "Ubicación de la cuenta", "9107406589": "No se pudieron cargar las cuentas de Claude.", "b8c2905c2b": "No se pudieron cargar las cuentas del Codex.", - "fd62f37c24": "Codex informó que este inicio de sesión {{value0}} no está actualizado.", - "b10cb4f696": "añadiendo", - "e4a28e8894": "Codex informó que el inicio de sesión {{value0}} necesita un nuevo inicio de sesión. Inicie sesión nuevamente antes de iniciar nuevas sesiones del Codex.", - "75ca9b718e": "Codex informó que la cuenta activa necesita un nuevo inicio de sesión. Vuelva a autenticarlo antes de iniciar nuevas sesiones del Codex.", + "fd62f37c24": "Codex informó que este login de {{value0}} está desactualizado.", + "b10cb4f696": "agregando", + "e4a28e8894": "Codex informó que el login de {{value0}} requiere un inicio de sesión nuevo. Inicia sesión de nuevo antes de iniciar nuevas sesiones de Codex.", + "75ca9b718e": "Codex informó que la cuenta activa requiere un inicio de sesión nuevo. Reautentícala antes de iniciar nuevas sesiones de Codex.", "b11078a9c2": "wsl", "350b2a1aa7": "Usa tu actual", - "e05d0ff737": "Utilice su inicio de sesión actual de {{value0}} Claude." + "e05d0ff737": "Usa tu login actual de Claude de {{value0}}." }, "AdvancedPane": { - "40b29e0bf3": "Reanudar", + "40b29e0bf3": "Reiniciar", "87a2cb2ac8": "Orca aplica este modo de red al inicio.", - "89958d7edf": "Reiniciar requerido", - "b3ad629640": "Úselo solo cuando una VPN o proxy corporativo interrumpa las descargas de actualizaciones con errores de protocolo HTTP/2. Afecta a todas las redes de Electron después del reinicio.", + "89958d7edf": "Reinicio requerido", + "b3ad629640": "Úsalo solo cuando una VPN o proxy corporativo rompa las descargas de actualizaciones con errores de protocolo HTTP/2. Afecta toda la red de Electron después del reinicio.", "6627e75c92": "Explicar la compatibilidad HTTP/1.1", "e9506d3377": "Compatibilidad HTTP/1.1", - "8b7a8df299": "Soluciones alternativas de bajo nivel para la resolución de problemas de soporte.", + "8b7a8df299": "Workarounds de bajo nivel para troubleshooting de soporte.", "8d8d8ac599": "Compatibilidad", "network": "Red", "networkDescription": "Enrutamiento de red a nivel de aplicación para proxies y entornos corporativos." @@ -4652,19 +4652,19 @@ "92f4238f1a": "WSL predeterminado", "fc806485ae": "Cargando WSL", "43663b5e69": "WSL", - "9bccf48906": "Ubicación del Agent", - "d00949e59b": "Mostrar agents instalados desde {{value0}}. Actualizar vuelve a verificar la RUTA en ese entorno.", + "9bccf48906": "Ubicación de Agents", + "d00949e59b": "Muestra Agents instalados desde {{value0}}. Actualizar vuelve a revisar PATH en ese entorno.", "c7c516946f": "WSL no está disponible en esta máquina.", "f97b986b7f": "wsl" }, "AgentSkillSetupPanel": { "0b810ec59f": "Presiona Enter para ejecutar el comando.", - "ed197f59a2": "comando copiar", + "ed197f59a2": "Copiar comando", "817d3f9f18": "Copiar comando", "5289300939": "No instalado", "9fcebceb2a": "Instalado", - "68a468752e": "De cheques...", - "c689392435": "Vuelva a comprobar", + "68a468752e": "Comprobando...", + "c689392435": "Volver a comprobar", "a31e2aa302": "No se pudo copiar el comando.", "378ad26865": "Comando copiado.", "copiedCommand": "Comando copiado.", @@ -4676,42 +4676,42 @@ }, "AgentsPane": { "d83834f5e6": "Detectando agents instalados…", - "024bd95089": "agentes", + "024bd95089": "agents", "e8da2af684": "Disponible para instalar", "ed3e110e61": "detectado", "02e0143be5": "Instalado", "110b74b022": "Sin agente (terminal en blanco)", "92033495ff": "Auto", - "9b175d0f5e": "Agente preseleccionado al abrir un nuevo espacio de trabajo.", - "385212c7a1": "Agente predeterminado", - "f9f127d664": "Override the binary path or name, and edit the default launch arguments or environment for this agent.", + "9b175d0f5e": "Agent preseleccionado al abrir un nuevo espacio de trabajo.", + "385212c7a1": "Agent predeterminado", + "f9f127d664": "Override de la ruta o nombre del binario, y edición de argumentos de lanzamiento o entorno predeterminados para este agent.", "f95b5c79b8": "Instalar", - "fe4d630c94": "Documentos", + "fe4d630c94": "Docs", "8dc0192e48": "Desactivado", "df123171d1": "No instalado", "c8794e622e": "Detectado", "5200dac9da": "Reiniciar", - "2e45ca29b6": "Dominio", + "2e45ca29b6": "Comando", "d4d2a45d63": "Activado", "1c9a9679ec": "{{value0}} disponibilidad", "0d9e293a02": "Refrescar", - "c9b33eb5c0": "Refrescante…", - "13647f9f80": "Vuelva a leer la RUTA de su shell y vuelva a detectar los agents instalados", - "dc4a2ffdc0": "Ampliar anulación del comando", - "cea7d97be1": "Anulación del comando Contraer", + "c9b33eb5c0": "Actualizando…", + "13647f9f80": "Vuelve a leer PATH de tu shell y vuelve a detectar los Agents instalados", + "dc4a2ffdc0": "Expandir override del comando", + "cea7d97be1": "Contraer override del comando", "db9e9e5887": "Personalizar comando", "959b67385b": "Establecer predeterminado", - "24e032fa34": "Por defecto", + "24e032fa34": "Predeterminado", "5f986a9b92": "Establecer como predeterminado", - "d7625cf8b2": "Agente predeterminado", + "d7625cf8b2": "Agent predeterminado", "cfb3f35775": "Argumentos", - "6f99bf5dd0": "No default arguments", + "6f99bf5dd0": "Sin argumentos predeterminados", "8fbe1f37c1": "Entorno", - "2d133152fa": "No default environment", - "agentPermissions": "Agent Permissions", - "agentPermissionsInfo": "Agent permissions info", - "agentPermissionsTooltip": "Custom agent arguments stay unchanged when switching modes.", - "agentPermissionsDescription": "Choose whether Orca launches agents with fewer permission prompts or with manual checks.", + "2d133152fa": "Sin entorno predeterminado", + "agentPermissions": "Permisos de Agents", + "agentPermissionsInfo": "Información de permisos de Agents", + "agentPermissionsTooltip": "No se aplica a agents con argumentos de lanzamiento sobrescritos.", + "agentPermissionsDescription": "Elige si Orca lanza agents con menos prompts de permisos o con checks manuales.", "agentPermissionsYolo": "Yolo", "agentPermissionsManual": "Manual", "3f1bdf3cb4": "El texto de entorno es demasiado grande para analizarlo de forma segura." @@ -4723,32 +4723,32 @@ }, "AppearancePane": { "3057983501": "No asignado", - "0cd9b8228f": "Elija el ícono de la aplicación que se muestra en el Dock y en el selector de ventanas.", - "ca1590d42f": "Icono de aplicación", + "0cd9b8228f": "Elige el icono de la app que se muestra en el Dock y en el selector de ventanas.", + "ca1590d42f": "Icono de la app", "61d842eca0": "Muestra el acceso directo de Orca Mobile en la barra lateral. Sigue estando disponible en Toolbox.", - "9da1020447": "Mostrar botón móvil de Orca", + "9da1020447": "Mostrar botón de Orca Mobile", "5db6ba961f": "Muestra el botón de Orca Mobile en la parte superior de la barra lateral izquierda.", - "fa882a3e6b": "Muestra el botón Automatizaciones en la parte superior de la barra lateral izquierda.", - "511f270ebb": "Botón Mostrar automatizaciones", - "661942ab7f": "Muestra el botón Tareas en la parte superior de la barra lateral izquierda.", - "cf81907069": "Botón Mostrar tareas", + "fa882a3e6b": "Muestra el botón de Automatizaciones en la parte superior de la barra lateral izquierda.", + "511f270ebb": "Mostrar botón de Automatizaciones", + "661942ab7f": "Muestra el botón de Tareas en la parte superior de la barra lateral izquierda.", + "cf81907069": "Mostrar botón de Tareas", "dc29f3cc0d": "Barra lateral", - "ea943d0db0": "Elija qué indicadores aparecen en la parte inferior de la ventana. También puede hacer clic derecho en la barra de estado para los mismos cambios.", + "ea943d0db0": "Elige qué indicadores aparecen en la parte inferior de la ventana. También puedes hacer clic derecho en la barra de estado para cambiar lo mismo.", "3e4175e5c6": "Barra de estado", "2df8f79aa5": "Muestra Orca en la barra de título.", "9868f39007": "Nombre de la aplicación de la barra de título", "4de76f6902": "Controla lo que aparece en la barra de título de la aplicación.", "6a272ca553": "Barra de título", - "e9f2ca5582": "Desactívelo para ocultar archivos que coincidan con .gitignore desde el explorador de archivos.", + "e9f2ca5582": "Desactívalo para ocultar en el explorador de archivos los archivos que coincidan con .gitignore.", "0fafabcf35": "Mostrar archivos ignorados por Git", - "75f07ab60c": "Mostrar archivos que coincidan con .gitignore en el explorador de archivos.", + "75f07ab60c": "Muestra en el explorador de archivos los archivos que coincidan con .gitignore.", "d496901cd0": "Explorador de archivos", - "42554f615f": "Elija la fuente utilizada por la interfaz de Orca.", + "42554f615f": "Elige la fuente usada por la interfaz de Orca.", "102d6b5f9b": "Fuente IDE", "ef89200c1f": "cuando no está en un panel de terminal.", - "f687711a9b": "Escale toda la interfaz de la aplicación. Usar", - "5e6d7aba8d": "Ampliación de la interfaz de usuario", - "622e1c3465": "Escale toda la interfaz de la aplicación.", + "f687711a9b": "Escala toda la interfaz de la app. Usa", + "5e6d7aba8d": "Zoom de la UI", + "622e1c3465": "Escala toda la interfaz de la app.", "fd89b5487c": "Luz", "7d26ccabe8": "Oscuro", "fb0e0b4453": "Sistema", @@ -4756,9 +4756,9 @@ "0f28e7b30c": "Elige cómo se ve Orca en la ventana de la aplicación.", "leftSidebarAppearance": { "title": "Apariencia de la barra lateral izquierda", - "rowDescription": "Haz que la barra lateral izquierda coincida con tu terminal, conserve el valor predeterminado o use un tinte.", + "rowDescription": "Haz que la barra lateral izquierda coincida con tu terminal, mantén el valor predeterminado o usa un tinte.", "default": "Predeterminado", - "matchTerminal": "Coincidir con terminal", + "matchTerminal": "Coincidir con el terminal", "tinted": "Con tinte", "tintColor": "Tinte de la barra lateral", "tintColorDescription": "El color que se mezcla en la superficie de la barra lateral izquierda.", @@ -4775,75 +4775,75 @@ "terminalTitle": "Terminal", "windowSidebarTitle": "Ventana y barra lateral", "windowSidebarSummary": "Barra lateral, barra de estado y explorador de archivos", - "statusBarCount": "Indicadores visibles: {{value0}}.", - "gitIgnoredGlossary": "Archivos coincidentes con .gitignore.", - "statusBarDescription": "Choose which indicators appear in the status bar." + "statusBarCount": "{{value0}} indicadores visibles.", + "gitIgnoredGlossary": "Archivos que coinciden con .gitignore.", + "statusBarDescription": "Elige qué indicadores aparecen en la barra de estado." }, "AutoRenameBranchFromWorkSetting": { "1626524572": "Nautilus", - "0de9fda203": "Desechar", + "0de9fda203": "Descartar", "c71770c455": "{basePrompt}", - "f19a56498d": "; la configuración del prefijo de rama aún se aplica.", - "800edb1e54": "arreglar-flujo-de-inicio de sesión", - "5d569f5199": ". Orca genera sólo el segmento final, como", + "f19a56498d": "; se seguirá aplicando tu prefijo de rama.", + "800edb1e54": "fix-login-flow", + "5d569f5199": ". Orca genera solo el segmento final, como", "570817d126": "y", - "56580dcf60": ". También puedes hacer referencia", - "9c9b54e4ea": "mensaje de nombre de rama incorporado", - "69bf4830c2": "para incluir a Orca", - "9241b59bf5": "Usar", - "a869d0edd8": "Plantilla de comando de nombre de sucursal", + "56580dcf60": ". También puedes referenciar", + "9c9b54e4ea": "prompt integrado para nombres de rama", + "69bf4830c2": "para incluir el", + "9241b59bf5": "Usa", + "a869d0edd8": "Plantilla de comando para nombre de rama", "e784ea62dc": "Avanzado", - "d9b65054ef": ") a un nombre corto que resume la tarea. Sólo se cambia el nombre de las ramas que Orca nombró a sí misma, y ​​nunca después de haber sido empujadas.", - "12ea4a408d": "Cuando un agente comienza a trabajar en un nuevo espacio de trabajo, Orca cambia el nombre de su rama generada automáticamente (p. ej.", - "ef787db0e3": "Rama de cambio de nombre automático", - "6a051586d2": "Cambie el nombre de la rama generada automáticamente según el trabajo una vez que comience un agente.", - "ec3e0c388e": "Ahorrar", - "cfd82406dd": "Ahorro...", + "d9b65054ef": ") a un nombre corto que resuma la tarea. Solo se renombran las ramas que Orca nombró por sí misma, y nunca después de que se hayan pusheado.", + "12ea4a408d": "Cuando un agente empieza a trabajar en un nuevo espacio de trabajo, Orca renombra su rama generada automáticamente (p. ej.", + "ef787db0e3": "Renombrar rama y worktree automáticamente", + "6a051586d2": "Renombra la rama generada automáticamente según el trabajo una vez que un agente empieza.", + "ec3e0c388e": "Guardar", + "cfd82406dd": "Guardando...", "40e7be7850": "Guardado", - "7c7e34a66d": "Cambios no guardados", - "a4fa380b67": "{mensaje del asistente}", - "2ee2779c05": "{primer mensaje}" + "7c7e34a66d": "Cambios sin guardar", + "a4fa380b67": "{assistantMessage}", + "2ee2779c05": "{firstPrompt}" }, "AutoRenameBranchPromptEditor": { - "63121132c0": "Desechar", - "4416b25d29": "Prefiera nombres de dominio de la tarea, evite los ID de ticket y mantenga los nombres fáciles de usar para los revisores.", - "39278f4411": "; la configuración del prefijo de rama aún se aplica.", - "ebb942a2ec": "arreglar-flujo-de-inicio de sesión", - "af2d9a2cc6": ". Orca genera sólo el segmento final, como", - "182d419b97": "mensaje de nombre de rama incorporado", - "2f5dc661fe": "Adjunto a Orca", - "7d6176f506": "Inmediato", - "5968112152": "Ahorrar", - "54ac229ad4": "Ahorro...", + "63121132c0": "Descartar", + "4416b25d29": "Prefiere sustantivos del dominio de la tarea, evita IDs de issues y mantén nombres fáciles de revisar.", + "39278f4411": "; se seguirá aplicando tu prefijo de rama.", + "ebb942a2ec": "fix-login-flow", + "af2d9a2cc6": ". Orca genera solo el segmento final, como", + "182d419b97": "prompt integrado para nombres de rama", + "2f5dc661fe": "Se agrega al", + "7d6176f506": "Prompt", + "5968112152": "Guardar", + "54ac229ad4": "Guardando...", "af0831a590": "Guardado", - "0691753cf2": "Cambios no guardados" + "0691753cf2": "Cambios sin guardar" }, "BaseRefPicker": { - "1b8e54151f": "No se encontraron sucursales coincidentes.", + "1b8e54151f": "No se encontraron ramas coincidentes.", "d166ff883d": "Actual", - "a4a9372eb2": "Buscando sucursales...", - "7db7fb87e5": "Buscar sucursales por nombre...", - "773a5687a3": "Usar primario", - "ade9a5bb03": ") para determinar el alcance de los resultados.", - "b468f46726": "aguas arriba/principal", - "80f7c82303": ") o una referencia completa (p. ej.", - "915ad97875": "río arriba", - "a5c16712c1": "Se detectaron varios controles remotos. Escriba un nombre remoto (p. ej.", - "9a14ec7400": "Elija una rama base a continuación", - "086ce7f369": "Siguiente rama principal ({{value0}})", - "2f3cda96f5": "Fijado para este repo", - "ee110e1830": "Sin referencia base predeterminada" + "a4a9372eb2": "Buscando ramas...", + "7db7fb87e5": "Buscar ramas por nombre...", + "773a5687a3": "Usar rama principal", + "ade9a5bb03": ") para acotar los resultados.", + "b468f46726": "upstream/main", + "80f7c82303": ") o una ref completa (p. ej.", + "915ad97875": "upstream", + "a5c16712c1": "Se detectaron varios remotes. Escribe el nombre de un remote (p. ej.", + "9a14ec7400": "Elige una rama base abajo", + "086ce7f369": "Siguiendo la rama principal ({{value0}})", + "2f3cda96f5": "Fijada para este repositorio", + "ee110e1830": "Sin ref base predeterminada" }, "BrowserDefaultZoomSetting": { - "2622126877": "Nivel de zoom aplicado a las pestañas del navegador recién abiertas.", - "bbeec087d3": "Se aplica a las pestañas del navegador recién abiertas.", + "2622126877": "Nivel de zoom aplicado a nuevas pestañas del navegador.", + "bbeec087d3": "Se aplica a nuevas pestañas del navegador.", "265597101f": "Zoom predeterminado" }, "BrowserHomePageSetting": { - "d4ddcd0056": "Ahorrar", + "d4ddcd0056": "Guardar", "37a30c5bfd": "https://google.com", "c6cbd1c105": "Página de inicio guardada.", - "6a37540f4b": "URL abierta al crear una nueva pestaña del navegador. Déjelo vacío para abrir una pestaña en blanco.", + "6a37540f4b": "URL que se abre al crear una nueva pestaña del navegador. Déjalo vacío para abrir una pestaña en blanco.", "70224e37b1": "Página de inicio predeterminada" }, "BrowserPane": { @@ -4854,333 +4854,333 @@ "8481ee0331": "Nuevo perfil del navegador", "6f2584b39e": "Agregar perfil", "e4aaf8051b": "menú de la barra de herramientas.", - "cd47bc9622": "Seleccione un perfil predeterminado para las nuevas pestañas del navegador. Importe cookies y cambie perfiles por pestaña a través del", + "cd47bc9622": "Selecciona un perfil predeterminado para nuevas pestañas del navegador. Importa cookies y cambia perfiles por pestaña desde el", "2d66a6efb5": "Sesión y cookies", - "aa1074bfe9": "Administre perfiles de navegador e importe cookies desde Chrome, Edge, Comet u otros navegadores.", + "aa1074bfe9": "Administra perfiles del navegador e importa cookies desde Chrome, Edge, Comet u otros navegadores.", "113cd2dc9b": "Sesión y cookies", "d3eb69c0aa": "Enrutamiento de enlaces", - "3e46903ad4": "Se utiliza al escribir texto que no es una URL en la barra de direcciones.", + "3e46903ad4": "Se usa al escribir texto que no es una URL en la barra de direcciones.", "0d9c987f21": "Motor de búsqueda predeterminado", - "7b225c78f5": "Motor de búsqueda utilizado al escribir texto que no es una URL en la barra de direcciones.", + "7b225c78f5": "Motor de búsqueda usado al escribir texto que no es una URL en la barra de direcciones.", "64898ecdab": "Crear", "7b649a578a": "Creando…", - "4399c77caa": "Por defecto", + "4399c77caa": "Predeterminado", "4af9a17947": "kagi", - "c0f85056d9": "Browser profiles on this Orca server.", - "86b7c83fee": "This computer", - "6480776a03": "Browser profiles for the selected host.", + "c0f85056d9": "Perfiles del navegador en este servidor de Orca.", + "86b7c83fee": "Este equipo", + "6480776a03": "Perfiles del navegador para el host seleccionado.", "5e19a692f7": "Host" }, "BrowserProfileRow": { "8e636cae25": "Perfil \"{{value0}}\" eliminado.", "2d4bea7f35": "Se borraron las cookies predeterminadas.", "ebb78dfd6f": "Desde archivo…", - "7df818977e": "De", + "7df818977e": "Desde ", "cdec84552f": "Importar cookies", - "796d846483": "No se importan cookies", + "796d846483": "No se han importado cookies", "c29648fe5b": "Activo", "d420c43729": "Se importaron {{value0}} cookies de {{value1}}{{value2}} a {{value3}}.", - "b4c167764d": "Se importaron {{value0}} cookies del archivo a {{value1}}.", + "b4c167764d": "Se importaron {{value0}} cookies desde un archivo a {{value1}}.", "a3f8c2d1e0b4": "Se importaron {{value0}} cookies de {{value1}} ({{value2}}) a {{value3}}.", "b4e9d3f2a1c5": "Se importaron {{value0}} cookies de {{value1}} a {{value2}}." }, "BrowserUseComputerUseNotice": { - "15b5e680ba": "Uso abierto de la computadora", - "79209b37b9": "Si la importación de cookies no es la opción adecuada, Uso de la Computadora puede controlar las aplicaciones locales y puede utilizar sesiones de navegador existentes cuando corresponda. Instale la habilidad Uso de computadora; macOS también requiere permisos de privacidad.", - "333984cf90": "Utilice una sesión de navegador existente" + "15b5e680ba": "Abrir Computer Use", + "79209b37b9": "Si importar cookies no encaja, Computer Use puede controlar apps locales y usar sesiones de navegador existentes donde aplique. Instala el skill Computer Use; macOS también requiere permisos de privacidad.", + "333984cf90": "Usar una sesión de navegador existente" }, "BrowserUseEnableSwitch": { - "aea3f45349": "Habilitar el uso del navegador del Agent" + "aea3f45349": "Activar Agent Browser Use" }, "BrowserUseExamples": { "1199258ace": "Copiar", - "1188e56af4": "Copiar mensaje de ejemplo", + "1188e56af4": "Copiar prompt de ejemplo", "b84807f228": "\"", "59722f31b4": "\"", - "c5325e91f6": "Pegue cualquiera de estos en Claude Code, Codex u otro agente en un proyecto donde esté instalada la habilidad.", - "2a180694f7": "Pruébelo: indicaciones de ejemplo", + "c5325e91f6": "Pega cualquiera de estos en Claude Code, Codex u otro agente en un proyecto donde el skill esté instalado.", + "2a180694f7": "Pruébalo: prompts de ejemplo", "5ec620ccc4": "No se pudo copiar.", "a602d43069": "Copiado {{value0}}." }, "BrowserUsePane": { "be6df68384": "Desde archivo…", - "e44c5d681e": "De", - "67d9a53f47": "Administrar perfiles para inicios de sesión separados", - "112f70adc4": "Última importación desde", - "72d4815523": "Incorpore sus inicios de sesión existentes a Orca para que los agents puedan acceder a páginas autenticadas. Importa al perfil predeterminado.", + "e44c5d681e": "Desde ", + "67d9a53f47": "Gestionar perfiles para logins separados", + "112f70adc4": "Última importación desde ", + "72d4815523": "Trae tus logins existentes a Orca para que los agents puedan acceder a páginas autenticadas. Importa al perfil predeterminado.", "2eb906706c": "Importar cookies del navegador", - "af8c83ed61": "Importe cookies de Chrome, Edge u otros navegadores para que los agents puedan reutilizar sus inicios de sesión.", - "68ea76eb71": "Instale la habilidad Uso del navegador para que los agents puedan operar el navegador de Orca.", - "2d6ead9ab2": "Instalar la habilidad de uso del navegador", + "af8c83ed61": "Importa cookies de Chrome, Edge u otros navegadores para que los agents puedan reutilizar tus logins.", + "68ea76eb71": "Instala el skill Browser Use para que los agents puedan operar el navegador de Orca.", + "2d6ead9ab2": "Instalar skill Browser Use", "e9f3f3b488": "Instalado en", - "9fca1f7f5d": "Registra el comando CLI de Orca para que los agents puedan organizar el navegador desde su shell.", - "c6065d205d": "Habilitar CLI de Orca", - "c79eff0213": "Registre la CLI de Orca para que los agents puedan controlar el navegador.", - "702488a5f7": "Deje que los agents de codificación controlen este navegador con sus inicios de sesión. Complete los tres pasos siguientes.", - "b8a1f2d84d": "Uso del navegador del Agent", - "96b91c6349": "Deje que los agents de codificación controlen este navegador con sus inicios de sesión.", + "9fca1f7f5d": "Registra el comando Orca CLI para que los agents puedan orquestar el navegador desde su shell.", + "c6065d205d": "Activar Orca CLI", + "c79eff0213": "Registra Orca CLI para que los agents puedan controlar el navegador.", + "702488a5f7": "Deja que los coding agents controlen este navegador con tus logins. Completa los tres pasos de abajo.", + "b8a1f2d84d": "Agent Browser Use", + "96b91c6349": "Deja que los coding agents controlen este navegador con tus logins.", "2ea4617e3a": "Se importaron {{value0}} cookies de {{value1}}{{value2}}.", - "721aee31b4": "Registré Orca CLI en PATH.", + "721aee31b4": "Se registró Orca CLI en PATH.", "180a9abf3a": "No se pudo cargar el estado de CLI.", "2ccfc9cff8": "Importar", "0462565413": "Reimportar", - "de9b2f32f3": "Permitir", - "ad8cb0ee22": "Arreglar RUTA", + "de9b2f32f3": "Activar", + "ad8cb0ee22": "Corregir PATH", "0289434ed6": "Activado", "8b3054dac7": "Registrando...", - "8f2675c2f3": "Se importaron {{value0}} cookies del archivo." + "8f2675c2f3": "Se importaron {{value0}} cookies desde un archivo." }, "BrowserUseSkillStep": { "0871b6998d": "Permite a los agents navegar y verificar páginas en el navegador de Orca.", - "459e24eebc": "Habilidad de uso del navegador" + "459e24eebc": "Skill Browser Use" }, "CliSection": { "8671e406f0": "Cancelar", "a4aafe46e3": "Ruta de destino:", - "e8012c03a1": "Permite a los agents utilizar el espacio de trabajo, la terminal y los comandos de progreso de Orca.", - "cliSkillTerminalTitle": "Configuración de habilidad CLI", - "cliSkillTerminalAria": "Terminal de instalación de la habilidad CLI", - "6053cf736c": "Habilidad CLI", - "36a6f919ba": "Ofrezca a los agents espacios de trabajo, terminales y flujos de trabajo de progreso compatibles con Orca.", - "04873eea3e": "Habilidades del Agent", - "7f2747f7dd": "Actualmente no está visible en PATH para este shell.", - "b0c310ab46": "Objetivo del lanzador existente:", - "15eaad0d31": "Ruta de comando:", - "5dae812f50": "Refrescar", - "52e640f3a0": "Actualizar el estado de la CLI", + "e8012c03a1": "Permite a los agents usar comandos de workspace, terminal y progreso de Orca.", + "cliSkillTerminalTitle": "Configuración del skill CLI", + "cliSkillTerminalAria": "Terminal de instalación del skill CLI", + "6053cf736c": "Skill CLI", + "36a6f919ba": "Da a los agents workflows de workspace, terminal y progreso integrados con Orca.", + "04873eea3e": "Agent skills", + "7f2747f7dd": "no está visible en PATH para este shell.", + "b0c310ab46": "Destino del launcher existente:", + "15eaad0d31": "Ruta del comando:", + "5dae812f50": "Actualizar", + "52e640f3a0": "Actualizar estado de la CLI", "38edbb5721": "Comando de shell", - "6930feda9e": "Utilice Orca desde su terminal para abrir la aplicación, administrar árboles de trabajo e interactuar con los terminales Orca.", - "c5c0f2641d": "CLI de Orca", + "6930feda9e": "Usa Orca desde tu terminal para abrir la app, administrar worktrees e interactuar con terminales de Orca.", + "c5c0f2641d": "Orca CLI", "d77352f2df": "No se pudo eliminar `{{value0}}` de PATH.", - "af5540930c": "Se eliminó `{{value0}}` de RUTA.", + "af5540930c": "Se eliminó `{{value0}}` de PATH.", "a2b13efa94": "No se pudo registrar `{{value0}}` en PATH.", - "9cbcd31338": "Registrado `{{value0}}` en PATH.", + "9cbcd31338": "Se registró `{{value0}}` en PATH.", "7baec27029": "No se pudo cargar el estado de CLI.", - "d00df2e397": "Registro", + "d00df2e397": "Registrar", "9a5f8a4568": "Eliminar", - "b0fca411a0": "Registrándose…", + "b0fca411a0": "Registrando…", "4c7e3e4c5f": "instalar", "068552b191": "Eliminando…", "8d96213669": "eliminar", - "aa6536977e": "Orca registrará {{value0}} para que el comando funcione desde su terminal.", - "a030816e3e": "Esto elimina el enlace simbólico del comando Shell. La propia Orca permanece instalada.", + "aa6536977e": "Orca registrará {{value0}} para que el comando funcione desde tu terminal.", + "a030816e3e": "Esto elimina el symlink del comando de shell. Orca permanece instalada.", "fa87db3d6e": "¿Registrar `{{value0}}` en PATH?", - "14444243ba": "¿Eliminar `{{value0}}` de RUTA?", + "14444243ba": "¿Eliminar `{{value0}}` de PATH?", "5d432fe44d": "instalado", - "8a9b784c60": "duro", - "d363e5929b": "Comprobando el registro CLI..." + "8a9b784c60": "desactualizado", + "d363e5929b": "Comprobando registro de CLI..." }, "CliSkillRuntimeSetup": { "04325573f8": "WSL", - "a58ba464ad": "Ubicación de la habilidad", - "0ed08febc5": "No se pudo registrar el comando de shell WSL.", - "3728a94fb6": "El comando de shell WSL necesita atención", - "775a4cfbb8": "El registro del comando de shell WSL no está disponible", + "a58ba464ad": "Ubicación del skill", + "0ed08febc5": "No se pudo registrar el comando de shell de WSL.", + "3728a94fb6": "El comando de shell de WSL necesita atención", + "775a4cfbb8": "El registro del comando de shell de WSL no está disponible", "c47127f222": "WSL predeterminado", - "0c9f3cf9da": "Elija dónde Orca verifica e instala las habilidades de los agentes globales.", + "0c9f3cf9da": "Elige dónde Orca comprueba e instala agent skills globales.", "f00d6aa9b5": "WSL no está disponible en esta máquina.", "7c776ff9d8": "wsl", - "fc0fcf72fd": "Registre el comando de shell WSL antes de configurar la habilidad." + "fc0fcf72fd": "Registra el comando de shell de WSL antes de configurar el skill." }, "CommitMessageAiPane": { - "841ed9884a": "Utilizado por repositorios que no han personalizado Source Control AI.", - "ad66ff886d": "Valores predeterminados de IA de control de fuente", - "347094560b": "Utilizado por repositorios que heredan los valores predeterminados globales de revisión alojada.", - "2dafc7646e": "Valores predeterminados de creación de reseñas alojadas", - "e9d46a544d": "Valores predeterminados utilizados cuando se abre el redactor de reseñas alojadas.", - "b125eabffa": "Abra la reseña alojada creada en su navegador después de enviarla.", - "7662715213": "Abrir revisión alojada después de la creación", - "b27b0809f3": "Ejecute la generación de detalles de revisión alojada una vez cuando se abra el compositor.", + "841ed9884a": "Usado por repositorios que no han personalizado Source Control AI.", + "ad66ff886d": "Valores predeterminados de Source Control AI", + "347094560b": "Usado por repositorios que heredan los valores predeterminados globales de revisión alojada.", + "2dafc7646e": "Valores predeterminados para crear revisiones alojadas", + "e9d46a544d": "Valores predeterminados usados cuando se abre el composer de revisión alojada.", + "b125eabffa": "Abre la revisión alojada creada en tu navegador después de enviarla.", + "7662715213": "Abrir revisión alojada después de crearla", + "b27b0809f3": "Ejecuta una vez la generación de detalles de la revisión alojada cuando se abra el composer.", "d5f0de6309": "Generar detalles al abrir Crear PR", - "6278c0ce43": "Prefiere las plantillas de solicitud de extracción del repositorio cuando no se establece ninguna descripción.", - "d8b6764d79": "Utilice la plantilla de revisión cuando esté disponible", - "e001734396": "Cree reseñas alojadas como borradores a menos que se modifiquen en el compositor.", - "6ba48f07a4": "Borrador por defecto", - "15b60d54b2": "p.ej. ollama ejecuta llama3.1 {rápido}", - "3f1b26cc91": "pasar la entrada del comando como argumento; de lo contrario, Orca lo canaliza en stdin.", - "4f722a5f53": "Usado por recetas de mensaje de commit, solicitud de extracción y nombre de rama que seleccionan el comando Personalizado. Usar", + "6278c0ce43": "Prefiere las plantillas de pull request del repositorio cuando no hay descripción.", + "d8b6764d79": "Usar plantilla de revisión cuando esté disponible", + "e001734396": "Crea revisiones alojadas como borradores salvo que se cambie en el composer.", + "6ba48f07a4": "Borrador predeterminado", + "15b60d54b2": "p. ej. ollama run llama3.1 {prompt}", + "3f1b26cc91": "para pasar la entrada del comando como argumento; si no, Orca la envía por stdin.", + "4f722a5f53": "Usado por recetas de commit-message, pull-request y nombre de rama que seleccionan Comando personalizado. Usa", "47e45cbd5a": "Comando personalizado", - "1ef29f8c29": "Línea de comando Orca se ejecuta cuando una receta de texto usa un comando personalizado.", + "1ef29f8c29": "Línea de comando que Orca ejecuta cuando una receta de texto usa Comando personalizado.", "2339a89104": "Agrega botones AI que ejecutan el agente seleccionado con la plantilla de comando para esa acción.", - "d5b45a3628": "Mostrar acciones de IA de control de código fuente", - "7bcad2b200": "Agrega recetas de acciones para acciones de commit de control de código fuente, solicitud de extracción, nombre de rama y reparación.", + "d5b45a3628": "Mostrar acciones de Source Control AI", + "7bcad2b200": "Agrega recetas de acción para commit, pull request, nombre de rama y arreglos de Source Control.", "d54c64163d": "activado", "4ec89c319e": "agent", "34d0348e34": "generar", "8cd2be0948": "mensaje", "ca433708cb": "commit", "0b7eafe55f": "ai", - "2c5436c018": "abierto", + "2c5436c018": "abrir", "6c84ba6de3": "plantilla", "ebed4d2a29": "borrador", - "02bab6542c": "pr", - "fdee745b87": "solicitud de fusión", - "b388463881": "solicitud de extracción", + "02bab6542c": "PR", + "fdee745b87": "merge request", + "b388463881": "pull request", "19e10a12bb": "revisión alojada", - "b8b6fd55b4": "{inmediato}", - "fc1a525fa5": "marcador de posición", - "a69e1fe91a": "inmediato", + "b8b6fd55b4": "{prompt}", + "fc1a525fa5": "placeholder", + "a69e1fe91a": "prompt", "1df7d71313": "binario", "407d28bde6": "cli", - "54038660e0": "dominio", - "25350d670f": "costumbre" + "54038660e0": "comando", + "25350d670f": "personalizado" }, "ComputerUsePane": { - "1735461723": "Permite a los agents inspeccionar y operar aplicaciones de escritorio locales.", - "93255aaf18": "Habilidad de uso de computadora", - "45f8e22c2e": "Abierto", - "d95d1cfab8": "Refrescar", + "1735461723": "Permite a los agents inspeccionar y operar apps de escritorio locales.", + "93255aaf18": "Skill Computer Use", + "45f8e22c2e": "Abrir", + "d95d1cfab8": "Actualizar", "0c29da5805": "Listo", - "3383ea1aab": "No se pudieron restablecer los permisos de uso de la computadora", - "f189f448a3": "Restablecer el acceso al uso de la computadora", - "5c45349665": "No se pudieron abrir los permisos de uso de la computadora", - "7801ac08ec": "Los permisos de uso de computadora solo se requieren en macOS", - "740766c291": "La configuración de uso de la computadora ya está completa", - "697005758f": "Privacidad y seguridad de macOS abiertas", - "2168fa5ab0": "No se pudieron cargar los permisos de uso de la computadora", - "0c9a33f468": "Capture ventanas de aplicaciones para que los agents puedan inspeccionar el estado visual.", + "3383ea1aab": "No se pudieron restablecer los permisos de Computer Use", + "f189f448a3": "Restablecer acceso de Computer Use", + "5c45349665": "No se pudieron abrir los permisos de Computer Use", + "7801ac08ec": "Los permisos de Computer Use solo se requieren en macOS", + "740766c291": "La configuración de Computer Use ya está completa", + "697005758f": "Se abrió Privacidad y seguridad de macOS", + "2168fa5ab0": "No se pudieron cargar los permisos de Computer Use", + "0c9a33f468": "Captura ventanas de apps para que los agents puedan inspeccionar el estado visual.", "07bbe4c4cb": "Capturas de pantalla", - "4d03dec2d0": "Lea los árboles de la interfaz de la aplicación y realice las acciones solicitadas.", + "4d03dec2d0": "Lee árboles de interfaz de apps y realiza las acciones solicitadas.", "6b5a2cd3a5": "Accesibilidad", "6b17602073": "Restablecer acceso", "506f2acf7a": "Restableciendo acceso...", "4b65070096": "darwin" }, "DeveloperPermissionsPane": { - "4c17304beb": "Refrescar", - "6326a4c5cc": "Utilice estos controles cuando una CLI, una aplicación local o una herramienta de automatización necesiten acceso privado a macOS. Orca no pregunta al inicio.", - "6f011b9bf6": "Las herramientas de terminal heredan la envoltura de privacidad de macOS de Orca.", + "4c17304beb": "Actualizar", + "6326a4c5cc": "Usa estos controles cuando una CLI, una app local o una herramienta de automatización necesite permisos de privacidad de macOS. Orca no pregunta al iniciar.", + "6f011b9bf6": "Las herramientas de terminal heredan el contexto de privacidad de macOS de Orca.", "bfa3402305": "No se pudo solicitar permiso", "66e94d6cf3": "Solicitud de permiso enviada", - "fa809e8ada": "Privacidad y seguridad de macOS abiertas", + "fa809e8ada": "Se abrió Privacidad y seguridad de macOS", "48d87edcd2": "Permiso concedido", "a552887288": "No se pudieron cargar los permisos de desarrollador", "4cfaa7e98a": "Herramientas de dispositivos Bluetooth y experimentos de hardware local.", - "b2210b1b4f": "bluetooth", - "dfbc12c8c8": "Herramientas de dispositivos y depuración de hardware que se comunican con dispositivos USB.", + "b2210b1b4f": "Bluetooth", + "dfbc12c8c8": "Depuración de hardware y herramientas que se comunican con dispositivos USB.", "bf51e4a542": "Dispositivos USB", - "f903bf20b5": "Descubrimiento y acceso a servidores de desarrollo en su red.", - "e7bb06007c": "Red Local", - "4a73f5217a": "Apple Events para scripts que controlan otras aplicaciones locales.", + "f903bf20b5": "Descubrimiento y acceso para servidores de desarrollo en tu red.", + "e7bb06007c": "Red local", + "4a73f5217a": "Apple Events para scripts que controlan otras apps locales.", "e119f0d66b": "Automatización", - "7ca17b62c8": "Recomendado cuando proyectos, worktrees o archivos con enlaces simbólicos usan carpetas protegidas de macOS.", - "c566bca278": "Acceso completo al disco", - "9f35980756": "Herramientas de inyección de pulsaciones de teclas, control de ventanas y automatización de UI.", + "7ca17b62c8": "Recomendado cuando proyectos, worktrees o archivos symlinked tocan carpetas protegidas de macOS.", + "c566bca278": "Acceso total al disco", + "9f35980756": "Herramientas de inyección de teclas, control de ventanas y automatización de UI.", "5b2f22ca2d": "Accesibilidad", - "0639db5496": "Herramientas de captura de pantalla, automatización visual y inspección de la interfaz de usuario.", + "0639db5496": "Herramientas de capturas de pantalla, automatización visual e inspección de UI.", "f24f31a884": "Grabación de pantalla", - "550cfa3750": "Captura de cámara web y aplicaciones de prueba locales controladas por cámara.", + "550cfa3750": "Captura de webcam y apps locales de prueba controladas por cámara.", "e5b5f3d6b9": "Cámara", - "cc8151d9fa": "Entrada de voz, transcripción, grabación de audio, sox, ffmpeg y CLI Whisper.", + "cc8151d9fa": "Entrada de voz, transcripción, grabación de audio, sox, ffmpeg y CLI de Whisper.", "16381e040a": "Micrófono", - "dac08ec03e": "Laboral..." + "dac08ec03e": "Trabajando..." }, "ExperimentalPane": { - "9762364929": "Usa APFS clone-copy en macOS cuando es posible; de lo contrario, crea enlaces simbólicos de las carpetas o archivos configurados en los árboles de trabajo creados.", - "24416f42cd": "Rutas compartidas en árboles de trabajo", - "fb82ea1d7a": "Materializa automáticamente archivos o carpetas configurados en árboles de trabajo recién creados.", - "a20d5ea365": "Mantiene visible un resaltado a nivel de panel después de que suena la terminal o los eventos de finalización del agente hasta que interactúa con ese panel. Experimental mientras sintonizamos la señal.", - "ec897e8d89": "Atención terminal", - "88b7613afb": "Resaltado de panel persistente para eventos de finalización de agente y timbre de terminal.", - "0277901cf7": "Agrega una entrada de Agents a la barra lateral izquierda con una fuente de árbol de trabajo encadenada para Agents completados, preguntas de bloqueo, estado no leído y eventos de creación de árbol de trabajo. Experimental: el modelo de evento y la interfaz de usuario pueden cambiar.", - "a05bcdaf57": "Vista de Agents", - "f63ea281e3": "Feed integrado en la barra lateral izquierda para completar agentes y estados de bloqueo.", - "ca2219fe5e": "Muestra una pequeña mascota animada fijada en la esquina inferior derecha. Elige un personaje (Claudino, OpenCode, Gremlin) o carga tu propio PNG, APNG, GIF, WebP, JPG o SVG desde el menú de mascotas de la barra de estado. Ocultarlo en cualquier momento desde el mismo menú sin desactivar esta configuración.", + "9762364929": "Usa APFS clone-copy en macOS cuando sea posible; si no, crea symlinks de las carpetas o archivos configurados en los worktrees creados.", + "24416f42cd": "Rutas compartidas en worktrees", + "fb82ea1d7a": "Materializa automáticamente archivos o carpetas configurados en worktrees recién creados.", + "a20d5ea365": "Mantiene visible un resaltado a nivel de panel después de una campana del terminal o eventos de finalización de agente hasta que interactúes con ese panel. Experimental mientras ajustamos la señal.", + "ec897e8d89": "Atención del terminal", + "88b7613afb": "Resaltado persistente del panel para campanas del terminal y eventos de finalización de agentes.", + "0277901cf7": "Agrega una entrada Agents a la barra lateral izquierda con un feed en hilos de worktrees para agentes completados, preguntas bloqueantes, estado sin leer y eventos de creación de worktrees. Experimental: el modelo de eventos y la UI pueden cambiar.", + "a05bcdaf57": "Vista Agents", + "f63ea281e3": "Feed en hilos en la barra lateral izquierda para finalizaciones de agentes y estados bloqueantes.", + "ca2219fe5e": "Muestra una pequeña mascota animada fijada en la esquina inferior derecha. Elige un personaje (Claudino, OpenCode, Gremlin) o carga tu propio PNG, APNG, GIF, WebP, JPG o SVG desde el menú de mascota de la barra de estado. Ocúltala cuando quieras desde el mismo menú sin desactivar este ajuste.", "dd6f0a1d45": "Mascota", "0e89a574ae": "Mascota animada flotante en la esquina inferior derecha.", "agentHibernation": { "copy": "Detiene los terminales de agentes en segundo plano que estén inactivos después del intervalo configurado y reanuda las sesiones compatibles cuando las vuelves a abrir. La suspensión de agentes conserva las opciones de inicio de los agentes iniciados por Orca. Los agentes iniciados manualmente pueden reanudarse con tus valores predeterminados actuales de Orca. Experimental mientras ajustamos el modelo de seguridad.", "description": "Detiene los terminales de agentes en segundo plano que estén inactivos después del intervalo configurado y reanuda las sesiones compatibles cuando las vuelves a abrir.", - "idleMinutesDescription": "Cuántos minutos inactivo debe esperar un agente en segundo plano completado antes de que Orca pueda suspenderlo.", + "idleMinutesDescription": "Cuántos minutos debe esperar inactivo un agente en segundo plano completado antes de que Orca pueda suspenderlo.", "idleMinutesLabel": "Suspender después de", "idleMinutesSuffix": "minutos", "title": "Suspensión de agentes", "toggleLabel": "Alternar suspensión de agentes" }, "newWorktreeCardStyle": { - "copy": "Previsualiza el diseño actualizado de tarjetas de worktree, la ubicación de metadatos, las opciones del menú de visualización de tarjeta y la presentación de estado.", - "description": "Previsualiza el diseño actualizado de tarjetas de worktree, la ubicación de metadatos, las opciones del menú de visualización de tarjeta y la presentación de estado.", + "copy": "Previsualiza el diseño actualizado de tarjetas de worktree, la ubicación de metadatos, las opciones del menú de visualización de tarjetas y la presentación de estado.", + "description": "Previsualiza el diseño actualizado de tarjetas de worktree, la ubicación de metadatos, las opciones del menú de visualización de tarjetas y la presentación de estado.", "title": "Nuevo estilo de tarjeta", "toggleLabel": "Alternar nuevo estilo de tarjeta" }, "nativeChat": { - "title": "chat nativo", - "description": "Obtenga una vista previa de la superficie de chat del escritorio para las sesiones de terminal de Claude y Codex.", - "copy": "Agrega una vista de chat nativa a la que puede cambiar desde los paneles de terminal compatibles de Claude y Codex. Experimental mientras ajustamos la fidelidad de la transcripción, la transmisión y la paridad terminal.", + "title": "Chat nativo", + "description": "Previsualiza la superficie de chat de escritorio para sesiones de terminal de Claude y Codex.", + "copy": "Agrega una vista de chat nativa a la que puedes cambiar desde paneles de terminal compatibles de Claude y Codex. Experimental mientras ajustamos la fidelidad de transcripción, streaming y paridad con el terminal.", "toggleLabel": "Alternar chat nativo", "defaultTitle": "Vista predeterminada", - "defaultCopy": "Elija cómo se abren las nuevas pestañas de terminal de Claude y Codex.", - "defaultViewLabel": "Vista de chat nativa predeterminada", - "defaultViewTerminal": "charla terminal", - "defaultViewNative": "chat nativo" + "defaultCopy": "Elige cómo se abren las nuevas pestañas de terminal de Claude y Codex.", + "defaultViewLabel": "Vista predeterminada de chat nativo", + "defaultViewTerminal": "Chat en terminal", + "defaultViewNative": "Chat nativo" } }, "FloatingWorkspacePane": { "aeaf76fda9": "Barra de estado", "9fb225f2d7": "Botón flotante", - "3c900e26e5": "El método abreviado de teclado funciona independientemente de dónde se muestre el interruptor.", - "5e5a8da236": "Ubicación del botón de alternancia", - "505001823e": "Elija el directorio del espacio de trabajo flotante", - "81afb79785": "Las nuevas pestañas de terminal flotante comienzan aquí. Las notas de Markdown se guardan en el espacio de trabajo flotante propiedad de la aplicación de Orca.", - "12aa09f10c": "Directorio de terminales", - "41eb95f7f0": "Muestra el botón y el panel del espacio de trabajo flotante.", - "5136813663": "Habilitar espacio de trabajo flotante", - "37df688d6f": "Habilite el espacio de trabajo flotante y elija dónde comienzan las nuevas pestañas.", - "1f67f39384": "Espacio de trabajo flotante" + "3c900e26e5": "El atajo de teclado funciona independientemente de dónde se muestre el botón.", + "5e5a8da236": "Ubicación del botón de activación", + "505001823e": "Elegir directorio del workspace flotante", + "81afb79785": "Las nuevas pestañas de terminal flotante comienzan aquí. Las notas Markdown se guardan en el workspace flotante propiedad de la app de Orca.", + "12aa09f10c": "Directorio del terminal", + "41eb95f7f0": "Muestra el botón y el panel del workspace flotante.", + "5136813663": "Activar workspace flotante", + "37df688d6f": "Activa el workspace flotante y elige dónde comienzan las nuevas pestañas.", + "1f67f39384": "Workspace flotante" }, "AgentCacheTimerSection": { "05de84a104": "1 hora", "54395ecd7c": "5 minutos", - "8b9e202e0a": "Haga coincidir esto con el TTL de caché de su proveedor. El valor predeterminado es 5 minutos.", + "8b9e202e0a": "Haz que esto coincida con el TTL de caché de tu proveedor. El valor predeterminado es 5 minutos.", "a2a8962138": "Duración del temporizador", "b4e7302944": "Temporizador de caché", "487b176240": "Muestra una cuenta regresiva en la barra lateral después de que un agente de Claude quede inactivo.", "9c20253679": "Muestra una cuenta regresiva después de que un agente de Claude quede inactivo.", - "fe590653c1": "Claude almacena en caché su conversación para reducir costos. Cuando está inactivo durante demasiado tiempo, la memoria caché caduca y el siguiente mensaje reenvía el contexto completo a un costo mayor. Esto muestra una cuenta atrás para que sepas cuándo reanudar.", - "a137f8854d": "Temporizador de caché rápido", - "80c454e8a6": "Haga coincidir esto con el TTL de caché de su proveedor." + "fe590653c1": "Claude cachea tu conversación para reducir costos. Si queda inactiva demasiado tiempo, la caché caduca y el siguiente mensaje reenvía todo el contexto con mayor costo. Esto muestra una cuenta regresiva para que sepas cuándo reanudar.", + "a137f8854d": "Prompt Cache Timer", + "80c454e8a6": "Haz que esto coincida con el TTL de caché de tu proveedor." }, "GeneralEditorSettingsSection": { - "f80603d293": "Muestre controles de notas de markdowns locales en modo de editor enriquecido y acciones de transferencia de agentes.", - "4edc104f0f": "Notas de revisión de Markdowns", - "5f02e6fb21": "Muestre los controles de notas de revisión de markdowns locales en el modo de editor enriquecido.", - "51161d1647": "Muestra la descripción general del minimapa al editar un archivo.", + "f80603d293": "Muestra controles de notas Markdown locales en modo de editor enriquecido y acciones de handoff de agentes.", + "4edc104f0f": "Notas Markdown de revisión", + "5f02e6fb21": "Muestra controles de notas Markdown locales en modo de editor enriquecido.", + "51161d1647": "Muestra la vista general del minimapa al editar un archivo.", "6690b1ffb9": "Minimapa", "5a1ea6eaa2": "Oculto", - "73a09aad63": "Mostrado", - "1de48ad940": "Árbol de archivos de diferencias predeterminado", - "1b87897af9": "Muestre u oculte el árbol de archivos al abrir vistas de diferencias combinadas.", + "73a09aad63": "Visible", + "1de48ad940": "Árbol de archivos de diff predeterminado", + "1b87897af9": "Muestra u oculta el árbol de archivos al abrir vistas de diff combinado.", "12cbc0d0d6": "Lado a lado", - "05b6df93b3": "En línea", - "7311f67ee7": "Vista de diferencias predeterminada", - "b492397d34": "Formato de presentación preferido para mostrar diferencias de git de forma predeterminada.", - "a5db1d3975": "EM", - "fc5c5306ff": "EM.", - "8112cd6dcf": "Cuánto tiempo espera Orca después de su última edición antes de guardar automáticamente. El primer lanzamiento predeterminado es", + "05b6df93b3": "Inline", + "7311f67ee7": "Vista de diff predeterminada", + "b492397d34": "Formato de presentación preferido para mostrar diffs de git de forma predeterminada.", + "a5db1d3975": "ms", + "fc5c5306ff": "ms.", + "8112cd6dcf": "Cuánto espera Orca después de tu última edición antes de guardar automáticamente. El valor predeterminado en el primer inicio es", "d6cf227ca0": "Retraso de guardado automático", - "1bec6d8318": "Cuánto tiempo espera Orca después de su última edición antes de guardar automáticamente.", - "70bb30feb1": "Guarde el editor y los cambios de diferencias editables automáticamente después de una breve pausa.", + "1bec6d8318": "Cuánto espera Orca después de tu última edición antes de guardar automáticamente.", + "70bb30feb1": "Guarda automáticamente los cambios del editor y de diffs editables después de una breve pausa.", "0df2e4fd12": "Guardar archivos automáticamente", - "d21136d9ef": "Configure cómo Orca persiste en las ediciones de archivos.", + "d21136d9ef": "Configura cómo Orca persiste ediciones de archivos.", "45c6e85c4d": "Editor", - "8f1afdfbd8": "Ajuste de palabras diferencial", - "4aa4d9fb73": "Ajuste líneas largas en editores de diferencias en lugar de requerir desplazamiento horizontal.", - "bf16ef0af2": "Apagado", - "3f6892f307": "En", - "b82f86d7d2": "Rich Markdown Spellcheck", - "5195f0b9ef": "Show browser spelling underlines and suggestions while editing rich Markdown." + "8f1afdfbd8": "Ajuste de línea en diffs", + "4aa4d9fb73": "Ajusta líneas largas en editores de diff en lugar de requerir desplazamiento horizontal.", + "bf16ef0af2": "Desactivado", + "3f6892f307": "Activado", + "b82f86d7d2": "Corrector ortográfico de Rich Markdown", + "5195f0b9ef": "Muestra subrayados y sugerencias ortográficas del navegador al editar Rich Markdown." }, "AdvancedNetworkSettingsSection": { - "3e431564b5": "host local, 127.0.0.1, *.interno", - "33ee3ca3af": "Opcional. Separe los hosts con comas, punto y coma o líneas nuevas.", + "3e431564b5": "localhost, 127.0.0.1, *.internal", + "33ee3ca3af": "Opcional. Separa hosts con comas, punto y coma o líneas nuevas.", "f6d76cc8f4": "Reglas de omisión de proxy", - "fb7130dcb9": "Hosts que deberían omitir el proxy HTTP configurado.", - "0adfce9fa7": "Admite URL http, https, calcetines, calcetines4 y calcetines5.", + "fb7130dcb9": "Hosts que deben omitir el proxy HTTP configurado.", + "0adfce9fa7": "Admite URLs http, https, socks, socks4 y socks5.", "476f302aca": "http://proxy.example.com:8080", - "1e214e265a": "Déjelo vacío para usar la configuración del proxy del sistema y las variables de entorno del proxy heredadas.", + "1e214e265a": "Déjalo vacío para usar la configuración de proxy del sistema y las variables de entorno de proxy heredadas.", "f00daf6324": "Proxy HTTP", - "823e0f15b1": "URL proxy para solicitudes de red Orca y terminales secundarios locales.", - "d93c7cd531": "Configure el enrutamiento de red a nivel de aplicación.", + "823e0f15b1": "URL de proxy para requests de red de Orca y procesos de terminal locales hijos.", + "d93c7cd531": "Configura el enrutamiento de red a nivel de app.", "c46cdbbd4e": "Red", "configureProxy": "Configurar proxy" }, @@ -5188,18 +5188,18 @@ "d58fccfd84": "Navegación", "5cb5475664": "Confirmar antes de cerrar pestañas fijadas", "36b2a5dc6d": "Muestra un diálogo de confirmación antes de cerrar una pestaña fijada.", - "projectRuntime": "Project Runtime", - "projectRuntimeDescription": "Default runtime for local Windows projects that do not override it." + "projectRuntime": "Runtime del proyecto", + "projectRuntimeDescription": "Runtime predeterminado para proyectos locales de Windows que no lo sobrescriben." }, "GeneralSupportSection": { "af7d9f4396": "¡Gracias por el apoyo!", - "6922c1fa2b": "Orca estrella en GitHub", - "511782265b": "Apoye el proyecto con una estrella de GitHub a través de la CLI de gh.", - "55a87e5fd1": "Apoya a la Orca", - "964acc6bb4": "Dar estrella", + "6922c1fa2b": "Dale una estrella a Orca en GitHub", + "511782265b": "Apoya el proyecto con una estrella de GitHub mediante la CLI gh.", + "55a87e5fd1": "Apoya a Orca", + "964acc6bb4": "Dar una estrella", "73b327e793": "Intentar otra vez", "c9f96d4234": "error", - "397719bee5": "Dando estrella...", + "397719bee5": "Dando una estrella...", "1e29570462": "dando estrella", "9d181300e3": "con estrella", "5c49f02662": "oculto", @@ -5211,16 +5211,16 @@ "8a52ca1d02": "Notas de la versión", "d89806cc89": "está listo para instalar.", "a6b37929dc": "Versión", - "8311da27ba": "está disponible. Haga clic en \"Instalar actualización\" para descargarla e instalarla.", + "8311da27ba": "está disponible. Haz clic en \"Instalar actualización\" para descargarla e instalarla.", "f44299636f": "Reiniciar para actualizar (", "42717918f4": "Instalar actualización (", "02dc082e70": "No se pudo iniciar la descarga de la actualización.", "e1a647adc5": "Buscar actualizaciones", - "ceb579abaf": "Busque actualizaciones de la aplicación e instale una versión más reciente de Orca.", + "ceb579abaf": "Busca actualizaciones de la app e instala una versión más reciente de Orca.", "d91ebfb87e": "Versión actual: {{value0}}", "f2b1ccc12a": "Actualizaciones", - "bd79d412f0": "La verificación de actualización falló. {{value0}}", - "b9ad70c30d": "Error de actualización. {{value0}}", + "bd79d412f0": "No se pudo buscar actualizaciones. {{value0}}", + "b9ad70c30d": "Error al actualizar. {{value0}}", "6405510b92": "error", "a0832ccdb1": "descargado", "2a48034c4c": "Descargando v{{value0}}... {{value1}}%", @@ -5234,20 +5234,20 @@ "7173352632": "idle" }, "GeneralWorkspaceSettingsSection": { - "3d538a98f7": "Elija las aplicaciones disponibles en el menú Abrir en de un espacio de trabajo.", + "3d538a98f7": "Elige las apps disponibles en el menú Abrir en de un espacio de trabajo.", "008f92085f": "Abrir en aplicaciones", "824b98a0d9": "Muestra una confirmación antes de eliminar automatizaciones y su historial de ejecución.", - "ea98373cd8": "Pregunte antes de eliminar automatizaciones", + "ea98373cd8": "Preguntar antes de eliminar automatizaciones", "d2dd2ca2e3": "Muestra un cuadro de diálogo de confirmación antes de eliminar una automatización y su historial de ejecución.", - "28bc3d085e": "Muestra una confirmación antes de eliminar un espacio de trabajo del menú contextual. Las eliminaciones fallidas aún muestran un recurso de Forzar eliminación.", + "28bc3d085e": "Muestra una confirmación antes de eliminar un espacio de trabajo desde el menú contextual. Las eliminaciones fallidas aún muestran la opción Forzar eliminación.", "9f380934cf": "Preguntar antes de eliminar espacios de trabajo", "5734db82af": "Muestra un cuadro de diálogo de confirmación antes de eliminar un espacio de trabajo.", - "4fbf910ded": "Cree espacios de trabajo dentro de una subcarpeta con nombre de repo.", + "4fbf910ded": "Crea espacios de trabajo dentro de una subcarpeta con el nombre del repositorio.", "ba3480642f": "Espacios de trabajo anidados", "a246f5ce6f": "Directorio raíz donde se crean las carpetas del espacio de trabajo.", - "5567191a6e": "Navegar", + "5567191a6e": "Examinar", "0e9fc0eadc": "Directorio de espacio de trabajo", - "e2955d9ccb": "Configure dónde se crean nuevos espacios de trabajo.", + "e2955d9ccb": "Configura dónde se crean nuevos espacios de trabajo.", "7511097c5d": "Espacio de trabajo" }, "GhosttyImportModal": { @@ -5256,54 +5256,54 @@ "b7ddae600c": "Hecho", "e4bda7ce6f": "No se encontró ninguna configuración de Ghostty en este sistema.", "b58d4c9051": "Claves no compatibles", - "674b5ccd6b": "No hay nuevas configuraciones para importar: su configuración actual ya coincide.", - "a4c5dec640": "Configuraciones para actualizar", + "674b5ccd6b": "No hay nuevos ajustes que importar; tus ajustes actuales ya coinciden.", + "a4c5dec640": "Ajustes por actualizar", "4466f4cdaa": "Importación completa", "023a52c1f7": "Cargando vista previa…", - "2763b0c045": "Revise la configuración que se importará desde su configuración de Ghostty.", + "2763b0c045": "Revisa los ajustes que se importarán desde tu configuración de Ghostty.", "d2f33670a9": "Importar desde Ghostty", "273e7e81fe": "Configuraciones", "1f744a72f4": "configuración" }, "GitPane": { - "d2eede4c54": "Agregue la atribución de Orca a commits, PR y problemas.", + "d2eede4c54": "Agrega la atribución de Orca a commits, PR e issues.", "e02ea23a32": "Atribución de Orca", "e71ce09c42": "orca", "b9b5771bb1": "atribución", "b5f534717a": "coautor", "9838c921ed": "coautor", - "afada55042": "asunto", + "afada55042": "issue", "b4ef5428a7": "pr", "895d3f70b8": "gh", "32dca11189": "github", - "c4f610d057": "Encabezados de límite de velocidad REST de GitLab CLI actuales cuando estén disponibles.", + "c4f610d057": "Encabezados actuales de límite de tasa REST de GitLab CLI cuando estén disponibles.", "0de4ae556c": "Presupuesto de la API de GitLab", "cdd793134e": "presupuesto API", "b9c011fbc2": "límite de tasa", - "3072428ac7": "glabro", + "3072428ac7": "glab", "8a527d48e3": "gitlab", "aa204f185f": "Límites de velocidad actuales de GitHub CLI REST, Search y GraphQL.", "612a440e57": "Presupuesto de la API de GitHub", - "2cde9044a8": "graficoql", - "36e3de3619": "de comparar con la historia obsoleta. Orca omite la actualización si esa rama tiene cambios no confirmados o commits solo locales.", - "d072a12995": "git diff principal...CABEZA", + "2cde9044a8": "graphql", + "36e3de3619": "comparen con historial obsoleto. Orca omite la actualización si esa rama tiene cambios sin commit o commits solo locales.", + "d072a12995": "git diff main...HEAD", "db3a127eb1": ". Esto mantiene comandos como", - "3ae3de8898": "maestro", + "3ae3de8898": "master", "5bf885be48": "o", - "ffba483bae": "principal", - "976afc6b3e": "Cuando crea un espacio de trabajo, Orca actualiza la base remota y adelanta de forma segura su sucursal local coincidente, como", - "1ec5c91e1d": "Elija si los nombres de las ramas usan su nombre de usuario de Git, un prefijo personalizado o ningún prefijo.", + "ffba483bae": "main", + "976afc6b3e": "Cuando creas un espacio de trabajo, Orca actualiza la base remota y adelanta de forma segura tu rama local correspondiente, como", + "1ec5c91e1d": "Elige si los nombres de rama usan tu usuario de Git, un prefijo personalizado o ningún prefijo.", "330f584b50": "Prefijo de rama", - "1ffaadf0a0": "Prefijo agregado a los nombres de las ramas al crear árboles de trabajo.", - "813e15b346": "costumbre", - "2351aa5a31": "nombre de usuario git", - "cc63fce906": "denominación de sucursales", - "b559bf9899": "p.ej. característica", - "aefa1ecb59": "No hay ningún nombre de usuario de git configurado", + "1ffaadf0a0": "Prefijo que se añade a los nombres de rama al crear worktrees.", + "813e15b346": "personalizado", + "2351aa5a31": "usuario de Git", + "cc63fce906": "nombres de rama", + "b559bf9899": "p. ej. feature", + "aefa1ecb59": "No hay ningún usuario de Git configurado", "f35007e6e8": "nombre de usuario git", "3d172725cc": "Ninguno", - "1f32ba27a6": "Costumbre", - "a182c5125e": "Nombre de usuario", + "1f32ba27a6": "Personalizado", + "a182c5125e": "Usuario de Git", "sourceControlGroupOrderTitle": "Orden de grupos de control de código fuente", "sourceControlGroupOrderDescription": "Elige si Cambios, Cambios preparados o Archivos sin seguimiento aparecen primero en Control de código fuente.", "changesFirst": "Cambios primero", @@ -5322,7 +5322,7 @@ }, "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.", - "ad31c3c5fb": "Haga clic con el botón central en Pegar desde la selección" + "ad31c3c5fb": "Pegar desde la selección con clic central" }, "IntegrationsPane": { "2122e15517": "Cada espacio de trabajo Linear conectado tiene una clave almacenada por el tiempo de ejecución activo. Las claves de acceso total pueden cubrir todos los equipos a los que puede acceder el propietario de la clave; Las claves restringidas se pueden reemplazar en cualquier momento.", @@ -5396,26 +5396,26 @@ "a565377c38": "no instalado", "15cf990798": "No autenticado", "f7eb5f0b24": "No instalado", - "3ba07f933b": "Connect issue trackers Orca can use to browse tasks and start workspaces with linked context.", - "70e885705b": "Task providers", - "1683acbac4": "Connect the source hosts Orca can use for pull requests, merge requests, checks, and review status.", - "298c65ecac": "Review providers" + "3ba07f933b": "Conecta issue trackers que Orca puede usar para explorar tareas e iniciar espacios de trabajo con contexto vinculado.", + "70e885705b": "Proveedores de tareas", + "1683acbac4": "Conecta los hosts de código que Orca puede usar para pull requests, merge requests, checks y estado de revisión.", + "298c65ecac": "Proveedores de revisión" }, "KagiSessionLinkForm": { - "92f0b4e472": "Claro", + "92f0b4e472": "Borrar", "9f741627a7": "Se borró el enlace de la sesión de Kagi.", - "d5c8b94c5b": "Ahorrar", + "d5c8b94c5b": "Guardar", "ff450194cd": "Enlace de sesión privada de Kagi", "e383683485": "https://kagi.com/search?token=...", "81409d9362": "Enlace de sesión privada opcional para la autenticación Kagi.", "3e5b7c6c25": "Enlace de sesión de Kagi guardado.", - "0911d5fa4c": "Ingrese un enlace de sesión privada de Kagi desde https://kagi.com/search?token=..." + "0911d5fa4c": "Introduce un enlace de sesión privada de Kagi desde https://kagi.com/search?token=..." }, "KeybindingsFileActions": { "abc49853fb": "Recargar desde el disco", - "a8a8d6b9d3": "Revelar en el Administrador de archivos", + "a8a8d6b9d3": "Mostrar en el administrador de archivos", "9e24c0e858": "Abrir en Cursor", - "1637f64033": "Abrir en código VS", + "1637f64033": "Abrir en VS Code", "98f1a23e1c": "Abrir con la aplicación predeterminada", "400397a10d": "Abrir menú de archivo de combinaciones de teclas", "1c2be2b2c6": "Editar archivo en Orca", @@ -5426,44 +5426,44 @@ "ManageSessionKillDialog": { "6bf4627168": "Cancelar", "ad9832aa26": ". Cualquier trabajo no guardado en ese panel se perderá. Esto no se puede deshacer.", - "8401328fed": "Renuncias forzadas", - "87dcafc85c": "¿Matar esta sesión?", - "0b0db4c68c": "Matar sesión", - "d3dba51b15": "Asesinato…" + "8401328fed": "Fuerza el cierre de ", + "87dcafc85c": "¿Finalizar esta sesión?", + "0b0db4c68c": "Finalizar sesión", + "d3dba51b15": "Finalizando…" }, "ManageSessionsSection": { - "33c2a1e1b4": "Matar sesión {{value0}}", + "33c2a1e1b4": "Finalizar sesión {{value0}}", "2896a50f50": "Ir a la terminal {{value0}}", "e26a60d9eb": "Sin sesiones.", "39c53d6d74": "Cargando…", - "5ed15e778c": "Reiniciar demonio", - "3282db098c": "Matar todas las sesiones", - "b3b1cc5708": "Refrescar", + "5ed15e778c": "Reiniciar servicio", + "3282db098c": "Finalizar todas las sesiones", + "b3b1cc5708": "Actualizar", "a795a9552a": "Sesiones", - "7c4889a724": "Recupérese de una terminal congelada o que se comporta mal cancelando sesiones o reiniciando el demonio subyacente.", + "7c4889a724": "Recupérate de una terminal congelada o que se comporta mal finalizando sesiones o reiniciando el servicio subyacente.", "d1b80fd5cd": "Administrar sesiones", - "9c940434af": "Vuelva al tiempo de ejecución local para reiniciar o finalizar las sesiones del demonio local.", + "9c940434af": "Vuelve al runtime local para reiniciar o finalizar las sesiones del servicio local.", "ad467eaadc": "La administración de sesiones no está disponible mientras un servidor de ejecución remoto está activo.", "8dbd96b463": "No se pudo finalizar la sesión.", "0735b7a586": "No se pudo cerrar la sesión; es posible que ya haya desaparecido.", - "bfba05dccd": "Sesión asesinada.", + "bfba05dccd": "Sesión finalizada.", "c535cbdd09": "No se pudieron cargar las sesiones.", "e3d1fbe008": "Reanudar", "a06ababda0": "matar a todos" }, "McpConfigFileRow": { "b145eb6009": "entorno:", - "e720c139cd": "Abierto", + "e720c139cd": "Abrir", "845ae248e8": "válido" }, "McpConfigSection": { "4d16a0d9ac": "Comprobado", - "b900cd6282": "No se encontró ninguna configuración de MCP. Agregue una configuración de espacio de trabajo vacío cuando desee que este repo defina sus propios servidores MCP.", - "3b224167ff": "servidor", + "b900cd6282": "No se encontró ninguna configuración de MCP. Agrega una configuración vacía de workspace cuando quieras que este repositorio defina sus propios servidores MCP.", + "3b224167ff": "server", "251b96564a": "detectado ·", "f34c152dc0": "Actualizar configuraciones de MCP", - "6bac9ddfc6": "Los repos SSH se leen a través del sistema de archivos remoto. La creación inicial se limita a la configuración raíz del espacio de trabajo.", - "96f5609b04": "Inspeccione las definiciones del servidor MCP que los agents pueden usar mientras trabajan en este repo.", + "6bac9ddfc6": "Los repositorios SSH se leen a través del sistema de archivos remoto. La creación inicial se limita a la configuración raíz del espacio de trabajo.", + "96f5609b04": "Inspecciona las definiciones de servidores MCP que los agentes pueden usar mientras trabajan en este repositorio.", "55eea3ef47": "Configuraciones de MCP", "9ee215caf6": ".mcp.json", "1f3665e35a": "Configuración de MCP creada", @@ -5472,56 +5472,56 @@ }, "MobileEmulatorAgentControlRow": { "1861982430": "No se pudo cargar el estado de CLI.", - "8af7a8bc38": "Los comandos apuntan al emulador activo del árbol de trabajo actual. Las coordenadas están normalizadas de 0 a 1.", + "8af7a8bc38": "Los comandos apuntan al emulador activo del worktree actual. Las coordenadas están normalizadas de 0 a 1.", "c7f3fe0a6e": "Comandos comunes del emulador", - "d94ca6a623": "Permite a los agents utilizar comandos CLI de Orca, incluido el control del emulador móvil.", - "67e19ee03c": "Habilidad CLI de Orca", + "d94ca6a623": "Permite a los agentes usar comandos de la CLI de Orca, incluido el control del emulador móvil.", + "67e19ee03c": "Skill Orca CLI", "aaf62a3dd2": "Instalado en", - "2fef055608": "Registra el comando CLI de Orca para que los agents puedan controlar el emulador activo desde su shell.", + "2fef055608": "Registra el comando de la CLI de Orca para que los agentes puedan controlar el emulador activo desde su shell.", "4f2205f3b6": "Habilitar CLI de Orca", - "ff4b7e65d6": "Deje que los agents de codificación controlen el emulador móvil activo con los comandos CLI de Orca.", - "2a674aa810": "Control del emulador móvil del Agent", + "ff4b7e65d6": "Permite que los agentes de codificación controlen el emulador móvil activo con comandos de la CLI de Orca.", + "2a674aa810": "Control del emulador móvil para agentes", "cdeaed9e37": "Registré Orca CLI en PATH.", "3d34423e88": "Registrando la CLI de Orca", - "3be27641c9": "para que los comandos del emulador se puedan ejecutar desde shells de agents.", + "3be27641c9": "para que los comandos del emulador se puedan ejecutar desde shells de agentes.", "3941719a56": "Comprobando la CLI de Orca antes de abrir la configuración de la skill." }, "MobileEmulatorExamples": { "edf13dd03b": "Copiar", - "c12b253997": "Copiar mensaje de ejemplo", + "c12b253997": "Copiar prompt de ejemplo", "d151e25078": "\"", "b525ff2b12": "\"", - "4daa95f25a": "Pegue cualquiera de estos en Claude Code, Codex u otro agente en un proyecto donde esté instalada la habilidad Orca CLI.", - "0820b3f84f": "Pruébelo: indicaciones de ejemplo", - "1f608e7d60": "No se pudo copiar el mensaje.", - "2b077b5544": "Mensaje copiado." + "4daa95f25a": "Pega cualquiera de estos en Claude Code, Codex u otro agente en un proyecto donde esté instalado el skill Orca CLI.", + "0820b3f84f": "Pruébalo: prompts de ejemplo", + "1f608e7d60": "No se pudo copiar el prompt.", + "2b077b5544": "Prompt copiado." }, "MobileEmulatorSettingsPane": { - "19d39113b6": "Deje que los agents de codificación controlen el emulador móvil activo con los comandos CLI de Orca.", - "f2f8d97bb6": "Control del emulador móvil del Agent", + "19d39113b6": "Permite que los agentes de codificación controlen el emulador móvil activo con comandos de la CLI de Orca.", + "f2f8d97bb6": "Control del emulador móvil para agentes", "143961d031": "Dispositivo predeterminado", "8aec2f99a0": "Actualizar la disponibilidad del emulador", "ae1612c58c": "Disponibilidad", - "f9af91ea26": "Muestra la acción Nuevo emulador móvil y permite que los agents se conecten al emulador activo.", + "f9af91ea26": "Muestra la acción Nuevo emulador móvil y permite que los agentes se conecten al emulador activo.", "700ddbf9b1": "Habilitar emulador móvil", - "bc39d0f115": "Configure la compatibilidad con el emulador móvil para Orca y agents de codificación.", + "bc39d0f115": "Configura la compatibilidad con el emulador móvil para Orca y agentes de codificación.", "6593c9ddd3": "Emulador móvil", "a4f1c82d90": "Desactivado", - "b5e2d93e01": "De cheques...", + "b5e2d93e01": "Comprobando...", "c6f3ea4f12": "Listo", "d704fb5023": "Necesita configuración", "06b06429c6": "Comprobando la compatibilidad con el SDK de Android y el simulador de iOS.", "6d1483d4a0": "1 dispositivo emulador detectado.", "0a452d4d3b": "{{value0}} dispositivos emuladores detectados.", "f62a1bb759": "Orca seleccionará automáticamente un dispositivo emulador después de que se detecten los dispositivos.", - "b2fd62ea75": "Dispositivo predeterminado para nuevas pestañas del emulador y comandos de conexión de agents. La selección automática prefiere un dispositivo que ya esté en ejecución." + "b2fd62ea75": "Dispositivo predeterminado para nuevas pestañas del emulador y comandos de conexión de agentes. La selección automática prefiere un dispositivo que ya esté en ejecución." }, "MobileNetworkInterfaceSection": { "63d5e4ae1e": "Regenera el código QR y escanéalo desde la aplicación móvil de Orca.", "87985ba6f5": "En este menú de Interfaz de red, elija la dirección Tailscale, generalmente una IP 100.x.y.z.", "1f7c26d36a": "Inicie sesión en la misma tailnet en ambos dispositivos.", "668016be7a": "en su computadora y teléfono.", - "1dc87a7fbc": "Escala de cola", + "1dc87a7fbc": "Tailscale", "51d29927eb": "Instalar", "9fc5d203ff": "Orca Mobile se conecta directamente a esta computadora. Para usarlo fuera de la misma red local, coloque su computadora y teléfono en la misma red superpuesta privada, luego genere el código QR con esa dirección de red seleccionada.", "39fad211d9": "Conéctate fuera de tu Wi-Fi con una tailnet", @@ -5530,81 +5530,81 @@ "d536b5e20d": "Elija qué dirección de red anunciar en el código QR. Utilice su dirección LAN para el emparejamiento de la misma red o una dirección de red superpuesta (Tailscale, ZeroTier) para el acceso entre redes.", "406a35121c": "Interfaz de red", "c541f67790": "Generar código QR", - "1e64659126": "Regenerado" + "1e64659126": "Regenerar" }, "MobilePane": { "dd3cd78d04": "Escanear con Orca Móvil", - "35100bca5d": "Mientras usas un terminal en tu teléfono, Orca lo reduce para que se ajuste a la pantalla de tu teléfono. Cuando cierra la aplicación o la abandona, esto controla si permanece en el tamaño del teléfono (para que las herramientas CLI interactivas no vuelvan a fluir) o cambia de tamaño a su escritorio. Siempre puedes usar Restaurar este terminal o Restaurar todos los terminales en el banner para cambiar el tamaño manualmente.", + "35100bca5d": "Mientras usas un terminal en tu teléfono, Orca lo reduce para que se ajuste a la pantalla del teléfono. Cuando cierras la app o cambias a otra, esto controla si se queda en tamaño de teléfono (para que las herramientas CLI interactivas no se reacomoden) o vuelve al tamaño del escritorio. Siempre puedes usar Restaurar este terminal o Restaurar todos los terminales en el banner para cambiar el tamaño manualmente.", "ee56f1c7e4": "Cuando sales de la aplicación móvil", "3939fd062c": "Revocar un dispositivo lo desconecta inmediatamente.", - "254a6d09e4": "emparejado", + "254a6d09e4": "Emparejado", "d7ce676270": "Dispositivos emparejados", - "e778ecb209": "O pegue este código en la aplicación móvil:", + "e778ecb209": "O pega este código en la app móvil:", "310924ad2c": "Escanea este código con la aplicación móvil de Orca. Cada código crea un token de dispositivo único.", "870e1b5ca5": "No se pudo revocar el dispositivo", "2e3dd0bc29": "Dispositivo revocado", "711231348f": "No se pudo copiar el código de emparejamiento", "e3c427e020": "No se pudo generar el código QR", - "cb9067c1c1": "El transporte de WebSocket no se está ejecutando", + "cb9067c1c1": "El transporte WebSocket no está en ejecución", "d714614dbf": "No se pudieron actualizar las interfaces de red", "ff865419dc": "Después de 30 minutos", "d4ba07d914": "Después de 5 minutos", "c474aa09d8": "Después de 1 minuto", - "aa1263e881": "Mantener en el tamaño del teléfono (predeterminado)", - "6436e56546": "Código QR para emparejamiento de móviles", + "aa1263e881": "Mantener en tamaño de teléfono (predeterminado)", + "6436e56546": "Código QR para emparejamiento móvil", "1b1b70279a": "Aún no hay dispositivos emparejados.", "1592afcc7a": "Aún no hay dispositivos emparejados. Escanea el código QR con la aplicación móvil de Orca." }, "MobileSettingsPane": { - "9a3c280e49": "Lanzamientos de GitHub", + "9a3c280e49": "GitHub Releases", "b0088412a1": "o el APK de Android desde", - "b5a2ed83ff": "Tienda de aplicaciones", - "c8491c17ef": "Controla Orca desde tu teléfono escaneando un código QR. Beta/vista previa anticipada: espere errores y cambios importantes. Obtenga la aplicación para iOS desde", + "b5a2ed83ff": "App Store", + "c8491c17ef": "Controla Orca desde tu teléfono escaneando un código QR. Beta/vista previa temprana: espera errores y cambios importantes. Obtén la app para iOS en", "e7a3ae8c4e": "Móvil", - "174f4a3c6d": "Controla terminales y agents desde tu teléfono.", + "174f4a3c6d": "Controla terminales y agentes desde tu teléfono.", "1de96ec8a6": "Mostrar botón de Orca Mobile", "682293cadf": "Mostrar el botón de Orca Mobile en la parte superior de la barra lateral izquierda.", "d4f2b65f30": "Mostrar el acceso directo de Orca Mobile en la barra lateral." }, "NotificationsPane": { "906b4afebf": "Enviar notificación de prueba", - "2772d2f257": "Omita las notificaciones cuando el árbol de trabajo desencadenante ya esté visible.", - "00cd406dbb": "Suprimir mientras estás concentrado", + "2772d2f257": "Omitir notificaciones cuando el worktree que las activó ya esté visible.", + "00cd406dbb": "Silenciar mientras está en foco", "2a42dd8d6f": "Volumen del sonido de notificación", - "4aa5085cd7": "Costumbre:", + "4aa5085cd7": "Personalizado:", "c258cb96dc": "Elige sonido de notificación", - "2a2033c388": "Elija la alerta que Orca reproduce cuando se entrega una notificación de escritorio.", + "2a2033c388": "Elige la alerta que Orca reproduce cuando se entrega una notificación de escritorio.", "88686e6ca8": "Sonido de notificación", "b6fc369244": "Un terminal en segundo plano emite un carácter de campana.", - "591fe605b9": "Campana terminal", - "55f901a59b": "Un agente de codificación finaliza y queda inactivo.", - "ca76d06fd2": "Tarea del Agent completada", + "591fe605b9": "Campana del terminal", + "55f901a59b": "Un agente de programación termina y queda inactivo.", + "ca76d06fd2": "Tarea del agente completada", "deff6d30da": "Notificaciones nativas del sistema para eventos en segundo plano.", "841c8c549f": "Habilitar notificaciones", "0fadad17ce": "No se pudo reproducir el sonido de notificación", - "406feb0aa6": "La notificación de prueba no fue entregada", + "406feb0aa6": "La notificación de prueba no se entregó", "6fc3781729": "Las notificaciones están deshabilitadas", - "4676a95bc3": "Verifique la configuración de notificaciones de su escritorio para Orca.", - "0cb93240b8": "El sistema no mostró la notificación.", + "4676a95bc3": "Revisa la configuración de notificaciones de tu escritorio para Orca.", + "0cb93240b8": "El sistema no mostró la notificación", "145227ca2b": "Abrir configuración", "d3d54e0915": "Notificación de prueba enviada", - "115437bc35": "Si no apareció ningún banner de macOS, habilite Permitir notificaciones para Orca.", + "115437bc35": "Si no apareció ningún banner de macOS, activa Permitir notificaciones para Orca.", "7f45542625": "Notificación de prueba solicitada", "98d70fb261": "No se pudo reproducir el sonido de notificación personalizado", - "c83b05a055": "Las notificaciones no son compatibles con este sistema.", + "c83b05a055": "Este sistema no admite notificaciones", "274af61bc0": "sistema", - "6e6df3a09a": "Elija un archivo personalizado", + "6e6df3a09a": "Elegir archivo personalizado", "76e02467b8": "Cambiar archivo personalizado", "d3756cf5bc": "desactivado" }, "OpenAiTranscriptionKeyDialog": { "fa83512e48": "Guardar clave", "07b26f2742": "Borrar clave", - "d246b2bdb3": "Las claves de tiempo de ejecución local se almacenan en ~/.orca utilizando el almacenamiento cifrado Electron cuando esté disponible.", + "d246b2bdb3": "Las claves del runtime local se almacenan en ~/.orca mediante el almacenamiento cifrado de Electron cuando está disponible.", "c3380e4ca5": "sk-...", "2f797018f0": "Clave API configurada", "16015322f9": "Clave API", - "07ed3e512e": "El audio se envía a OpenAI solo cuando se selecciona un modelo de voz OpenAI.", + "07ed3e512e": "El audio se envía a OpenAI solo cuando se selecciona un modelo de voz de OpenAI.", "439e91879e": "Transcripción OpenAI" }, "OpenAiTranscriptionSettingsRow": { @@ -5613,17 +5613,17 @@ "a622bc3b37": "Reemplazar clave", "3b0ab3fc0b": "Conectado", "27e0cb656d": "Transcripción OpenAI", - "893790e13b": "Agregue una clave API de OpenAI antes de seleccionar modelos de voz a texto en la nube.", - "b59b9b2b51": "Clave API configurada para modelos de voz a texto en la nube." + "893790e13b": "Agrega una clave API de OpenAI antes de seleccionar modelos de conversión de voz a texto en la nube.", + "b59b9b2b51": "Clave API configurada para modelos de conversión de voz a texto en la nube." }, "OpenInMenuSetting": { "03b00b1f64": "Aplicación personalizada", - "c1d817e027": "Agregado", + "c1d817e027": "Añadida", "e4064916aa": "Agregar aplicación", - "9d0413817d": "Elija las aplicaciones disponibles en el menú Abrir en de un espacio de trabajo.", + "9d0413817d": "Elige las aplicaciones disponibles en el menú Abrir en de un workspace.", "6ed52fe71e": "Abrir en aplicaciones", "eb55b87570": "El comando que escribirías en Terminal para abrir esta aplicación.", - "ba1422ee07": "comando terminal", + "ba1422ee07": "Comando de terminal", "e1fc0085c6": "Etiqueta de menú", "a261931d29": "Quitar aplicación", "af7d1c3656": "Editar aplicación", @@ -5634,37 +5634,37 @@ "f79084947b": "Nueva aplicación" }, "OrchestrationExamplesDialog": { - "3d1aa105e3": "Copiar mensaje", + "3d1aa105e3": "Copiar prompt", "9b4c004998": "Hecho", - "969cec9739": "Copiar mensaje de ejemplo {{value0}}", - "4e46da1889": "No se pudo copiar el mensaje.", - "80c6f2feb8": "Mensaje de ejemplo copiado." + "969cec9739": "Copiar prompt de ejemplo {{value0}}", + "4e46da1889": "No se pudo copiar el prompt.", + "80c6f2feb8": "Prompt de ejemplo copiado." }, "OrchestrationPane": { - "52e0634e2c": "Solicite a un agente coordinador que utilice la orquestación para traspasos, traspasos de árboles de trabajo y agents secundarios secuenciales o paralelos.", - "ae79504732": "como usarlo", + "52e0634e2c": "Pídele a un agente coordinador que use la orquestación para handoffs, handoffs de worktree y agentes secundarios secuenciales o paralelos.", + "ae79504732": "Cómo usarlo", "7bc082f4de": "Copiar comando de instalación", "832f1f3ee6": "¿Prefieres tu propia terminal?", - "9bedd2a6e5": "Permite a los agents traspasar contexto y coordinar el trabajo a través de Orca.", - "07641b9768": "Habilidad de orquestación", - "2aacdb0517": "Coordine a los agents de codificación en traspasos, traspasos de árboles de trabajo y trabajo de agents secundarios.", - "191ac34567": "Orquestación de Agents" + "9bedd2a6e5": "Permite a los agentes transferir contexto y coordinar el trabajo a través de Orca.", + "07641b9768": "Skill de orquestación", + "2aacdb0517": "Coordina agentes de programación en handoffs, handoffs de worktree y trabajo de agentes secundarios.", + "191ac34567": "Orquestación de agentes" }, "OrchestrationSetupCard": { - "e7d2a5146c": "Permite a los agents traspasar contexto y coordinar el trabajo a través de Orca.", - "2777ff0fdc": "Habilidad de orquestación" + "e7d2a5146c": "Permite a los agentes transferir contexto y coordinar el trabajo a través de Orca.", + "2777ff0fdc": "Skill de orquestación" }, "OrchestrationSkillAgentCoverage": { - "6dec5ce2d2": "Cobertura del Agent", - "ffe13e36fb": "Desaparecido", + "6dec5ce2d2": "Cobertura de agentes", + "ffe13e36fb": "Falta", "1e8f8d8fae": "Listo" }, "OrchestrationSkillPromptDialog": { - "f08d45293d": "comando copiar", + "f08d45293d": "Copiar comando", "35550f3b3b": "Hecho", - "1bdce1911e": "Copiar comando de instalación de habilidad de orquestación", - "b99f375eb2": "Ejecute este comando en una terminal para instalar la habilidad de orquestación para sus agents.", - "2914abcfa2": "Instalar habilidad de orquestación", + "1bdce1911e": "Copiar comando de instalación del skill de orquestación", + "b99f375eb2": "Ejecuta este comando en una terminal para instalar el skill de orquestación para tus agentes.", + "2914abcfa2": "Instalar skill de orquestación", "d3dc559225": "No se pudo copiar el comando de instalación.", "239bf9132b": "Comando de instalación copiado." }, @@ -5672,33 +5672,33 @@ "dc8404a930": "Crear archivo de diagnóstico", "a5acaffdb6": "Desechar", "aca2c8a367": "Enviar a soporte", - "798b6f0be5": "Abrir archivo para revisar", + "798b6f0be5": "Abrir archivo de revisión", "2ae9a6b63e": "Hecho", "7f14a1733c": "Eliminar archivo enviado", "2801d4ce22": "Copiar ID de referencia", - "d8be621237": "Abre primero el archivo para revisar.", + "d8be621237": "Abre primero el archivo de revisión.", "61676df223": "Diagnósticos enviados. Comparte este ID de referencia con soporte: {{value0}}.", - "fd7b3891af": "Abriste el archivo para revisar ({{value0}}). Envía ese archivo a soporte o descártalo.", - "62340d4439": "Tu archivo para revisar está listo ({{value0}}). Ábrelo para ver qué se enviaría y luego elige si quieres enviarlo a soporte.", - "19ec5e29b3": "Recopila actividad reciente y errores de la app en un archivo redactado que puedes revisar antes de enviarlo. Nada se sube hasta que elijas enviarlo." + "fd7b3891af": "Abriste el archivo de revisión ({{value0}}). Envía ese archivo a soporte o descártalo.", + "62340d4439": "Tu archivo de revisión está listo ({{value0}}). Ábrelo para ver qué se enviaría y luego elige si quieres enviarlo a soporte.", + "19ec5e29b3": "Recopila actividad reciente y errores de la app en un archivo depurado que puedes revisar antes de enviarlo. Nada se sube hasta que elijas enviarlo." }, "PrivacyDiagnosticsSection": { "af2fc82cde": "Enviar diagnósticos de la app a soporte", - "c18cbe45df": "Diagnósticos enviados eliminados", + "c18cbe45df": "Se eliminaron los diagnósticos enviados", "7a4944595b": "No se pudo copiar el ID de referencia", "13eb2c65a1": "ID de referencia copiado", - "860bca9ec9": "Archivo para revisar descartado", + "860bca9ec9": "Archivo de revisión descartado", "49fc6c80e8": "Diagnósticos enviados", - "db3228e01a": "Archivo para revisar abierto", - "a2b3505c77": "Archivo para revisar creado" + "db3228e01a": "Archivo de revisión abierto", + "a2b3505c77": "Archivo de revisión creado" }, "PrivacyPane": { - "36e0e2e63b": "variable de entorno. Desconfigúrelo y reinicie para volver a habilitarlo.", + "36e0e2e63b": "variable de entorno. Quítala y reinicia para volver a habilitarla.", "79a0f3c16c": "La telemetría está desactivada por el", - "e3970bbbf5": "La telemetría está deshabilitada porque se establece una variable de entorno de CI. Desactívalo y reinicia.", + "e3970bbbf5": "La telemetría está desactivada porque hay una variable de entorno de CI configurada. Quítala y reinicia.", "fe904ac984": "Compartir datos de uso anónimos", "77410e0566": "Política de privacidad", - "8bfdd23a88": "Ayúdanos a descubrir qué construir a continuación. Orca envía recuentos anónimos de qué funciones utiliza y dónde fallan las cosas.", + "8bfdd23a88": "Ayúdanos a descubrir qué crear a continuación. Orca envía recuentos anónimos de qué funciones usas y dónde fallan las cosas.", "afec8b03be": "ci" }, "QuickCommandsPane": { @@ -5707,146 +5707,146 @@ "8c877dec41": "Global", "c6b155911b": "Todos los comandos", "5aacc8f7dc": "Agregar comando", - "c36912efd5": "Ejecútelos desde el botón Comandos rápidos en la barra de pestañas, o haga clic derecho dentro de cualquier terminal.", + "c36912efd5": "Ejecútalos desde el botón Comandos rápidos en la barra de pestañas o haz clic derecho dentro de cualquier terminal.", "f91b649324": "Comandos guardados", - "3d9dc558e8": "Este comando rápido se eliminará de su lista guardada.", + "3d9dc558e8": "Este comando rápido se eliminará de tu lista guardada.", "3edf3deaf8": "¿Eliminar \"{{value0}}\"?", "9fcfc29519": "Insertar", - "9b3e338d62": "Ingresar", + "9b3e338d62": "Enter", "4ccc63da87": "Agente", "0252ddd578": "Sin texto de comando", - "7784912ed6": "repo", - "2bb9e38e93": "Intitulado", + "7784912ed6": "repositorio", + "2bb9e38e93": "Sin título", "3eb9897ab0": "No hay comandos en los ámbitos seleccionados.", - "38d61927e6": "No se guardaron comandos rápidos.", + "38d61927e6": "No hay comandos rápidos guardados.", "44923dd982": "destructivo", - "ec1ed99e70": "Borrar", + "ec1ed99e70": "Eliminar", "d1d0976320": "Ninguno" }, "RecentTabOrderControl": { - "3b17c81ede": "Orden de tiras de pestañas", + "3b17c81ede": "Orden de la barra de pestañas", "6e6a3fcc61": "Más reciente", - "7a546f2309": "Orden de tabulación", - "a867a0889f": "Tira reciente o de pestañas." + "7a546f2309": "Orden de pestañas", + "a867a0889f": "Reciente o por barra de pestañas." }, "RepositoryHooksSection": { "af49e2a19e": "El archivo está presente, pero Orca no pudo encontrar definiciones válidas de `scripts` o `issueCommand`.", - "3397879bee": "y existen comandos locales, elija cuál ejecutar.", + "3397879bee": "y hay comandos locales, elige cuáles se ejecutan.", "39da2ae12f": "orca.yaml", - "ac9038d2cc": "cuando ambos", - "32fec28f5b": "Fuente de comando", - "bbbd6e0bc4": "Fuente de comando y orca.yaml", + "ac9038d2cc": "Cuando ambos", + "32fec28f5b": "Origen de comandos", + "bbbd6e0bc4": "Origen de comandos y orca.yaml", "c9bc1bfd8f": "Avanzado", - "610d90fdbd": "Fuente del comando y detalles de orca.yaml.", - "52aef29e69": "Déjelo en blanco para usar el repo predeterminado desde", - "4084720f47": "Completo {{artifact_url}}", - "13394103bd": "Comando de problema personalizado de GitHub", - "70ad20f883": "para el problema vinculado o la URL de PR.", - "b997331366": "Anulación opcional. Usar", - "2cc27dc12b": "Anulación opcional por usuario para el comando de problema vinculado.", - "b91a0f297d": "Scripts locales y compartidos que se ejecutan antes de archivar un árbol de trabajo.", - "9a100323ff": "Guión de archivo", - "21fb607a87": "Comportamiento predeterminado cuando se crea un nuevo árbol de trabajo.", - "793dcee97d": "cuando correr", - "63e1783173": "Elija el comportamiento predeterminado cuando haya un script de configuración disponible.", + "610d90fdbd": "Origen de comandos y detalles de orca.yaml.", + "52aef29e69": "Déjalo en blanco para usar el valor predeterminado del repositorio desde", + "4084720f47": "Completar {{artifact_url}}", + "13394103bd": "Comando personalizado de Issue de GitHub", + "70ad20f883": "para la URL del Issue o PR vinculado.", + "b997331366": "Anulación opcional. Usa", + "2cc27dc12b": "Anulación opcional por usuario para el comando de Issue vinculado.", + "b91a0f297d": "Scripts locales y compartidos que se ejecutan antes de archivar un worktree.", + "9a100323ff": "Script de archivado", + "21fb607a87": "Comportamiento predeterminado cuando se crea un nuevo worktree.", + "793dcee97d": "Cuándo ejecutar", + "63e1783173": "Elige el comportamiento predeterminado cuando haya un script de configuración disponible.", "fb6bebcf7e": "Cuándo ejecutar la configuración", - "30d555acd2": "Scripts locales y compartidos que se ejecutan después de crear un nuevo árbol de trabajo.", - "52b31baf02": "Guión de configuración", - "8567127a40": "Scripts que se ejecutan cuando se crean o archivan árboles de trabajo. Los scripts locales se almacenan en esta máquina; Los scripts `orca.yaml` se comparten con tu equipo.", - "ff082fe7c6": "Ganchos para árbol de trabajo", - "5d940bde5c": "Agregar secuencia de comandos local", + "30d555acd2": "Scripts locales y compartidos que se ejecutan después de crear un nuevo worktree.", + "52b31baf02": "Script de configuración", + "8567127a40": "Scripts que se ejecutan cuando se crean o archivan worktrees. Los scripts locales se almacenan en esta máquina; los scripts de `orca.yaml` se comparten con tu equipo.", + "ff082fe7c6": "Hooks de worktree", + "5d940bde5c": "Agregar script local", "8c2893fae0": "Se ejecuta como un script de shell único. Guardado en esta máquina.", - "40a446ae16": "- sólo para ti, en esta máquina", + "40a446ae16": "- solo para ti, en esta máquina", "2d03a514db": "local", "7e4427b4a2": "para cambiar.", "b113344b6a": "Editar", "f828e1de19": "- compartido con tu equipo", - "673a7fd10e": "De cheques...", + "673a7fd10e": "Comprobando...", "5426ecbdcb": "Los scripts locales no se ejecutarán", - "b2b06c7ce8": "Variables de entorno disponibles (pase el cursor para obtener más detalles):", + "b2b06c7ce8": "Variables de entorno disponibles (pasa el cursor para ver más detalles):", "95a0411b3e": "plantilla", "175daba180": "Ejemplo", - "b20c5df6ca": "Agregue un archivo `orca.yaml` para habilitar los valores predeterminados de configuración, archivo o automatización de problemas compartidos para este repo. Plantilla de ejemplo:", - "56f9a4a1d0": "Using `orca.yaml`", - "623e0c9f31": "`orca.yaml` could not be parsed", - "5a67e4793d": "No `orca.yaml` detected", - "07ba35bc68": "Check the indentation under `scripts:`. Hook keys should use two spaces, and command lines should use four.", - "787ca433ef": "Define only the supported keys: `scripts`, `setup`, `archive`, and `issueCommand`.", - "ecc73d9125": "Compare your file against the working template below and copy that shape if needed.", + "b20c5df6ca": "Agrega un archivo `orca.yaml` para habilitar valores predeterminados compartidos de configuración, archivado o automatización de Issues para este repositorio. Plantilla de ejemplo:", + "56f9a4a1d0": "Usando `orca.yaml`", + "623e0c9f31": "No se pudo analizar `orca.yaml`", + "5a67e4793d": "No se detectó `orca.yaml`", + "07ba35bc68": "Revisa la indentación bajo `scripts:`. Las claves de hooks deben usar dos espacios y las líneas de comandos deben usar cuatro.", + "787ca433ef": "Define solo las claves admitidas: `scripts`, `setup`, `archive` e `issueCommand`.", + "ecc73d9125": "Compara tu archivo con la plantilla funcional de abajo y copia esa estructura si hace falta.", "925f9e0dc4": "texto-primer plano", - "0cc712b823": "El archivo de configuración principal existe en la raíz del repo, pero Orca aún no pudo analizar las definiciones de gancho admitidas.", + "0cc712b823": "El archivo de configuración principal existe en la raíz del repositorio, pero Orca aún no pudo analizar las definiciones de hooks admitidas.", "c90b858573": "texto-ámbar-700 oscuro:texto-ámbar-300", "aba825233f": "El archivo contiene claves de configuración que esta versión de Orca no reconoce. Es posible que necesites actualizar Orca o revisar el archivo en busca de errores tipográficos.", - "ca424ff135": "Los valores predeterminados de automatización de problemas y gancho compartido se definen en el repo y están disponibles para todos los que lo usan.", + "ca424ff135": "Los valores predeterminados compartidos de hooks y automatización de Issues están definidos en el repositorio y disponibles para todos los que lo usan.", "32f417fe17": "texto-esmeralda-700 oscuro:texto-esmeralda-300", "8bfe65fc60": "Usar comandos locales", - "8d6c56bff8": "Ejecute ambos", - "0fa21e19ec": "Nombre del espacio de trabajo, normalmente basado en el nombre de la sucursal.", - "54c73d88d0": "Ruta al árbol de trabajo que se está creando. Los comandos de configuración se ejecutan desde este directorio.", - "30952c4aa4": "Ruta al pago del repo principal. Útil para copiar archivos compartidos, como .env, en un árbol de trabajo.", + "8d6c56bff8": "Ejecutar ambos", + "0fa21e19ec": "Nombre del workspace, normalmente basado en el nombre de la rama.", + "54c73d88d0": "Ruta al worktree que se está creando. Los comandos de configuración se ejecutan desde este directorio.", + "30952c4aa4": "Ruta al checkout principal del repositorio. Útil para copiar archivos compartidos, como .env, a un worktree.", "9b821fa19d": "# p.ej. echo \"Limpiando $ORCA_WORKSPACE_NAME\"", - "6f90ebe3fd": "Se ejecuta antes de archivar o eliminar un árbol de trabajo.", - "a3fc966677": "# p.ej. pnpm instalar cp \"$ORCA_ROOT_PATH/.env\" \"$ORCA_WORKTREE_PATH/.env\"", - "f0710e1c83": "Se ejecuta después de que se crea un nuevo árbol de trabajo; instalar departamentos, copiar archivos env, ejecutar migraciones.", + "6f90ebe3fd": "Se ejecuta antes de archivar o eliminar un worktree.", + "a3fc966677": "# p. ej. pnpm install cp \"$ORCA_ROOT_PATH/.env\" \"$ORCA_WORKTREE_PATH/.env\"", + "f0710e1c83": "Se ejecuta después de que se crea un nuevo worktree; instalar dependencias, copiar archivos de entorno, ejecutar migraciones.", "8561b0665f": "orca.yaml primero, luego tus comandos locales.", - "0e8b2a520d": "Ignorar orca.yaml; ejecute solo sus comandos locales.", - "83dc78202a": "Sólo locales", - "29397e8bbc": "Ejecute solo comandos de repo comprometidos; ignorar los comandos locales.", + "0e8b2a520d": "Ignorar orca.yaml; ejecutar solo tus comandos locales.", + "83dc78202a": "Solo local", + "29397e8bbc": "Ejecutar solo comandos confirmados en el repositorio; ignorar comandos locales.", "d88b6ff88f": "orca.yaml solamente", - "99e3264a49": "Ejecute la configuración solo cuando lo elija.", + "99e3264a49": "Ejecutar la configuración solo cuando lo elijas.", "15debc1fd9": "Saltar por defecto", - "022ba10cf2": "Ejecute la configuración automáticamente.", + "022ba10cf2": "Ejecutar la configuración automáticamente.", "d3ef1ab247": "Ejecutar por defecto", "90b1f50137": "Preguntar antes de ejecutar la instalación.", - "e03d9a8f38": "pregunta cada vez", + "e03d9a8f38": "Preguntar cada vez", "8dbe6bedf5": "inválido", "0e0dd5b9a5": "cargado", "9b12f15b1e": "cuando uno existe.", "c85c2c88a2": "{{artifact_url}}", - "fac13f8c1e": "autorizado", + "fac13f8c1e": "autoritativo", "0518758f38": "ambos", "d2b3016c20": "compartido", "4611b78617": "fuente de comando", "c5a55a2d2e": "avanzado", "b5e3e77e89": "acción", - "0ce113fd7b": "Los scripts locales se guardan, pero el origen del script está configurado únicamente en orca.yaml.", - "7f78e5eea6": "Se guardan los scripts locales. Orca todavía está revisando orca.yaml antes de poder recomendar qué fuente de script usar.", + "0ce113fd7b": "Los scripts locales están guardados, pero el origen de scripts está configurado solo en orca.yaml.", + "7f78e5eea6": "Los scripts locales están guardados. Orca sigue revisando orca.yaml antes de poder recomendar qué origen de scripts usar.", "2b6356e744": "Guardado", - "81057d5f71": "Ahorro...", + "81057d5f71": "Guardando...", "da37d6f10e": "Copiar", - "3149964b66": "copiado", - "waitForSetupBeforeAgent": "Espere a que se complete la configuración antes de iniciar el agent", - "waitForSetupBeforeAgentHelp": "Active esto cuando el programa de instalación instale dependencias, servidores MCP o archivos de configuración que el agent necesite durante el inicio." + "3149964b66": "Copiado", + "waitForSetupBeforeAgent": "Espera a que se complete la configuración antes de iniciar el agente", + "waitForSetupBeforeAgentHelp": "Activa esto cuando la configuración instale dependencias, servidores MCP o archivos de configuración que el agente necesite durante el inicio." }, "RepositoryIconPicker": { - "2b7d27b93c": "Utilice el color del repo {{value0}}", + "2b7d27b93c": "Usar {{value0}} como color del repositorio", "fde066a63b": "Las cargas PNG deben tener un tamaño de 256 KB o menos.", - "cc1286e263": "favicon", + "cc1286e263": "Favicon", "03ca1a4e9b": "ejemplo.com", "381b4844fd": "Subir PNG", - "7da623abcc": "Se utiliza de forma predeterminada: GitHub siempre proporciona una, incluso cuando el propietario no ha configurado una imagen personalizada.", + "7da623abcc": "Se usa de forma predeterminada: GitHub siempre proporciona uno, incluso cuando el propietario no configuró una imagen personalizada.", "39da8a10bf": "Usar avatar de GitHub", - "c490787d24": "emojis", + "c490787d24": "Emoji", "b2d7fd2116": "Icono", "2d8bd302fa": "Avatar", - "913c55833d": "Color de repo personalizado {{value0}}", - "0e5f0693c1": "Elija un color de repo personalizado", + "913c55833d": "Color de repositorio personalizado {{value0}}", + "0e5f0693c1": "Elige un color de repositorio personalizado", "642dc29c6d": "Color", - "549d126081": "Reiniciar", - "4e2a14f967": "Icono de Repo", - "d71df44587": "No se pudo resolver el repo de GitHub.", - "f79972271a": "No se encontró ningún control remoto de GitHub para este repo.", - "4d039317f4": "favicon del sitio web", - "acf31559a0": "Ingrese una URL de sitio web válida.", - "868c5c9b56": "No se pudo importar el icono del repo" + "549d126081": "Restablecer", + "4e2a14f967": "Icono del repositorio", + "d71df44587": "No se pudo resolver el repositorio de GitHub.", + "f79972271a": "No se encontró ningún control remoto de GitHub para este repositorio.", + "4d039317f4": "Favicon del sitio web", + "acf31559a0": "Ingresa una URL de sitio web válida.", + "868c5c9b56": "No se pudo importar el icono del repositorio" }, "RepositoryPane": { "15a99d9b9f": "Las rutas relativas se resuelven desde la raíz de este proyecto.", "8ccacbeb5a": "Usar global", - "e9bd57a336": "Ubicación del árbol de trabajo", - "e63bb96a9b": "Directorio específico del proyecto para nuevos árboles de trabajo.", - "f88db4fece": "Base de árbol de trabajo predeterminada", - "8984d06520": "Rama base o referencia predeterminada al crear árboles de trabajo.", + "e9bd57a336": "Ubicación del worktree", + "e63bb96a9b": "Directorio específico del proyecto para nuevos worktrees.", + "f88db4fece": "Base de worktree predeterminada", + "8984d06520": "Rama o ref base predeterminada al crear worktrees.", "e641c359de": "Ícono y color del proyecto utilizados en la barra lateral y las pestañas.", "26fef02bf3": "Icono de proyecto", "c7ef4415de": "Nombre para mostrar", @@ -5856,56 +5856,56 @@ "ee5a290616": "Abierto como carpeta. Las funciones de Git no están disponibles para este espacio de trabajo.", "323debba71": "Tipo:", "499a437335": "Identidad", - "availableHosts": "Available Hosts", - "availableHostsDescription": "Hosts where this project is set up.", - "availableHostsHelp": "Project paths and worktree settings are host-specific; creating a workspace can target any ready setup.", - "viewingHost": "Viewing host", - "currentSetup": "Current", - "hostSetupStateReady": "Ready", - "hostSetupStateNotSetUp": "Not set up", - "hostSetupStateSettingUp": "Setting up", + "availableHosts": "Hosts disponibles", + "availableHostsDescription": "Hosts donde está configurado este proyecto.", + "availableHostsHelp": "Las rutas del proyecto y los ajustes de worktree son específicos de cada host; al crear un workspace puedes usar cualquier configuración lista.", + "viewingHost": "Viendo host", + "currentSetup": "Actual", + "hostSetupStateReady": "Listo", + "hostSetupStateNotSetUp": "Sin configurar", + "hostSetupStateSettingUp": "Configurando", "hostSetupStateError": "Error", - "hostSetupStateUnsupported": "Unsupported", - "setupPathPending": "Path pending", + "hostSetupStateUnsupported": "No compatible", + "setupPathPending": "Ruta pendiente", "openSetup": "Abrir", - "removeSetup": "Remove", - "hostSetupBlockedVersion": "Orca server version is incompatible", - "hostSetupMissingCapability": "Update Orca on this host to set up projects", - "setupProjectOnHost": "Set up on another host", - "setupProjectOnHostHelp": "Choose a host, then import an existing checkout, clone the repository there, or track a setup that will be provisioned later.", + "removeSetup": "Eliminar", + "hostSetupBlockedVersion": "La versión del servidor Orca no es compatible", + "hostSetupMissingCapability": "Actualiza Orca en este host para configurar proyectos", + "setupProjectOnHost": "Configurar en otro host", + "setupProjectOnHostHelp": "Elige un host y luego importa un checkout existente, clona el repositorio allí o registra una configuración que se aprovisionará más tarde.", "setupExistingFolder": "Importar carpeta existente", - "setupExistingFolderHelp": "Make this project available on another host by linking a checkout that already exists there.", - "setupExistingFolderPathPlaceholder": "/path/to/project/on/host", - "cloneUrlPlaceholder": "Repository URL", - "cloneDestinationPlaceholder": "/destination/on/host", + "setupExistingFolderHelp": "Haz que este proyecto esté disponible en otro host vinculando un checkout que ya existe allí.", + "setupExistingFolderPathPlaceholder": "/ruta/al/proyecto/en/host", + "cloneUrlPlaceholder": "URL del repositorio", + "cloneDestinationPlaceholder": "/destino/en/host", "setupKindGit": "Repositorio Git", "setupKindFolder": "Carpeta", "settingUpHost": "Importando...", "setupHost": "Importar", "cloningHost": "Clonando...", "cloneHost": "Clonar", - "creatingPendingSetup": "Creating...", - "createPendingSetup": "Track setup", - "hostSetupCheckingCapability": "Checking host capabilities", - "hostAvailability": "Host availability", - "hostAvailabilityHelp": "Add this same project on another connected host.", - "addToAnotherHost": "Add to another host", - "addProjectHost": "Add project to host", - "addProjectHostHelp": "Choose where this project should also be available.", + "creatingPendingSetup": "Creando...", + "createPendingSetup": "Registrar configuración", + "hostSetupCheckingCapability": "Comprobando capacidades del host", + "hostAvailability": "Disponibilidad del host", + "hostAvailabilityHelp": "Agrega este mismo proyecto en otro host conectado.", + "addToAnotherHost": "Agregar a otro host", + "addProjectHost": "Agregar proyecto al host", + "addProjectHostHelp": "Elige dónde también debería estar disponible este proyecto.", "closeHostSetup": "Cerrar", "setupHostLabel": "Host", - "browseFolder": "Browse folder", - "browseFolderHelp": "Use an existing checkout or folder on this host.", - "otherWaysToAdd": "Other ways to add", - "cloneFromUrl": "Clone from URL", - "cloneFromUrlHelp": "Clone this repository onto the selected host.", - "addPlannedHost": "Add host placeholder", - "addPlannedHostHelp": "Remember this host and finish adding the project later.", - "existingFolder": "Existing folder", - "addPlannedHostToHost": "Add {{host}}", - "addPlannedHostConfirm": "This only records that the project should be available on this host. You can add the folder or clone later.", - "projectRuntime": "Project Runtime", - "projectRuntimeDescription": "Choose whether this project runs on Windows or WSL." + "browseFolder": "Explorar carpeta", + "browseFolderHelp": "Usa un checkout o una carpeta existente en este host.", + "otherWaysToAdd": "Otras formas de agregar", + "cloneFromUrl": "Clonar desde URL", + "cloneFromUrlHelp": "Clona este repositorio en el host seleccionado.", + "addPlannedHost": "Agregar placeholder de host", + "addPlannedHostHelp": "Recuerda este host y termina de agregar el proyecto más tarde.", + "existingFolder": "Carpeta existente", + "addPlannedHostToHost": "Agregar {{host}}", + "addPlannedHostConfirm": "Esto solo registra que el proyecto debería estar disponible en este host. Puedes agregar la carpeta o clonar más tarde.", + "projectRuntime": "Runtime del proyecto", + "projectRuntimeDescription": "Elige si este proyecto se ejecuta en Windows o WSL." }, "RepositorySourceControlAiActionRows": { "548a6e1281": "Plantilla de comando", @@ -5920,38 +5920,38 @@ "RepositorySourceControlAiCustomCommand": { "0704dd55cd": "Comando de repositorio", "e56668c291": "Usar global", - "fbb77e122a": "Reserva de Repo para acciones de texto que seleccionan el comando personalizado.", + "fbb77e122a": "Fallback del repositorio para acciones de texto que seleccionan Comando personalizado.", "ebffc5a28c": "Comando personalizado", - "f9941f0caf": "p.ej. ollama ejecuta llama3.1 {rápido}" + "f9941f0caf": "p. ej., ollama run llama3.1 {prompt}" }, "RepositorySourceControlAiEnablement": { - "84233d1bb3": "Apagado", - "bea897eec2": "En", + "84233d1bb3": "Desactivado", + "bea897eec2": "Activado", "62511a575d": "Usar global", "30ae6dcce8": "El valor predeterminado global es", - "cf5959c834": "Control de fuente AI habilitado", + "cf5959c834": "Source Control AI activado", "show": "Mostrar", "hide": "Ocultar", - "showActionsLabel": "Mostrar acciones de IA de control de código fuente", - "visibilityHelper": "Controla si los botones de IA de control de código fuente se muestran para este repositorio. La generación usada por funciones separadas sigue la configuración de esas funciones. El valor predeterminado global es {{value0}}." + "showActionsLabel": "Mostrar acciones de Source Control AI", + "visibilityHelper": "Controla si se muestran los botones de Source Control AI para este repositorio. La generación usada por funciones separadas sigue los ajustes de esas funciones. El valor predeterminado global es {{value0}}." }, "RepositorySourceControlAiHostedReviewDefaults": { - "053ccfbf52": "Apagado", - "777443bf89": "En", + "053ccfbf52": "Desactivado", + "777443bf89": "Activado", "ffc3b26b26": "Usar global", "a68849a859": "El valor predeterminado global es", - "aa6ee4b7d6": "Valores predeterminados de creación de reseñas alojadas", - "629ed8a9d3": "Abrir revisión alojada después de la creación", + "aa6ee4b7d6": "Valores predeterminados para crear revisiones alojadas", + "629ed8a9d3": "Abrir la revisión alojada después de crearla", "14f1eb99d0": "Generar detalles al abrir Crear PR", - "d32b87e754": "Utilice la plantilla de revisión cuando esté disponible", - "981eae7e14": "Borrador por defecto" + "d32b87e754": "Usar la plantilla de revisión cuando esté disponible", + "981eae7e14": "Borrador de forma predeterminada" }, "RepositorySourceControlAiSection": { - "67b3ff5467": "Desechar", - "8b8bc5913a": "Recetas de acción del repositorio. La configuración global se utiliza hasta que este repositorio la personalice.", - "71b003b62b": "IA de control de fuente", - "152268c295": "Ahorrar", - "57e6e9d4b1": "Ahorro...", + "67b3ff5467": "Descartar", + "8b8bc5913a": "Recetas de acción del repositorio. Se usa la configuración global hasta que este repositorio las personaliza.", + "71b003b62b": "Source Control AI", + "152268c295": "Guardar", + "57e6e9d4b1": "Guardando...", "ccb07dd027": "Guardado", "e57dde9d93": "Cambios no guardados" }, @@ -5959,12 +5959,12 @@ "8b82879581": "Cualquier persona con una concesión activa puede conectarse hasta que la revoques. Revocar el acceso compartido desconecta a los clientes activos inmediatamente.", "68ec21309f": "Revocar acceso", "6f6d5188ed": "Revocar {{value0}}", - "87b16cd11d": "Creado", + "87b16cd11d": "Creado ", "434e4a6af6": "Enlace actual", - "fd83b94095": "Aún no hay acceso al servidor compartido.", + "fd83b94095": "Aún no hay acceso compartido al servidor.", "27cf8507ad": "Actualizar acceso compartido", - "f031182867": "Acceso al servidor compartido", - "df142657a5": "Aún no usado", + "f031182867": "Acceso compartido al servidor", + "df142657a5": "Aún no se ha usado", "b18d1764ef": "Usado por última vez {{value0}}" }, "RuntimeEnvironmentsPane": { @@ -5993,20 +5993,20 @@ "64b6bea541": "Servidor activo", "99ac81fb43": "Cambiado a {{value0}}.", "b5b5114cb0": "Se eliminó {{value0}}.", - "6cb6eae14f": "No se pudo guardar el entorno de ejecución.", + "6cb6eae14f": "No se pudo guardar el runtime.", "7b5986c8df": "Se guardó {{value0}}. Usa Servidor activo para cambiar cuando esté listo.", "a5b58465b6": "Conectado a {{value0}}.", "5ef712f407": "Ya existe un servidor llamado \"{{value0}}\".", "0c55a47480": "Se requieren nombre y código de emparejamiento.", - "e6410d72c3": "No se pudieron cargar los entornos de ejecución.", + "e6410d72c3": "No se pudieron cargar los runtimes.", "6ef71985da": "Sin punto final", "ed3e3f069d": "Esto elimina el servidor guardado de Orca. No cambia el servidor activo.", "b2fda48c39": "Al quitar el servidor activo, este navegador se desconecta de ese host. Las sesiones existentes del host se conservan.", "9f7665a01b": "Al quitar el servidor activo, Orca primero vuelve al Escritorio local. Las sesiones existentes del host se conservan.", "3595fd1948": "Nuevo enlace", "54dee18f5c": "Ocultar formulario", - "8cf8790697": "Los servidores guardados enrutan este navegador a través de un entorno de ejecución de Orca emparejado.", - "f75ce1c7a5": "Local mantiene el comportamiento actual del escritorio. Los servidores guardados enrutan las llamadas de cliente compatibles a través del entorno de ejecución remoto.", + "8cf8790697": "Los servidores guardados enrutan este navegador a través de un runtime de Orca emparejado.", + "f75ce1c7a5": "Local mantiene el comportamiento actual del escritorio. Los servidores guardados enrutan las llamadas de cliente compatibles a través del runtime remoto.", "d25f0688b1": "Eliminar", "f3a3d6d834": "Capacidades de {{value0}}", "0ef838094a": "Protocolo {{value0}}", @@ -6016,47 +6016,47 @@ "c8791efc45": "Estado no disponible", "5120beaac6": "Comprobando…", "4b5c6d7e8f": "No se informaron capacidades", - "hostModelCapabilityUnknown": "Compatibilidad con el modelo del host: comprobando las capacidades del servidor", - "hostModelCapabilitySupported": "Compatibilidad con el modelo del host: lista", - "hostModelCapabilityMissing": "Compatibilidad con el modelo del host: actualiza el servidor para {{value0}}", + "hostModelCapabilityUnknown": "Soporte de modelo de host: comprobando capacidades del servidor", + "hostModelCapabilitySupported": "Soporte de modelo de host: listo", + "hostModelCapabilityMissing": "Soporte de modelo de host: actualiza el servidor para {{value0}}", "hostModelCapabilityProjectSetup": "configuración del proyecto", - "hostModelCapabilityTaskSourceContext": "contexto de origen de la tarea", - "hostModelCapabilityWorkspaceRunContext": "contexto de ejecución del espacio de trabajo", - "3f67e8078a": "Usa este ordenador de forma predeterminada. Elige un servidor guardado solo cuando quieras que los proyectos, archivos, terminales, comprobaciones de proveedores y traspaso entre navegador/móvil compatibles pasen por ese servidor.", - "2c85efb3e8": "Al seleccionar un servidor guardado, este navegador usará ese entorno de ejecución de Orca emparejado como host predeterminado.", + "hostModelCapabilityTaskSourceContext": "contexto de origen de tareas", + "hostModelCapabilityWorkspaceRunContext": "contexto de ejecución del workspace", + "3f67e8078a": "Usa esta computadora de forma predeterminada. Elige un servidor guardado solo cuando quieras que los proyectos, archivos, terminales, checks de proveedores y traspaso entre navegador/móvil compatibles pasen por ese servidor.", + "2c85efb3e8": "Al seleccionar un servidor guardado, este navegador usará ese runtime de Orca emparejado como host predeterminado.", "serverConnected": "Conectado", "serverChecking": "Comprobando…", "serverDisconnected": "Desconectado", "disconnectedServer": "Desconectado de {{value0}}.", "connectToRemoteServers": "Conectarse a servidores remotos", - "connectToRemoteServersHelp": "Empareja otro entorno de ejecución de Orca y luego conéctalo o desconéctalo aquí. Usa Avanzado > Servidor activo solo cuando quieras cambiar el host predeterminado.", - "activeServerRowHelp": "Servidor activo para proyectos, terminales y comprobaciones de proveedores enrutados por servidor.", + "connectToRemoteServersHelp": "Empareja otro runtime de Orca y luego conéctalo o desconéctalo aquí. Usa Avanzado > Servidor activo solo cuando quieras cambiar el host predeterminado.", + "activeServerRowHelp": "Servidor activo para proyectos, terminales y checks de proveedores enrutados por servidor.", "disconnect": "Desconectar", "connect": "Conectar", "advanced": "Avanzado", "serverDetails": "Detalles del servidor", "advertiseThisApp": "Anunciar esta app como servidor", - "advertiseThisAppHelp": "Crea enlaces de acceso para que navegadores, clientes móviles u otro cliente de Orca se conecten de vuelta a esta app en ejecución.", - "runtimeReachable": "{{value0}} está disponible." + "advertiseThisAppHelp": "Crea enlaces de acceso para que navegadores, clientes móviles u otro cliente de Orca se conecten a esta app en ejecución.", + "runtimeReachable": "{{value0}} está accesible." }, "RuntimePairingGeneratedUrlRows": { "0495f68959": "Copiar {{value0}}" }, "RuntimePairingUrlGenerator": { - "849825e829": "Pegue esta URL de emparejamiento en otro cliente Orca.", - "2e5c4e3c93": "Emparejar otro cliente Orca", + "849825e829": "Pega esta URL de emparejamiento en otro cliente de Orca.", + "2e5c4e3c93": "Emparejar otro cliente de Orca", "f7cafdc9f3": "El enlace del navegador no está disponible en esta compilación. La URL de emparejamiento todavía funciona para los clientes de Orca.", "6b9ca3e69b": "Abrir en el navegador", - "1ca2e5194d": "Utilice esta URL desde un navegador que pueda llegar a la dirección seleccionada.", + "1ca2e5194d": "Usa esta URL desde un navegador que pueda llegar a la dirección seleccionada.", "8de0f84fff": "Generar enlace de acceso", - "279e0dcb57": "127.0.0.1 solo funciona en esta computadora. Utilice una LAN, Tailscale o una dirección personalizada para otro dispositivo.", + "279e0dcb57": "127.0.0.1 solo funciona en esta computadora. Usa una LAN, Tailscale o una dirección personalizada para otro dispositivo.", "45cf476df3": "host, host:puerto o wss://host/ruta", "4531ea3158": "Dirección personalizada", "360c548cf3": "Actualizar direcciones de conexión", "de6d5cff95": "Esta computadora (", "de77eb1b65": "Dirección de conexión", - "ff80904fc4": "Cree una concesión de acceso revocable para clientes de navegador o de escritorio.", - "f8500e134a": "Comparte este servidor Orca", + "ff80904fc4": "Crea una concesión de acceso revocable para clientes de navegador o escritorio.", + "f8500e134a": "Compartir este servidor Orca", "d6c081adf4": "No se pudo copiar la URL.", "df0aa45a86": "URL de emparejamiento copiada.", "13704d635e": "URL del cliente web copiada.", @@ -6066,7 +6066,7 @@ "2ed55c841a": "No se pudo generar la URL de emparejamiento.", "11d5248e62": "URL de emparejamiento generada.", "6dd594a507": "URL del cliente web generada.", - "2752126f3e": "El emparejamiento en tiempo de ejecución no está disponible.", + "2752126f3e": "El emparejamiento de runtime no está disponible.", "95b8be4cea": "No se pudieron actualizar las interfaces de red.", "1b4e0bbcc5": "No se pudieron cargar las concesiones de acceso compartido.", "b91e36a986": "web", @@ -6080,20 +6080,20 @@ }, "Settings": { "3bf149e873": "Configuración del proyecto > {{value0}}", - "075341c763": "Nuevas características que aún están tomando forma. Pruébalos.", + "075341c763": "Nuevas funciones que aún están tomando forma. Pruébalas.", "8b017f2506": "Experimental", - "499c1cd7f9": "Configuraciones de compatibilidad de bajo nivel para solucionar problemas.", + "499c1cd7f9": "Ajustes de compatibilidad de bajo nivel para solucionar problemas.", "1c87f8d024": "Avanzado", "c1b43dc4e2": "Datos de uso anónimos y controles de telemetría.", "d7e3f62d70": "Privacidad y telemetría", "9b83cc62c2": "Acceso a la privacidad de macOS para herramientas de desarrollo lanzadas en terminales.", "65660d4548": "Permisos de macOS", - "c6c01ac209": "Controla terminales y agents desde tu teléfono.", + "c6c01ac209": "Controla terminales y agentes desde tu teléfono.", "c40dadaac8": "Móvil", "c2ee313198": "Usa máquinas existentes por SSH para archivos, terminales, Git y espacios de trabajo.", - "9b02492d1f": "Anfitriones SSH", - "b5ee17826b": "Pair remote Orca runtimes for persistent sessions, richer remote state, and web or mobile handoff.", - "7686cb5c36": "Conecte este navegador a un servidor Orca guardado.", + "9b02492d1f": "Hosts SSH", + "b5ee17826b": "Empareja runtimes remotos de Orca para sesiones persistentes, estado remoto más completo y traspaso web o móvil.", + "7686cb5c36": "Conecta este navegador a un servidor Orca guardado.", "bd0181eeca": "Servidores remotos de Orca", "8acf3f22e0": "Estadísticas de Orca más análisis de uso de Claude, Codex y OpenCode.", "954a8f5aef": "Estadísticas y uso", @@ -6105,9 +6105,9 @@ "d7a3e635b6": "Entrada y edición", "6d1a27e193": "Tema, zoom, apariencia de la aplicación y del terminal, barras laterales y barra de estado.", "2b4474780a": "Apariencia", - "3d9adfe6a5": "Terminal global, navegador y pestañas de markdowns.", + "3d9adfe6a5": "Pestañas globales de terminal, navegador y Markdown.", "3eb22a3ada": "Espacio de trabajo flotante", - "01f9d36292": "Configure la compatibilidad con el emulador móvil para Orca y agents de codificación.", + "01f9d36292": "Configura la compatibilidad con el emulador móvil para Orca y agentes de codificación.", "f75daf1002": "Emulador móvil", "ad9788036f": "Página de inicio, enrutamiento de enlaces y cookies de sesión.", "c46215ea03": "Navegador", @@ -6115,81 +6115,81 @@ "13d4fe30ad": "Comandos rápidos", "b79b5b31e9": "Shells, renderizador, sesiones y comportamiento del terminal.", "3de4bbb841": "Terminal", - "dd72ed437a": "Elija qué proveedores de tareas aparecen en la página Tareas y en la barra lateral.", + "dd72ed437a": "Elige qué proveedores de tareas aparecen en la página Tareas y en la barra lateral.", "11faa2f7dd": "Fuentes de tareas", - "cfa34f4465": "Nomenclatura de sucursales, referencias base, atribución y autor de Git AI.", + "cfa34f4465": "Nombres de rama, refs base, atribución y Git AI Author.", "70100f94c7": "Git y control de código fuente", - "b07041697f": "Conecte GitHub, GitLab, Linear y servicios de alojamiento de origen.", + "b07041697f": "Conecta GitHub, GitLab, Linear y servicios de alojamiento de código fuente.", "c9ca101a3b": "Integraciones", - "f9b77539fd": "Valores predeterminados del espacio de trabajo, configuración y mantenimiento de aplicaciones.", + "f9b77539fd": "Valores predeterminados del workspace, configuración de la app y mantenimiento.", "7807c11c4d": "General", - "6855b0f77d": "Termine los flujos de trabajo principales que hacen que Orca sea útil para el trabajo de agentes paralelos.", + "6855b0f77d": "Termina los flujos de trabajo principales que hacen que Orca sea útil para el trabajo de agentes en paralelo.", "6d119427ef": "Lista de verificación de incorporación", "eb1176a14e": "Dictado local de voz a texto con modelos en el dispositivo.", "5063bb47a5": "Voz", - "7118953f14": "Permita que los agents controlen cualquier aplicación en su computadora.", - "c9841721cb": "Uso de la computadora", - "475980f53d": "Coordine múltiples agents de codificación a través de Orca.", + "7118953f14": "Permite que los agentes controlen cualquier app en tu computadora.", + "c9841721cb": "Computer Use", + "475980f53d": "Coordina varios agentes de codificación a través de Orca.", "00c3a7950d": "Orquestación", - "21f09426ea": "Opcional. Orca trabaja con los inicios de sesión de su proveedor existente; agregue cuentas solo si desea que Orca le ayude a cambiar entre ellas.", + "21f09426ea": "Opcional. Orca funciona con tus inicios de sesión de proveedores existentes; agrega cuentas solo si quieres que Orca te ayude a cambiar entre ellas.", "ad6c529693": "Cuentas de proveedores de IA", - "ec1ba547f7": "Administre agents de IA, establezca un valor predeterminado y personalice comandos.", + "ec1ba547f7": "Administra agentes de IA, define uno predeterminado y personaliza comandos.", "8afa676615": "Agentes", "add3b97ee6": "\"", "3c88ec55d6": "No se encontraron configuraciones para \"", - "c7ad095d96": "Cargando configuración...", - "acc7bbdefd": "Presione ESC nuevamente para salir de la configuración", - "43b68e10f0": "No has guardado los cambios de autor de Git AI. Irse los descartará.", - "17bdee4ff1": "¿Descartar los cambios de autor de Git AI no guardados?", + "c7ad095d96": "Cargando ajustes...", + "acc7bbdefd": "Presiona ESC de nuevo para salir de ajustes", + "43b68e10f0": "Tienes cambios no guardados de Git AI Author. Al salir se descartarán.", + "17bdee4ff1": "¿Descartar cambios no guardados de Git AI Author?", "084d8fac5b": "Privacidad y seguridad", "23931df7e8": "Hosts remotos", - "mobile_group": "Mobile", + "mobile_group": "Móvil", "8bd117d669": "Interfaz", "e1578cd4bc": "Flujos de trabajo", - "9abb9be3bc": "Configuración", + "9abb9be3bc": "Configuración inicial", "23c6874fdf": "Capacidades de IA", "2309068a6f": "destructivo", "65358016ea": "Desechar", - "dev": "Dev Tools", - "devDescription": "Dev-only tools for exercising UI states." + "dev": "Herramientas de desarrollo", + "devDescription": "Herramientas solo de desarrollo para probar estados de la UI." }, "SettingsFormControls": { "42a4d15a30": "No hay fuentes coincidentes.", "b55371ea18": "Fuentes", - "c766f8ac75": "Alternar sugerencias de fuentes", - "74bcecd5ec": "Claro", + "c766f8ac75": "Mostrar u ocultar sugerencias de fuentes", + "74bcecd5ec": "Borrar", "a4ff6143f8": "Borrar selección de fuente", "b661b034ec": "· Por defecto:", "ceefb9d7f1": "No se encontraron temas.", "9119fb2268": "Actual", - "4e11f87ca6": "Demostración", + "4e11f87ca6": "Mostrando", "fbb428db98": "Seleccionado:", "fac59213fc": "Buscar temas integrados", - "cb330ef7f8": "de {{value0}}", - "c822571b2e": "coincidente con \"{{value0}}\"", + "cb330ef7f8": " de {{value0}}", + "c822571b2e": " que coinciden con \"{{value0}}\"", "3119c012a5": "cadena", "builtin_themes": "Integrados", - "imported_from": "Imported from {{value0}}", + "imported_from": "Importado desde {{value0}}", "imported_themes": "Importados", - "search_terminal_themes": "Search terminal themes" + "search_terminal_themes": "Buscar temas de terminal" }, "SettingsSidebar": { "e0900f83e7": "SSH", "5c9669ff9c": "Proyectos", - "dbceaa8840": "Configuración de búsqueda", - "60f8a673a7": "Volver a la aplicación", - "6503182299": "Lista de verificación de incorporación", - "82db1b7de4": "Lista de verificación de incorporación, {{value0}} de {{value1}} realizada. Mostrar guía de configuración.", + "dbceaa8840": "Buscar ajustes", + "60f8a673a7": "Volver a la app", + "6503182299": "Checklist de onboarding", + "82db1b7de4": "Checklist de onboarding, {{value0}} de {{value1}} completados. Mostrar guía de configuración.", "df38d612b7": "Aún no se han agregado proyectos.", - "3e483e256b": "No hay configuraciones de proyecto coincidentes." + "3e483e256b": "No hay ajustes de proyecto coincidentes." }, "ShortcutFilterRail": { "28b63545bf": "Estado", - "8a1e78c14b": "Filtros de estado de acceso directo", - "df8466f3fc": "Borrar búsqueda de accesos directos", - "f733c4b89f": "Comando o teclas de búsqueda", - "02dc7d4251": "encontrar atajos", - "1d5634ba31": "{{value0}} atajo" + "8a1e78c14b": "Filtros de estado de atajos", + "df8466f3fc": "Borrar búsqueda de atajos", + "f733c4b89f": "Buscar comando o teclas", + "02dc7d4251": "Buscar atajos", + "1d5634ba31": "Atajo de {{value0}}" }, "ShortcutRowsList": { "4ce3cd24d9": "Ningún atajo coincide con esos filtros." @@ -6198,20 +6198,20 @@ "0762983d13": "Terminal primero", "63308571d8": "Orca primero", "c43c7ff5f9": "Decide quién intercepta primero los atajos", - "c3a554288e": "Atajos en la Terminal", - "0f55c6f15c": "Elija si Orca o el terminal enfocado gana cuando los atajos se superponen." + "c3a554288e": "Atajos en la terminal", + "0f55c6f15c": "Elige si Orca o la terminal enfocada gana cuando los atajos se superponen." }, "ShortcutsPane": { "4b7ae34062": "directamente.", "38e86e206a": "Personaliza los atajos visualmente o edítalos", "47f8f7aef9": "Atajos de teclado", - "f0b35b0b2e": "Deshabilitado mientras un terminal o TUI tiene el foco del teclado.", + "f0b35b0b2e": "Deshabilitado mientras una terminal o TUI tiene el foco del teclado.", "5c65d5db9d": "Terminal primero", "dfa8ff612f": "También se ejecuta mientras una terminal o TUI tiene el foco del teclado.", "2a0e8aeccf": "Orca primero", "3c0fac059a": "Todavía se ejecuta mientras una terminal tiene el foco del teclado.", - "25b0004fbf": "Terminal activo", - "781cb74d22": "Se ejecuta desde paneles de terminales.", + "25b0004fbf": "Terminal activa", + "781cb74d22": "Se ejecuta desde paneles de terminal.", "cb02e00202": "Terminal", "d8c988dab4": "~/.orca/keybindings.json" }, @@ -6222,54 +6222,54 @@ "2cb4bb7e5d": "Argumentos CLI", "0740d30915": "Comando personalizado", "ee0e5c2a48": "Usar agente predeterminado", - "bf84dea6af": "Utilice variables sólo cuando desee que Orca inyecte contexto. Deje al agente como predeterminado para seguir su preferencia normal de agente.", - "a79c567194": "Recetas de acción", - "cf01d41bce": "Agente, argumentos CLI y plantilla de comando utilizados por cada botón AI de control de código fuente.", + "bf84dea6af": "Usa variables solo cuando quieras que Orca inyecte contexto. Deja el agente predeterminado para seguir tu preferencia normal de agente.", + "a79c567194": "Recetas de acciones", + "cf01d41bce": "Agente, argumentos CLI y plantilla de comando usados por cada botón de IA de control de código fuente.", "a9359c8aa9": "Error desconocido", - "b5f46664d3": "No se pudo guardar la acción predeterminada de AI de control de código fuente: {{value0}}", - "d18d665e12": "Ahorrar", - "4f549a5fa8": "Ahorro...", + "b5f46664d3": "No se pudo guardar el valor predeterminado de la acción de IA de control de código fuente: {{value0}}", + "d18d665e12": "Guardar", + "4f549a5fa8": "Guardando...", "9d3cc627f8": "Guardado", "817128d94e": "Cambios no guardados", - "7ab1437a12": "solicitud de extracción", + "7ab1437a12": "pull request", "e5b24893ba": "commit", - "06a9dab64d": "cheques", + "06a9dab64d": "checks", "cb67b938c5": "arreglar", "2037c78a6f": "plantilla", "eb7e8f3b39": "modelo", - "d74fdc776c": "dominio", - "673369fe0c": "cli", + "d74fdc776c": "comando", + "673369fe0c": "CLI", "db9bd75d10": "argumentos", - "926d58e87f": "agent" + "926d58e87f": "agente" }, "SparsePresetSettingsSection": { - "6fa754d20f": "Borrar", + "6fa754d20f": "Eliminar", "fe1f2c6572": "Editar {{value0}}", - "88bfbf1a9c": "No hay pocos ajustes preestablecidos guardados para este repositorio.", - "d7565029a9": "Nuevo preestablecido", - "17f8c4ce10": "Administre conjuntos de directorios guardados para la creación de árboles de trabajo dispersos.", - "388513be2d": "Ajustes preestablecidos de pago dispersos", - "a05bc9183f": "Guardar preestablecido", + "88bfbf1a9c": "No hay presets sparse guardados para este repositorio.", + "d7565029a9": "Nuevo preset", + "17f8c4ce10": "Gestiona conjuntos de directorios guardados para crear worktrees sparse.", + "388513be2d": "Presets de sparse checkout", + "a05bc9183f": "Guardar preset", "2d7d45e991": "Cancelar", - "c240a16f25": "Utilice rutas relativas al repo como paquetes/web o aplicaciones/api.", - "fde7ff2cc3": "paquetes/web compartida/ui", + "c240a16f25": "Usa rutas relativas al repositorio como packages/web o apps/api.", + "fde7ff2cc3": "packages/web shared/ui", "caf33029cc": "Directorios", - "3b6f1abd3e": "p.ej. solo web", + "3b6f1abd3e": "p. ej., web-only", "a6fcdd9e3c": "Nombre", - "b9922ec194": "Cancelar edición preestablecida", - "694cc55ecb": "Los directorios guardados se utilizan al crear árboles de trabajo dispersos para este repositorio.", + "b9922ec194": "Cancelar edición del preset", + "694cc55ecb": "Los directorios guardados se usan al crear worktrees sparse para este repositorio.", "8b64731aaf": "+{{value0}} más", "755c6a1a0d": "Confirmar", "a7bcf206b1": "Eliminando", "ba9ad2d4cd": "Fecha de actualización desconocida", "568d7e1e49": "Actualizado {{value0}}", - "d7b3f0bdc3": "directorios {{value0}}", + "d7b3f0bdc3": "{{value0}} directorios", "9d3c087fc0": "1 directorio", - "8deb7024ab": "Cargando ajustes preestablecidos escasos...", - "92c08ccae3": "No se pudieron cargar los ajustes preestablecidos escasos.", + "8deb7024ab": "Cargando presets sparse...", + "92c08ccae3": "No se pudieron cargar los presets sparse.", "3dfa765ca7": "Se guardarán {{value0}} directorios.", "b532b9c17d": "Se guardará 1 directorio.", - "623b4cf910": "Editar preajuste", + "623b4cf910": "Editar preset", "68bbcd864a": "nuevo", "2ef2b2674b": "Eliminar {{value0}}" }, @@ -6280,38 +6280,38 @@ "c0f1c80166": "No hay destinos SSH configurados.", "639ceb3698": "Agregar destino", "51d7dba44d": "Importar", - "a7d28dff81": "Agregue un host remoto para conectarse a él en Orca.", - "94c5284560": "Objetivos", - "f495689b82": "Importación fallida", - "f8050f6307": "Servidor {{value0}} sincronizado {{value1}}", + "a7d28dff81": "Agrega un host remoto para conectarte a él en Orca.", + "94c5284560": "Destinos", + "f495689b82": "Error al importar", + "f8050f6307": "Sincronización completada: {{value0}} host{{value1}}", "68c13b4589": "Prueba fallida", "81d08bcddf": "Conexión exitosa", "2c4ee7332b": "No se pudo restablecer el relé remoto", - "db2e48975e": "Restablecimiento remoto del relé", + "db2e48975e": "Relé remoto restablecido", "025e107643": "No se pudieron finalizar los terminales remotos", - "90e308c98b": "Terminales remotas terminadas", - "a43de1d3ee": "Falló la desconexión", - "e95d5ae10e": "La conexión falló", - "c2a69510e3": "No se pudo eliminar el objetivo", - "a0237eb1ca": "Objetivo eliminado", - "2227ce47b6": "No se pudo guardar el objetivo", - "f602009125": "Objetivo agregado", - "b4ba0ce33d": "Objetivo actualizado", - "3879cbaa52": "El tiempo de espera del terminal debe estar entre 60 y {{value0}} segundos, o mantener los terminales activos hasta que se reinicien.", + "90e308c98b": "Terminales remotos finalizados", + "a43de1d3ee": "Error al desconectar", + "e95d5ae10e": "Error de conexión", + "c2a69510e3": "No se pudo eliminar el destino", + "a0237eb1ca": "Destino eliminado", + "2227ce47b6": "No se pudo guardar el destino", + "f602009125": "Destino agregado", + "b4ba0ce33d": "Destino actualizado", + "3879cbaa52": "El tiempo de espera del terminal debe estar entre 60 y {{value0}} segundos, o mantener los terminales activos hasta restablecer.", "4db9afce1c": "El puerto debe estar entre 1 y 65535", - "0e5aa04161": "Se requiere un alias de configuración de host o SSH", + "0e5aa04161": "Se requiere un host o alias de configuración SSH", "f1fc50dad2": "No se pudieron cargar destinos SSH", "0cda732f43": "La prueba de conexión falló" }, "SshPassphraseDialog": { "d5a234456f": "Cancelar", - "c3ce71aad6": "Introduzca la frase de contraseña", + "c3ce71aad6": "Introduce la frase de contraseña", "abaa0dc653": "Introduce la contraseña", - "ce4fdf7914": "Ingrese la contraseña para", - "dbf9b6f2d0": "Introduzca la contraseña para", + "ce4fdf7914": "Introduce la frase de contraseña para ", + "dbf9b6f2d0": "Introduce la contraseña para ", "c55f105262": "No se pudo cancelar la solicitud de credencial SSH", "b8e88fd0de": "No se pudo enviar la credencial SSH", - "405066423c": "Descubrir", + "405066423c": "Desbloquear", "bec2c1318f": "Conectar", "8a349e3fac": "Frase de contraseña para {{value0}}", "cab3d5f5a5": "Contraseña para {{value0}}", @@ -6320,16 +6320,16 @@ }, "SshTargetCard": { "ec6543cee9": "Conectar", - "0e53e9f8e8": "Prueba", + "0e53e9f8e8": "Probar", "1810b51482": "Conectando", "4c86f30877": "Desconectar", - "7f7b3d7ab4": "Eliminar objetivo", - "3d21a22d0e": "Eliminando objetivo", - "3d8af2949f": "Editar objetivo", + "7f7b3d7ab4": "Eliminar destino", + "3d21a22d0e": "Eliminando destino", + "3d8af2949f": "Editar destino", "762a48c662": "Restablecer el relé remoto", - "97dea4e8cf": "Restablecer el relé remoto", - "da16e108e6": "Terminales remotos finales", - "c77f1abfe3": "Terminar con terminales remotos", + "97dea4e8cf": "Restableciendo el relé remoto", + "da16e108e6": "Finalizar terminales remotos", + "c77f1abfe3": "Finalizando terminales remotos", "18968ede9e": "Error", "f0871e6bfb": "conectar", "47e94bd6ba": "conectado", @@ -6337,11 +6337,11 @@ "8ce71262f4": "terminales hasta el reinicio" }, "SshTargetDestructiveActions": { - "7e66942808": "Esto detendrá las sesiones de terminal activas en este destino SSH. Volver a conectarlos no los restaurará.", - "accf177a03": "¿Acabar con los terminales remotos?", - "26be00392d": "Esto fuerza la detención de la retransmisión remota para este destino SSH. Los terminales remotos activos y los reenvíos de puertos para este destino finalizarán.", - "570a7a0574": "¿Reiniciar el relé remoto?", - "3bb0cf0ee4": "Esto eliminará el objetivo y finalizará cualquier terminal remoto activo.", + "7e66942808": "Esto detendrá las sesiones de terminal activas en este destino SSH. Volver a conectarse no las restaurará.", + "accf177a03": "¿Finalizar terminales remotos?", + "26be00392d": "Esto fuerza la detención del relé remoto para este destino SSH. Los terminales remotos activos y los reenvíos de puertos de este destino finalizarán.", + "570a7a0574": "¿Restablecer relé remoto?", + "3bb0cf0ee4": "Esto eliminará el destino y finalizará todos los terminales remotos activos.", "4808966c41": "Eliminar destino SSH" }, "SshTargetForm": { @@ -6351,84 +6351,84 @@ "b574994adc": "Usa Finalizar terminales remotos o Restablecer relé cuando quieras detenerlos.", "71fc546097": "Mantener los terminales activos hasta restablecer", "92f80edbfd": "Persistencia de terminales remotos", - "feae1d1e69": "Opcional. Equivalente a ProxyJump/ssh -J.", - "11bcb4507a": "bastion.ejemplo.com", - "b2ab248ded": "Anfitrión de salto", - "3b01ca44a0": "Opcional. Se utiliza para hacer túneles (por ejemplo, Cloudflare Access, ProxyCommand).", - "f42d844544": "p.ej. acceso a la nube ssh --hostname %h", + "feae1d1e69": "Opcional. Equivale a ProxyJump / ssh -J.", + "11bcb4507a": "bastion.example.com", + "b2ab248ded": "Host de salto", + "3b01ca44a0": "Opcional. Se usa para túneles (p. ej., Cloudflare Access, ProxyCommand).", + "f42d844544": "p. ej., cloudflared access ssh --hostname %h", "c7d0e18ecb": "Comando proxy", - "cb91f6375c": "Opcional. El agente SSH se utiliza de forma predeterminada.", + "cb91f6375c": "Opcional. Se usa el agente SSH de forma predeterminada.", "d6a5f2ee5c": "~/.ssh/id_ed25519 (dejar vacío para el agente SSH)", "63c0c145c1": "Archivo de identidad", "c94cfa634c": "Puerto", - "47e082bc17": "desplegar", + "47e082bc17": "deploy", "dc1dc52aaa": "Nombre de usuario", - "2ee9bcd2e8": "servidor, implementar@servidor:2222, ssh://servidor", - "ce370ce674": "Anfitrión o alias *", - "b8dab0aa7b": "mi servidor", + "2ee9bcd2e8": "server, deploy@server:2222, ssh://server", + "ce370ce674": "Host o alias *", + "b8dab0aa7b": "Mi servidor", "298de87a88": "Etiqueta", "9518545cb6": "Agregar destino", "a62b4cb39a": "Guardar cambios", "29af933cd5": "Nuevo destino SSH", "f2331ce599": "Editar destino SSH", "7c13f58c91": "Hasta restablecer", - "55c56cf2c7": "Tiempo de espera tras desconectar (segundos)", + "55c56cf2c7": "Tiempo de espera después de desconectar (segundos)", "4a342f44c1": "Conexión avanzada", "e9609ddca6": "Proxy, host de salto y reutilización de conexión", - "8c922dffba": "Reutilizar la conexión SSH para configurar más rápido", + "8c922dffba": "Reutilizar la conexión SSH para una configuración más rápida", "53e9aabfc0": "Usa multiplexación de OpenSSH cuando está disponible. Desactívalo para hosts con restricciones SSH personalizadas." }, "TasksPane": { - "3a72b9745e": "Elija qué proveedores de tareas aparecen en la página Tareas y en los accesos directos de la barra lateral.", + "3a72b9745e": "Elige qué proveedores de tareas aparecen en la página Tareas y en los accesos directos de la barra lateral.", "f71d8a9dd3": "Proveedores de tareas", - "71644aba56": "Elija qué proveedores de tareas aparecen en el selector de fuente de la página Tareas y en los accesos directos de la barra lateral. Al menos un proveedor debe permanecer visible.", + "71644aba56": "Elige qué proveedores de tareas aparecen en el selector de fuentes de la página Tareas y en los accesos directos de la barra lateral. Al menos un proveedor debe permanecer visible.", "93e72ef659": "Fuentes de tareas", - "8e1305fcc6": "Muestre Jira en el selector de fuente de Tareas y en los accesos directos de la barra lateral.", + "8e1305fcc6": "Muestra Jira en el selector de fuentes de Tareas y en los accesos directos de la barra lateral.", "6b23a34f6d": "Jira", - "e4170c9615": "Muestre Linear en el selector de fuente de Tareas y en los accesos directos de la barra lateral.", + "e4170c9615": "Muestra Linear en el selector de fuentes de Tareas y en los accesos directos de la barra lateral.", "09ae2d7c51": "Linear", - "dd67a1b6e1": "Muestre GitLab en el selector de fuente de Tareas y en los accesos directos de la barra lateral.", + "dd67a1b6e1": "Muestra GitLab en el selector de fuentes de Tareas y en los accesos directos de la barra lateral.", "7c5d7fdc20": "GitLab", - "1db47236cd": "Muestre GitHub en el selector de fuente de Tareas y en los accesos directos de la barra lateral.", + "1db47236cd": "Muestra GitHub en el selector de fuentes de Tareas y en los accesos directos de la barra lateral.", "e14063e727": "GitHub" }, "TerminalAppearanceSection": { "a14a427ae4": "Grosor de la línea divisoria del panel.", "f27a99978d": "Grosor del divisor", - "db632cb50e": "Opacidad aplicada a paneles que no están actualmente activos.", + "db632cb50e": "Opacidad aplicada a los paneles que no están activos actualmente.", "a6fdd6a3b1": "Opacidad del panel inactivo", - "1b79379d4f": "Controle la atenuación del panel inactivo y divida el espesor del divisor.", - "e1a5c25555": "Paneles terminales", - "04cdf85dec": "Opacidad del cursor terminal.", + "1b79379d4f": "Controla la atenuación de paneles inactivos y el grosor del divisor de splits.", + "e1a5c25555": "Paneles de terminal", + "04cdf85dec": "Opacidad del cursor del terminal.", "b9f1804422": "Opacidad del cursor", "2de6b5a699": "Utiliza la variante parpadeante de la forma del cursor seleccionada.", "74736cc9b1": "Cursor parpadeante", - "2e5aec3cf6": "Subrayar", - "52854a5608": "Bloquear", - "e070e8aeba": "Bar", + "2e5aec3cf6": "Subrayado", + "52854a5608": "Bloque", + "e070e8aeba": "Barra", "db270cc9a9": "Forma del cursor", - "d455f2ef4f": "Apariencia predeterminada del cursor para los paneles del terminal Orca.", - "abcb4dd019": "Cursor de terminales", - "70beb1bbc7": "Avance", - "31f6e61085": "Actualmente las ligaduras", - "870377082f": "Apagado", - "84bd22f2cd": "En", + "d455f2ef4f": "Apariencia predeterminada del cursor para los paneles de terminal de Orca.", + "abcb4dd019": "Cursor del terminal", + "70beb1bbc7": "Vista previa", + "31f6e61085": "Las ligaduras están", + "870377082f": "Desactivadas", + "84bd22f2cd": "Activadas", "bc9ff84d61": "Auto", - "be8da35e7f": "Ligaduras de fuentes", + "be8da35e7f": "Ligaduras de fuente", "4b1f29598e": "Automático: deshabilitado para \"{{value0}}\".", "400e950ca5": "Automático: habilitado para \"{{value0}}\".", - "04569feb07": "Siempre desactivado, incluso para las fuentes que los incluyen.", - "7234abcd08": "Siempre encendido. Las fuentes sin ligaduras simplemente se muestran tal cual.", - "7233d594bf": "Renderice ligaduras de programación (por ejemplo, =>, !=, ===) para las fuentes que las incluyen. \"Auto\" habilita ligaduras solo para fuentes de ligadura conocidas (Fira Code, JetBrains Mono, Cascadia Code, Iosevka, etc.).", - "bafc80efbc": "Controla el multiplicador de altura de la línea terminal.", + "04569feb07": "Siempre desactivadas, incluso en fuentes que las incluyen.", + "7234abcd08": "Siempre activadas. Las fuentes sin ligaduras simplemente se renderizan tal cual.", + "7233d594bf": "Renderiza ligaduras de programación (p. ej., =>, !=, ===) en las fuentes que las incluyen. \"Auto\" activa ligaduras solo para fuentes de ligaduras conocidas (Fira Code, JetBrains Mono, Cascadia Code, Iosevka, etc.).", + "bafc80efbc": "Controla el multiplicador de altura de línea del terminal.", "c084eb7d4c": "Altura de línea", - "36af8ad94c": "Controla el peso de la fuente del texto del terminal.", + "36af8ad94c": "Controla el peso de fuente del texto del terminal.", "4aae5db258": "Peso de fuente", - "f04b17a50e": "Familia de fuentes de terminal predeterminada para nuevos paneles y actualizaciones en vivo.", + "f04b17a50e": "Familia de fuentes predeterminada del terminal para paneles nuevos y actualizaciones en vivo.", "a408266e67": "Familia de fuentes", "855a76343a": "Importar desde Ghostty", - "711e589f18": "Tipografía de terminal predeterminada para nuevos paneles y actualizaciones en vivo.", - "048aac8a64": "Tipografía de terminales", + "711e589f18": "Tipografía predeterminada del terminal para paneles nuevos y actualizaciones en vivo.", + "048aac8a64": "Tipografía del terminal", "4415beb958": "desactivado", "4e7d41a9f0": "activado", "e90afcc44f": "apagado", @@ -6437,41 +6437,41 @@ "dimUnfocusedPanes": "Atenuar paneles sin foco." }, "TerminalFontSizeSetting": { - "9b5252c85a": "píxeles", - "0f4c92e595": "Tamaño de fuente de terminal predeterminado para nuevos paneles y actualizaciones en vivo.", + "9b5252c85a": "px", + "0f4c92e595": "Tamaño de fuente predeterminado del terminal para paneles nuevos y actualizaciones en vivo.", "a4a352b1e9": "Tamaño de fuente" }, "TerminalPane": { "4263e940e0": "Al presionar la tecla JIS Yen (¥), se envía una barra invertida (\\\\).", - "19f4935159": "Yen JIS (¥) a Barra invertida (\\\\)", + "19f4935159": "Yen JIS (¥) a barra invertida (\\)", "1c337bef4a": "Controla si al presionar la tecla JIS Yen (¥) se envía una barra invertida (\\\\).", - "3fe1c5bfe0": "Apagado", - "c73d510938": "Bien", + "3fe1c5bfe0": "Desactivado", + "c73d510938": "Derecha", "e7aec1fd60": "Izquierda", "badb1219fc": "Ambos", "43c2ff7b0e": "Auto", "0a10420e1a": "Opción como Alt", - "ce3aadf0b2": "La tecla Opción {{value0}} envía Alt/Esc; el otro compone caracteres especiales.", + "ce3aadf0b2": "La tecla Opción {{value0}} envía Alt/Esc; la otra compone caracteres especiales.", "b62373091a": "Ambas teclas de opción envían secuencias Alt/Esc.", - "d8998bb328": "La opción compone caracteres especiales para la distribución de su teclado.", + "d8998bb328": "La tecla Opción compone caracteres especiales para la distribución de tu teclado.", "d21c493808": "Automático: detectado: {{value0}}.", "2561d3fc1b": "Controla si la tecla Opción de macOS envía secuencias Alt/Esc o compone caracteres.", "96be03b8eb": "PowerShell 7+", - "d26174e1dd": "WindowsPowerShell", + "d26174e1dd": "Windows PowerShell", "fe20f79dd1": "Versión de PowerShell", "822f62ddcd": "Descargar PowerShell 7+", "a016ffbeed": "Auto usa Windows PowerShell ahora y cambia a PowerShell 7+ cuando está instalado.", - "5ed5c95344": "Elija entre Windows PowerShell y PowerShell 7+ para obtener nuevos paneles de terminal.", - "3d88af864d": "Elija si la opción de shell de PowerShell inicia Windows PowerShell o PowerShell 7+ para nuevos paneles de terminal.", + "5ed5c95344": "Elige entre Windows PowerShell y PowerShell 7+ para nuevos paneles de terminal.", + "3d88af864d": "Elige si la opción de shell PowerShell inicia Windows PowerShell o PowerShell 7+ para nuevos paneles de terminal.", "8a956cc91e": "Caracteres tratados como límites de palabras para la selección con doble clic.", "4bebcc2b2c": "Separadores de palabras", "12e06178fa": "filas", - "907b0b9d3e": "Costumbre", + "907b0b9d3e": "Personalizado", "5336c096af": "{{value0}} filas", - "81d86b2dd2": "Filas de terminal de escritorio retenidas para paneles nuevos y abiertos.", - "9df53f7c14": "Filas de desplazamiento hacia atrás", - "c3810b2b42": "Filas de terminal de escritorio retenidas.", - "267d020745": "Desplazamiento hacia atrás, límites de palabras y comportamientos de terminal específicos de la plataforma.", + "81d86b2dd2": "Filas de scrollback conservadas para paneles de terminal nuevos y abiertos.", + "9df53f7c14": "Filas de scrollback", + "c3810b2b42": "Filas de scrollback conservadas.", + "267d020745": "Scrollback, límites de palabras y comportamientos de terminal específicos de la plataforma.", "5e5f06c82c": "Avanzado", "003df129fe": "Dividir horizontalmente", "623e62df99": "Dividir horizontalmente", @@ -6483,45 +6483,45 @@ "d23b43c5be": "Ubicación del script de configuración", "34a0dfa06e": "Dónde se ejecuta el script de configuración del repositorio cuando se crea un nuevo espacio de trabajo.", "21f8da2078": "Script de configuración del espacio de trabajo", - "6e6480a7df": "Deje que los programas en la terminal (tmux, Neovim, fzf, SSH) se copie al portapapeles de su sistema.", + "6e6480a7df": "Permite que los programas del terminal (tmux, Neovim, fzf, SSH) copien al portapapeles del sistema.", "3338dcf8c1": "Permitir escrituras en el portapapeles TUI (OSC 52)", - "69c64a479c": "Deje que tmux, Neovim y fzf copie al portapapeles del sistema a través de PTY (incluso a través de SSH).", - "4729c645fc": "Copie automáticamente las selecciones de terminal al portapapeles.", + "69c64a479c": "Permite que tmux, Neovim y fzf copien al portapapeles del sistema a través de PTY (incluso por SSH).", + "4729c645fc": "Copia automáticamente las selecciones del terminal al portapapeles.", "902f5dee1f": "Copiar al seleccionar", "9129b7e805": "Al pasar el cursor sobre un panel de terminal, se activa sin necesidad de hacer clic.", "8eefeaa3da": "El foco sigue al ratón", - "96fe15def8": "Comportamiento del mouse y del portapapeles para paneles de terminales.", - "45721f3e67": "Interacción terminal", - "9c0b1c1792": "En", + "96fe15def8": "Comportamiento del mouse y del portapapeles en los paneles de terminal.", + "45721f3e67": "Interacción del terminal", + "9c0b1c1792": "Activado", "c1fc9e9444": "Aceleración de GPU", "e0996d141a": "Auto prueba WebGL, con respaldo DOM para renderizadores riesgosos o no compatibles.", "7eaccc1424": "Siempre se intenta WebGL para paneles de terminales.", - "fe4acf36c6": "WebGL deshabilitado; Renderizador DOM para máxima compatibilidad.", - "f07dfb4466": "Controla si el terminal utiliza la representación WebGL de xterm.js. Auto prueba WebGL cuando el renderizador es compatible, con una alternativa conservadora de Linux para software o renderizadores de GPU desconocidos.", - "72bc9334a0": "Comportamiento del renderizador de terminal para paneles activos y paneles nuevos.", - "2fba319f21": "Representación", - "cc8c5ca224": "predeterminado de Windows", + "fe4acf36c6": "WebGL desactivado; renderizador DOM para máxima compatibilidad.", + "f07dfb4466": "Controla si el terminal usa el renderizado WebGL de xterm.js. Auto prueba WebGL cuando el renderizador es compatible, con una alternativa conservadora en Linux para renderizadores de GPU por software o desconocidos.", + "72bc9334a0": "Comportamiento del renderizador del terminal para paneles activos y paneles nuevos.", + "2fba319f21": "Renderizado", + "cc8c5ca224": "Predeterminado de Windows", "d78fc4fdef": "Cargando distribuciones", "219aaa59f4": "Distribución WSL", "2503f1e86b": "Se utiliza para nuevos paneles de terminal WSL y detección de agentes locales cuando el espacio de trabajo activo aún no está dentro de WSL.", "5fe79a5e56": "Elija qué distribución WSL utilizarán los nuevos terminales WSL y los análisis de agentes locales.", "b637dd57a7": "WSL", - "f61ac77f16": "Git bash", + "f61ac77f16": "Git Bash", "0f1b8669e6": "Símbolo del sistema", "eb7fc4d98a": "PowerShell", - "27e301f22c": "Shell predeterminado", - "09bf02de9a": "Shell utilizado al abrir un nuevo panel de terminal. Entra en vigor para nuevos terminales.", - "bd68f3170d": "Elija el shell predeterminado para los nuevos paneles de terminal en Windows.", - "a55eee649f": "Shell predeterminado para nuevos paneles de terminal en Windows.", - "87e678a8af": "Cáscara de Windows", + "27e301f22c": "Shell predeterminada", + "09bf02de9a": "Shell usada al abrir un nuevo panel de terminal. Se aplica a terminales nuevos.", + "bd68f3170d": "Elige la shell predeterminada para nuevos paneles de terminal en Windows.", + "a55eee649f": "Shell predeterminada para nuevos paneles de terminal en Windows.", + "87e678a8af": "Shell de Windows", "05efc0bada": "verdadero", "348246b06f": "FALSO", "5936387ddd": "auto", - "adbafefe56": "costumbre", - "16753eea48": "En Windows, haga clic derecho y pegue el portapapeles. Ctrl+clic derecho abre el menú contextual.", - "9c178cf8aa": "Haga clic derecho para pegar", - "af0c3b6e39": "En Windows, haga clic derecho y pegue el portapapeles en la terminal. Utilice Ctrl+clic derecho para abrir el menú contextual.", - "29154326bb": "en", + "adbafefe56": "personalizado", + "16753eea48": "En Windows, hacer clic derecho pega el portapapeles. Ctrl+clic derecho abre el menú contextual.", + "9c178cf8aa": "Clic derecho para pegar", + "af0c3b6e39": "En Windows, hacer clic derecho pega el portapapeles en el terminal. Usa Ctrl+clic derecho para abrir el menú contextual.", + "29154326bb": "activado", "ab20575a8a": "apagado", "ab3a1f9068": "wsl.exe", "ask_before_closing_running_terminals_title": "Ask Before Closing Running Terminals", @@ -6541,94 +6541,94 @@ }, "TerminalSettingsPreview": { "a63953a48a": "Vista previa del tema {{value0}}", - "2c248fcc27": "Tema de vista previa", - "f8931d407d": "Mostrar divisor de panel en vista previa", + "2c248fcc27": "Vista previa del tema", + "f8931d407d": "Mostrar divisor de panel en la vista previa", "50419052fe": "Divisor de panel", "d06664e889": "oscuro" }, "TerminalThemeSections": { - "db210115c5": "Vista previa del modo de luz", - "5e0c24b5c8": "Controla la línea divisoria dividida entre paneles en modo claro.", - "ec2e33ad80": "Color del divisor de luz", - "d56af60e6f": "Elige el tema utilizado cuando Orca está en modo claro.", - "8273bc75d7": "Tema ligero", + "db210115c5": "Vista previa del modo claro", + "5e0c24b5c8": "Controla la línea divisoria entre paneles en modo claro.", + "ec2e33ad80": "Color del divisor claro", + "d56af60e6f": "Elige el tema usado cuando Orca está en modo claro.", + "8273bc75d7": "Tema claro", "bc8e8a251a": "Vista previa del modo oscuro", "cbe56a0f79": "Controla la línea divisoria entre paneles en modo oscuro.", - "b739d2abfe": "Color divisor oscuro", - "7add204bd5": "Elija el tema del terminal utilizado en modo oscuro.", + "b739d2abfe": "Color del divisor oscuro", + "7add204bd5": "Elige el tema del terminal usado en modo oscuro.", "9499ad1dc4": "Tema oscuro", - "f012172e21": "Elija el tema utilizado para los paneles de terminal en modo oscuro.", + "f012172e21": "Elige el tema usado para los paneles de terminal en modo oscuro.", "catalog_title": "Temas de terminal", "catalog_description": "Elige temas de terminal y colores de divisor para los modos oscuro y claro.", "target_title": "Modo del tema", "target_label": "Modo del tema", - "target_aria": "Modo del tema de terminal", + "target_aria": "Modo de tema del terminal", "target_dark": "Oscuro", "target_light": "Claro", - "match_dark_mode": "Coincidir con modo oscuro", + "match_dark_mode": "Coincidir con el modo oscuro", "match_dark_mode_description": "Comparte el tema de terminal oscuro y el color del divisor en modo claro.", - "light_preview_description": "Muestra la apariencia efectiva del terminal claro.", - "dark_preview_description": "Muestra la apariencia efectiva del terminal oscuro." + "light_preview_description": "Muestra la apariencia clara efectiva del terminal.", + "dark_preview_description": "Muestra la apariencia oscura efectiva del terminal." }, "TerminalWindowSection": { - "1705318506": "color magenta ANSI", + "1705318506": "Color magenta ANSI", "03c855d15f": "Restablecer todas las anulaciones de color", "63f8d9336e": "Anulaciones de color", - "e86e09b5c7": "Anula los colores de terminales individuales.", - "1d1920dc8a": "Oculte el cursor del mouse al escribir en la terminal.", - "3530908ef9": "Ocultar el mouse mientras escribe", - "1846f6ee6a": "Relleno vertical alrededor de la cuadrícula del terminal en píxeles.", - "1afcc1d973": "Acolchado vertical", - "25e2f8e8e1": "Relleno horizontal alrededor de la cuadrícula del terminal en píxeles.", - "36b8402015": "Acolchado horizontal", + "e86e09b5c7": "Anula colores individuales de la terminal.", + "1d1920dc8a": "Oculta el cursor del mouse al escribir en la terminal.", + "3530908ef9": "Ocultar cursor al escribir", + "1846f6ee6a": "Relleno vertical alrededor de la cuadrícula de la terminal, en píxeles.", + "1afcc1d973": "Relleno vertical", + "25e2f8e8e1": "Relleno horizontal alrededor de la cuadrícula de la terminal, en píxeles.", + "36b8402015": "Relleno horizontal", "53ce336e15": "Reinicie Orca para aplicar el cambio de desenfoque de ventana.", - "c65bb9ce63": "Reiniciar requerido", - "97950bb087": "Aplique desenfoque de fondo a la ventana del terminal. Requiere reinicio.", + "c65bb9ce63": "Reinicio requerido", + "97950bb087": "Aplica desenfoque de fondo a la ventana de la terminal. Requiere reiniciar.", "2b82242f43": "Desenfoque de ventana", "809f37738d": "Controla la transparencia del fondo del terminal. 1 es completamente opaco, 0 es completamente transparente.", "ea7b1a158e": "Opacidad del fondo", "03acb60aa0": "Controla la transparencia del fondo del terminal.", - "00eaa6b881": "Configuración de apariencia y fondo de la ventana.", + "00eaa6b881": "Ajustes de apariencia y fondo de la ventana.", "b96ba13ed1": "Ventana", "42e01a6055": "Color blanco brillante ANSI", "16948119cb": "Blanco brillante", "1601140f03": "Color cian brillante ANSI", "f94adc4113": "Cian brillante", "fe4d89ef85": "Color magenta brillante ANSI", - "e56e7d6ea0": "magenta brillante", - "bef6c0f6bf": "ANSI color azul brillante", - "66820332fa": "azul brillante", + "e56e7d6ea0": "Magenta brillante", + "bef6c0f6bf": "Color azul brillante ANSI", + "66820332fa": "Azul brillante", "e2ef5f4ab7": "Color amarillo brillante ANSI", "936a326be3": "Amarillo brillante", "0ffb02f921": "Color verde brillante ANSI", - "7dafd57730": "verde brillante", + "7dafd57730": "Verde brillante", "667de68863": "Color rojo brillante ANSI", - "32b1b6acd7": "Lacre", - "f30c492769": "ANSI color negro brillante", + "32b1b6acd7": "Rojo brillante", + "f30c492769": "Color negro brillante ANSI", "260d69ce9a": "Negro brillante", "1be593d3e8": "ANSI brillante", - "28846b1ca6": "color blanco ANSI", + "28846b1ca6": "Color blanco ANSI", "0cb4459fb8": "Blanco", "bd4c759327": "Color cian ANSI", - "fb8bb4eb1f": "cian", + "fb8bb4eb1f": "Cian", "d5e92fcd94": "Magenta", "9635a71c51": "Color azul ANSI", "292a4c7316": "Azul", "09c1c6b096": "Color amarillo ANSI", "bb516de873": "Amarillo", - "8a673d4206": "color verde ANSI", + "8a673d4206": "Color verde ANSI", "8f2092b315": "Verde", "b41270f5ca": "Color rojo ANSI", "3a78f30b50": "Rojo", - "cf4437a2f7": "Color negro ANSI.", + "cf4437a2f7": "Color negro ANSI", "adfdee23cb": "Negro", - "68e9f07de0": "Norma ANSI", + "68e9f07de0": "ANSI normal", "fb8c6f1967": "Color para texto en negrita. Vuelve al color normal si no se establece.", "862e463f7f": "Texto en negrita", - "b2c0857c49": "Color del texto del texto seleccionado", - "8b450b5305": "Selección en primer plano", + "b2c0857c49": "Color del texto seleccionado", + "8b450b5305": "Primer plano de selección", "74d8555f85": "Color de fondo del texto seleccionado", - "40c3cfd30a": "Antecedentes de selección", + "40c3cfd30a": "Fondo de selección", "7f4063076c": "Color del texto debajo del cursor (cursor de bloque)", "a2d9f095a7": "Texto del cursor", "cd0700762b": "Color del cursor", @@ -6642,103 +6642,103 @@ "907131d741": "Reiniciando…" }, "UIZoomControl": { - "c2c64b24d0": "Reiniciar" + "c2c64b24d0": "Restablecer" }, "VoicePane": { "68de13f72c": "No se pudo eliminar el modelo.", "1ba81c0ff0": "recomendado", "cfde55c7b0": "No se pudo descargar el modelo.", - "43fd4f454b": "Modelo de habla", - "7cf715f891": "se lleva a cabo.", - "295d84b849": "una vez para empezar, otra vez para parar. Mantener: dictar mientras", - "ff9a680010": "Alternar: presionar", + "43fd4f454b": "Modelo de voz", + "7cf715f891": "se mantiene presionado.", + "295d84b849": "una vez para empezar y otra para parar. Mantener: dicta mientras", + "ff9a680010": "Alternar: pulsa", "ba4a900d1d": "Modo de dictado", "0121960365": "Habilitar dictado de voz", - "366e1b4f36": "para dictar texto en cualquier panel enfocado.", - "4465596675": "Prensa", + "366e1b4f36": "para dictar texto en cualquier panel con foco.", + "4465596675": "Pulsa", "62d2a84d31": "No se pudo borrar la clave API de OpenAI", "37aba8bb63": "Clave API de OpenAI borrada", "8572bbb537": "No se pudo guardar la clave API de OpenAI", "506df81ba6": "Clave API de OpenAI guardada", - "91980ce124": "{{value0}}MB", + "91980ce124": "{{value0}} MB", "61a16c8141": "Extrayendo...", "b6536a1d12": "extrayendo", - "8f4d2a51d7": "desconectado", - "d504ab05f0": "transmisión", + "8f4d2a51d7": "sin conexión", + "d504ab05f0": "streaming", "fbe5990716": "Seleccionar modelo", - "e24f7d43d2": "Seleccione un modelo de discurso. Los modelos locales funcionan sin conexión; Los modelos en la nube requieren una clave API.", - "174da92062": "Sostener", - "118b3c2dee": "Palanca", - "901985625d": "palanca", - "ad5d036ecc": "No se pudo solicitar permiso del micrófono. El dictado de voz no estaba habilitado.", + "e24f7d43d2": "Selecciona un modelo de voz. Los modelos locales funcionan sin conexión; los modelos en la nube requieren una clave API.", + "174da92062": "Mantener", + "118b3c2dee": "Alternar", + "901985625d": "alternar", + "ad5d036ecc": "No se pudo solicitar permiso para el micrófono. El dictado de voz no se habilitó.", "f9a9cf6928": "Se requiere permiso del micrófono antes de habilitar el dictado de voz.", - "1eac933202": "Se abrió Privacidad y seguridad de macOS. Habilite el dictado nuevamente después de otorgar acceso.", + "1eac933202": "Se abrió Privacidad y seguridad de macOS. Vuelve a habilitar el dictado después de otorgar acceso.", "cd9fe37556": "Permiso de micrófono concedido", "6fa734ed95": "Eliminar {{value0}}" }, "WorktreeSymlinksSection": { "1c1e35b219": "Quitar {{value0}}", - "b814c618e2": "Caminos vinculados", + "b814c618e2": "Rutas vinculadas", "31ebab5403": "No hay rutas compartidas configuradas para este repositorio.", - "ea06227efa": "agregado", + "ea06227efa": "agregada", "b2429aeb31": "Agregar", - "ab40b8a5f1": "Sin coincidencias. Sigue escribiendo para agregar una ruta personalizada.", - "4cd2a4c077": "Escriba una ruta (por ejemplo, .env o node_modules)…", + "ab40b8a5f1": "No hay coincidencias. Sigue escribiendo para agregar una ruta personalizada.", + "4cd2a4c077": "Escribe una ruta (por ejemplo, .env o node_modules)…", "241325302c": "Agregar ruta", - "7ff265071d": "Cuando se crea un nuevo árbol de trabajo, cada ruta enumerada aquí se copia con APFS clone-copy en macOS cuando es posible; de lo contrario, se enlaza simbólicamente desde el checkout principal.", - "4755f120b6": "Rutas compartidas del árbol de trabajo", - "b07ef5a8b6": "Rutas para materializar desde el checkout principal en árboles de trabajo recién creados.", - "d72ba8dc68": "{{value0}} caminos", - "9ea912d811": "1 camino" + "7ff265071d": "Cuando se crea un nuevo worktree, cada ruta listada aquí se copia como clon APFS en macOS cuando es posible; de lo contrario, se enlaza simbólicamente desde el checkout principal.", + "4755f120b6": "Rutas compartidas de worktree", + "b07ef5a8b6": "Rutas para materializar desde el checkout principal en worktrees recién creados.", + "d72ba8dc68": "{{value0}} rutas", + "9ea912d811": "1 ruta" }, "WslCliRegistration": { "c6f6f89d7c": "Cancelar", "119fef6cd2": "Ruta de destino:", - "1dbb0377d9": "Objetivo del lanzador existente:", - "554305956d": "Ruta de comando:", - "9b6627522c": "Refrescar", - "ab6b022a5c": "Actualizar el estado de la CLI de WSL", - "d9c6880dbd": "comando de shell WSL", + "1dbb0377d9": "Destino del lanzador existente:", + "554305956d": "Ruta del comando:", + "9b6627522c": "Actualizar", + "ab6b022a5c": "Actualizar estado de la CLI de WSL", + "d9c6880dbd": "Comando de shell de WSL", "52d990420e": "No se pudo eliminar `{{value0}}` de WSL.", "89c7414cf5": "Se eliminó `{{value0}}` de WSL.", "6f91ad1333": "No se pudo registrar `{{value0}}` en WSL.", - "951536dda5": "Registrado `{{value0}}` en WSL.", + "951536dda5": "Se registró `{{value0}}` en WSL.", "26b4b3b00f": "No se pudo cargar el estado de la CLI de WSL.", - "290bfff3ab": "Registro", + "290bfff3ab": "Registrar", "f951f85196": "Eliminar", "4c4a9178a3": "Registrando...", "41a1480d3e": "instalar", "4598b18464": "Eliminando...", "7c3bb36706": "eliminar", - "7ee4e52b99": "Orca registrará {{value0}} para que el comando funcione desde terminales WSL.", - "d8216eb22e": "Esto elimina el comando de shell WSL. La propia Orca permanece instalada en Windows.", + "7ee4e52b99": "Orca registrará {{value0}} para que el comando funcione desde terminales de WSL.", + "d8216eb22e": "Esto elimina el comando de shell de WSL. Orca permanece instalada en Windows.", "e49688f67f": "¿Registrar `{{value0}}` en WSL?", "61ac55278e": "¿Eliminar `{{value0}}` de WSL?", "e2b0ee267f": "duro", - "7aa456a460": "Registre `orca-ide` en ~/.local/bin dentro de WSL.", - "0307677bb9": "Comprobando el registro de WSL CLI..." + "7aa456a460": "Registra `orca-ide` en ~/.local/bin dentro de WSL.", + "0307677bb9": "Comprobando el registro de la CLI de WSL..." }, "accounts": { "search": { "86edc96bc9": "barra de estado", - "e949b08ffb": "límite de tasa", - "7e67d7d1b6": "trabajo", - "421c6be25e": "identificación", + "e949b08ffb": "límite de uso", + "7e67d7d1b6": "wrk", + "421c6be25e": "ID", "be8b621bdc": "espacio de trabajo", - "8dcbef1856": "código abierto", + "8dcbef1856": "opencode", "38d22ff8d6": "Anulación opcional del ID del espacio de trabajo si falla la búsqueda automática.", "4ee2029e9c": "ID del espacio de trabajo de OpenCode Go", "9c4e40cf6b": "sesión", - "61f7d1fcbe": "galleta", - "d1d2ae383c": "Pegue su cookie de sesión de opencode.ai para obtener el límite de velocidad.", + "61f7d1fcbe": "cookie", + "d1d2ae383c": "Pega tu cookie de sesión de opencode.ai para obtener límites de uso.", "6ed1401020": "Cookie de sesión de OpenCode Go", "b7c2cee442": "experimental", - "7118d2f908": "cartas credenciales", - "933deaf732": "juramento", - "8630464352": "cli", - "e8e1ff3887": "gemini", - "bada4a3218": "Extrae las credenciales de OAuth de su instalación local de CLI de Gemini para autenticarse con Google.", - "d819755b02": "Utilice las credenciales de la CLI de Gemini", + "7118d2f908": "credenciales", + "933deaf732": "OAuth", + "8630464352": "CLI", + "e8e1ff3887": "Gemini", + "bada4a3218": "Extrae las credenciales de OAuth de tu instalación local de la CLI de Gemini para autenticarte con Google.", + "d819755b02": "Usar credenciales de la CLI de Gemini", "35b461d817": "iniciar sesión", "f2d666a886": "opcional", "8b06729e0f": "activo", @@ -6746,24 +6746,24 @@ "06662af91e": "cuenta", "70d1b8def5": "codex", "87a4a8584e": "Elija qué cuenta Codex guardada opcional impulsa las lecturas de cuotas en vivo.", - "a4bcfd6f86": "Cuenta activa del Codex", - "042885c07c": "fuera de fecha", - "02c438bc7b": "venció", + "a4bcfd6f86": "Cuenta activa de Codex", + "042885c07c": "desactualizado", + "02c438bc7b": "caducado", "77e32a2ad3": "volver a autenticar", "c759741d77": "cuota", - "b40d5b6570": "Cambio de cuenta opcional para Codex y recuperación de límite de tasa en vivo.", - "17c5d244eb": "Cuentas del Codex", + "b40d5b6570": "Cambio de cuenta opcional para Codex y obtención de límites de uso en vivo.", + "17c5d244eb": "Cuentas de Codex", "e14049e1a8": "claude", "dd75a73991": "Cambio de cuenta opcional para Claude preservando el contexto de chat compartido.", - "75682e1b62": "Cuentas Claude", + "75682e1b62": "Cuentas de Claude", "e02c136ad0": "autenticación", "9f70aa706c": "proveedor", "488a7e9206": "Linux", - "0b4d948eb5": "wsl", - "bdbd1e668e": "ventanas", + "0b4d948eb5": "WSL", + "bdbd1e668e": "Windows", "593720c17f": "ubicación", "b84a5b0c8a": "Elija si las cuentas de proveedor se inspeccionan y agregan en este dispositivo o en WSL.", - "d09fb5ca92": "Ubicación de la cuenta" + "d09fb5ca92": "Ubicación de cuentas" } }, "advanced": { @@ -6771,9 +6771,9 @@ "a7002e1ac4": "actualizador", "e61ed8ab33": "actualizaciones", "6576fce4d2": "solución de problemas", - "79e0947e95": "apoyo", + "79e0947e95": "soporte", "4383251647": "VPN", - "f98a60af11": "apoderado", + "f98a60af11": "proxy", "65bf6af262": "compatibilidad", "621233008b": "http/1.1", "f8ff125ebe": "http1", @@ -6783,66 +6783,66 @@ "4d44352eea": "red", "2b4d26d11e": "redes", "e04e9db503": "avanzado", - "585f56fae0": "Utilice HTTP/1.1 para redes Electron cuando HTTP/2 falle detrás de un proxy.", + "585f56fae0": "Usar HTTP/1.1 para la red de Electron cuando HTTP/2 falle detrás de un proxy.", "11eea3da72": "Compatibilidad HTTP/1.1" } }, "agents": { "search": { "2814401339": "instalado", - "719f53350c": "camino", + "719f53350c": "ruta", "839e82c81f": "detectar", "f622b8eb2a": "Linux", - "d608654c03": "wsl", - "77c02fa3c3": "ventanas", + "d608654c03": "WSL", + "77c02fa3c3": "Windows", "d2952dfd74": "ubicación", - "96ba2373b6": "agent", - "cbdd7f3b9e": "Elija si los agents instalados se detectan en este dispositivo o en WSL.", - "ef804b7337": "Ubicación del Agent", - "01926b9d8c": "Configure agents de codificación de IA, agente predeterminado y anulaciones de comandos.", + "96ba2373b6": "agente", + "cbdd7f3b9e": "Elige si los agentes instalados se detectan en este dispositivo o en WSL.", + "ef804b7337": "Ubicación del agente", + "01926b9d8c": "Configura agentes de codificación de IA, el agente predeterminado y anulaciones de comandos.", "bb9ad95777": "Agentes", - "d8f3a8b8a0": "por defecto", - "167daeb5e9": "dominio", + "d8f3a8b8a0": "predeterminado", + "167daeb5e9": "comando", "be59907510": "anular", "a6d594c17d": "instalar", "f2932bf22b": "detectado", - "2afd3b5858": "permitir", + "2afd3b5858": "habilitar", "60393e1b17": "desactivar", - "2e188c771c": "esconder", - "87fffe6c20": "espectáculo", - "e2b7c0dcd7": "github", + "2e188c771c": "ocultar", + "87fffe6c20": "mostrar", + "e2b7c0dcd7": "GitHub", "66b6b82eb4": "despierto", "dbc8aca6b0": "dormir", - "845ad9128a": "fuerza", - "48f84d10f1": "correr", - "affbf130f6": "laboral", + "845ad9128a": "energía", + "48f84d10f1": "en ejecución", + "affbf130f6": "trabajando", "0d1c334987": "tapa", - "ff8de8a2ad": "mostrar", - "0d752916f8": "manos", + "ff8de8a2ad": "pantalla", + "0d752916f8": "hooks", "6984d4291a": "estado", - "13b20636a6": "espera", + "13b20636a6": "esperando", "8599603496": "hecho", "ea71995548": "eliminar", "c1317fe641": "restaurar", "5963143e00": "ajustes", "042c551bc5": "configuración", - "f412abbba5": "claude", - "5ded38b843": "codex", + "f412abbba5": "Claude", + "5ded38b843": "Codex", "be7ea3553b": "pestaña", "6956646a1e": "título", "32836788b0": "título generado", "966890236d": "nombre", "848dcae8d3": "generado", - "52115d0d7c": "auto", - "c64059f50d": "inmediato", - "5784ae8c43": "rebautizar", + "52115d0d7c": "automático", + "c64059f50d": "prompt", + "5784ae8c43": "renombrar", "8a17fd6026": "estable", "a79d266f71": "sesión", "afbf35be68": "sesión estable", - "agentPermissions": "Agent Permissions", - "agentPermissionsDescription": "Switch agent permission defaults between Yolo and Manual.", - "agentRuntime": "Tiempo de ejecución del Agent", - "agentRuntimeDescription": "Elija si los agents se detectan y ejecutan en Windows o en WSL de forma predeterminada." + "agentPermissions": "Permisos de agentes", + "agentPermissionsDescription": "Cambia los permisos predeterminados de los agentes entre Yolo y Manual.", + "agentRuntime": "Runtime del agente", + "agentRuntimeDescription": "Elige si los agentes se detectan y ejecutan en Windows o en WSL de forma predeterminada." } }, "appearance": { @@ -6851,30 +6851,30 @@ "f586abfa35": "azul", "651f35b2c6": "conmutador", "e5bc35d59e": "ventana", - "d18b54ca90": "muelle", + "d18b54ca90": "Dock", "1f2880a9d5": "orca", "2cfb3420c0": "icono de la aplicación", "e80c2af428": "Elija el ícono de la aplicación que se muestra en el Dock y en el selector de ventanas.", "2b313598c6": "Icono de aplicación", - "839fb1e3ed": "caja de instrumento", - "ac79fe4a04": "espectáculo", - "648eeada79": "esconder", + "839fb1e3ed": "herramientas", + "ac79fe4a04": "mostrar", + "648eeada79": "ocultar", "6cf5f54ce1": "botón", "5bff6a2ef0": "barra lateral", "5e5b8878bf": "teléfono", "74618577c7": "móvil", "682293cadf": "Muestra el botón de Orca Mobile en la parte superior de la barra lateral izquierda.", - "1de96ec8a6": "Mostrar botón móvil de Orca", + "1de96ec8a6": "Mostrar botón de Orca Mobile", "4c920ab2d1": "cronograma", "58f4e22fa2": "automatización", "b186f3cefb": "automatizaciones", "ae13a0d340": "Muestra el botón Automatizaciones en la parte superior de la barra lateral izquierda.", - "caa27e1a8e": "Botón Mostrar automatizaciones", + "caa27e1a8e": "Mostrar botón de Automatizaciones", "6b846424cc": "lineal", "2ee4810f38": "github", "0d5a74b606": "tareas", "9a248333c7": "Muestra el botón Tareas en la parte superior de la barra lateral izquierda.", - "155a1e7438": "Botón Mostrar tareas", + "155a1e7438": "Mostrar botón de Tareas", "a895d0f938": "marca", "51f957ce39": "nombre", "36e006efc1": "aplicación", @@ -6883,13 +6883,13 @@ "fdd31b00d0": "Nombre de la aplicación de la barra de título", "c1bca1885a": "explorador de archivos", "9f2df826ac": "ignorado", - "08c86bf58e": "gitignorar", + "08c86bf58e": ".gitignore", "bce3ac317a": "git", "7164edf71a": "Atenúe los archivos que coincidan con .gitignore en el explorador de archivos.", "f8129fb544": "Mostrar archivos ignorados por Git", "2f12e1aa3a": "interfaz de usuario", "5095258df2": "interfaz", - "fab91464dd": "idea", + "fab91464dd": "IDE", "8b36fb3f64": "tipografía", "a0e09aed9c": "tipo de letra", "24094af355": "fuente", @@ -6899,25 +6899,25 @@ "0952091186": "escala", "3ae5de6101": "zoom", "adddb91a3d": "Escale toda la interfaz de la aplicación.", - "c5e933970f": "Ampliación de la interfaz de usuario", + "c5e933970f": "Zoom de la interfaz", "3a9b69d734": "sistema", - "44d873fd18": "luz", + "44d873fd18": "claro", "262fe1d24f": "oscuro", "0709c794f7": "Elige cómo se ve Orca en la ventana de la aplicación.", "71e06350b4": "Tema", "dc02c8759d": "espacio de trabajo", "43cfba3b95": "servidor", - "46d21eef62": "servidor local", + "46d21eef62": "localhost", "006e67b279": "puertos", "896eb53fd4": "barra de estado", - "0ececfa190": "Muestre los puertos del espacio de trabajo en vivo en la barra de estado.", + "0ececfa190": "Muestra los puertos activos de los espacios de trabajo en la barra de estado.", "cf409b6c4d": "Puertos", "cb1cc62cf8": "espacio", "90bdc043ea": "disco", "96b4fb0064": "terminal", - "4ddbde4999": "UPC", + "4ddbde4999": "CPU", "4355f18ac6": "memoria", - "9c4d5f0894": "gerente", + "9c4d5f0894": "administrador", "c690a15849": "recurso", "81ef5abc2f": "Muestra el uso de CPU, memoria, sesiones de terminal y disco del espacio de trabajo en la barra de estado.", "7cf005b29f": "Administrador de recursos", @@ -6925,34 +6925,34 @@ "f4997e0f8a": "conexión", "a278406ed5": "remoto", "6ecad74eb3": "ssh", - "f17d66d0d2": "Show remote host connection status in the status bar.", + "f17d66d0d2": "Muestra el estado de conexión de hosts remotos en la barra de estado.", "57fb424c56": "Hosts remotos", - "35565867cb": "disparo a la luna", + "35565867cb": "moonshot", "de586def95": "suscripción", "00a028f25f": "uso", "40e5c3c285": "kimi", "c927a155d5": "Muestra el uso de la suscripción de Kimi en la barra de estado.", "3a6c028ea8": "Uso de Kimi", "edbf0f63a0": "costo", - "afbb6a3767": "fichas", - "d77537b580": "código abierto-ir", - "a9d56852eb": "código abierto", - "7f72de7cbe": "Muestra el token OpenCode Go y el uso de costos en la barra de estado.", + "afbb6a3767": "tokens", + "d77537b580": "opencode-go", + "a9d56852eb": "opencode", + "7f72de7cbe": "Muestra el consumo de tokens y el costo de OpenCode Go en la barra de estado.", "bc046e7899": "Uso de OpenCode Go", "51b0ccd6a2": "Google", "2804a920ad": "gemini", - "9660c5b2f1": "Muestra el token Gemini y el uso de costos en la barra de estado.", + "9660c5b2f1": "Muestra el consumo de tokens y el costo de Gemini en la barra de estado.", "5bfb874d05": "Uso de Gemini", - "97957e374e": "abierto", + "97957e374e": "openai", "8dfd676c28": "codex", - "e9e4412545": "Muestra el token Codex y el uso de costos en la barra de estado.", - "54b1acf24f": "Uso del Codex", + "e9e4412545": "Muestra el consumo de tokens y el costo de Codex en la barra de estado.", + "54b1acf24f": "Uso de Codex", "dea0a9a665": "anthropic", "c9fe3a7876": "claude", - "de50c6f516": "Muestra el token de Claude y el uso de costos en la barra de estado.", + "de50c6f516": "Muestra el consumo de tokens y el costo de Claude en la barra de estado.", "9dc15020d7": "Uso de Claude", "language": { - "locale": "lugar", + "locale": "locale", "i18n": "i18n", "translation": "traducción" }, @@ -6979,24 +6979,24 @@ "rename": { "branch": { "search": { - "0971762141": "estuche-kebab", - "a482f6a423": "babosa", + "0971762141": "kebab-case", + "a482f6a423": "slug", "7adefcdd94": "plantilla", - "10485c4fc5": "dominio", - "50139297e6": "aviso incorporado", + "10485c4fc5": "comando", + "50139297e6": "prompt integrado", "502aa57681": "instrucciones", - "40d21f2efc": "inmediato", - "672387fb77": "Plantilla de comando del Agent utilizada al generar nombres de sucursales.", - "722551c5b3": "Plantilla de comando de nombre de sucursal", + "40d21f2efc": "prompt", + "672387fb77": "Plantilla de comando del agente utilizada al generar nombres de rama.", + "722551c5b3": "Plantilla de comando para nombre de rama", "f41833025e": "generar", - "ed677944cc": "árbol de trabajo", + "ed677944cc": "worktree", "3ef3cbe98c": "agent", "f0acf64301": "nombre de la criatura", "7803423877": "auto", - "55a1860e47": "rebautizar", + "55a1860e47": "renombrar", "9319bd9827": "rama", - "ea94b9da8a": "Cambie el nombre de la rama generada automáticamente según el trabajo una vez que comience un agente.", - "427f2cd1eb": "Rama de cambio de nombre automático" + "ea94b9da8a": "Renombra automáticamente la rama generada según el trabajo cuando se inicia un agente.", + "427f2cd1eb": "Renombrar rama automáticamente" } } } @@ -7004,14 +7004,14 @@ "browser": { "search": { "7539f6336c": "perfil", - "1c1e097985": "arco", - "533a253deb": "borde", - "75a0d435b7": "cromo", - "854ef6ce83": "acceso", + "1c1e097985": "Arc", + "533a253deb": "Edge", + "75a0d435b7": "Chrome", + "854ef6ce83": "inicio de sesión", "3910a41f32": "autenticación", "2e7f951773": "importar", "66dd641a47": "sesión", - "29193a51d5": "galletas", + "29193a51d5": "cookies", "2d2d995c58": "navegador", "060ac1fcba": "Importe cookies de Chrome, Edge u otros navegadores para utilizar los inicios de sesión existentes dentro de Orca.", "96afedcb5c": "Sesión y cookies", @@ -7020,9 +7020,9 @@ "68d1db8929": "markdown", "90425d313c": "cambio", "72c58f7792": "vista web", - "82ba1c80ea": "servidor local", - "bea27bac4b": "campo de golf", - "44d14df30d": "avance", + "82ba1c80ea": "localhost", + "bea27bac4b": "enlaces", + "44d14df30d": "vista previa", "5cb082b3e3": "Enrutamiento de enlaces", "95944898e0": "porcentaje", "483a0eb5e0": "nueva pestaña", @@ -7034,25 +7034,25 @@ "072b7f5c1f": "Zoom predeterminado", "0bb34eacc9": "consulta", "8b8ed06e4b": "omnibox", - "3538b3aaeb": "simbólico", + "3538b3aaeb": "token", "0732ebe6fb": "privado", "e1c2a57f07": "kagi", "ad40e75d13": "bing", - "1f8153acfb": "patopatogo", + "1f8153acfb": "DuckDuckGo", "8a489aab8d": "Google", "72b4b89970": "motor", "16bd69cd82": "buscar", "0628d5943b": "Motor de búsqueda utilizado al escribir texto que no es una URL en la barra de direcciones.", "5e755920c9": "Motor de búsqueda predeterminado", "4596a52cf7": "aterrizaje", - "5164c47e31": "blanco", + "5164c47e31": "en blanco", "4fda4fb066": "URL", "0dbb1eaf4e": "página principal", "291f480a5e": "hogar", "a942905148": "URL abierta al crear una nueva pestaña del navegador. Déjelo vacío para abrir una pestaña en blanco.", "c3903322d2": "Página de inicio predeterminada", - "19ea5607cf": "Etiquetas del árbol de trabajo de localhost", - "4e0fdf0a3f": "Abra los puertos del espacio de trabajo como URL de host local de Orca específicas del árbol de trabajo para que las pestañas del navegador sean más fáciles de distinguir." + "19ea5607cf": "Etiquetas de localhost para worktrees", + "4e0fdf0a3f": "Abre los puertos del workspace como URL localhost de Orca específicas del worktree para que las pestañas del navegador sean más fáciles de distinguir." }, "use": { "search": { @@ -7063,16 +7063,16 @@ "62e2a790c0": "sesión existente", "63a66da648": "navegador del sistema", "20c1323d1e": "uso de la computadora", - "ab349a2dd0": "arco", - "2e1b09897b": "borde", - "088e7a9012": "cromo", + "ab349a2dd0": "Arc", + "2e1b09897b": "Edge", + "088e7a9012": "Chrome", "96ce3d2de2": "autenticación", - "48557f639c": "acceso", + "48557f639c": "inicio de sesión", "d5ad1f7aad": "importar", "02837ee497": "sesión", - "fb8178824f": "galletas", + "fb8178824f": "cookies", "ba4eb53b72": "uso del navegador", - "2fb24d17db": "Importe cookies de Chrome, Edge u otros navegadores para que los agents puedan reutilizar sus inicios de sesión.", + "2fb24d17db": "Importa cookies de Chrome, Edge u otros navegadores para que los agentes puedan reutilizar tus inicios de sesión.", "614c756ab1": "Importar cookies del navegador", "cee44fb442": "automatización", "a57c2172dc": "navegador-agente", @@ -7080,17 +7080,17 @@ "f5b8fdddf5": "orca-cli", "e5a784bc54": "instalar", "9d97446873": "agent", - "a2d489263e": "habilidad", - "a7e82445fa": "Instale la habilidad Uso del navegador para que los agents puedan operar el navegador de Orca.", - "a1414dcefb": "Instalar la habilidad de uso del navegador", + "a2d489263e": "skill", + "a7e82445fa": "Instala el skill Browser Use para que los agentes puedan controlar el navegador de Orca.", + "a1414dcefb": "Instalar skill Browser Use", "e56c7b55c9": "configuración", - "034c5e8d7f": "permitir", - "7e0dcb257a": "caparazón", - "3ffafc9b95": "dominio", - "30c74aaa1f": "camino", + "034c5e8d7f": "habilitar", + "7e0dcb257a": "shell", + "3ffafc9b95": "comando", + "30c74aaa1f": "PATH", "ff05cbc344": "orca", - "85fab5e12c": "cli", - "890ddf943d": "Registre la CLI de Orca para que los agents puedan controlar el navegador.", + "85fab5e12c": "CLI", + "890ddf943d": "Registra la CLI de Orca para que los agentes puedan controlar el navegador.", "50f0860e18": "Habilitar CLI de Orca" } } @@ -7104,32 +7104,32 @@ "8e9cc598d7": "generar", "b7d50da4d8": "plantilla", "7e264b926b": "borrador", - "b261c88609": "pr", - "110be48b81": "solicitud de extracción", + "b261c88609": "PR", + "110be48b81": "pull request", "001ca3f2af": "Valores predeterminados utilizados cuando se abre el compositor Crear PR.", "eefd33788c": "Valores predeterminados de creación de PR", "d32936bb2a": "rama", "127d512e75": "commit", "d22a6459e4": "conflictos", "53e8504fb2": "ci", - "c46e665f7e": "cheques", + "c46e665f7e": "checks", "37c65bbb44": "arreglar", - "402f101af8": "inmediato", + "402f101af8": "prompt", "8e0bcc5d99": "modelo", - "f4731b22bf": "dominio", - "57c851a68c": "cli", + "f4731b22bf": "comando", + "57c851a68c": "CLI", "61117e57f3": "argumentos", "0f29331fed": "argumentos", - "18b6d38835": "Agente, argumentos CLI y plantilla de comando utilizados por cada botón AI de control de código fuente.", + "18b6d38835": "Agente, argumentos de CLI y plantilla de comando usados por cada botón de IA de control de código fuente.", "3c4e5e5938": "Recetas de acción", "ee14a9e9f7": "activado", - "82109d627d": "control de fuente", + "82109d627d": "control de código fuente", "542e1a00a7": "codex", "f121bec167": "claude", "93e5210da8": "mensaje", - "c33cb1b982": "ai", - "0b946b2abe": "Agrega recetas de acciones para acciones de commit de control de código fuente, solicitud de extracción, nombre de rama y reparación.", - "24dbdfca78": "Mostrar acciones de IA de control de código fuente" + "c33cb1b982": "IA", + "0b946b2abe": "Agrega recetas de acciones para commit, pull request, nombre de rama y correcciones de control de código fuente.", + "24dbdfca78": "Mostrar acciones de IA para control de código fuente" } } } @@ -7137,13 +7137,13 @@ "computer": { "use": { "search": { - "6e88da3508": "habilidad", + "6e88da3508": "skill", "798be54d7e": "automatización", "e27f8bafbf": "captura de pantalla", "26c1290d83": "grabación de pantalla", "82f01c2d2c": "accesibilidad", "fefb452f5b": "uso de la computadora", - "9210db582b": "Permita que los agents inspeccionen capturas de pantalla y operen aplicaciones locales cuando usted lo solicite.", + "9210db582b": "Permite que los agentes inspeccionen capturas de pantalla y controlen aplicaciones locales cuando lo pidas.", "442bec10fe": "Uso de la computadora" } } @@ -7154,13 +7154,13 @@ "3363889768": "Red local, USB y Bluetooth", "6c82846f66": "dispositivo", "11653d3f42": "mdns", - "78a10b826f": "buen día", + "78a10b826f": "bonjour", "e3fbc48083": "bluetooth", "c4a4a02ea4": "USB", "fa3239cd42": "red local", "acad3d4743": "Permitir que las herramientas del dispositivo y de la red local se utilicen desde sesiones de terminal.", - "3e0131e45d": "nube", - "ce07159ff5": "de oficina", + "3e0131e45d": "iCloud", + "ce07159ff5": "escritorio", "a0c19119fb": "descargas", "4438f81bfa": "documentos", "c10e36cbd1": "acceso completo al disco", @@ -7175,7 +7175,7 @@ "39bb49e662": "Grabación de pantalla y accesibilidad", "00e954319e": "susurro", "1e6e27b202": "ffmpeg", - "f061f08b7b": "medias", + "f061f08b7b": "sox", "a765112513": "video", "b192432ef0": "audio", "af122938a3": "voz", @@ -7184,7 +7184,7 @@ "6eca1636b7": "Permitir herramientas de captura de voz, transcripción, cámara web y medios.", "302c0c42f9": "Micrófono y cámara", "4e225e7c56": "herramientas de desarrollo", - "6db4fca386": "macos", + "6db4fca386": "macOS", "0c13b249e3": "tcc", "2270ccff3f": "privacidad", "a98aa11a9c": "permisos", @@ -7195,41 +7195,41 @@ }, "experimental": { "search": { - "44c7f209d5": "módulos_nodo", - "4ad605f222": "ambiente", + "44c7f209d5": "node_modules", + "4ad605f222": "env", "3021571c30": "compartido", - "f082788cfe": "campo de golf", + "f082788cfe": "enlaces", "3028f0bd3a": "enlace", "bff1ff7768": "enlaces simbólicos", "c387565812": "enlace simbólico", - "10b52f79c1": "árboles de trabajo", - "d23ae13990": "árbol de trabajo", + "10b52f79c1": "worktrees", + "d23ae13990": "worktree", "0d24759f14": "experimental", - "603d29ed74": "Materializa automáticamente archivos o carpetas configurados en árboles de trabajo recién creados usando APFS clone-copy en macOS cuando es posible; de lo contrario, usa enlaces simbólicos.", - "78c2a8dc74": "Rutas compartidas en árboles de trabajo", + "603d29ed74": "Materializa automáticamente archivos o carpetas configurados en worktrees recién creados usando APFS clone-copy en macOS cuando sea posible; de lo contrario, usa symlinks.", + "78c2a8dc74": "Rutas compartidas en worktrees", "7b79081695": "no leído", - "f10d307468": "terminación", + "f10d307468": "finalización", "5f067ba0f9": "agent", "7695fd30e9": "notificación", "8facf10138": "campana", - "edc49480a1": "cristal", - "268e99d957": "destacar", + "edc49480a1": "panel", + "268e99d957": "resaltado", "01567f19ca": "atención", "9bb3bd5098": "terminal", - "11877246fc": "Resaltado de panel persistente para eventos de finalización de agente y timbre de terminal.", - "9e4ddf776d": "Atención terminal", + "11877246fc": "Resaltado persistente del panel para eventos de timbre de terminal y finalización de agentes.", + "9e4ddf776d": "Atención de terminal", "fe5688b761": "barra lateral", "ca5d1f3f46": "línea de tiempo", "d01b3882ba": "notificaciones", "244a0ecd3d": "actividad", - "92a9357d1f": "vista de agents", + "92a9357d1f": "vista de agentes", "fa72e71f05": "agentes", - "4d63251595": "Feed integrado en la barra lateral izquierda para completar agentes y estados de bloqueo.", - "ccc5548ac5": "Vista de Agents", + "4d63251595": "Feed en hilos en la barra lateral izquierda para finalizaciones de agentes y estados bloqueantes.", + "ccc5548ac5": "Vista de agentes", "9af7a518db": "personaje", "791fefc0b0": "esquina", "65df471ab2": "animado", - "9f5609bfb8": "cubrir", + "9f5609bfb8": "superposición", "2a33975d72": "mascota", "b54cea709b": "compañero", "051203d37c": "mascota", @@ -7256,8 +7256,8 @@ "worktrees": "worktrees" }, "nativeChat": { - "title": "chat nativo", - "description": "Obtenga una vista previa de la superficie de chat del escritorio para las sesiones de terminal de Claude y Codex." + "title": "Chat nativo", + "description": "Previsualiza la vista de chat de escritorio para sesiones de terminal de Claude y Codex." } } }, @@ -7265,8 +7265,8 @@ "workspace": { "search": { "94f4d013c8": "barra de estado", - "a452146574": "botón de alternancia", - "6765b85e48": "directorio de inicio", + "a452146574": "botón para alternar", + "6765b85e48": "directorio inicial", "a38bfc3f77": "panel rápido", "52db6e3baf": "notas", "156ffeee08": "nota", @@ -7277,43 +7277,43 @@ "ebeedb2f6a": "terminal rápido", "6f183fa1b9": "terminal flotante", "a08e482f6d": "espacio de trabajo flotante", - "b96b5ee6cf": "Habilite el espacio de trabajo flotante, elija dónde comienzan las nuevas pestañas y elija dónde aparece el botón de alternancia.", + "b96b5ee6cf": "Habilita el espacio de trabajo flotante, elige dónde comienzan las nuevas pestañas y dónde aparece el botón para alternar.", "b2b60e7163": "Espacio de trabajo flotante" } } }, "general": { "search": { - "bdfb6dc21b": "como", - "e6b01c8e30": "comentario", - "b65665703a": "apoyo", + "bdfb6dc21b": "me gusta", + "e6b01c8e30": "comentarios", + "b65665703a": "soporte", "06ea5a69a6": "github", "e4fb4516d0": "estrella", - "e0b8c8bc25": "Apoye el proyecto con una estrella de GitHub a través de la CLI de gh.", - "36a72f0d9e": "Orca estrella en GitHub", - "c61b14be7c": "asimilar", + "e0b8c8bc25": "Apoya el proyecto con una estrella de GitHub mediante la CLI gh.", + "36a72f0d9e": "Dar una estrella a Orca en GitHub", + "c61b14be7c": "grok", "5d9ba08673": "copiloto", - "f472e97440": "ayudante", + "f472e97440": "aider", "3c30fe2d51": "gemini", "5fdf1dc2d1": "omp", "9b0bc30160": "pi", - "882c4896fd": "código abierto", + "882c4896fd": "opencode", "27d9b996ba": "codex", - "5baf51c4d9": "claudio abierto", - "aea7d2cccb": "claudia abierta", + "5baf51c4d9": "open claude", + "aea7d2cccb": "openclaude", "95b63edde7": "claude", "41c2f9a025": "por defecto", "8ea37a05bc": "agent", "e2da948f59": "Preseleccione un agente de codificación de IA en el compositor del nuevo espacio de trabajo.", "db11502270": "Agente predeterminado", - "3462308bd3": "fichas", + "3462308bd3": "tokens", "660528b048": "costo", "585beac3f8": "ttl", - "0efc9d96ad": "inmediato", - "939b80f5fd": "minutero", + "0efc9d96ad": "prompt", + "939b80f5fd": "temporizador", "b2601a778c": "caché", - "40c9585e43": "Temporizador de cuenta regresiva que muestra el tiempo hasta que caduque el caché de mensajes (agents Claude).", - "1e0f28c6f1": "Temporizador de caché rápido", + "40c9585e43": "Temporizador de cuenta regresiva que muestra el tiempo hasta que caduque la caché de prompts (agentes Claude).", + "1e0f28c6f1": "Temporizador de caché de prompts", "e49e739a59": "descargar", "c9d8c1ce66": "notas de lanzamiento", "9e86ccd05c": "versión", @@ -7322,13 +7322,13 @@ "e15af4eb64": "Buscar actualizaciones", "6382fe9724": "npx", "baa263d6d8": "agentes", - "bda108e66c": "habilidad", - "244e3fb4c8": "Instale la habilidad Orca para que los agents sepan cómo usar la CLI de Orca.", - "2d9f7b42df": "Habilidad del Agent", + "bda108e66c": "skill", + "244e3fb4c8": "Instala el skill de Orca para que los agentes sepan usar la CLI de Orca.", + "2d9f7b42df": "Skill de agente", "0a00691c06": "comando de shell", - "dbeb1f348e": "dominio", + "dbeb1f348e": "comando", "88d3df9ce9": "terminal", - "fb4f338a3d": "camino", + "fb4f338a3d": "PATH", "924a660a78": "cli", "ca529079bf": "Registre o elimine el comando CLI de Orca.", "327e3fa70d": "CLI de Orca", @@ -7339,80 +7339,80 @@ "750420dd9a": "control", "fe62b3f09f": "control", "2a254b725e": "pestaña", - "ca812803ea": "orden de tabulación reciente", - "e53d585ed6": "Tira reciente o de pestañas.", - "256d92554d": "Orden de tabulación", + "ca812803ea": "orden de pestañas recientes", + "e53d585ed6": "Recientes o tira de pestañas.", + "256d92554d": "Orden de pestañas", "22572e99c1": "anotaciones", "1ff67ba40c": "notas", - "4dd5684836": "revisar", + "4dd5684836": "revisión", "d05f629d2c": "markdown", - "694613d47f": "Muestre los controles de notas de revisión de markdowns locales en el modo de editor enriquecido.", - "128bc09325": "Notas de revisión de Markdowns", - "a0014961ae": "voluta", + "694613d47f": "Muestra los controles de notas de revisión Markdown locales en el modo de editor enriquecido.", + "128bc09325": "Notas de revisión Markdown", + "a0014961ae": "desplazamiento", "3ca5ab78a5": "código", "e3919429c0": "descripción general", "9c72990db8": "minimapa", "716a4dfb1f": "Muestra la descripción general del minimapa al editar un archivo.", "6f584fcb48": "Minimapa", "19baae651b": "barra lateral", - "973ed6bfbf": "diferencia combinada", + "973ed6bfbf": "diff combinado", "0a02059549": "árbol de archivos", "2f42852568": "árbol", - "3b5733573e": "diferencia", - "dec71988f0": "Muestre u oculte el árbol de archivos al abrir vistas de diferencias combinadas.", - "adec13f2ef": "Árbol de archivos de diferencias predeterminado", + "3b5733573e": "diff", + "dec71988f0": "Muestra u oculta el árbol de archivos al abrir vistas de diff combinado.", + "adec13f2ef": "Árbol de archivos de diff predeterminado", "be24c7cd67": "dividir", "233f7e2f37": "lado a lado", "0a5fa65926": "en línea", "2b463f0bf9": "vista", - "ecb9415c80": "Formato de presentación preferido para mostrar diferencias de git de forma predeterminada.", - "2760c9933f": "Vista de diferencias predeterminada", + "ecb9415c80": "Formato de presentación preferido para mostrar diffs de git de forma predeterminada.", + "2760c9933f": "Vista de diff predeterminada", "b2799ba622": "milisegundos", "146728ac2c": "demora", "86f54575c7": "guardado automático", "8ea61ad55c": "Cuánto tiempo espera Orca después de su última edición antes de guardar automáticamente.", "14e46c745b": "Retraso de guardado automático", - "4469b6fa4e": "ahorrar", - "e9d948d3c3": "Guarde el editor y los cambios de diferencias editables automáticamente después de una breve pausa.", + "4469b6fa4e": "guardar", + "e9d948d3c3": "Guarda automáticamente los cambios del editor y de diffs editables después de una breve pausa.", "ae21e806ce": "Guardar archivos automáticamente", "c56cb6f1c2": "red", "3566fce83f": "servidor local", "91a46caafc": "no_proxy", "3a73054565": "derivación", - "20b711ac9e": "apoderado", - "eb8946b2c9": "Hosts que deberían omitir el proxy HTTP configurado.", + "20b711ac9e": "proxy", + "eb8946b2c9": "Hosts que deben omitir el proxy HTTP configurado.", "8436ff6f8e": "Reglas de omisión de proxy", - "e55d62dfa4": "plataforma de lanzamiento", - "9da6c875e5": "muelle", + "e55d62dfa4": "launchpad", + "9da6c875e5": "dock", "b9096a44cf": "https_proxy", "8f03d44672": "http_proxy", - "e3b1d42f95": "URL proxy para solicitudes de red Orca y terminales secundarios locales.", + "e3b1d42f95": "URL del proxy para solicitudes de red de Orca y procesos de terminal locales secundarios.", "c29f23ab57": "Proxy HTTP", "6c2ce8457c": "explorador de archivos", - "c9d9636f24": "descubridor", - "68d03d9980": "código vs", + "c9d9636f24": "finder", + "68d03d9980": "vscode", "ebf8f056b5": "zed", "0cb3d94f00": "cursor", - "8fb00fcd05": "lanzacohetes", + "8fb00fcd05": "lanzador", "e1ee631696": "editor", "5a9df5566f": "abrir menú", "b8093e9a93": "abrir en", - "a916662068": "Elija las aplicaciones disponibles en el menú Abrir en de un espacio de trabajo.", - "451d4af994": "Abrir en aplicaciones", + "a916662068": "Elige las apps disponibles en el menú Abrir en de un espacio de trabajo.", + "451d4af994": "Apps de Abrir en", "7e9b556873": "saltar", "ca86dd6e27": "diálogo", "9f8558233a": "confirmar", "7edf4f69e2": "automatización", "84c67d0108": "borrar", "a0c44061ee": "Muestra un cuadro de diálogo de confirmación antes de eliminar una automatización y su historial de ejecución.", - "d0a65b27fd": "Pregunte antes de eliminar automatizaciones", - "df10666259": "árbol de trabajo", + "d0a65b27fd": "Preguntar antes de eliminar automatizaciones", + "df10666259": "worktree", "ae98c9cf36": "Muestra un cuadro de diálogo de confirmación antes de eliminar un espacio de trabajo.", "913242091d": "Preguntar antes de eliminar espacios de trabajo", "93f6ec5e70": "directorio", "9bde064915": "subcarpeta", "ec5049e510": "anidado", - "b9cffd374d": "Cree espacios de trabajo dentro de una subcarpeta con nombre de repo.", + "b9cffd374d": "Crea espacios de trabajo dentro de una subcarpeta con el nombre del repositorio.", "141f71c69f": "Espacios de trabajo anidados", "7887a2c262": "carpeta", "7baf524b04": "espacio de trabajo", @@ -7420,10 +7420,10 @@ "4c95d08fa2": "Directorio de espacio de trabajo", "161a86a9da": "Confirmar antes de cerrar pestañas fijadas", "8e593f04fc": "Muestra un diálogo de confirmación antes de cerrar una pestaña fijada.", - "defaultProjectRuntime": "Default Project Runtime", - "defaultProjectRuntimeDescription": "Choose the runtime inherited by local Windows projects.", - "d2d2d929c0": "Rich Markdown Spellcheck", - "4497e2e2bb": "Show browser spelling underlines and suggestions while editing rich Markdown." + "defaultProjectRuntime": "Runtime de proyecto predeterminado", + "defaultProjectRuntimeDescription": "Elige el runtime que heredarán los proyectos locales de Windows.", + "d2d2d929c0": "Corrector ortográfico de Rich Markdown", + "4497e2e2bb": "Muestra subrayados y sugerencias ortográficas del navegador al editar Rich Markdown." } }, "git": { @@ -7432,46 +7432,46 @@ "1b93c1143c": "atribución", "8461c908ae": "coautor", "61f9f5d1fc": "coautor", - "af0a144bfb": "asunto", - "6bdea421bb": "pr", + "af0a144bfb": "issue", + "6bdea421bb": "PR", "16f53f7323": "gh", "d088806071": "github", - "118c23484b": "Agregue la atribución de Orca a commits, PR y problemas.", + "118c23484b": "Agrega la atribución de Orca a commits, PR e issues.", "bc7d9f69ce": "Atribución de Orca", "40f9b815fd": "presupuesto API", "b7e52124c7": "límite de tasa", - "ead733645f": "glabro", + "ead733645f": "glab", "4808f065b3": "gitlab", - "2b4a72885d": "Encabezados de límite de velocidad REST de GitLab CLI actuales cuando estén disponibles.", + "2b4a72885d": "Encabezados actuales de límite de tasa REST de la CLI de GitLab cuando estén disponibles.", "83ecb3f470": "Presupuesto de la API de GitLab", - "65b69d9f80": "graficoql", + "65b69d9f80": "graphql", "1139f61512": "Límites de velocidad actuales de GitHub CLI REST, Search y GraphQL.", "ff86e354c4": "Presupuesto de la API de GitHub", - "035134fcd9": "árbol de trabajo", - "0c75583ca9": "sin peligro", - "bae91effdd": "base fresca", + "035134fcd9": "worktree", + "0c75583ca9": "de forma segura", + "bae91effdd": "base actualizada", "de06e9d105": "referencia base", - "ab0e22c9f6": "actualizar local principal", - "d9f70d51a0": "principal obsoleto", - "0849b571fe": "A hoy", - "c41e345153": "detrás principal", - "6ee3cfff02": "diferencia git", - "564942ffc5": "origen/principal", - "28192e3a63": "maestro", - "e3e9adde59": "principal", - "0e993bf00f": "Cuando crea un espacio de trabajo, Orca actualiza la base remota y adelanta de forma segura su rama local coincidente, como principal o maestra. Esto evita que comandos como git diff main...HEAD se comparen con el historial obsoleto. Orca omite la actualización si esa rama tiene cambios no confirmados o commits solo locales.", - "f8bda25f29": "Mantenga actualizado el principal local", - "769ddd7f81": "costumbre", + "ab0e22c9f6": "actualizar main local", + "d9f70d51a0": "main obsoleta", + "0849b571fe": "actualizada", + "c41e345153": "por detrás de main", + "6ee3cfff02": "git diff", + "564942ffc5": "origin/main", + "28192e3a63": "master", + "e3e9adde59": "main", + "0e993bf00f": "Cuando creas un espacio de trabajo, Orca actualiza la base remota y adelanta de forma segura la rama local correspondiente, como main o master. Esto evita que comandos como git diff main...HEAD se comparen con un historial obsoleto. Orca omite la actualización si esa rama tiene cambios sin commit o commits solo locales.", + "f8bda25f29": "Mantener main local actualizada", + "769ddd7f81": "personalizado", "1d2fae1fa2": "nombre de usuario git", - "f83c8937c4": "denominación de sucursales", - "5ecd91c5ef": "Prefijo agregado a los nombres de las ramas al crear árboles de trabajo.", + "f83c8937c4": "nombres de ramas", + "5ecd91c5ef": "Prefijo agregado a los nombres de ramas al crear worktrees.", "68bd65fdb8": "Prefijo de rama", "sourceControlGroupOrderTitle": "Orden de grupos de control de código fuente", "sourceControlGroupOrderDescription": "Elige si Cambios, Cambios preparados o Archivos sin seguimiento aparecen primero en Control de código fuente.", "groupOrder": "orden de grupos", "changesFirst": "cambios primero", "stagedFirst": "cambios preparados primero", - "untrackedFirst": "sin seguimiento primero", + "untrackedFirst": "archivos sin seguimiento primero", "sourceControl": "control de código fuente", "gitChanges": "cambios de git", "compareAgainstUpstreamTitle": "Base de comparación predeterminada", @@ -7494,15 +7494,15 @@ "26c83b06c5": "Linux", "71905435dd": "x11", "7059cfb00a": "portapapeles", - "c4440c3986": "pasta", - "5fb84ba77f": "ratón del medio", + "c4440c3986": "pegar", + "5fb84ba77f": "botón central del mouse", "31ba58c8ae": "clic central", "de51e18ee9": "selección primaria", "e5cd0e7a46": "selección", "e25165320e": "edición", - "b51d47ceb7": "aporte", + "b51d47ceb7": "entrada", "874d88f4a6": "Habilitado de forma predeterminada en Linux y macOS. Linux usa el portapapeles de selección del sistema; otras plataformas utilizan un búfer privado.", - "d952ce9b46": "Haga clic con el botón central en Pegar desde la selección" + "d952ce9b46": "Pegar desde la selección con clic central" } }, "integrations": { @@ -7511,31 +7511,31 @@ "3c3d3d8ffa": "conectar", "faa0b5a0d9": "clave API", "c450244ad7": "integración", - "7319e3015b": "lineal", - "16a486a49d": "Conecte Linear para explorar y vincular problemas.", + "7319e3015b": "linear", + "16a486a49d": "Conecta Linear para explorar y vincular issues.", "b027b4b318": "Integración Linear", - "20540996ef": "cartas credenciales", + "20540996ef": "credenciales", "2ec2bd328c": "token de API", - "7345b7c3e6": "atlasiano", + "7345b7c3e6": "atlassian", "e1263dd748": "jira", - "76f6af7c57": "Conecte Jira Cloud o actualice las credenciales del token API de Jira.", + "76f6af7c57": "Conecta Jira Cloud o actualiza las credenciales del token de API de Jira.", "617603509b": "Integración de Jira", - "8c568d761c": "solicitud de extracción", - "33180e8c10": "autohospedado", + "8c568d761c": "pull request", + "33180e8c10": "self-hosted", "129fc59aa8": "gitea", "d0d019dc29": "Autenticación de Gitea a través de variables de entorno de token API.", "aab86d64e5": "Integración de Gitea", - "03a7b275be": "alharaca", - "ed63380247": "repos azules", - "b38b5d27f1": "devops azules", + "03a7b275be": "ado", + "ed63380247": "azure repos", + "b38b5d27f1": "azure devops", "7b1f3984bb": "Autenticación de Azure DevOps Repos mediante variables de entorno de token.", "af6611fa6e": "Integración de Azure DevOps", "50d20817f7": "bitbucket", "c97d58a0f3": "Autenticación de Bitbucket Cloud a través de variables de entorno de token API.", "67a2a0e868": "Integración de Bitbucket", - "371ee914d2": "solicitud de fusión", - "581844769a": "señor", - "b40cbe5de4": "glabro", + "371ee914d2": "merge request", + "581844769a": "MR", + "b40cbe5de4": "glab", "b939695c69": "gitlab", "6e2ab619c6": "Autenticación de GitLab a través de la CLI glab.", "b50b71ef9d": "Integración de GitLab", @@ -7548,10 +7548,10 @@ "jira": { "integration": { "card": { - "8ff73fef62": "Los tokens de Jira están cifrados por el tiempo de ejecución activo y almacenados localmente. Volver a ingresar la misma URL del sitio y el mismo correo electrónico reemplaza el token API de ese sitio.", + "8ff73fef62": "Los tokens de Jira están cifrados por el runtime activo y se almacenan localmente. Volver a ingresar la misma URL del sitio y el mismo correo electrónico reemplaza el token de API de ese sitio.", "9046a20d4c": "Desconectar {{value0}}", "eaffa454e9": "Actualizar", - "cec06a0f79": "Pruebas…", + "cec06a0f79": "Probando…", "ab350991b8": "Verificado", "d914d7ab70": "Verificando…", "5936977fcd": "Cancelar", @@ -7559,19 +7559,19 @@ "1ab7f551f3": "Token API de Atlassian", "09d310e42d": "tu@ejemplo.com", "27dae4ab60": "https://example.atlassian.net", - "a28f417220": "Conecta Jira", + "a28f417220": "Conectar Jira", "9bb34706ca": "Conectado", "efaab83c5d": "Agregar sitio", "09742875cd": "Jira", "255bfe98ec": "Prueba", "5fb1562315": "error", - "3df81cb0ac": "OK", + "3df81cb0ac": "ok", "2e8bb790fd": "Conectar", "33a8b261ee": "Actualizar credenciales", "d5c5b47bb9": "actualizar", "fb854902d9": "conectando", - "9a9f8d4910": "Conecte Jira Cloud para explorar, crear y vincular incidencias.", - "74f3063026": "{{value0}} sitio{{value1}} conectado" + "9a9f8d4910": "Conecta Jira Cloud para explorar, crear y vincular issues.", + "74f3063026": "{{value0}} sitio{{value1}} con conexión" } } }, @@ -7579,67 +7579,67 @@ "emulator": { "search": { "2348045036": "Elija qué dispositivo emulador abre Orca de forma predeterminada.", - "2bb2e09225": "habilidad móvil", + "2bb2e09225": "skill móvil", "bbe4267416": "tipo de emulador", "64494f03c3": "adjuntar emulador", - "6f728f1456": "grifo del emulador", - "f8b871d655": "agente cli", - "2e0b45b2ba": "Utilice los comandos de Orca CLI para enumerar, adjuntar, tocar y escribir en un emulador móvil.", - "ea3eac39bb": "Control CLI del Agent", - "8ef0f08d36": "tiempo de ejecución", - "27397fe8e9": "herramientas de línea de comando xcode", + "6f728f1456": "tap del emulador", + "f8b871d655": "CLI de agente", + "2e0b45b2ba": "Usa comandos de Orca CLI para listar, adjuntar, tocar y escribir en un emulador móvil.", + "ea3eac39bb": "Control por CLI de Agent", + "8ef0f08d36": "runtime", + "27397fe8e9": "herramientas de línea de comandos de Xcode", "7650063d17": "simctl", "3211e7acf9": "xcrun", "42bfab45d8": "disponibilidad", - "ea1f51b980": "Compruebe si los dispositivos Xcode, simctl, server-sim y emulador están listos.", + "ea1f51b980": "Comprueba si Xcode, simctl, serve-sim y los dispositivos emuladores están listos.", "0b95dfd5b3": "Disponibilidad del emulador", "04c5f5d901": "dispositivo", - "25d7bfbcd4": "udid", - "ec3c4043fd": "ipad predeterminado", + "25d7bfbcd4": "UDID", + "ec3c4043fd": "iPad predeterminado", "1dc8c52ffa": "iPhone predeterminado", "ab4814f3c5": "simulador predeterminado", "54184cb9c5": "Dispositivo emulador predeterminado", "b8ddd13195": "emulador de agente", "1ad6fb6230": "dispositivo predeterminado", - "ac0a985873": "habilidad del emulador", - "9353854ff3": "emulador de orcas", - "d4b7833894": "orca cli", - "84e5706975": "Simulación de servicio", - "7c5a8a2bee": "código x", + "ac0a985873": "skill del emulador", + "9353854ff3": "emulador de Orca", + "d4b7833894": "Orca CLI", + "84e5706975": "serve-sim", + "7c5a8a2bee": "Xcode", "bec7231663": "iPad", "49727355a3": "iPhone", "6b6407dc1f": "emulador", "2d67f708ce": "simulador", - "c5eca29310": "simulador de ios", + "c5eca29310": "simulador de iOS", "25159de808": "emulador móvil", - "9595354cff": "Configure la compatibilidad con el emulador móvil para Orca y agents de codificación.", + "9595354cff": "Configura la compatibilidad con el emulador móvil para Orca y agentes de codificación.", "cdd3c31918": "Emulador móvil" } }, "pane": { "search": { - "dbccde3a60": "cerca", - "9e16be01d6": "fondo", - "3a5e31e84b": "dejar", - "8015fd9523": "sostener", - "aa3f736042": "cambiar el tamaño", + "dbccde3a60": "cerrar", + "9e16be01d6": "segundo plano", + "3a5e31e84b": "salir", + "8015fd9523": "mantener", + "aa3f736042": "redimensionar", "356c31d6dc": "ancho", "fadcbfdd99": "adaptar", "ad08035c5f": "teléfono", "6cd2bfdb0e": "restaurar", "b34ad5b3a7": "terminal", "6db86f445f": "móvil", - "707fc78052": "Elija qué sucede con los terminales que estaba viendo en el móvil después de cerrar la aplicación o salir.", - "1e711aca11": "Cuando sales de la aplicación móvil", + "707fc78052": "Elige qué sucede con los terminales que estabas viendo en el móvil después de cerrar la aplicación o cambiar a otra.", + "1e711aca11": "Al salir de la aplicación móvil", "126afc5dbd": "remoto", "70f505f3c3": "LAN", "1802188b5d": "wifi", - "dd6e671aa9": "DIRECCIÓN", + "dd6e671aa9": "dirección", "1f70d63998": "IP", - "d0c89bc4a9": "cubrir", + "d0c89bc4a9": "overlay", "87711f4b8f": "VPN", - "16bff559a0": "red de cola", - "c690e3ee38": "escala de cola", + "16bff559a0": "tailnet", + "c690e3ee38": "Tailscale", "a023683767": "interfaz", "7b37c2e557": "red", "3190ef67a4": "Elija qué dirección de red usar para el emparejamiento móvil.", @@ -7651,9 +7651,9 @@ "13419718b3": "Administrar dispositivos móviles emparejados.", "9d3a9397ba": "Dispositivos conectados", "2128a21096": "escanear", - "e518cbd61c": "par", + "e518cbd61c": "emparejar", "4a0c826f3d": "código", - "3c1807a81a": "qr", + "3c1807a81a": "QR", "7fb728fb2b": "Empareje un dispositivo móvil escaneando un código QR.", "d49925710a": "Emparejamiento móvil" } @@ -7662,16 +7662,16 @@ "search": { "b730ff7049": "experimental", "8d4ba0ef09": "beta", - "6bfa001752": "apk", - "a7eececc1d": "androide", + "6bfa001752": "APK", + "a7eececc1d": "Android", "7e801801ac": "remoto", "0b7e585cb9": "escanear", "59b1d75fd1": "código", - "87816d1c59": "qr", - "cf2c93b479": "par", + "87816d1c59": "QR", + "cf2c93b479": "emparejar", "f4ed142753": "teléfono", "f213400800": "móvil", - "671eb4173c": "Controla terminales y agents desde tu teléfono.", + "671eb4173c": "Controla terminales y agentes desde tu teléfono.", "ffd52a96e4": "Móvil", "1de96ec8a6": "Mostrar botón de Orca Mobile", "682293cadf": "Mostrar el botón de Orca Mobile en la parte superior de la barra lateral izquierda." @@ -7682,17 +7682,17 @@ "search": { "aa288005c3": "prueba", "ca8faa40d7": "notificaciones", - "4e30b1925e": "Active una notificación de escritorio de muestra utilizando la ruta de entrega nativa.", + "4e30b1925e": "Activa una notificación de escritorio de muestra usando el mecanismo nativo de entrega.", "ef9b311346": "Enviar notificación de prueba", - "ecdeff4993": "volumen", + "ecdeff4993": "sonoridad", "d58b64dddf": "volumen", "dc7d7c07cd": "sonido", "eeb6f77322": "Volumen de reproducción para sonidos de notificación que no pertenecen al sistema.", "aace1a62c6": "Volumen de notificaciones", "ef86a782cc": "bong", - "3014ad1b8f": "timbre", - "079c29aeb5": "flaco", - "722face52f": "acac", + "3014ad1b8f": "ding", + "079c29aeb5": "flac", + "722face52f": "aac", "6ecb8418cb": "m4a", "d16ae23645": "ogg", "57e34a31cd": "wav", @@ -7700,48 +7700,48 @@ "6e08f78315": "audio", "c718793e95": "Elija el archivo de audio integrado, del sistema o local que Orca reproduce para las notificaciones de escritorio.", "ea8cb8d9ce": "Sonido de notificación", - "4ada6bfde9": "filtración", - "fa60d8e4ab": "reprimir", + "4ada6bfde9": "filtrado", + "fa60d8e4ab": "suprimir", "a4c3b29a3c": "enfocado", - "7247b97a31": "Evite notificar cuando Orca esté enfocado en el árbol de trabajo activo.", - "96562a72c6": "Suprimir mientras estás concentrado", + "7247b97a31": "Evita enviar notificaciones cuando Orca esté enfocado en el worktree activo.", + "96562a72c6": "Suprimir mientras está enfocado", "a2ab73b325": "atención", "ae0487f8fd": "campana", "c638ae989d": "terminal", - "d3f1c48677": "Notificar cuando un terminal en segundo plano emite un carácter de campana.", - "a5edee1d99": "Campana terminal", + "d3f1c48677": "Notifica cuando un terminal en segundo plano emita un carácter de campana.", + "a5edee1d99": "Campana del terminal", "193e1f107c": "tarea", "dd9d3e5f0f": "idle", - "5f7472d3fb": "completo", + "5f7472d3fb": "completado", "7fa07e9600": "agent", - "10d83ef8dc": "Notificar cuando un agente de codificación pasa de estar en funcionamiento a estar inactivo.", - "bdc1edaeb4": "Tarea del Agent completada", + "10d83ef8dc": "Notifica cuando un agente de codificación pase de estar trabajando a estar inactivo.", + "bdc1edaeb4": "Tarea de Agent completada", "adbc3a0fcf": "nativo", "72539aede4": "sistema", - "51ae2183e1": "de oficina", + "51ae2183e1": "escritorio", "0534c76311": "Interruptor maestro para notificaciones de escritorio de Orca.", "4a210b2f72": "Habilitar notificaciones" } }, "orchestration": { "search": { - "f5d39af41e": "agents infantiles", - "c766a01978": "manos libres", + "f5d39af41e": "agentes secundarios", + "c766a01978": "handoff", "08c65b12a2": "ejemplos", "f278fd04db": "codex", "32c5098e7b": "claude", "21c28ccdf7": "coordinador", - "741dfc03fa": "obrero", - "ca54c69806": "TROZO DE CUERO", + "741dfc03fa": "worker", + "ca54c69806": "DAG", "7ad948b714": "tarea", - "eee028ae14": "despacho", + "eee028ae14": "dispatch", "9a5ebdca31": "mensajería", "91fc8ab7e5": "coordinación", "13ba5c6cbd": "agentes", - "d86705ba77": "multiagente", + "d86705ba77": "multi-agent", "a7f76b4ca7": "orquestación", - "e05ff36753": "Coordine múltiples agents de codificación a través de mensajes, DAG de tareas, despacho y puertas de decisión.", - "c34045764e": "Orquestación de Agents" + "e05ff36753": "Coordina varios agentes de codificación mediante mensajería, DAG de tareas, dispatch y controles de decisión.", + "c34045764e": "Orquestación de agentes" } }, "privacy": { @@ -7749,28 +7749,28 @@ "3922051573": "datos", "e8bc614a18": "desactivar", "d8191ae5ca": "variable de entorno", - "94e04427f6": "ambiente", + "94e04427f6": "env", "664f1a8984": "integración continua", - "5854a5c752": "ci", - "69637f4dc4": "orca_telemetría_disabled", - "058550f6bc": "no_seguimiento", + "5854a5c752": "CI", + "69637f4dc4": "orca_telemetry_disabled", + "058550f6bc": "do_not_track", "83a6cd79b3": "no rastrear", "f7a2d9f137": "Variables de entorno que deshabilitan la transmisión de telemetría.", "e058a3c98d": "Variables de entorno de telemetría", - "1686c07fee": "apoyo", - "c0494ff48a": "diagnóstico", + "1686c07fee": "soporte", + "c0494ff48a": "diagnósticos", "8b08f32366": "Diagnósticos de la aplicación y controles para compartir con soporte.", - "6d258d2ed6": "Diagnóstico", + "6d258d2ed6": "Diagnósticos", "ead1deded2": "compartir", "27a27b2f63": "optar por no participar", - "4d4bb76bf4": "optar por", + "4d4bb76bf4": "optar por participar", "b021b9cb81": "anónimo", "79c319948b": "uso", - "77d3180def": "telemetria", + "77d3180def": "telemetría", "b707cc3981": "Ayude a mejorar Orca enviando eventos anónimos de uso de funciones.", "57b283461a": "Compartir datos de uso anónimos", "2b5a5c312f": "posthog", - "4104f6f0f3": "analítica", + "4104f6f0f3": "analíticas", "10124159f1": "privacidad", "aa3b794c17": "Datos anónimos de uso del producto, diagnósticos y controles de telemetría.", "5c508bad41": "Privacidad y telemetría" @@ -7779,20 +7779,20 @@ "quick": { "commands": { "search": { - "3c316e6ef8": "hilo", - "b86c727100": "mpn", + "3c316e6ef8": "yarn", + "b86c727100": "npm", "b949a7c0a0": "pnpm", - "0b78c4a165": "lanzamiento", - "2d8aff42be": "correr", + "0b78c4a165": "iniciar", + "2d8aff42be": "ejecutar", "1c5bdcd0f2": "repositorio", - "89d2a9ad9f": "repo", + "89d2a9ad9f": "repositorio", "f58b92a48f": "proyecto", "8bf43c2dad": "global", - "a26ecdb77b": "retazo", + "a26ecdb77b": "snippet", "d07d130849": "atajo", "0073cf8ce9": "terminal", "cfffa6cdb6": "comandos", - "fecb031823": "dominio", + "fecb031823": "comando", "236d4cfac8": "rápido", "d691c4e8d8": "Comandos de terminal guardados que se pueden ejecutar desde cualquier terminal, con alcance global o para un proyecto específico.", "4c8945952b": "Comandos rápidos" @@ -7801,21 +7801,21 @@ }, "repository": { "search": { - "bc7e504b8e": ".orca/comando-edición", + "bc7e504b8e": ".orca/issue-command", "603c68b68c": "orca.yaml", "9dc60d7f6d": "github", "ec70364df2": "flujo de trabajo", - "66b584bd6c": "emitir comando", - "2011a6a4f2": "comando de problema de github", - "d42d1e49c0": "Comando de problema vinculado basado en archivos configurado a través de orca.yaml y anulación local opcional.", - "d86ea12d16": "Comando de problema personalizado de GitHub", + "66b584bd6c": "comando de Issue", + "2011a6a4f2": "comando de Issue de GitHub", + "d42d1e49c0": "Comando de Issue vinculado basado en archivo configurado mediante orca.yaml y una anulación local opcional.", + "d86ea12d16": "Comando de Issue personalizado de GitHub", "c5e8bdbcbb": "omitir por defecto", "a69c5cbe90": "ejecutar por defecto", "80c490b012": "preguntar", "f9d84b7971": "política de ejecución de configuración", "c00a549e03": "Elija el comportamiento predeterminado cuando haya un script de configuración disponible.", "cdfe398068": "Cuándo ejecutar la configuración", - "5e9445bbfd": "autorizado", + "5e9445bbfd": "autoritativa", "f1e1bfa89f": "fuente", "1d90a6cfbb": "ambos", "fcb8fa8144": "compartido", @@ -7825,19 +7825,19 @@ "d141897c90": "Fuente del comando y detalles de orca.yaml.", "cc11699c3d": "Avanzado", "bf460fded8": "yaml", - "9cad92fe77": "ganchos orca.yaml", + "9cad92fe77": "hooks de orca.yaml", "6b80f7d3c8": "scripts de configuración local", - "a1a4c51d58": "comando de archivo", - "fbfd2386e8": "script de archivo", - "4c17787d7b": "archivo", - "8655e3387b": "manos", - "acd1157f0c": "Scripts locales y compartidos que se ejecutan antes de archivar un árbol de trabajo.", - "bce0ca23c6": "Guión de archivo", + "a1a4c51d58": "comando de archivado", + "fbfd2386e8": "script de archivado", + "4c17787d7b": "archivar", + "8655e3387b": "hooks", + "acd1157f0c": "Scripts locales y compartidos que se ejecutan antes de archivar un worktree.", + "bce0ca23c6": "Script de archivado", "491b05d6e6": "comando de configuración", - "a31b43a7f8": "guión de configuración", + "a31b43a7f8": "script de configuración", "5590388dfa": "configuración", - "baaf70bb37": "Scripts locales y compartidos que se ejecutan después de crear un nuevo árbol de trabajo.", - "b79df26937": "Guión de configuración", + "baaf70bb37": "Scripts locales y compartidos que se ejecutan después de crear un nuevo worktree.", + "b79df26937": "Script de configuración", "d73fb47b45": ".claude/mcp.json", "db11b337c4": ".claude.json", "26f42fe773": ".cursor/mcp.json", @@ -7846,23 +7846,23 @@ "343f0a508c": "mcp", "3c31801626": "Inspeccione los archivos de configuración del servidor MCP a nivel de proyecto.", "31bd0a2420": "Configuraciones de MCP", - "84da7fa2d7": "módulos_nodo", - "0a3a582794": "ambiente", + "84da7fa2d7": "node_modules", + "0a3a582794": "env", "3c180a251c": "enlace", - "f1c53f2820": "árbol de trabajo", - "7e228fc439": "enlaces simbólicos", - "c06adcf136": "enlace simbólico", - "ed885e589f": "Rutas para materializar desde el checkout principal en árboles de trabajo recién creados.", - "01b3377ebc": "Rutas compartidas del árbol de trabajo", - "fff8834983": "inmediato", + "f1c53f2820": "worktree", + "7e228fc439": "symlinks", + "c06adcf136": "symlink", + "ed885e589f": "Rutas para materializar desde el checkout principal en worktrees recién creados.", + "01b3377ebc": "Rutas compartidas del worktree", + "fff8834983": "prompt", "fa3131f223": "modelo", - "130d76dc16": "rebautizar", - "917dce844a": "nombre de la sucursal", - "8068d8d0f1": "pr", - "5ff7fe1ade": "solicitud de extracción", + "130d76dc16": "renombrar", + "917dce844a": "nombre de rama", + "8068d8d0f1": "PR", + "5ff7fe1ade": "pull request", "eec39b3de6": "mensaje de commit", - "cfad7ce5f3": "ai", - "a47f51127e": "control de fuente", + "cfad7ce5f3": "IA", + "a47f51127e": "control de código fuente", "6cc5c65e64": "Anulaciones de generación de git específicas del proyecto.", "eec3995dc6": "Autor de Git AI", "cc876ca5f2": "repositorio", @@ -7874,26 +7874,26 @@ "4e2529722c": "directorios", "1ff4f12c0c": "directorio", "9f5ae26ccd": "preajustes", - "095fca94fe": "programar", - "aa42616e3d": "verificar", - "4f3c0230c2": "escaso", - "90a331fd68": "Conjuntos de directorios guardados para la creación de árboles de trabajo dispersos.", - "1f0f20bbb6": "Ajustes preestablecidos de pago dispersos", - "4733ec2395": "../árboles de trabajo", + "095fca94fe": "preset", + "aa42616e3d": "checkout", + "4f3c0230c2": "sparse", + "90a331fd68": "Conjuntos de directorios guardados para crear worktrees sparse.", + "1f0f20bbb6": "Presets de sparse checkout", + "4733ec2395": "../worktrees", "58d8bca414": "relativo", "a325a89dff": "ruta del espacio de trabajo", - "f3e6dee5fe": "ruta del árbol de trabajo", - "cd33a5525e": "Directorio específico del proyecto para nuevos árboles de trabajo.", - "443d127b5a": "Ubicación del árbol de trabajo", + "f3e6dee5fe": "ruta del worktree", + "cd33a5525e": "Directorio específico del proyecto para nuevos worktrees.", + "443d127b5a": "Ubicación del worktree", "9811f3d152": "rama", - "f41cef5083": "referencia base", - "f571081ec4": "Rama base o referencia predeterminada al crear árboles de trabajo.", - "094adbe930": "Base de árbol de trabajo predeterminada", + "f41cef5083": "ref base", + "f571081ec4": "Rama base o ref predeterminada al crear worktrees.", + "094adbe930": "Base predeterminada del worktree", "27733eb6c1": "favicon", - "1e73e840ff": "emojis", + "1e73e840ff": "emoji", "cb4b4de666": "avatar", "c1075178cf": "insignia", - "6d8de2f090": "maleficio", + "6d8de2f090": "hex", "8d045419b1": "color", "b2546efab5": "icono del repositorio", "6438a94c63": "icono de proyecto", @@ -7903,16 +7903,16 @@ "92af66c7ce": "nombre del proyecto", "883aad2801": "Detalles de visualización específicos del proyecto para la barra lateral y las pestañas.", "7e1e456a95": "Nombre para mostrar", - "availableHosts": "Available Hosts", - "availableHostsDescription": "Hosts where this project is set up.", + "availableHosts": "Hosts disponibles", + "availableHostsDescription": "Hosts donde está configurado este proyecto.", "host": "host", "ssh": "ssh", "remote": "remote", "vm": "vm", "keepForkUpToDate": "Mantener el fork actualizado", "keepForkUpToDateDescription": "Avanza este fork de forma segura desde upstream con fast-forward.", - "projectRuntime": "Project Runtime", - "projectRuntimeDescription": "Choose whether this project runs on Windows or WSL.", + "projectRuntime": "Runtime del proyecto", + "projectRuntimeDescription": "Elige si este proyecto se ejecuta en Windows o WSL.", "copy": "copy", "clone": "clone", "apfs": "apfs" @@ -7927,15 +7927,15 @@ "5cd7dca3b8": "remoto", "d760866285": "cliente", "09568ccc65": "servidor", - "ebd5369acf": "ambiente", - "d198440ce3": "tiempo de ejecución", + "ebd5369acf": "entorno", + "d198440ce3": "runtime", "baec27aa8f": "Conecte este navegador a un servidor Orca guardado.", - "3517fb2ec0": "Servidor activo", - "c6e5a03aa0": "caja de desarrollo", + "3517fb2ec0": "Servidores Orca remotos", + "c6e5a03aa0": "dev box", "f1575f1e09": "cliente web", "81444c4102": "URL de emparejamiento", "104f4d7dbd": "emparejamiento", - "4575341c77": "Elija el escritorio local, agregue un servidor Orca remoto guardado o genere una URL de emparejamiento." + "4575341c77": "Agrega un servidor Orca remoto guardado, genera una URL de emparejamiento o ajusta el runtime predeterminado avanzado." } } }, @@ -7945,61 +7945,61 @@ "4811a8264a": "terminal primero", "afda131738": "orca primero", "0ecfc47434": "conflicto", - "0f8cb15582": "agent", - "f1adebbe8c": "caparazón", - "7f1b38f59a": "tui", + "0f8cb15582": "agente", + "f1adebbe8c": "shell", + "7f1b38f59a": "TUI", "7e3fc707aa": "terminal", "0ecba9aa5f": "teclado", - "ebd7d81e1d": "Elija si Orca o el terminal enfocado gana cuando los atajos se superponen.", - "f052906167": "Atajos en la Terminal" + "ebd7d81e1d": "Elige si Orca o el terminal con foco tiene prioridad cuando los atajos se superponen.", + "f052906167": "Atajos en terminal" } }, "ssh": { "search": { - "d41f296f64": "silbido", + "d41f296f64": "ping", "237b391f7c": "conexión", - "8cb870b109": "prueba", - "7efd17e816": "ssh", + "8cb870b109": "probar", + "7efd17e816": "SSH", "96ca5d9a0b": "Pruebe la conectividad con un destino SSH.", - "a3058f3605": "Conexión de prueba", - "2cd40ba0d0": "anfitriones", + "a3058f3605": "Probar conexión", + "2cd40ba0d0": "hosts", "5220501141": "configuración", "3b12e064a4": "importar", - "7f251a45a8": "Importe hosts desde ~/.ssh/config.", - "41a3127094": "Importar desde configuración SSH", + "7f251a45a8": "Importa hosts desde ~/.ssh/config.", + "41a3127094": "Importar desde SSH config", "f9493b80c0": "servidor", - "8fb1cc87cc": "anfitrión", + "8fb1cc87cc": "host", "09395490af": "objetivo", "00d1fda01a": "nuevo", "f7b6383aec": "agregar", - "62826efbe9": "Agregue un nuevo destino SSH remoto.", + "62826efbe9": "Agrega un nuevo host SSH.", "f5a691bb6c": "Agregar destino SSH", "d4bcd497c7": "remoto", - "74c6d90d78": "Administre objetivos SSH remotos.", + "74c6d90d78": "Administra hosts SSH.", "380a788da7": "Conexiones SSH" } }, "tasks": { "search": { - "58cda6f9c0": "esconder", + "58cda6f9c0": "ocultar", "44083ae418": "mostrar", - "604d8e4089": "atlasiano", + "604d8e4089": "Atlassian", "5430396e11": "jira", - "412ec3c702": "lineal", + "412ec3c702": "Linear", "11f001cdd4": "gitlab", "c10ac2125e": "github", "3d81c26d78": "fuente", "cf0e3e0c2f": "proveedor", "2ec54bee51": "tareas", - "765f0c544d": "Elija qué proveedores de tareas aparecen en la página Tareas y en los accesos directos de la barra lateral.", + "765f0c544d": "Elige qué proveedores de tareas aparecen en la página Tareas y en los accesos directos de la barra lateral.", "5b8e4aace5": "Proveedores de tareas" } }, "terminal": { "clipboard": { "search": { - "5fb3512e8c": "pasta", - "a38508c419": "Copiar", + "5fb3512e8c": "pegar", + "a38508c419": "copiar", "d106f44fb4": "remoto", "043b32faa1": "ssh", "9fda309db9": "fzf", @@ -8009,21 +8009,21 @@ "10d73e22d3": "portapapeles", "9dfc125cd3": "osc52", "62d1208b90": "osc 52", - "459fea094a": "Permita que los programas en la terminal se copie al portapapeles del sistema a través de OSC 52, incluso a través de SSH.", - "74db8721e4": "Permitir escrituras en el portapapeles TUI (OSC 52)", - "4043e294d2": "gnomo", + "459fea094a": "Permite que los programas del terminal copien al portapapeles del sistema mediante OSC 52, incluso por SSH.", + "74db8721e4": "Permitir escrituras TUI en el portapapeles (OSC 52)", + "4043e294d2": "GNOME", "cf83ac3dbd": "Linux", "737cef6de1": "x11", "e87c6d776d": "automático", "664789b73a": "auto", "c38c18be15": "selección", "797fdfe4ca": "seleccionar", - "603818e8d8": "Copie automáticamente las selecciones de terminal al portapapeles tan pronto como se realice una selección.", + "603818e8d8": "Copia automáticamente las selecciones del terminal al portapapeles en cuanto se hace una selección.", "3bdc84f059": "Copiar al seleccionar" } }, "search": { - "c047f398cc": "lanzamiento", + "c047f398cc": "iniciar", "b872de3926": "ubicación", "fd6c24313d": "nuevo", "f44643328e": "pestaña", @@ -8031,36 +8031,36 @@ "54a9b3725b": "horizontal", "de7bc1d5f5": "dividir", "7a48c7715b": "espacio de trabajo", - "6b659fff2a": "guion", + "6b659fff2a": "script", "4529806908": "configuración", - "2610ee3b56": "Dónde se ejecuta el script de configuración del repositorio cuando se crea un nuevo espacio de trabajo: una división vertical (predeterminada), una división horizontal o una pestaña de fondo titulada 'Configuración'.", + "2610ee3b56": "Dónde se ejecuta el script de configuración del repositorio cuando se crea un nuevo workspace: una división vertical (predeterminada), una división horizontal o una pestaña en segundo plano titulada 'Setup'.", "5be2d67678": "Ubicación del script de configuración", "0ce176909a": "tema", "4ba8623632": "paleta", "11fd3fbcf2": "ansi", "d8bd6182b8": "anular", "674b7c8436": "color", - "3023e01415": "Anula los colores de terminales individuales.", + "3023e01415": "Anula colores individuales del terminal.", "aed2a4b4eb": "Anulaciones de color", "6eaf7ee0e4": "cursor", - "34fe1af39d": "mecanografía", + "34fe1af39d": "escritura", "ee611ae238": "esconder", "ea364ce6e4": "ratón", - "77201c0bb2": "Oculte el cursor del mouse al escribir en la terminal.", - "d1fe5f99ff": "Ocultar el mouse mientras escribe", + "77201c0bb2": "Oculta el cursor del mouse al escribir en el terminal.", + "d1fe5f99ff": "Ocultar el mouse al escribir", "f25d948664": "margen", "b2f52cb96c": "espaciado", "e8baf0d12c": "relleno", "4655567c37": "Relleno vertical alrededor de la cuadrícula del terminal en píxeles.", - "692c4ad032": "Acolchado vertical", + "692c4ad032": "Relleno vertical", "75691e4911": "Relleno horizontal alrededor de la cuadrícula del terminal en píxeles.", - "b4f182f24d": "Acolchado horizontal", - "6c2f9f05c8": "vitalidad", + "b4f182f24d": "Relleno horizontal", + "6c2f9f05c8": "vibrancy", "4f7f8f28ca": "transparencia", "f6dd9ff606": "fondo", "71eb45e293": "difuminar", "0838b3717b": "ventana", - "bc2054657a": "Aplique desenfoque de fondo a la ventana del terminal. Requiere reinicio.", + "bc2054657a": "Aplica desenfoque de fondo a la ventana del terminal. Requiere reiniciar.", "72d0482137": "Desenfoque de ventana", "7db59c4738": "alfa", "46d99ef4bb": "opacidad", @@ -8070,23 +8070,23 @@ "88561b3499": "congelado", "0a05629060": "recuperar", "f66a7cf715": "terminal", - "6892fb1019": "Reanudar", - "cde233f5da": "desplazarse hacia atrás", + "6892fb1019": "reiniciar", + "cde233f5da": "scrollback", "3982d88725": "historia", - "456da64d4d": "claro", - "920573d65b": "matar a todos", - "a3e5297c10": "matar", + "456da64d4d": "borrar", + "920573d65b": "finalizar todo", + "a3e5297c10": "finalizar", "a8d2784214": "administrar", "d802a578bf": "sesiones", "9f2dda133c": "pty", - "f35400f7e8": "demonio", - "f72abc493c": "Recupérese de terminales congeladas finalizando sesiones, borrando el desplazamiento hacia atrás guardado o reiniciando el demonio.", + "f35400f7e8": "servicio", + "f72abc493c": "Recupérate de terminales congelados finalizando sesiones, borrando el scrollback guardado o reiniciando el servicio.", "6f5d486a68": "Administrar sesiones", "10f9fb6fea": "ajustes", "2ade3ea490": "configuración", "fd752b3cac": "importar", - "82b63d07fe": "fantasmal", - "73e9422f19": "Importación única de configuraciones de terminal Ghostty compatibles.", + "82b63d07fe": "ghostty", + "73e9422f19": "Importación única de ajustes de terminal compatibles de Ghostty.", "a979df0083": "Importar desde Ghostty", "4cec42dbf7": "internacional", "b495dc6a9f": "jis", @@ -8120,92 +8120,92 @@ "rows": "filas", "f7d56b6281": "Filas de terminal de escritorio retenidas.", "7674e758e1": "Filas de desplazamiento hacia atrás", - "411229c636": "luz", + "411229c636": "claro", "781f49d942": "divisor", - "77d9f9cd55": "Controla la línea divisoria dividida entre paneles en modo claro.", - "595b97b446": "Color del divisor de luz", - "7718d70356": "avance", + "77d9f9cd55": "Controla la línea divisoria entre paneles en modo claro.", + "595b97b446": "Color del divisor claro", + "7718d70356": "vista previa", "1dee533bd9": "Elige el tema utilizado cuando Orca está en modo claro.", - "1d89457764": "Tema ligero", - "da864e6cec": "modo de luz", + "1d89457764": "Tema claro", + "da864e6cec": "modo claro", "f268092ee3": "El modo claro puede usar su propio tema de terminal.", "match_dark_mode_title": "Coincidir con modo oscuro", "f785374072": "oscuro", "9c32726f47": "Controla la línea divisoria entre paneles en modo oscuro.", - "8987db7ff2": "Color divisor oscuro", - "13f6310dd3": "Elija el tema del terminal utilizado en modo oscuro.", + "8987db7ff2": "Color del divisor oscuro", + "13f6310dd3": "Elige el tema del terminal usado en modo oscuro.", "ec07ce9b02": "Tema oscuro", "f036794286": "activo", - "846a7a1204": "cristal", - "d1fa00a9cb": "flotar", + "846a7a1204": "panel", + "d1fa00a9cb": "pasar el cursor", "b5116e7b12": "sigue", - "f5d1e3d472": "enfocar", - "17cc3ea102": "Al pasar el cursor sobre un panel de terminal, se activa sin necesidad de hacer clic. Refleja la configuración de enfoque que sigue al mouse de Ghostty. Las selecciones y el cambio de ventana se mantienen seguros.", - "c6178a2b4d": "El foco sigue al ratón", - "f637a7dee9": "espesor", + "f5d1e3d472": "foco", + "17cc3ea102": "Al pasar el cursor sobre un panel de terminal, se activa sin tener que hacer clic. Refleja la configuración focus-follows-mouse de Ghostty. Las selecciones y el cambio de ventana se mantienen seguros.", + "c6178a2b4d": "El foco sigue al mouse", + "f637a7dee9": "grosor", "e58d4040d0": "Grosor de la línea divisoria del panel.", "2d5ab88b7c": "Grosor del divisor", "6c4c85ba43": "oscurecimiento", - "18dd5026c6": "Opacidad aplicada a paneles que no están actualmente activos.", + "18dd5026c6": "Opacidad aplicada a los paneles que no están activos actualmente.", "72bbcbd1dd": "Opacidad del panel inactivo", - "d4f7d1ce5c": "Opacidad del cursor terminal.", + "d4f7d1ce5c": "Opacidad del cursor del terminal.", "7f1e356a54": "Opacidad del cursor", - "25f606d9e5": "parpadear", + "25f606d9e5": "parpadeo", "a27f6edf52": "Utiliza la variante parpadeante de la forma del cursor seleccionada.", "b03d01fd49": "Cursor parpadeante", "eefd1d8332": "subrayar", - "015c82349f": "bloquear", - "a6e9dcc829": "bar", - "275a9d6395": "Apariencia predeterminada del cursor para los paneles del terminal Orca.", + "015c82349f": "bloque", + "a6e9dcc829": "barra", + "275a9d6395": "Apariencia predeterminada del cursor para los paneles de terminal de Orca.", "97bcfff662": "Forma del cursor", "1abcf4d7de": "Linux", "7d924d870d": "gráficos", - "bc7ae1f7c0": "representación", + "bc7ae1f7c0": "renderizado", "fffa9ab980": "renderizador", "6cddc858ba": "webgl", "4b4e80d850": "aceleración", "db82cb13b0": "GPU", "8f9f953de7": "Controla si el terminal utiliza la representación WebGL de xterm.js. Auto prueba WebGL cuando el renderizador es compatible, con respaldo conservador para software o renderizadores de GPU desconocidos.", "13a2502dfc": "Aceleración de GPU", - "d5e6c7fab1": "características de la fuente", - "a16224d16a": "calto", + "d5e6c7fab1": "características de fuente", + "a16224d16a": "calt", "6ded6297fe": "iosevka", - "e3aeea308e": "código cascada", - "35c2311a33": "cerebros jet mono", - "7f7640c29e": "codigo fira", + "e3aeea308e": "cascadia code", + "35c2311a33": "jetbrains mono", + "7f7640c29e": "fira code", "7ab424c4d3": "ligadura", "afc8d5f790": "ligaduras", "103cdb862f": "tipografía", - "893aa92997": "Renderice ligaduras de programación (por ejemplo, => → ≠ ≥) para las fuentes que las incluyen. \"Auto\" habilita ligaduras solo para fuentes de ligadura conocidas (Fira Code, JetBrains Mono, Cascadia Code, Iosevka, etc.).", + "893aa92997": "Renderiza ligaduras de programación (p. ej., => → ≠ ≥) para las fuentes que las incluyen. \"Auto\" habilita ligaduras solo para fuentes con ligaduras conocidas (Fira Code, JetBrains Mono, Cascadia Code, Iosevka, etc.).", "58da1ae45d": "Ligaduras de fuentes", - "7341e3d00e": "altura de la línea", - "36a1b38bc8": "Controla el multiplicador de altura de la línea terminal.", + "7341e3d00e": "altura de línea", + "36a1b38bc8": "Controla el multiplicador de altura de línea del terminal.", "0f2fb0cb74": "Altura de línea", "20ce287cc6": "peso", - "98c18f2c77": "Controla el peso de la fuente del texto del terminal.", + "98c18f2c77": "Controla el grosor de la fuente del texto del terminal.", "28ea41bd2d": "Peso de fuente", "b0bb76ae6b": "fuente", - "0acdc17891": "Familia de fuentes de terminal predeterminada para nuevos paneles y actualizaciones en vivo.", + "0acdc17891": "Familia de fuentes predeterminada del terminal para nuevos paneles y actualizaciones en vivo.", "e989914ad6": "Familia de fuentes", "33031c1465": "tamaño del texto", - "0fe0073f0c": "Tamaño de fuente de terminal predeterminado para nuevos paneles y actualizaciones en vivo.", + "0fe0073f0c": "Tamaño de fuente predeterminado del terminal para nuevos paneles y actualizaciones en vivo.", "5930244899": "Tamaño de fuente", "warp_import": { "title": "Importar desde Warp", - "description": "Import Warp themes as Orca terminal themes.", + "description": "Importa temas de Warp como temas de terminal de Orca.", "keyword_warp": "warp", - "keyword_themes": "themes", + "keyword_themes": "temas", "keyword_yaml": "yaml", - "keyword_legacy_title": "Import themes from Warp" + "keyword_legacy_title": "Importar temas desde Warp" }, "yaml_import": { - "title": "Import from YAML", - "description": "Import theme YAML files as Orca terminal themes.", + "title": "Importar desde YAML", + "description": "Importa archivos YAML de temas como temas de terminal de Orca.", "keyword_yaml": "yaml", - "keyword_custom": "custom" + "keyword_custom": "personalizado" }, - "ask_before_closing_running_terminals_title": "Ask Before Closing Running Terminals", - "ask_before_closing_running_terminals_description": "Show a confirmation before closing a terminal that has a running command or agent.", + "ask_before_closing_running_terminals_title": "Preguntar antes de cerrar terminales en ejecución", + "ask_before_closing_running_terminals_description": "Muestra una confirmación antes de cerrar un terminal que tiene un comando o agente en ejecución.", "scrollSpeed": { "title": "Velocidad de desplazamiento", "description": "Ajusta el desplazamiento normal del terminal, el desplazamiento rápido con modificador y la velocidad de rueda en TUI de pantalla completa." @@ -8219,13 +8219,13 @@ "windows": { "search": { "4d09141a42": "menú contextual", - "fcfa53920b": "pasta", + "fcfa53920b": "pegar", "e55186fe2b": "clic derecho", - "28ff08ed35": "ventanas", + "28ff08ed35": "Windows", "e7d2793b03": "terminal", - "8ba875c132": "En Windows, haga clic derecho y pegue el portapapeles en la terminal. Utilice Ctrl+clic derecho para abrir el menú contextual.", - "f0b8448570": "Haga clic derecho para pegar", - "04994f6929": "por defecto", + "8ba875c132": "En Windows, hacer clic derecho pega el portapapeles en el terminal. Usa Ctrl+clic derecho para abrir el menú contextual.", + "f0b8448570": "Clic derecho para pegar", + "04994f6929": "predeterminado", "fc564eadaf": "debian", "4ee2579c32": "ubuntu", "5074ad8b5f": "distribución", @@ -8238,16 +8238,16 @@ "4af2f7526e": "versión", "d414022016": "pwsh", "768613e483": "powershell 7", - "f9162f0b8e": "powershell de windows", + "f9162f0b8e": "windows powershell", "2d99cd91be": "powershell", "41a69bc24d": "Elija si la opción de shell de PowerShell inicia Windows PowerShell o PowerShell 7+ para nuevos paneles de terminal.", "860e0e6402": "Versión de PowerShell", "07ec155fb6": "bash.exe", - "5a2db98d23": "intento", + "5a2db98d23": "bash", "591912177b": "git bash", "12519edb5d": "símbolo del sistema", "6cd20b9e64": "cmd", - "7c7056940a": "caparazón", + "7c7056940a": "shell", "713c4a2f92": "Elija el shell predeterminado para los nuevos paneles de terminal en Windows.", "13715f9d23": "Shell predeterminado" } @@ -8258,23 +8258,23 @@ "search": { "f6e0dfa61c": "nube", "2d206de105": "clave API", - "04c25a6fb0": "abierto", + "04c25a6fb0": "openai", "b9dee49cd7": "descargar", "10d45a9fce": "stt", - "3d8b853963": "discurso", + "3d8b853963": "voz", "080202facb": "modelo", "7640ed9848": "voz", - "56defcd6c3": "Seleccione un modelo de voz a texto local o en la nube para usarlo en el dictado.", - "7e62cd7c41": "Modelo de habla", - "931b1a9e53": "empujar para hablar", - "064a9bd94a": "sostener", - "6fa48bcd41": "palanca", + "56defcd6c3": "Selecciona un modelo de voz a texto local o en la nube para usarlo en el dictado.", + "7e62cd7c41": "Modelo de voz", + "931b1a9e53": "pulsar para hablar", + "064a9bd94a": "mantener pulsado", + "6fa48bcd41": "alternar", "d86f5600da": "modo", "089d31a45b": "dictado", - "748b33e531": "Alternar o mantener presionado el comportamiento de dictado.", + "748b33e531": "Alterna el dictado o mantén pulsado para hablar.", "6a3abb4338": "Modo de dictado", "e360027a65": "micrófono", - "698376a38d": "Alternancia maestra para funciones de dictado de voz.", + "698376a38d": "Interruptor principal para las funciones de dictado por voz.", "20574cbc72": "Habilitar dictado de voz", "322d457a0d": "transcripción", "dcc7846641": "Configure la clave API de OpenAI utilizada para los modelos de voz a texto en la nube.", @@ -8287,15 +8287,15 @@ "action": { "recipe": { "options": { - "commitMessage": "Genere el mensaje de commit a partir de cambios preparados.", - "pullRequest": "Genere el título y la descripción de la reseña alojada.", - "branchName": "Cambie el nombre de las ramas creadas por Orca desde la tarea inicial del agente.", - "fixCommitFailure": "Inicie un agente cuando falle un enlace de commit o una commit de git.", - "fixChecks": "Inicie un agente a partir de comprobaciones fallidas de revisión alojada.", - "resolveConflicts": "Inicie un agente para conflictos de fusión de revisión local o alojada.", + "commitMessage": "Genera el mensaje de commit a partir de cambios staged.", + "pullRequest": "Genera el título y la descripción de la review alojada.", + "branchName": "Renombra ramas creadas por Orca a partir de la tarea inicial del agente.", + "fixCommitFailure": "Inicia un agente cuando falla un hook de commit o un git commit.", + "fixChecks": "Inicia un agente a partir de checks fallidos de una review alojada.", + "resolveConflicts": "Inicia un agente para conflictos de merge locales o de una review alojada.", "customCommand": "Comando personalizado", "supportedAgents": "Agentes compatibles con esta receta: {{value0}}.", - "unsupportedSavedAgent": "{{value0}} no puede ejecutar esta receta de generación de texto. Seleccione uno de los agentes compatibles a continuación.", + "unsupportedSavedAgent": "{{value0}} no puede ejecutar esta acción de generación de texto. Elige uno de los agentes compatibles a continuación.", "resolveComments": "Inicia un agente a partir de comentarios de PR o MR sin resolver seleccionados." } } @@ -8303,78 +8303,78 @@ } }, "agent-awake-copy": { - "e5995ce268": "Mantener la computadora activa mientras los agents están trabajando", - "95d3031db2": "Mantiene esta computadora y pantalla activas mientras los agents están trabajando. El comportamiento de cierre de la tapa sigue la configuración de energía de este dispositivo.", - "a42f6fbdd8": "Mantiene esta computadora y pantalla activas mientras los agents están trabajando. Orca también le pide a este dispositivo que permanezca despierto cuando la tapa está cerrada, sujeto a su política de energía." + "e5995ce268": "Mantener la computadora activa mientras los agentes están trabajando", + "95d3031db2": "Mantiene esta computadora y la pantalla activas mientras los agentes están trabajando. El comportamiento al cerrar la tapa sigue la configuración de energía de este dispositivo.", + "a42f6fbdd8": "Mantiene esta computadora y la pantalla activas mientras los agentes están trabajando. Orca también le pide a este dispositivo que permanezca activo cuando la tapa está cerrada, sujeto a su política de energía." }, "agent-status-hooks-copy": { - "7707c15abb": "Ganchos de estado del Agent", - "a68a642835": "Muestra los estados de trabajo, espera y finalización en Orca. Apague para quitar los ganchos administrados por Orca y deje de reinstalarlos." + "7707c15abb": "Hooks de estado del agente", + "a68a642835": "Muestra los estados trabajando, esperando y completado en Orca. Desactívalo para quitar los hooks administrados por Orca y dejar de reinstalarlos." }, "agent-generated-tab-title-copy": { "19ad21615a": "Generar títulos de pestañas automáticamente", - "b036c7a409": "Derive nombres breves de pestañas estables a partir del primer mensaje de agente conocido. Los cambios de nombre manuales siempre ganan." + "b036c7a409": "Deriva nombres de pestañas breves y estables a partir del primer prompt conocido del agente. Los cambios de nombre manuales siempre tienen prioridad." }, "WarpThemeImportModal": { "title": "Importar desde Warp", - "description": "Import Warp themes as Orca terminal themes.", - "yaml_title": "Import theme YAML", - "yaml_description": "Import theme YAML files (Warp format) as Orca terminal themes.", - "yaml_no_themes_found": "No themes found in the selected files.", - "choose_file": "Choose File", - "choose_folder": "Choose Folder", - "loading": "Loading Warp themes...", - "found_theme_one": "Found 1 theme", - "found_theme_other": "Found {{value0}} themes", - "found_in_source": " in {{value0}}", - "clear_all": "Clear all", - "select_all": "Select all", - "colors_only": "Colors only", - "no_themes_found": "No custom Warp themes found.", - "builtin_themes_hint": "Warp's preloaded themes are part of the Warp app and can't be read from disk. Orca already includes most of them, like Dracula, Gruvbox, Solarized, and Tokyo Night.", - "custom_theme_yaml_hint": "Los temas personalizados y de la comunidad deben existir como archivos YAML en una carpeta de temas de Warp para que la importación automática los encuentre. Si clonaste el repositorio público de temas de Warp, usa Choose Folder para importar esa copia.", - "choose_manually": "Choose a theme YAML file or folder to import manually.", - "skipped_files": "Skipped files", - "more_skipped_files": "{{value0}} more skipped files.", + "description": "Importa temas de Warp como temas de terminal de Orca.", + "yaml_title": "Importar YAML de tema", + "yaml_description": "Importa archivos YAML de temas (formato Warp) como temas de terminal de Orca.", + "yaml_no_themes_found": "No se encontraron temas en los archivos seleccionados.", + "choose_file": "Elegir archivo", + "choose_folder": "Elegir carpeta", + "loading": "Cargando temas de Warp...", + "found_theme_one": "Se encontró 1 tema", + "found_theme_other": "Se encontraron {{value0}} temas", + "found_in_source": " en {{value0}}", + "clear_all": "Borrar selección", + "select_all": "Seleccionar todo", + "colors_only": "Solo colores", + "no_themes_found": "No se encontraron temas personalizados de Warp.", + "builtin_themes_hint": "Los temas preinstalados de Warp forman parte de la app de Warp y no se pueden leer desde el disco. Orca ya incluye la mayoría, como Dracula, Gruvbox, Solarized y Tokyo Night.", + "custom_theme_yaml_hint": "Los temas personalizados y de la comunidad deben existir como archivos YAML en una carpeta de temas de Warp para que la importación automática los encuentre. Si clonaste el repositorio público de temas de Warp, usa Elegir carpeta para importar esa copia.", + "choose_manually": "Elige un archivo YAML de tema o una carpeta para importarlo manualmente.", + "skipped_files": "Archivos omitidos", + "more_skipped_files": "{{value0}} archivos omitidos más.", "cancel": "Cancelar", - "import_theme_one": "Import 1 Theme", - "import_theme_other": "Import {{value0}} Themes", - "import_themes": "Import Themes" + "import_theme_one": "Importar 1 tema", + "import_theme_other": "Importar {{value0}} temas", + "import_themes": "Importar temas" }, "useWarpThemeImport": { - "unknown_error": "Unknown error", - "imported_one": "Imported 1 theme", - "imported_other": "Imported {{value0}} themes", - "import_failed": "Failed to import themes", - "over_limit_one": "Importing these themes would exceed the {{value0}} custom terminal theme limit. Deselect 1 new theme and try again.", - "over_limit_other": "Importing these themes would exceed the {{value0}} custom terminal theme limit. Deselect {{value1}} new themes and try again." + "unknown_error": "Error desconocido", + "imported_one": "Se importó 1 tema", + "imported_other": "Se importaron {{value0}} temas", + "import_failed": "No se pudieron importar los temas", + "over_limit_one": "Importar estos temas superaría el límite de {{value0}} temas personalizados de terminal. Deselecciona 1 tema nuevo e inténtalo de nuevo.", + "over_limit_other": "Importar estos temas superaría el límite de {{value0}} temas personalizados de terminal. Deselecciona {{value1}} temas nuevos e inténtalo de nuevo." }, "YamlThemeImportButton": { - "label": "Import from YAML" + "label": "Importar desde YAML" }, "cli": { "source": { "control": { "integration": { "cards": { - "d5b3be8ecd": "Re-check", - "8cbc39f862": "Learn more", + "d5b3be8ecd": "Volver a comprobar", + "8cbc39f862": "Más información", "707180d09c": "glab auth login", - "4be0616873": "The GitLab CLI is installed but not authenticated. Run this command in a terminal:", - "54a640af7a": "Install GitLab CLI", - "b56fd5676a": "Install the GitLab CLI to enable merge requests, issues, and pipelines.", - "faddeb763d": "GitLab CLI status is not available in this runtime yet.", + "4be0616873": "La CLI de GitLab está instalada, pero no autenticada. Ejecuta este comando en un terminal:", + "54a640af7a": "Instalar la CLI de GitLab", + "b56fd5676a": "Instala la CLI de GitLab para habilitar merge requests, issues y pipelines.", + "faddeb763d": "El estado de la CLI de GitLab aún no está disponible en este runtime.", "a47f71e357": "CLI.", "2a6b359e75": "glab", - "1f2b347bd3": "Merge requests, issues, todos, and pipelines via the", + "1f2b347bd3": "Merge requests, issues, todos y pipelines mediante la", "8d90249d22": "gh auth login", - "2e44dda68a": "The GitHub CLI is installed but not authenticated. Run this command in a terminal:", - "7755c28af5": "Install GitHub CLI", - "23cb5a0dee": "Install the GitHub CLI to enable pull requests, issues, and checks.", - "6f30fc4216": "GitHub CLI status is not available in this runtime yet.", + "2e44dda68a": "La CLI de GitHub está instalada, pero no autenticada. Ejecuta este comando en un terminal:", + "7755c28af5": "Instalar la CLI de GitHub", + "23cb5a0dee": "Instala la CLI de GitHub para habilitar pull requests, issues y checks.", + "6f30fc4216": "El estado de la CLI de GitHub aún no está disponible en este runtime.", "6b2cfb52b4": "gh", - "b4d900e7f1": "Pull requests, issues, and checks via the", - "account_scope_prefix": "Account scope" + "b4d900e7f1": "Pull requests, issues y checks mediante la", + "account_scope_prefix": "Alcance de la cuenta" } } } @@ -8384,27 +8384,27 @@ "tracker": { "integration": { "cards": { - "c90f2ef419": "Re-check", - "dd3529015d": "Disconnect {{value0}}", - "8b2408a8e5": "Jira is connected for this runtime. Re-check if the connected site list looks stale.", - "8c20e76308": "Each connected Jira site has one token stored by the active runtime.", - "c24e56c532": "Test", - "3e7c10d286": "Testing...", - "a2c0015fb8": "Verified", - "e2ff968276": "Connect Jira", - "60996beda6": "Add Jira site", - "7ca5ffffdb": "Browse, create, and start work from Jira Cloud issues.", - "a1093a06c7": "Checking Jira access before showing setup actions.", - "9fa04a032e": "{{value0}} site{{value1}} connected", - "cef18762a2": "Add access with a Personal API key from your Linear settings. Full-access keys can see every team the key owner can reach.", - "6224fe9d34": "Each connected Linear workspace has one key stored by the active runtime. Full-access keys can cover all teams the key owner can access; restricted keys can be replaced any time.", - "1a12e33fe5": "Add Linear access", - "622c224082": "Add workspace access", - "eae4a9f16b": "Add Linear access to browse and link issues.", - "fe9231215b": "Checking Linear access before showing setup actions.", - "e1f5e6424c": "{{value0}} workspace{{value1}} connected", + "c90f2ef419": "Volver a comprobar", + "dd3529015d": "Desconectar {{value0}}", + "8b2408a8e5": "Jira está conectado para este runtime. Vuelve a comprobar si la lista de sitios conectados parece desactualizada.", + "8c20e76308": "Cada sitio de Jira conectado tiene un token almacenado por el runtime activo.", + "c24e56c532": "Probar", + "3e7c10d286": "Probando...", + "a2c0015fb8": "Verificado", + "e2ff968276": "Conectar Jira", + "60996beda6": "Agregar sitio de Jira", + "7ca5ffffdb": "Explora, crea y empieza a trabajar desde issues de Jira Cloud.", + "a1093a06c7": "Comprobando el acceso a Jira antes de mostrar las acciones de configuración.", + "9fa04a032e": "Conexión completada para {{value0}} sitio{{value1}}", + "cef18762a2": "Agrega acceso con una clave API personal desde tus ajustes de Linear. Las claves de acceso completo pueden ver todos los equipos a los que puede acceder el propietario de la clave.", + "6224fe9d34": "Cada workspace de Linear conectado tiene una clave almacenada por el runtime activo. Las claves de acceso completo pueden cubrir todos los equipos a los que puede acceder el propietario de la clave; las claves restringidas se pueden reemplazar en cualquier momento.", + "1a12e33fe5": "Agregar acceso a Linear", + "622c224082": "Agregar acceso a workspace", + "eae4a9f16b": "Agrega acceso a Linear para explorar y vincular issues.", + "fe9231215b": "Comprobando el acceso a Linear antes de mostrar las acciones de configuración.", + "e1f5e6424c": "Conexión completada para {{value0}} workspace{{value1}}", "disconnect_all": "Desconectar todo", - "account_scope_prefix": "Account scope" + "account_scope_prefix": "Alcance de la cuenta" } } } @@ -8414,55 +8414,55 @@ "control": { "integration": { "cards": { - "793a06e899": "Re-check", - "1a9475dace": "Learn more", - "19fb419c12": "Gitea credentials are configured but could not authenticate. Check the token, API base URL, and repository permissions, then restart Orca if environment variables changed.", - "60708f23da": "only when Orca cannot derive the API URL from the remote.", + "793a06e899": "Volver a comprobar", + "1a9475dace": "Más información", + "19fb419c12": "Las credenciales de Gitea están configuradas, pero no se pudo autenticar. Comprueba el token, la URL base de la API y los permisos del repositorio; después reinicia Orca si cambiaron las variables de entorno.", + "60708f23da": "solo cuando Orca no pueda derivar la URL de la API desde el remote.", "709057ad91": "ORCA_GITEA_API_BASE_URL", - "6da9dfa5de": "for private repositories, and set", + "6da9dfa5de": "para repositorios privados, y configura", "6d5c2a3005": "ORCA_GITEA_TOKEN", - "fcbe0469fd": "Public repositories are detected from their git remote. Set", - "0613928cb3": "Gitea status is not available in this runtime yet.", - "05863d2599": "Pull requests and commit statuses via the Gitea REST API.", - "52f75876be": "Pull requests and commit statuses for detected repositories", - "0b5242f8a2": "{{value0}} · Pull requests and commit statuses", - "40f678df73": "Azure DevOps credentials are configured but could not authenticate. Check the token, API base URL, and repository permissions, then restart Orca if environment variables changed.", - "7bd345e3f6": "only when Orca cannot derive the API base URL from the git remote.", + "fcbe0469fd": "Los repositorios públicos se detectan desde su remote de git. Configura", + "0613928cb3": "El estado de Gitea aún no está disponible en este runtime.", + "05863d2599": "Pull requests y estados de commit mediante la API REST de Gitea.", + "52f75876be": "Pull requests y estados de commit para los repositorios detectados", + "0b5242f8a2": "{{value0}} · Pull requests y estados de commit", + "40f678df73": "Las credenciales de Azure DevOps están configuradas, pero no se pudo autenticar. Comprueba el token, la URL base de la API y los permisos del repositorio; después reinicia Orca si cambiaron las variables de entorno.", + "7bd345e3f6": "solo cuando Orca no pueda derivar la URL base de la API desde el remote de git.", "186a6689df": "ORCA_AZURE_DEVOPS_API_BASE_URL", - "b8a10b07c1": ". Set", + "b8a10b07c1": ". Configura", "fbfd237f5e": "ORCA_AZURE_DEVOPS_ACCESS_TOKEN", - "087feb92f1": ", or set", + "087feb92f1": ", o configura", "48842720d2": "ORCA_AZURE_DEVOPS_TOKEN", - "7bbc9c64f0": "Set", - "f3f47dc7de": "Azure DevOps status is not available in this runtime yet.", - "0eb50d5593": "Pull requests and build statuses via Azure DevOps REST API tokens.", - "54636c65d4": "Pull requests and build statuses for detected Azure Repos", - "ea204f5e03": "{{value0}} · Pull requests and build statuses", - "6154b02093": "Bitbucket credentials are configured but could not authenticate. Check the token and repository permissions, then restart Orca if environment variables changed.", + "7bbc9c64f0": "Configura", + "f3f47dc7de": "El estado de Azure DevOps aún no está disponible en este runtime.", + "0eb50d5593": "Pull requests y estados de build mediante tokens de la API REST de Azure DevOps.", + "54636c65d4": "Pull requests y estados de build para Azure Repos detectados", + "ea204f5e03": "{{value0}} · Pull requests y estados de build", + "6154b02093": "Las credenciales de Bitbucket están configuradas, pero no se pudo autenticar. Comprueba el token y los permisos del repositorio; después reinicia Orca si cambiaron las variables de entorno.", "e63fe8f627": "ORCA_BITBUCKET_ACCESS_TOKEN", "19416c874c": "ORCA_BITBUCKET_API_TOKEN", - "fc71a0e7aa": "and", + "fc71a0e7aa": "y", "63a7f47392": "ORCA_BITBUCKET_EMAIL", - "24ac1c69dc": "Bitbucket status is not available in this runtime yet.", - "a924e8dcd1": "Pull requests and build statuses via Bitbucket Cloud API tokens.", - "0fa5629dad": "Pull requests and build statuses" + "24ac1c69dc": "El estado de Bitbucket aún no está disponible en este runtime.", + "a924e8dcd1": "Pull requests y estados de build mediante tokens de la API de Bitbucket Cloud.", + "0fa5629dad": "Pull requests y estados de build" } } } } }, "computerUseSummary": { - "permissionsRequired": "{{value0}} permission{{value1}} required before agents can operate app windows.", - "checkingTitle": "Checking Computer Use access.", - "checkingDescription": "Orca is checking macOS privacy permissions for the Computer Use helper.", - "unavailableTitle": "Computer Use is unavailable.", - "unavailableDescription": "Computer Use permissions are unavailable because {{value0}}.", - "readyTitle": "Computer Use is ready.", - "readyDescription": "Agents can inspect and operate app windows when you ask.", - "permissionsTitle": "Finish setup to use local apps." + "permissionsRequired": "Requiere {{value0}} permiso{{value1}} antes de que los agentes puedan operar ventanas de apps.", + "checkingTitle": "Comprobando el acceso a Computer Use.", + "checkingDescription": "Orca está comprobando los permisos de privacidad de macOS para el helper de Computer Use.", + "unavailableTitle": "Computer Use no está disponible.", + "unavailableDescription": "Los permisos de Computer Use no están disponibles porque {{value0}}.", + "readyTitle": "Computer Use está listo.", + "readyDescription": "Los agentes pueden inspeccionar y operar ventanas de apps cuando se lo pidas.", + "permissionsTitle": "Completa la configuración para usar apps locales." }, "computerUseSkillRuntime": { - "thisDevice": "This device" + "thisDevice": "Este dispositivo" }, "WorkspaceDirectorySetting": { "1a2b3c4d5e": "Predeterminado del cliente", @@ -8470,32 +8470,32 @@ "3c4d5e6f7a": "Anula el valor predeterminado del cliente", "4d5e6f7a8b": "Hereda el valor predeterminado del cliente", "5e6f7a8b9c": "Restablecer", - "6f7a8b9cad": "Usa una ruta relativa (p. ej. .orca/worktrees) para una ubicación por proyecto, o una ruta absoluta para una carpeta compartida." + "6f7a8b9cad": "Usa una ruta relativa (p. ej., .orca/worktrees) para una ubicación por proyecto, o una ruta absoluta para una carpeta compartida." }, "ProviderHostScopeControl": { "scope_label": "{{value0}}: {{value1}}", - "change_host": "Open Remote Servers" + "change_host": "Abrir servidores remotos" }, "providerAccountScope": { - "remoteServer": "Remote server: {{value0}}", - "remoteServerCredentials": "Credentials and account checks for this provider are owned by this remote server. Use Settings > Remote Orca Servers > Advanced to edit another default runtime scope.", + "remoteServer": "Servidor remoto: {{value0}}", + "remoteServerCredentials": "Las credenciales y comprobaciones de cuenta de este proveedor pertenecen a este servidor remoto. Usa Configuración > Servidores Orca remotos > Avanzado para editar otro ámbito de runtime predeterminado.", "localMac": "Local Mac", - "localCredentials": "Credentials and account checks for this provider are owned by this desktop client. Use Settings > Remote Orca Servers > Advanced to edit server-owned credentials.", - "remoteServerRateLimit": "{{value0}} API budget is fetched from the CLI on this remote server. Use Settings > Remote Orca Servers > Advanced to view another default runtime budget.", - "localRateLimit": "{{value0}} API budget is fetched from the CLI on this desktop client. Use Settings > Remote Orca Servers > Advanced to view server-owned budgets." + "localCredentials": "Las credenciales y comprobaciones de cuenta de este proveedor pertenecen a este cliente de escritorio. Usa Configuración > Servidores Orca remotos > Avanzado para editar credenciales propiedad del servidor.", + "remoteServerRateLimit": "El presupuesto de API de {{value0}} se obtiene desde la CLI en este servidor remoto. Usa Configuración > Servidores Orca remotos > Avanzado para ver otro presupuesto de runtime predeterminado.", + "localRateLimit": "El presupuesto de API de {{value0}} se obtiene desde la CLI en este cliente de escritorio. Usa Configuración > Servidores Orca remotos > Avanzado para ver presupuestos propiedad del servidor." }, "settingOwnership": { - "clientDefault": "Client default", - "sourceControlAiDefaults": "Recipes, prompts, and hosted-review defaults are shared by this client; model choices and discovery stay scoped to the host where the agent runs.", - "projectOnThisHost": "Project on this host", - "repositorySourceControlAi": "These overrides apply to this project setup and inherit the client Source Control AI defaults until customized.", - "agentLaunchDefaults": "Default agent, command overrides, CLI arguments, and launch environment are client preferences. SSH and remote server launches still validate host availability at run time.", - "clientDefaultProjectScopes": "Client default + project scopes", - "terminalQuickCommands": "Commands are saved on this client, then scoped globally or to a project setup so they run from the selected terminal context.", - "hostOverride": "Host override", - "workspaceDirectory": "The client default is inherited until a host needs its own worktree directory.", - "providerHost": "Provider host", - "providerAccounts": "Credentials and account checks belong to the local client or selected remote server that owns the provider integration." + "clientDefault": "Predeterminado del cliente", + "sourceControlAiDefaults": "Las recetas, prompts y valores predeterminados de revisión alojada se comparten en este cliente; las elecciones de modelo y el descubrimiento quedan limitados al host donde se ejecuta el agente.", + "projectOnThisHost": "Proyecto en este host", + "repositorySourceControlAi": "Estas anulaciones se aplican a la configuración de este proyecto y heredan los valores predeterminados de Source Control AI del cliente hasta que se personalicen.", + "agentLaunchDefaults": "El agente predeterminado, las anulaciones de comandos, los argumentos de CLI y el entorno de lanzamiento son preferencias del cliente. Los lanzamientos por SSH y servidor remoto siguen validando la disponibilidad del host en tiempo de ejecución.", + "clientDefaultProjectScopes": "Predeterminado del cliente + ámbitos de proyecto", + "terminalQuickCommands": "Los comandos se guardan en este cliente y luego se limitan globalmente o a una configuración de proyecto para que se ejecuten desde el contexto de terminal seleccionado.", + "hostOverride": "Anulación del host", + "workspaceDirectory": "Se hereda el valor predeterminado del cliente hasta que un host necesite su propio directorio de worktree.", + "providerHost": "Host del proveedor", + "providerAccounts": "Las credenciales y comprobaciones de cuenta pertenecen al cliente local o al servidor remoto seleccionado que posee la integración del proveedor." }, "RepositoryForkSyncSection": { "defaultBranch": "rama predeterminada", @@ -8521,42 +8521,42 @@ "syncNow": "Sincronizar ahora", "modeLabel": "Modo de sincronización del fork", "ask": "Preguntar", - "safeAuto": "Seguro automático", + "safeAuto": "Automático seguro", "off": "Desactivado" }, "DefaultWindowsProjectRuntimeSetting": { - "defaultRuntime": "Default project runtime", + "defaultRuntime": "Runtime predeterminado del proyecto", "windows": "Windows", "wsl": "WSL", - "selectDistro": "Select distro", - "windowsDescription": "Projects inherit Windows unless a project overrides it.", - "wslUnavailable": "WSL is not available. Projects that inherit WSL will need repair.", - "distroRequired": "Choose a WSL distro before projects can inherit WSL.", - "wslDescription": "Projects inherit {{value0}} via WSL unless a project overrides it." + "selectDistro": "Seleccionar distro", + "windowsDescription": "Los proyectos heredan Windows, a menos que un proyecto lo anule.", + "wslUnavailable": "WSL no está disponible. Los proyectos que hereden WSL necesitarán reparación.", + "distroRequired": "Elige una distro de WSL antes de que los proyectos puedan heredar WSL.", + "wslDescription": "Los proyectos heredan {{value0}} mediante WSL, a menos que un proyecto lo anule." }, "ProjectWindowsRuntimeSetting": { - "projectRuntime": "Project runtime", - "defaultRuntime": "Default ({{value0}})", + "projectRuntime": "Runtime del proyecto", + "defaultRuntime": "Predeterminado ({{value0}})", "windows": "Windows", "wsl": "WSL", - "selectDistro": "Select distro", - "runtimeChangeHelp": "Runtime changes apply to new terminals, agent checks, and skill discovery for this project. Existing terminals keep their current runtime.", - "wslUnavailable": "WSL is not available. Switch this project to Windows or repair WSL.", - "distroMissing": "{{value0}} is not installed in WSL. Choose an installed distro or switch this project to Windows.", - "distroRequired": "Choose a WSL distro or switch this project to Windows.", - "inheritedWsl": "No project override. General settings select {{value0}} via WSL.", - "projectWsl": "This project runs in {{value0}} via WSL.", - "inheritedWindows": "No project override. General settings select Windows.", - "projectWindows": "This project runs on Windows.", + "selectDistro": "Seleccionar distro", + "runtimeChangeHelp": "Los cambios de runtime se aplican a nuevas terminales, checks de agentes y descubrimiento de skill para este proyecto. Las terminales existentes conservan su runtime actual.", + "wslUnavailable": "WSL no está disponible. Cambia este proyecto a Windows o repara WSL.", + "distroMissing": "{{value0}} no está instalado en WSL. Elige una distro instalada o cambia este proyecto a Windows.", + "distroRequired": "Elige una distro de WSL o cambia este proyecto a Windows.", + "inheritedWsl": "Sin anulación del proyecto. La configuración general selecciona {{value0}} mediante WSL.", + "projectWsl": "Este proyecto se ejecuta en {{value0}} mediante WSL.", + "inheritedWindows": "Sin anulación del proyecto. La configuración general selecciona Windows.", + "projectWindows": "Este proyecto se ejecuta en Windows.", "liveTerminalSingular": "{{count}} terminal activo", "liveTerminalPlural": "{{count}} terminales activos", "activeTaskSingular": "{{count}} tarea activa", "activeTaskPlural": "{{count}} tareas activas", "runtimeSessionJoin": "{{value0}} y {{value1}}", - "runtimeSessionWarning": "{{value0}} seguirá ejecutándose en el runtime actual. Deja que las tareas terminen o reinicia las terminales antes de continuar.", - "pendingRuntimeChange": "Runtime change pending. New project work will use the selected runtime after you apply.", + "runtimeSessionWarning": "Lo siguiente seguirá ejecutándose en el runtime actual: {{value0}}. Deja que las tareas terminen o reinicia las terminales antes de continuar.", + "pendingRuntimeChange": "Cambio de runtime pendiente. El nuevo trabajo del proyecto usará el runtime seleccionado después de aplicar.", "cancel": "Cancelar", - "applyRuntimeChange": "Apply runtime change" + "applyRuntimeChange": "Aplicar cambio de runtime" }, "PrivacyDiagnosticsRows": { "5a7cbe069a": "DO_NOT_TRACK=1 está configurado: crear y enviar archivos de diagnóstico está deshabilitado.", @@ -8566,62 +8566,62 @@ "e27c8d45bf": "Los diagnósticos están desactivados por una variable de entorno." }, "ShortcutCommandBlock": { - "eb72c52c28": "Press a shortcut, or double-tap a modifier (e.g. {{value0}}). Esc cancels.", - "70b5d25583": "{{value0}} shortcut", - "287e07ddde": "Modified", - "3c83cd7d1c": "Disabled", - "07939d084e": "Reset {{value0}} to default", - "9b02917027": "Reset to default", - "a799f90f82": "Disable {{value0}}", - "25e6e76618": "Disable shortcut", - "035a822ef0": "Add shortcut", - "a0e2ef0e61": "Add another shortcut for {{value0}}", - "245c83af24": "Add another shortcut", - "482a60225d": "Enable {{value0}}", - "6287677c37": "Enable", - "01481b964c": "Add shortcut for {{value0}}" + "eb72c52c28": "Pulsa un atajo o toca dos veces una tecla modificadora (p. ej., {{value0}}). Esc cancela.", + "70b5d25583": "Atajo de {{value0}}", + "287e07ddde": "Modificado", + "3c83cd7d1c": "Desactivado", + "07939d084e": "Restablecer {{value0}} al valor predeterminado", + "9b02917027": "Restablecer al valor predeterminado", + "a799f90f82": "Desactivar {{value0}}", + "25e6e76618": "Desactivar atajo", + "035a822ef0": "Agregar atajo", + "a0e2ef0e61": "Agregar otro atajo para {{value0}}", + "245c83af24": "Agregar otro atajo", + "482a60225d": "Activar {{value0}}", + "6287677c37": "Activar", + "01481b964c": "Agregar atajo para {{value0}}" }, "ShortcutRecorderButton": { - "1a13bb054d": "Press shortcut keys for {{value0}}. Escape cancels.", - "3732775d74": "Add shortcut for {{value0}}", - "88764af2c1": "Change shortcut for {{value0}}", - "30feb099d6": "Change shortcut {{value0}} of {{value1}} for {{value2}}", - "5d982a2a1f": "Listening for shortcut", - "152e0bcd64": "Add shortcut", - "5bd56445da": "Change shortcut", - "f5ed5dcbf6": "Press keys…" + "1a13bb054d": "Pulsa las teclas del atajo para {{value0}}. Escape cancela.", + "3732775d74": "Agregar atajo para {{value0}}", + "88764af2c1": "Cambiar atajo para {{value0}}", + "30feb099d6": "Cambiar atajo {{value0}} de {{value1}} para {{value2}}", + "5d982a2a1f": "Esperando atajo", + "152e0bcd64": "Agregar atajo", + "5bd56445da": "Cambiar atajo", + "f5ed5dcbf6": "Pulsa teclas…" }, "ShortcutRemoveButton": { - "9e29aff18b": "Eliminar {{value0}} acceso directo {{value1}}", - "2a9588b1c2": "Remove this binding" + "9e29aff18b": "Eliminar atajo {{value1}} de {{value0}}", + "2a9588b1c2": "Eliminar esta combinación" }, "BrowserLocalhostWorktreeLabelsSetting": { - "8ac8c3ad19": "Etiquetas del árbol de trabajo de localhost", - "1db3c8b983": "Abra los puertos del espacio de trabajo como URL de host local de Orca específicas del árbol de trabajo para que las pestañas del navegador sean más fáciles de distinguir." + "8ac8c3ad19": "Etiquetas de worktree para localhost", + "1db3c8b983": "Abre los puertos del workspace como URLs localhost de Orca específicas por worktree para que las pestañas del navegador sean más fáciles de distinguir." }, "AgentRuntimeSetting": { - "label": "Tiempo de ejecución del Agent", + "label": "Runtime de agentes", "wsl": "WSL", "loadingWsl": "Cargando WSL", - "selectDistro": "Seleccionar distribución", - "windowsDescription": "Detecte e inicie agents en Windows para proyectos que no anulan su tiempo de ejecución.", + "selectDistro": "Seleccionar distro", + "windowsDescription": "Detecta y lanza agentes en Windows para proyectos que no anulan su runtime.", "wslUnavailable": "WSL no está disponible en esta máquina.", - "distroRequired": "Elija una distribución WSL antes de que los proyectos puedan heredar WSL.", - "wslDescription": "Detecta y lanza agents en {{value0}} a través de WSL para proyectos que no anulan su tiempo de ejecución." + "distroRequired": "Elige una distro de WSL antes de que los proyectos puedan heredar WSL.", + "wslDescription": "Detecta y lanza agentes en {{value0}} mediante WSL para proyectos que no anulan su runtime." }, "MobileEmulatorSdkStatus": { "536026130e": "SDK de emulador", "dde0ec1cd8": "Cadenas de herramientas que Orca utiliza para ejecutar emuladores. Android funciona en cualquier sistema operativo a través del SDK de Android; Los simuladores de iOS necesitan Xcode en macOS.", "027cbf668a": "Android SDK", - "f6d080d128": "Usando la ruta configurada", + "f6d080d128": "Usando ruta configurada", "7fe4bd5907": "Detectado en", - "2784f0b22d": "Extraviado. Instale Android Studio, luego cree un dispositivo virtual.", + "2784f0b22d": "No encontrado. Instala Android Studio y luego crea un dispositivo virtual.", "b94ff260e6": "Descargar Android Studio", - "18925b082d": "Localizar la carpeta SDK", - "8c52684db8": "Claro", + "18925b082d": "Ubicar carpeta del SDK", + "8c52684db8": "Borrar", "76eb88b88e": "iOS Simulator (Xcode)", "c6f3ea4f12": "Listo", - "e4f14b50d7": "Instale Xcode y agregue un tiempo de ejecución del simulador de iOS.", + "e4f14b50d7": "Instala Xcode y agrega un runtime de iOS Simulator.", "63fe73a1ea": "No se pudo actualizar la carpeta SDK de Android." }, "AppearanceAdvancedDisclosure": { @@ -8633,7 +8633,7 @@ "secondaryActionClicked": "Secondary action clicked", "primaryAction": "Primary action", "primaryActionClicked": "Primary action clicked", - "branchHasChanges": "branch has changes", + "branchHasChanges": "la rama tiene cambios", "viewChangesClicked": "View Changes clicked", "forceDeleteClicked": "Force Delete clicked", "devOnlyCallback": "Dev-only callback.", @@ -8658,142 +8658,142 @@ "devOnly": "Dev only" }, "EphemeralVmRecipeRow": { - "useInWorkspace": "Uso en el espacio de trabajo" + "useInWorkspace": "Usar en el espacio de trabajo" }, "EphemeralVmRuntimesSection": { - "cleanupFailed": "Error de limpieza", - "cleanupRunning": "limpieza en ejecución", + "cleanupFailed": "Error al limpiar", + "cleanupRunning": "Limpieza en curso", "cleanupDisabled": "Limpieza deshabilitada", - "running": "Correr", - "failed": "Fallido", - "loadFailed": "No se pudieron cargar tiempos de ejecución de VM temporales.", - "cleanupFailedToast": "No se pudo limpiar el tiempo de ejecución temporal de la VM.", - "markedCleaned": "Tiempo de ejecución de VM temporal marcado como limpio.", - "cleaned": "Se limpió el tiempo de ejecución temporal de la VM.", + "running": "En ejecución", + "failed": "Con error", + "loadFailed": "No se pudieron cargar los runtimes de VM temporales.", + "cleanupFailedToast": "No se pudo limpiar el runtime de VM temporal.", + "markedCleaned": "Runtime de VM temporal marcado como limpio.", + "cleaned": "Se limpió el runtime de VM temporal.", "copiedCleanupCommand": "Comando de limpieza copiado.", - "copiedCleanupPayload": "Carga útil de limpieza copiada.", + "copiedCleanupPayload": "Payload de limpieza copiado.", "copyCleanupFailed": "No se pudo copiar el comando de limpieza.", - "title": "Tiempos de ejecución temporales de VM", - "description": "Los tiempos de ejecución creados por recetas son propiedad del espacio de trabajo. Limpie las entradas obsoletas después de fallas, creaciones fallidas o recuperación manual.", - "refresh": "Actualizar tiempos de ejecución de VM temporales", - "loading": "Comprobando tiempos de ejecución de VM temporales...", - "empty": "No es necesario limpiar tiempos de ejecución de VM temporales.", - "copyCleanup": "comando copiar", - "retry": "Reintentar la limpieza", - "cleanup": "Limpieza" + "title": "Runtimes de VM temporales", + "description": "Los runtimes creados por recetas pertenecen al espacio de trabajo. Limpia entradas obsoletas después de cierres inesperados, creaciones fallidas o recuperación manual.", + "refresh": "Actualizar runtimes de VM temporales", + "loading": "Comprobando runtimes de VM temporales…", + "empty": "No hay runtimes de VM temporales que limpiar.", + "copyCleanup": "Copiar comando", + "retry": "Reintentar limpieza", + "cleanup": "Limpiar" }, "ephemeralVms": { "search": { "title": "Entornos por espacio de trabajo", - "description": "Descubra cómo las recetas propiedad del repo brindan a cada espacio de trabajo su propio entorno desechable bajo demanda." + "description": "Aprende cómo las recetas del repositorio dan a cada espacio de trabajo su propio entorno desechable bajo demanda." } }, "EphemeralVmsPane": { - "loadError": "No se pudieron cargar recetas.", - "copyError": "No se pudo copiar el mensaje.", - "skillTitle": "Habilidad Entornos por espacio de trabajo", - "skillDescription": "Configura, crea, autentica y valida recetas de entornos propiedad del repo.", - "whatTitle": "Qué hace la habilidad contigo", - "whatScaffold": "Escribe la receta y los scripts para su proveedor, conectados a través de un servidor Orca o SSH.", - "whatBuild": "Crea una imagen base reutilizable e inicia la sesión de su agent.", - "whatValidate": "Lo valida para que puedas crear un espacio de trabajo en él.", - "promptHint": "En cualquier espacio de trabajo, pregúntale a tu agent:", + "loadError": "No se pudieron cargar las recetas.", + "copyError": "No se pudo copiar el prompt.", + "skillTitle": "Skill de entornos por espacio de trabajo", + "skillDescription": "Configura, construye, autentica y valida recetas de entornos del repositorio.", + "whatTitle": "Lo que la skill hace contigo", + "whatScaffold": "Escribe la receta y los scripts para tu proveedor, conectados mediante un servidor Orca o SSH.", + "whatBuild": "Crea una imagen base reutilizable e inicia sesión con tu agente.", + "whatValidate": "Valida el entorno para que puedas crear un espacio de trabajo sobre él.", + "promptHint": "En cualquier espacio de trabajo, pídele a tu agente:", "copy": "Copiar", - "copied": "copiado", + "copied": "Copiado", "recipes": "Recetas", - "recipesHelp": "Las recetas que su agent agrega a orca.yaml aparecen aquí, listas para iniciar un espacio de trabajo.", - "refresh": "Actualizar recetas de VM efímeras", - "checking": "Comprobando recetas...", + "recipesHelp": "Las recetas que tu agente agrega a orca.yaml aparecen aquí, listas para iniciar un espacio de trabajo.", + "refresh": "Actualizar recetas de VMs efímeras", + "checking": "Comprobando recetas…", "none": "Aún no se han encontrado recetas." }, "ephemeralVmsExperimentalSetting": { - "description": "Muestra controles de configuración y objetivos de ejecución del espacio de trabajo para entornos bajo demanda de propiedad de repos.", - "toggleLabel": "Alternar entornos por espacio de trabajo" + "description": "Muestra controles de configuración y destinos de ejecución de espacios de trabajo para entornos bajo demanda del repositorio.", + "toggleLabel": "Activar o desactivar entornos por espacio de trabajo" } }, "right": { "sidebar": { "BulkActionBar": { - "79a9f5f712": "fuera del escenario (", - "ef5f5bd06e": "Escenario (", - "60ed678138": "seleccionado" + "79a9f5f712": "Unstage (", + "ef5f5bd06e": "Stage (", + "60ed678138": "seleccionados" }, "ChecksPanel": { - "2ef90c9819": "Inició un agente de inteligencia artificial para los cheques rotos.", - "a0181a8d76": "Inició un agente de IA para los conflictos.", + "2ef90c9819": "Se inició un agente de IA para los checks con errores.", + "a0181a8d76": "Se inició un agente de IA para los conflictos.", "34464d00b9": "actualizado", "058039787c": "Cancelar", - "2ab7fd4b6d": "Ahorrar", - "dda5924a40": "Las comprobaciones requieren una rama de Git y un contexto de revisión alojado", - "976cefd02f": "Cheques no disponibles", - "b5dd73a105": "Seleccione un espacio de trabajo para ver cheques", + "2ab7fd4b6d": "Guardar", + "dda5924a40": "Los checks requieren una rama de Git y contexto de revisión alojada", + "976cefd02f": "Checks no disponibles", + "b5dd73a105": "Selecciona un espacio de trabajo para ver los checks", "a4ef4e0832": "Ningún espacio de trabajo seleccionado", - "5594400d73": "No hay cheques rotos que arreglar.", - "abf59262fb": "Revise el mensaje antes de iniciar un agente.", + "5594400d73": "No hay checks con errores que corregir.", + "abf59262fb": "Revisa el prompt antes de iniciar un agente.", "4ede779461": "Resolver conflictos de revisión con IA", "3b203c62f8": "Esto eliminará permanentemente el comentario del PR.", "ea9b649ce3": "¿Eliminar comentario?", - "5788d1059d": "No se pudo actualizar el hilo de revisión. Consulta el presupuesto de la API de GitHub.", + "5788d1059d": "No se pudo actualizar el hilo de revisión. Revisa el presupuesto de la API de GitHub.", "07871c0589": "Vincular otro PR", - "7202f4a40a": "desvincular PR", - "7f4489f370": "Refrescar", - "5c88c6db07": "Abierto el {{value0}}", - "7fad8509fe": "Arreglar con IA", - "71026ca2cb": "Refrescante…", + "7202f4a40a": "Desvincular PR", + "7f4489f370": "Actualizar", + "5c88c6db07": "Abrir en {{value0}}", + "7fad8509fe": "Corregir con IA", + "71026ca2cb": "Actualizando…", "889cdfba04": "Crear {{value0}}", - "98f4c37b33": "Empujar y crear {{value0}}", + "98f4c37b33": "Hacer push y crear {{value0}}", "b6ce28da5b": "{{value0}} #{{value1}} ya está abierto", "cf9e69f3be": "{{value0}} ya está abierto", "192e686e57": "Abrir en {{value0}}", - "6633c7a1fb": "Rama de publicación", - "fdb27637f2": "Publicación…", + "6633c7a1fb": "Publicar rama", + "fdb27637f2": "Publicando…", "e56c42122e": "destructivo", - "786e3c143f": "Borrar", + "786e3c143f": "Eliminar", "653c105ecc": "Más acciones de PR", "f316a8ca2b": "No hay comentarios sin resolver seleccionados.", "d00ebdc402": "Resolver comentarios de {{value0}} con IA", - "ed3f79c031": "Revisa el prompt antes de iniciar un agente. Los hilos seleccionados se marcan como resueltos después del inicio.", - "f273f2271c": "Agente iniciado. Marcados {{value0}} como resueltos, omitidos {{value1}}, con error {{value2}}.", - "aa95b81a3a": "Agente iniciado. Marcados {{value0}} como resueltos, omitidos {{value1}}, con error {{value2}}.", + "ed3f79c031": "Revisa el prompt antes de iniciar un agente. Los hilos seleccionados se marcarán como resueltos después del inicio.", + "f273f2271c": "Agente iniciado. Marcados como resueltos: {{value0}}; omitidos: {{value1}}; con error: {{value2}}.", + "aa95b81a3a": "Agente iniciado. Marcados como resueltos: {{value0}}; omitidos: {{value1}}; con error: {{value2}}.", "495b2f8c4b": "Agente iniciado, pero no se pudieron marcar los comentarios seleccionados como resueltos.", - "3c3ad3a1d2": "Agente iniciado. Ningún comentario seleccionado se puede marcar como resuelto en el host." + "3c3ad3a1d2": "Agente iniciado. No se puede marcar ningún comentario seleccionado como resuelto en el host." }, "CreatePullRequestDialog": { "2bc1b4345e": "Cancelar", - "27ef4b195c": "Elija una rama base diferente antes de crear un {{value0}}.", + "27ef4b195c": "Elige una rama base diferente antes de crear un {{value0}}.", "7ef56f3efe": "Crear como borrador", - "0c9f9a568c": "Admite el formato Markdown. Utilice Generar con IA para completar automáticamente sus cambios.", + "0c9f9a568c": "Admite formato Markdown. Usa Generar con IA para autocompletarlo a partir de tus cambios.", "02b2ce911f": "Descripción (opcional)", "1cd53359db": "Descripción", "68314b4369": "Título", - "694550a610": "principal", - "0fad57a14c": "Busque sucursales remotas o ingrese el nombre de una sucursal.", - "8584ccb43c": "sucursal base", - "6f5f1962b6": "rama principal", + "694550a610": "main", + "0fad57a14c": "Busca ramas remotas o ingresa el nombre de una rama.", + "8584ccb43c": "Rama base", + "6f5f1962b6": "Rama head", "b504b3ceb1": "detalles antes de crear la reseña alojada.", - "f658ff2455": "Confirme la sucursal de destino y los detalles de {{value0}} antes de crear la revisión alojada.", + "f658ff2455": "Confirma la rama de destino y los detalles de {{value0}} antes de crear la revisión alojada.", "b7f43474d7": "Crear {{value0}}", - "7a21f0dae8": "Abierto el {{value0}}", + "7a21f0dae8": "Abrir en {{value0}}", "edc35a7027": "{{value0}} #{{value1}} ya está abierto", - "a154fe55e6": "Empujar y crear {{value0}}", + "a154fe55e6": "Hacer push y crear {{value0}}", "21c7a1daa0": "{{value0}} ya está abierto", "db9cee18f7": "Crear {{value0}}" }, "CreateHostedReviewComposer": { - "741ff8a0d2": "Empujar y crear {{value0}}" + "741ff8a0d2": "Hacer push y crear {{value0}}" }, "CreatePullRequestGenerateButton": { "4012459f8a": "Generar con IA", - "a0501572c1": "Genera detalles {{value0}} con IA", - "d47fd63012": "Generando detalles {{value0}}. Haga clic para detener.", + "a0501572c1": "Generar detalles de {{value0}} con IA", + "d47fd63012": "Generando detalles de {{value0}}. Haz clic para detener.", "bdf83ccb15": "Generando {{value0}}", - "f5513bdeb1": "generando", + "f5513bdeb1": "Generando", "a6ea6dc3aa": "Generando…", - "e61d7e7ad4": "Dejar de generar detalles {{value0}}", - "e041998cad": "dejar de generar" + "e61d7e7ad4": "Dejar de generar detalles de {{value0}}", + "e041998cad": "Dejar de generar" }, "FileExplorer": { - "79b1537dd3": "Seleccione un espacio de trabajo para buscar archivos", + "79b1537dd3": "Selecciona un espacio de trabajo para explorar archivos", "4da4d89845": "Volver al explorador", "6ed5ce817b": "Buscar", "2f4483d6c4": "No hay archivos que coincidan con este filtro" @@ -8803,36 +8803,36 @@ "21fe46ed36": "Nuevo archivo" }, "FileExplorerRow": { - "addc01145f": "Borrar", - "fc747429bf": "Rebautizar", - "0df0e5abac": "Buscar en la carpeta", + "addc01145f": "Eliminar", + "fc747429bf": "Renombrar", + "0df0e5abac": "Buscar en carpeta", "d6a25618aa": "Contraer carpeta", - "d87a4c42e1": "Abrir vista previa de Markdowns", + "d87a4c42e1": "Abrir vista previa de Markdown", "c2112579f6": "Descargar", - "dd112c81d2": "Abrir en el navegador Orca", - "1bb9be455c": "Agregar como proyecto...", - "0fec99bfd7": "Duplicado", + "dd112c81d2": "Abrir en Orca Browser", + "1bb9be455c": "Agregar como proyecto…", + "0fec99bfd7": "Duplicar", "f61af83316": "Nueva carpeta", "37c875d827": "Nuevo archivo", "b3e288bf41": "No se pudo descargar '{{value0}}'.", - "1a3df04ae1": "Abierto", - "bce4d4e44f": "Descargado '{{value0}}'", + "1a3df04ae1": "Abrir", + "bce4d4e44f": "Se descargó '{{value0}}'", "e26010014a": "Ignorado por .gitignore", - "a06551beee": "Ingresar", + "a06551beee": "Intro", "128a99ed5e": "No asignado", "2de3b21934": "markdown", "66a29dde82": "Copiar ruta relativa", "42e10cbf57": "Copiar rutas relativas", "b5d436aa30": "Copiar ruta", "98a79948b3": "Copiar", - "b234ab25b4": "No se pudo copiar el archivo al portapapeles", + "b234ab25b4": "No se pudo copiar el archivo en el portapapeles", "f9d7ca753d": "Copiar rutas", "3161c4e425": "carpeta", "e887fa4b2e": "Abrir en terminal" }, "FileExplorerToolbar": { - "d238264654": "Mostrar archivos ignorados de Git", - "78f133232c": "Mostrar archivos de puntos", + "d238264654": "Mostrar archivos ignorados por Git", + "78f133232c": "Mostrar dotfiles", "31b4c3195d": "Más acciones del explorador", "d95e30fe28": "Actualizar explorador", "6026b16950": "Contraer todo", @@ -8845,20 +8845,20 @@ }, "GitHistoryPanel": { "cf7cad58d2": "Aún no hay commits", - "781a8bcf7b": "Cargando gráfico...", + "781a8bcf7b": "Cargando gráfico…", "d0fb0f4bf2": "Actualizar commits", - "9f7535d22b": "Las referencias son nombres de ramas o etiquetas que apuntan a esa commit exacta. Solo aparecen cuando Git tiene una referencia con nombre para la commit.", - "9289ba0cb9": "¿Qué son los árbitros?", + "9f7535d22b": "Las refs son nombres de ramas o tags que apuntan a ese commit exacto. Solo aparecen cuando Git tiene una ref con nombre para el commit.", + "9289ba0cb9": "¿Qué son las refs?", "d836037d02": "Commits", - "8232c8b2f2": "Confirmación abierta {{value0}}: {{value1}}", + "8232c8b2f2": "Abrir commit {{value0}}: {{value1}}", "9a8b85882d": "cargando", "62e685d5ec": "idle", "111e1d0db4": "error", - "e5e81e59a6": "Cambiar el tamaño de las commits", + "e5e81e59a6": "Cambiar tamaño del panel de commits", "6d1e0a7c3b": "No se pudieron cargar los archivos del commit" }, "HostedReviewActions": { - "4d5fb5a284": "Cerca", + "4d5fb5a284": "Cerrar", "9845a71e17": "Más acciones", "2bfaf4379c": "Más acciones de {{value0}}", "377269db6f": "{{value0}} reabierto", @@ -8866,36 +8866,36 @@ "78f5ff294c": "Esto reabrirá el {{value0}}.", "a3d572a4de": "Esto cerrará el {{value0}}.", "e4aca40024": "Eliminar espacio de trabajo", - "eefd50457e": "Eliminando...", + "eefd50457e": "Eliminando…", "3ce211ece6": "Reabrir {{value0}}", - "6645ac7dd1": "Reapertura...", + "6645ac7dd1": "Reabriendo…", "b25f63edd7": "abierto", - "d2ca293f3d": "Laboral...", + "d2ca293f3d": "Procesando…", "ef064cb7c3": "por defecto", "59b4dccf70": "destructivo" }, "PortsPanel": { "3ea4a02a8f": "Cancelar", - "4eb801ce93": "servidor de desarrollo", + "4eb801ce93": "dev-server", "8dfed0a15c": "Etiqueta (opcional)", - "17bea6e391": "servidor local", - "a3721a50b0": "Anfitrión remoto", + "17bea6e391": "localhost", + "a3721a50b0": "Host remoto", "d57545ff92": "Igual que remoto", - "b950b1948b": "Puerto Local", + "b950b1948b": "Puerto local", "9e5a4118b0": "Puerto remoto", - "c9d106547a": "Adelante", + "c9d106547a": "Reenviar", "c7e920aa7c": "anunciado como {{value0}}", "e740075063": "Eliminar", "b3548e59f4": "Editar", "fe2730d050": "Copiar {{value0}}", "b22b128b2a": "Abrir en el navegador", - "75aeea592f": "Abra {{value0}} en el navegador", + "75aeea592f": "Abrir {{value0}} en el navegador", "de349d4560": "abre {{value0}}", "907eb53ed2": "Reenviar un puerto", - "04efd3dad4": "Reenvíe un puerto para acceder a servicios remotos en su máquina local.", + "04efd3dad4": "Reenvía un puerto para acceder a servicios remotos en tu máquina local.", "1f0d2a24f9": "Sin puertos reenviados", "36b1b2984a": "Detectado", - "ddbe58d74e": "reenviado", + "ddbe58d74e": "Reenviados", "a103dae837": "Agregar", "6bc058dbe1": "Puertos", "d4c3cd679c": "Reconectando...", @@ -8906,9 +8906,9 @@ "57d930fa45": "PID", "5dd86dcf2f": "Proceso", "b1ff94fa27": "Protocolo", - "729be0b4e5": "Amable", - "0f1d8cd324": "Unir", - "1c1c18cefc": "DIRECCIÓN", + "729be0b4e5": "Tipo", + "0f1d8cd324": "Bind", + "1c1c18cefc": "Dirección", "f9528da632": "Detener proceso", "a223459512": "Mostrar detalles", "bdac206faf": "Copiar detalles", @@ -8920,25 +8920,25 @@ "c1b115c375": "Ningún espacio de trabajo seleccionado", "98e9a414f8": "No se pudo abrir el navegador", "a00f3a2840": "No se pudieron actualizar los puertos", - "97b562d21d": "Proceso detenido el :{{value0}}", - "9079776663": "Ahorrar", + "97b562d21d": "Proceso detenido en :{{value0}}", + "9079776663": "Guardar", "c57eda6822": "editar", "9f475dc994": "Reenviando...", - "d7c83cfd24": "Ahorro...", + "d7c83cfd24": "Guardando...", "31e80cff2d": "Reenvíe un puerto remoto a su máquina local.", "10360598a4": "Actualice la configuración de reenvío de puertos.", "80206251c8": "Editar reenvío de puerto", "4bc9b00912": "espacio de trabajo", "3e13cb63ee": "Desconocido", "472054d94c": "Puerto :{{value0}}", - "1119f90ad7": "recipiente", + "1119f90ad7": "contenedor", "d32820d3e2": "Externo", "4db4b5e435": "Otros espacios de trabajo", "38b16cfbef": "No se detectaron puertos", - "0d63d94db3": "Exploración...", + "0d63d94db3": "Escaneando...", "935dda7718": "Espacio de trabajo activo", - "740aca88ab": "Error al escanear el puerto del espacio de trabajo.", - "5be4f7f727": "Menú Puerto {{value0}}", + "740aca88ab": "Error al escanear los puertos del espacio de trabajo.", + "5be4f7f727": "Menú del puerto {{value0}}", "7550998473": "Copiar", "1004af16ab": "Copiar {{value0}}" }, @@ -8949,7 +8949,7 @@ "4107975b3a": "en", "6aeda362ed": "resultado", "98c8435e36": "Seleccione un espacio de trabajo para buscar", - "1ec640c9c7": "fósforo", + "1ec640c9c7": "coincidencia", "dcc294f28d": "(resultados truncados)" }, "SearchFilters": { @@ -8960,8 +8960,8 @@ }, "SearchHeader": { "6234a5ef85": "Usar expresión regular", - "4567e6e0b6": "Coincidir con toda la palabra", - "464ae3974f": "Caso de partido", + "4567e6e0b6": "Coincidir palabra completa", + "464ae3974f": "Distinguir mayúsculas y minúsculas", "693cbeadd0": "Buscar" }, "SearchQueryRow": { @@ -8989,65 +8989,65 @@ "c085946bda": "Copiar nota en la línea {{value0}}", "1623bf4e19": "Copiar nota", "655633c08a": "Enviado", - "3eb9b2805e": "Abrir nota sobre {{value0}}", - "0d963bf982": "Abierto {{value0}}", + "3eb9b2805e": "Abrir nota en {{value0}}", + "0d963bf982": "Abrir {{value0}}", "59654650d3": "Borrar notas para {{value0}}", "ac8cbe3bf5": "Pase el cursor sobre una línea en la vista de diferencias y haga clic en + para agregar una nota.", - "286dbda4d6": "Rever", + "286dbda4d6": "Reintentar", "476b77745b": "Cambiar referencia base", - "ed34038d0d": "Actualizar comparación de sucursales", - "493f963029": "Cambiar referencia base", - "3278b2767b": "adelante", + "ed34038d0d": "Actualizar comparación de ramas", + "493f963029": "Cambiar ref base", + "3278b2767b": "por delante", "11b5dd8e41": "Comparando contra", - "783a808870": "Cerca", - "a9bf7c171a": "Commit fallida", + "783a808870": "Cerrar", + "a9bf7c171a": "Falló el commit", "03d238218c": "Detalles", - "011f9713fc": "Commit bloqueada", + "011f9713fc": "Commit bloqueado", "cc199ccc5f": "Más commits y acciones remotas", "4d6e1fd7f3": "Más acciones", "37a81f29ad": "Generando mensaje de commit. Haga clic para detener.", - "b94112eb9e": "mensaje de Commit", + "b94112eb9e": "Mensaje de commit", "0d0a8359d3": "Mensaje", - "15b7f210d7": "Revise el mensaje antes de iniciar un agente.", - "054ead86b1": "Solucionar errores de Commit con IA", - "9e5ccd00aa": "Contexto de error de Commit no disponible", - "f0a2dc9e46": "Personalizar el lanzamiento...", + "15b7f210d7": "Revisa el prompt antes de iniciar un agente.", + "054ead86b1": "Solucionar error de commit con IA", + "9e5ccd00aa": "Contexto del error de commit no disponible", + "f0a2dc9e46": "Personalizar lanzamiento...", "ec7bfced55": "Elija un agente para solucionar el error de commit", "dd43c47089": "Elija un agente para este error de commit", - "30b8d4f181": "Soluciona el error de commit con IA", - "4b37ae99b0": "Inicie el agente de IA predeterminado para solucionar este error de commit", - "ae743199cd": "Elija una rama base diferente antes de crear un {{value0}}.", + "30b8d4f181": "Solucionar error de commit con IA", + "4b37ae99b0": "Iniciar el agente de IA predeterminado para solucionar este error de commit", + "ae743199cd": "Elige una rama base diferente antes de crear un {{value0}}.", "318e2a7f88": "Espere a que termine la generación con IA.", - "f76307c1f7": "Elija una rama base.", + "f76307c1f7": "Elige una rama base.", "4f76c0a9de": "La rama base debe ser distinta de la rama HEAD.", "c5e4175139": "Más {{value0}} y acciones remotas", "78ddfd0bb4": "Crear como borrador", "e64a632456": "principal", - "6055949c50": "{{value0}} rama base", + "6055949c50": "Rama base de {{value0}}", "1f7119f604": "Base", "9484270f45": "Generando título y descripción…", "a0dc20fc93": "Descripción (opcional)", "a8873e1d62": "{{value0}} descripción", "7d6a8f0082": "Título", "a6eda33521": "{{value0}} título", - "02d8c04339": "Genera detalles {{value0}} con IA", + "02d8c04339": "Generar detalles de {{value0}} con IA", "aee92f8684": "Generar", "e868cec4e1": "Generando…", - "b355e740b2": "Dejar de generar detalles {{value0}}", - "527e130b6f": "dejar de generar", + "b355e740b2": "Dejar de generar detalles de {{value0}}", + "527e130b6f": "Dejar de generar", "e1970d327d": "Nuevo {{value0}}", "f4c766f1ca": "Elija el agente y la plantilla de comando para esta ejecución.", "1a6a6e0bc5": "Generar detalles de reseña alojada", - "6b122529d4": "Generar mensaje de Commit", + "6b122529d4": "Generar mensaje de commit", "e48caaf0dd": "Inició un agente de IA para los conflictos.", - "901140f47d": "Revise el mensaje antes de iniciar un agente.", + "901140f47d": "Revisa el prompt antes de iniciar un agente.", "19652ddd76": "Resolver conflictos con IA", - "c9ad22888e": "Elija el objetivo de comparación de sucursales para este repositorio.", + "c9ad22888e": "Elige el destino de comparación de ramas para este repositorio.", "574d2f4413": "Borrar notas", "05bb8f4a48": "Cancelar", "48db37cca9": "Ver todo", - "78ce2d37ac": "Empuja al tenedor", - "c05fe04839": "Empuja hacia la bifurcación en {{value0}} (no origen)", + "78ce2d37ac": "Push al fork", + "c05fe04839": "Hace push al fork en {{value0}} (no a origin)", "c35baf2f1e": "Filtrar archivos…", "2fe2a67580": "Más acciones de notas", "eae2d051af": "Copiar todas las notas", @@ -9059,34 +9059,34 @@ "a5e5a11090": "Descartar todo falló: no se pueden eliminar los archivos antes de descartarlos", "8a5ba6a988": "No se pudo cargar la diferencia de commit", "fe5bd1a610": "Creando {{value0}}...", - "812cb992ee": "Abierto el {{value0}}", + "812cb992ee": "Abrir en {{value0}}", "eef5446523": "{{value0}} #{{value1}} ya está abierto", "0453ca3a9a": "{{value0}} creado, pero Orca no pudo actualizarlo todavía.", - "f99560ab29": "Cancelar {{value0}} falló", + "f99560ab29": "No se pudo abortar {{value0}}", "eae7a1da5f": "No se pudieron borrar las notas.", "657e0c90ad": "{{value0}} nota{{value1}}", - "df5040e3c3": "fuera del escenario", - "8cde1a2fb0": "Escenario", + "df5040e3c3": "Quitar del stage", + "8cde1a2fb0": "Agregar al stage", "d54dd48b0b": "Descartar cambios", "989f3d5e34": "Restaurar archivo", "2830dd64a2": "eliminado", "11463f7a98": "Eliminar archivo sin seguimiento", "d62bc0c7d8": "sin seguimiento", - "ab31221779": "Carpeta sin escenario", - "bfe9011a0e": "Carpeta de escenario", + "ab31221779": "Quitar carpeta del stage", + "bfe9011a0e": "Agregar carpeta al stage", "6d7f2a47e5": "Descartar carpeta", "9b367363b6": "Eliminar sin seguimiento en la carpeta", "540ca8f78c": "Cancelar fusión", "425f138269": "Abortar rebase", "04832d8047": "rebase", - "c105a61960": "unir", + "c105a61960": "merge", "d7a5942e41": "{{value0}}: {{value1}} sin resolver", - "c56ba7fa06": "diferencia", - "94c42b252e": "Maryland", + "c56ba7fa06": "Diff", + "94c42b252e": "MD", "e59bca888a": "markdown", - "b6922abb13": "No se puede cargar la comparación de ramas.", - "715d229c86": "Comparación de sucursales no disponible", - "97d8b03cdf": "Error en la comparación de sucursales", + "b6922abb13": "No se pudo cargar la comparación de ramas.", + "715d229c86": "Comparación de ramas no disponible", + "97d8b03cdf": "Error en la comparación de ramas", "424ee0e5bf": "error", "834cb3f23d": "Arreglar con IA", "60bd988f0b": "Corrección de IA", @@ -9098,30 +9098,30 @@ "26511c22b4": "Creando...", "7a09d7f9d2": "base", "383cf92c73": "árbol", - "d7ae61269b": "Comprometidos en Sucursal", + "d7ae61269b": "Commits en la rama", "48a003c1b1": "Cambios preparados", "d4ef4bafc5": "Cambios", "522f44dce5": "Archivos sin seguimiento", "3636d0f686": "listo", "d2e9189866": "todo", "a0cc0e6b4e": "cargando", - "9339382454": "Dejar de escena todo", - "24d2598eff": "poner en escena todo", + "9339382454": "Quitar todo del stage", + "24d2598eff": "Agregar todo al stage", "ce41708855": "Descartar todo", "2f609a2e7c": "Eliminar todo sin seguimiento", - "9bb062a886": "no comprometido", + "9bb062a886": "sin commit", "9febd8ab5f": "crear_pr", - "f62ce91ade": "origen", + "f62ce91ade": "origin", "3a231c845b": "desconocido", "3baf6c77b4": "Copiar todas las notas al portapapeles", "72f2bea3f4": "Expandir notas", "d13edef890": "Contraer notas", - "0fad573938": "No comprometido", + "0fad573938": "Sin commit", "77afaa8152": "Todo", "d6fb1df5fe": "{{value0}} ya está abierto", "05838cfdeb": "{{value0}} conflicto", "d206117f90": "{{value0}} conflicto ({{value1}})", - "0b5b8c234c": "Abierto {{value0}} ({{value1}})", + "0b5b8c234c": "Abrir {{value0}} ({{value1}})", "d97ef8f221": "líneas {{value0}}-{{value1}}", "6f8bfa0eb9": "línea {{value0}}", "c569d29a02": "ambos modificados", @@ -9131,9 +9131,9 @@ "24773ee581": "añadido por nosotros", "c03d7c952f": "añadido por ellos", "5b176fa431": "ambos eliminados", - "31f6d46278": "Irresoluto", + "31f6d46278": "Sin resolver", "2c417432b7": "Resuelto localmente", - "f3a8b2c1d0e5": "Ingrese un título {{value0}}.", + "f3a8b2c1d0e5": "Ingresa un título para {{value0}}.", "e2b7a1c0d9f4": "No se pudo crear {{value0}}", "hugeRepoIgnorePrompt": "Este repositorio tiene demasiados cambios activos. ¿Agregar \"{{value0}}\" a .gitignore?", "hugeRepoIgnoreAction": "Agregar a .gitignore", @@ -9154,56 +9154,56 @@ "473f18758e": "Configuración de IA de Source Control", "createPrIntentConfigureAi": "Agrega un mensaje de commit o configura la IA de Source Control.", "createPrIntentGenerateFailed": "No se pudo generar un mensaje de commit. Agrega uno y vuelve a intentarlo.", - "createPrIntentCommitFailed": "No se pudieron confirmar los cambios. Corrige el problema y vuelve a intentar Crear PR.", + "createPrIntentCommitFailed": "No se pudo hacer commit de los cambios. Corrige el issue y vuelve a intentar Crear PR.", "createPrIntentNeedsSync": "Sincroniza esta rama antes de crear una revisión.", "createPrIntentBranchNotReady": "La rama aún no está lista para crear una revisión.", "createPrIntentPublishing": "Publicando rama…", "createPrIntentForcePushing": "Haciendo force push con lease…", - "createPrIntentPushing": "Subiendo commits…", + "createPrIntentPushing": "Haciendo push de commits…", "createPrIntentRemoteFailed": "No se pudo actualizar la rama remota. Vuelve a intentar Crear PR.", "createPrIntentGeneratingDetails": "Generando detalles de revisión…", "createPrIntentBranchChangedDuringDetails": "La rama cambió mientras se generaban los detalles de la revisión. Vuelve a intentar Crear PR.", "createPrIntentCreatingReview": "Creando revisión…", "a91f8e2b01": "Ver como lista", "b82e9f3c12": "Ver como árbol", - "f71c4a8d90": "Más acciones de control de fuente", + "f71c4a8d90": "Más acciones de Source Control", "c8e4a1f902": "Filtro: {{value0}}", "b3c8f1a902": "Filtrar archivos por nombre", "d4f8c2a901": "Limpiar y cerrar filtro", "e8a1c4b203": "vs", - "f9b2441bb6": "1 commit adelante de {{value0}}", - "b715ef615b": "{{value0}} commits adelante de {{value1}}", - "c1a8f3e204": "1 commit detrás de {{value0}}", - "d2b9g4f315": "{{value0}} commits detrás de {{value1}}", + "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}}", "e9c2a5d416": "A la par con {{value0}}" }, "SourceControlAgentActionDialog": { "8e856842d1": "No se pudo iniciar el agente seleccionado.", "c075d00de1": "No se puede resolver la conexión del espacio de trabajo.", "38b899cc02": "Todos los repositorios", - "808cfe0a3b": "este repositorio", - "994cddd1f7": "no guardar" + "808cfe0a3b": "Este repositorio", + "994cddd1f7": "No guardar" }, "SourceControlAgentActionDialogForm": { - "013c9ac04a": "Fuera de", - "1bb611240f": "Utilice {basePrompt} para el mensaje predeterminado de Orca.", - "23280cbab1": "Esta plantilla no incluye {basePrompt}, por lo que el agente no recibirá el mensaje predeterminado de Orca.", + "013c9ac04a": "Guardar para", + "1bb611240f": "Usa {basePrompt} para el prompt predeterminado de Orca.", + "23280cbab1": "Esta plantilla no incluye {basePrompt}, por lo que el agente no recibirá el prompt predeterminado de Orca.", "5421a96acb": "Guardar e iniciar agente", - "6cefcdfba1": "Puede cambiarlo más tarde en la configuración de AI de control de fuente.", - "c29f9cf266": "Guarde este mensaje y no muestre esta reseña la próxima vez", - "d8f40128ee": "{basePrompt} es el mensaje predeterminado de Orca.", + "6cefcdfba1": "Puedes cambiarlo más tarde en la configuración de IA de Source Control.", + "c29f9cf266": "Guardar este prompt y no mostrar esta revisión la próxima vez", + "d8f40128ee": "{basePrompt} es el prompt predeterminado de Orca.", "ea4788705e": "Cancelar", "7ec6abbf2a": "Reiniciar", - "f4f3c9ca4a": "plantilla de aviso", + "f4f3c9ca4a": "Plantilla de prompt", "1bc0bdbb5e": "Lanzamiento:", - "fe119187bb": "--soneto modelo", + "fe119187bb": "--model sonnet", "bc8dc39f4b": "Argumentos CLI", "b99c33cec5": "Ajustes", "15c5d85706": "Agente", "3e8f21954f": "error", "74168d7ada": "idle", - "1d47db9bf0": "No hay agents habilitados", - "c7ff8cef11": "Detectando agents...", + "1d47db9bf0": "No hay agentes habilitados", + "c7ff8cef11": "Detectando agentes...", "b0da3a4d3e": "La receta de lanzamiento ya está guardada", "bff4795a6d": "Cambia el agente, los argumentos o la plantilla del prompt para actualizar la receta guardada.", "5c75b24735": "Personaliza lo que recibe el agente antes de que Orca lo inicie." @@ -9216,12 +9216,12 @@ }, "SourceControlTextGenerationDialogForm": { "25fcd8e49a": "Guardar valores predeterminados", - "d91b0a189d": "guardar receta", + "d91b0a189d": "Guardar receta", "1f6fcfb6cf": "Plantilla de comando", - "551ffd111b": "--soneto modelo", + "551ffd111b": "--model sonnet", "4eab815004": "Argumentos CLI", "914c8f6ac2": "Comando personalizado", - "cce2cbd01d": "Elige agente", + "cce2cbd01d": "Elegir agente", "9c14186dd2": "Agente" }, "activity": { @@ -9235,7 +9235,7 @@ "checks": { "panel": { "content": { - "3916814392": "detrás (commit base:", + "3916814392": "por detrás (commit base:", "755be805f6": "Sin comentarios", "751f7c6e5c": "Mostrando los primeros 100 comentarios por fuente", "94557d68e2": "Comentarios", @@ -9244,107 +9244,107 @@ "0fc6f743b3": "por", "8987d5a3dd": "Resuelto", "ba20d1a896": "Responder a {{value0}}", - "f6a40263ff": "Ahorrar", + "f6a40263ff": "Guardar", "b062f55f29": "Cancelar", "c1f6fc006a": "Responder", "2ba0a32bdd": "bot", "6cc6eace26": "Borrar", "03ca88f623": "Editar", - "d3923d18fe": "ir a comentar", + "d3923d18fe": "Ir al comentario", "1abb17aac9": "Más", "74c6885b8a": "Más acciones de comentarios", - "cbcc4ab3db": "Mostrando los primeros 100 cheques", - "0dca6bfab5": "Abrir detalles del cheque", - "991f50c7e4": "Aún no se han reportado controles", + "cbcc4ab3db": "Mostrando los primeros 100 checks", + "0dca6bfab5": "Abrir detalles del check", + "991f50c7e4": "No hay checks configurados", "9ad98f2a17": "pendiente", - "5e52f4ef7f": "defecto", - "02ca4f9074": "paso", - "e15a8b77ef": "No hay detalles en línea disponibles para esta verificación.", - "679bf2093c": "Copiar la cola del registro", - "d713f500b2": "Cola de registro (últimas 200 líneas)", + "5e52f4ef7f": "fallando", + "02ca4f9074": "correcto", + "e15a8b77ef": "No hay detalles en línea disponibles para este check.", + "679bf2093c": "Copiar extracto del log", + "d713f500b2": "Extracto del log", "a916648574": "Abrir detalles", - "07eccfa397": "No hay detalles disponibles para este cheque.", - "49731703ea": "Empleos", + "07eccfa397": "No hay detalles disponibles para este check.", + "49731703ea": "Jobs", "f2fe8a4e8f": "Anotaciones", - "d098e5529a": "Producción", + "d098e5529a": "Salida", "2dd5ddabc4": "flujo de trabajo #", - "aa8494ae3c": "controlar #", - "00e1c1658a": "Terminado", - "fd46a70f1a": "Comenzó", + "aa8494ae3c": "check #", + "00e1c1658a": "Completado", + "fd46a70f1a": "Iniciado", "a54ae21c6f": "Estado:", - "e4e3af15ee": "Ver todos los detalles", - "2524d1fb83": "Cola de registro disponible con todos los detalles.", - "a2fb3f4408": "Mostrando los primeros 100 empleos", + "e4e3af15ee": "Ver detalles completos", + "2524d1fb83": "Cola del log disponible en los detalles completos.", + "a2fb3f4408": "Mostrando los primeros 100 jobs", "df137989b3": "Mostrando las primeras 20 anotaciones", - "1f2b980522": "Cargando detalles del cheque...", + "1f2b980522": "Cargando detalles del check…", "0c96cd25e5": "Resolver", - "3a71a6ed0b": "Resuelva los conflictos antes de que se puedan completar las comprobaciones y la fusión.", + "3a71a6ed0b": "Resuelve los conflictos antes de que los checks y el merge puedan completarse.", "60186d8498": "Los conflictos bloquean esto", - "c16762ac8c": "Las comprobaciones y comentarios a continuación muestran el contexto obtenido actualmente.", + "c16762ac8c": "Los checks y comentarios a continuación muestran el contexto obtenido actualmente.", "9d0e7bcefc": "Sin acciones de PR de bloqueo", - "5856874b59": "Orca actualizará los controles mientras este panel permanezca abierto.", - "5341023167": "controlar", + "5856874b59": "Orca actualizará los checks mientras este panel permanezca abierto.", + "5341023167": "check", "b45db92d0e": "Arreglar", - "5d4ebf9391": "Inspeccione los detalles o inicie un pase de reparación de IA.", - "b652f38caf": "cheque fallido", - "87cd07c69a": "Esta rama tiene conflictos que deben ser resueltos.", - "0975eeaaef": "Archivos conflictivos", + "5d4ebf9391": "Inspecciona los detalles o inicia una pasada de corrección con IA.", + "b652f38caf": "check fallido", + "87cd07c69a": "Esta rama tiene conflictos que deben resolverse", + "0975eeaaef": "Archivos con conflictos", "6fa7f8723f": "commit", "2b2be92919": "Añadir comentario", "7440d09d2c": "Iniciar conversación", "b37ebdc51c": "Comentarios no disponibles.", "90206b6353": "comentario", "95ad090b01": "hilo", - "365254cc1b": "no resolver", - "7f793b571d": "Arrastre para cambiar el tamaño de los cheques", + "365254cc1b": "Marcar como no resuelto", + "7f793b571d": "Arrastra para cambiar el tamaño de los checks", "ee07b33924": "desconocido", "cdbfda4dec": "Anotación", "066fedd446": "Trabajos fallidos", - "ae8a04ef17": "Los detalles del archivo de conflicto no están disponibles", - "73d0675356": "Detalles refrescantes del conflicto...", - "f5bc5c4cf1": "The hosting provider reports conflicts, but local Git did not reproduce them. Refresh the review or push the branch to recalculate mergeability.", - "5bc9bda2af": "Run from this worktree", - "e87fb3d929": "Copy mergeability refresh commands", - "1e53e45072": "Copied", - "084c516efb": "Copy commands", + "ae8a04ef17": "Los detalles del archivo en conflicto no están disponibles", + "73d0675356": "Actualizando detalles del conflicto…", + "f5bc5c4cf1": "El proveedor de hosting informa conflictos, pero Git local no los reprodujo. Actualiza la revisión o haz push de la rama para recalcular la capacidad de merge.", + "5bc9bda2af": "Ejecutar desde este worktree", + "e87fb3d929": "Copiar comandos para actualizar la capacidad de merge", + "1e53e45072": "Copiado", + "084c516efb": "Copiar comandos", "5dc3af25c0": "Seleccionar comentario", - "d7a2f9c401": "Send unresolved {{value0}} comments", + "d7a2f9c401": "Enviar {{value0}} comentarios sin resolver", "d91f2a6c39": "Enviar {{value0}} comentarios en cola a la IA", - "a6de3e5a20": "Borrar comentarios en cola", + "a6de3e5a20": "Limpiar comentarios en cola", "49ea0937e4": "Agregar comentario a la lista de resolución", "9fecebb29d": "Agregar", - "b8c4e2a1f7": "View full logs", - "f8a2c91d04": "Queue for agent", - "b4e8a1c902": "Queued", + "b8c4e2a1f7": "Ver logs completos", + "f8a2c91d04": "Poner en cola para el agente", + "b4e8a1c902": "En cola", "7c1f0a2b11": "Abrir", - "e8b4c1a903": "Resolved · {{value0}}", - "c3a8e5d710": "Needs review · {{value0}}", - "a7f0c7e8d1": "Queue", - "8a621a2c4f": "Grouped", - "b13f85d75c": "Timeline", - "f5cf324efa": "Comment display options", - "5e6e5a13fa": "View", - "actionRequiredHint": "This check needs a manual action on GitHub (for example, approving the workflow run) before merging is unblocked." + "e8b4c1a903": "Resuelto · {{value0}}", + "c3a8e5d710": "Necesita revisión · {{value0}}", + "a7f0c7e8d1": "Poner en cola", + "8a621a2c4f": "Agrupados", + "b13f85d75c": "Cronología", + "f5cf324efa": "Opciones de visualización de comentarios", + "5e6e5a13fa": "Vista", + "actionRequiredHint": "Este check requiere una acción manual en GitHub (por ejemplo, aprobar la ejecución del workflow) antes de que el merge quede desbloqueado." }, "empty": { "state": { - "3322603418": "Pull request status unavailable", - "5b0cfae9a5": "Cree un {{value0}} para iniciar comprobaciones y revisiones.", + "3322603418": "Estado del pull request no disponible", + "5b0cfae9a5": "Crea un {{value0}} para iniciar checks y revisión.", "13e1c7d5ed": "No se encontró {{value0}}", "d372072df1": "La actualización de GitHub está pausada por el presupuesto de límite de velocidad actual", - "7c299df37b": "No se encontró ninguna solicitud de extracción", + "7c299df37b": "No se encontró ningún pull request", "3d4af82ff4": "Actualizando el estado de GitHub para esta rama", - "938b5606a6": "Comprobando la solicitud de extracción", + "938b5606a6": "Comprobando si hay un pull request", "6ba2440770": "Esperando actualizar el estado de GitHub para esta rama", "2bdd7aaf2d": "No se pudo actualizar el estado de GitHub. Se conservaron los datos almacenados en caché existentes.", - "5f478ab3d3": "No se pudo actualizar la solicitud de extracción", - "6ce9d4e069": "Empuje su rama antes de crear un {{value0}}.", - "76e15946a9": "La rama tiene commits no enviadas", - "f8543140cc": "Publique esta rama antes de crear un {{value0}}.", - "41252bc53f": "Sucursal no publicada", - "05e4aec17b": "{{value0}} cheques estarán disponibles una vez completada la operación.", + "5f478ab3d3": "No se pudo actualizar el pull request", + "6ce9d4e069": "Haz push de la rama antes de crear un {{value0}}.", + "76e15946a9": "La rama tiene commits sin push", + "f8543140cc": "Publica esta rama antes de crear un {{value0}}.", + "41252bc53f": "Rama no publicada", + "05e4aec17b": "Los checks de {{value0}} estarán disponibles cuando termine la operación", "d77c513c1e": "{{value0}} en progreso", - "b597440265": "Refresh GitHub status for this branch to load checks and review." + "b597440265": "Actualiza el estado de GitHub de esta rama para cargar checks y revisión." } } } @@ -9353,19 +9353,19 @@ "mr": { "merge": { "state": { - "04a3015a12": "Capaz de fusionarse", - "53c6d3b7e9": "GitLab dice que este MR se puede fusionar, pero el proceso aún está en ejecución", - "65c847ad1e": "Cheques pendientes", - "b41fbc180c": "GitLab dice que este MR se puede fusionar, pero algunos trabajos de canalización fallaron", + "04a3015a12": "Se puede hacer merge", + "53c6d3b7e9": "GitLab dice que este MR puede hacer merge, pero el pipeline sigue en ejecución", + "65c847ad1e": "Checks pendientes", + "b41fbc180c": "GitLab dice que este MR puede hacer merge, pero algunos jobs del pipeline fallaron", "49ac4fec10": "Los controles fallaron", - "22b7e50621": "Los informes de GitLab fusionan conflictos", + "22b7e50621": "GitLab informa conflictos de merge", "96b05e374c": "Conflictos", - "d63bb6f76e": "Esta solicitud de fusión aún es un borrador.", + "d63bb6f76e": "Este merge request aún está en borrador", "b2715092c6": "Borrador", "2388413f28": "Esta solicitud de fusión está cerrada", "88d044c42f": "Cerrado", - "ee482a2bad": "Esta solicitud de fusión ya está fusionada", - "fae95ae20d": "Fusionado" + "ee482a2bad": "Este merge request ya se hizo merge", + "fae95ae20d": "Mergeado" } } } @@ -9376,8 +9376,8 @@ "864111caa2": "Posición de la barra de actividades", "e8e2e4ce74": "Alternar barra lateral derecha", "441733b630": "Puertos", - "83a10e3c44": "cheques", - "0314901467": "Control de fuente", + "83a10e3c44": "Checks", + "0314901467": "Control de código fuente", "06219e4cb1": "Buscar", "8bc2bbc3a0": "Explorador", "45b78f03bc": "lado", @@ -9389,8 +9389,8 @@ "ef182dcb12": "buscar", "fc3095d2ed": "explorador", "aiVaultSessionHistory": "Agentes", - "folderWorkspaces": "Árboles de trabajo adjuntos", - "parentPrChecks": "Comprobaciones de PR" + "folderWorkspaces": "Worktrees adjuntos", + "parentPrChecks": "Checks de PR" }, "right": { "panel": { @@ -9400,9 +9400,9 @@ "cf5a7aba6f": "Lista", "d6d9c3c947": "Cita", "f49e0a21e0": "Código", - "542bf6a7e2": "Itálico", - "256300f8ea": "Atrevido", - "87aff03d63": "Envío...", + "542bf6a7e2": "Cursiva", + "256300f8ea": "Negrita", + "87aff03d63": "Enviando...", "commentTooLarge": "El comentario es demasiado grande para enviarlo de forma segura." } } @@ -9416,10 +9416,10 @@ "launch": { "a8b97d2318": "Se inició un agente de IA para el error de commit.", "5540ff50cc": "No se pudo generar el comando de inicio del agente.", - "9bbd9077a2": "No hay agents de IA habilitados. Configure los agents en Configuración.", - "d481ab22f9": "El agente de IA guardado no está disponible. Utilice Personalizar inicio para elegir otro agente.", - "f2b47026e8": "El mensaje de error de Commit está vacío. Actualice la configuración de AI de control de fuente.", - "4f4e0418a0": "No se pudo generar el mensaje del agente.", + "9bbd9077a2": "No hay agentes de IA habilitados. Configura los agentes en Configuración.", + "d481ab22f9": "El agente de IA guardado no está disponible. Usa Personalizar inicio para elegir otro agente.", + "f2b47026e8": "El prompt de error de commit está vacío. Actualiza la configuración de IA de Source Control.", + "4f4e0418a0": "No se pudo generar el prompt del agente.", "216f762bd7": "No se puede resolver la conexión del espacio de trabajo." } } @@ -9427,11 +9427,11 @@ }, "discard": { "confirmation": { - "2ae5a785b3": "¿Descartar todos los cambios no preparados?", - "ddf36f291c": "Esto eliminará y revertirá todos los cambios preparados. Se eliminarán los archivos nuevos preparados. Esto no se puede deshacer.", - "5ddd8cac7f": "¿Descartar todos los cambios por etapas?", + "2ae5a785b3": "¿Descartar todos los cambios unstaged?", + "ddf36f291c": "Esto quitará del stage y revertirá todos los cambios staged. Se eliminarán los archivos nuevos staged. Esto no se puede deshacer.", + "5ddd8cac7f": "¿Descartar todos los cambios staged?", "1426c2efff": "Esto revertirá todos los cambios realizados en este archivo. Esto no se puede deshacer.", - "d4df3a61df": "¿Descartar cambios a \"{{value0}}\"?", + "d4df3a61df": "¿Descartar cambios en \"{{value0}}\"?", "40e9357b2a": "Esto restaurará el archivo desde HEAD y descartará la eliminación. Esto no se puede deshacer.", "5c0bdbc4cb": "¿Restaurar \"{{value0}}\"?", "d97bf697c9": "Esto eliminará permanentemente este archivo. Esto no se puede deshacer.", @@ -9439,7 +9439,7 @@ }, "dialog": { "3bc61dc989": "Cancelar", - "15efa778e3": "Desechar", + "15efa778e3": "Descartar", "6de99d162b": "entrada", "42f89dd030": "archivos", "e7611dca35": "archivo", @@ -9450,59 +9450,59 @@ }, "dropdown": { "items": { - "7aad2c0240": "Operación de revisión alojada en curso...", + "7aad2c0240": "Operación de review alojada en curso…", "9e779995dd": "Crear {{value0}}", - "226b85a3a7": "Buscar", + "226b85a3a7": "Fetch", "323bb614aa": "Commit y sincronizar", "2b8e6595fd": "Commit", - "04d709801d": "Fetch from remote without merging" + "04d709801d": "Fetch del remoto sin hacer merge" } }, "primary": { "action": { "ed93b4f14f": "Commit", - "946a8a05ea": "Crea un {{value0}} para esta rama", + "946a8a05ea": "Crear un {{value0}} para esta rama", "e7ffa46946": "Crear {{value0}}", - "95550cff15": "Empujar", - "d64292a938": "Jalar", + "95550cff15": "Push", + "d64292a938": "Pull", "795f1509c5": "Sincronizar", - "390abeab93": "Empuje forzado", - "1884cf34af": "Publicar esta rama en origen", - "7b4d02e6b8": "Rama de publicación", - "3d5dccef0b": "Nada que comprometer. PR ya está fusionado.", - "41d4bcf157": "Comprobando el estado de las PR...", - "acce237921": "Nada que comprometer. La sucursal no tiene cambios para publicar.", - "fa3bd4f40c": "Prepare al menos un archivo para commit", - "5a477d80cb": "Organiza todos los cambios", - "18a0fca877": "Escenario todo", - "f01f16d77f": "Ingrese un mensaje de commit para commit", - "ab41fb926b": "Realizar cambios por etapas", - "2d8f185fbc": "Prepare todos los cambios antes de enviar archivos parcialmente preparados", - "a6457b46a7": "Resolver conflictos antes de comprometerse", + "390abeab93": "Force Push", + "1884cf34af": "Publicar esta rama en origin", + "7b4d02e6b8": "Publicar rama", + "3d5dccef0b": "No hay nada para hacer commit. El PR ya tiene merge.", + "41d4bcf157": "Comprobando el estado del PR…", + "acce237921": "No hay nada para hacer commit. La rama no tiene cambios para publicar.", + "fa3bd4f40c": "Haz stage de al menos un archivo para crear un commit", + "5a477d80cb": "Hacer stage de todos los cambios", + "18a0fca877": "Hacer stage de todo", + "f01f16d77f": "Ingresa un mensaje de commit para hacer commit", + "ab41fb926b": "Hacer commit de los cambios staged", + "2d8f185fbc": "Haz stage de todos los cambios antes de commitear archivos parcialmente staged", + "a6457b46a7": "Resuelve los conflictos antes de hacer commit", "484f45c439": "{{value0}} en progreso…", - "6f7a8b9c0d": "Remote operation in progress…", - "7f8a9b0c1d": "Remote operation in progress — try again once it finishes", - "74fc171e99": "Empuje forzado en progreso...", - "16aee3a5c1": "Compromiso en progreso...", - "e61b0d7a3c": "Cambia a una rama antes de publicar commits.", - "1d47e850cf": "Envía actualizaciones a la rama de revisión vinculada", - "c39d0c75c3": "El destino de la rama de revisión vinculada no está disponible.", + "6f7a8b9c0d": "Operación remota en curso…", + "7f8a9b0c1d": "Operación remota en curso — inténtalo de nuevo cuando termine", + "74fc171e99": "Force Push en curso…", + "16aee3a5c1": "Commit en curso…", + "e61b0d7a3c": "Haz checkout de una rama antes de publicar commits.", + "1d47e850cf": "Haz push de actualizaciones a la rama de review vinculada", + "c39d0c75c3": "El destino de la rama de review vinculada no está disponible.", "8c6d15a07d": "Crear PR", "d37e68f61d": "Preparando la rama para revisión…", "c72e5e65d1": "Prepara esta rama y crea un {{value0}}", - "b8e4f2a901": "Wait for the remote operation to finish.", - "c9f3a1b802": "Resolve conflicts before creating a {{value0}}.", - "d2a8c4e703": "No changes on this branch to include in a {{value0}}.", - "e3b9d5f814": "Cannot create a {{value0}} from the default branch.", - "f4c0e6a925": "Commit changes before creating a {{value0}}.", - "a5d1f7b036": "Publish commits before creating a {{value0}}.", - "b6e2a8c147": "Push commits before creating a {{value0}}.", - "c7f3b9d258": "Sync this branch before creating a {{value0}}.", - "d8a4c0e369": "Authenticate before creating a {{value0}}.", - "e9b5d1f470": "Check out a branch before creating a {{value0}}.", - "f0c6e2a581": "This branch is not ready for a {{value0}} yet.", - "8f9a0b1c2d": "Nothing to commit. Branch is up to date.", - "h3i4j5k607": "Checking whether this branch can create a {{value0}}…" + "b8e4f2a901": "Espera a que termine la operación remota.", + "c9f3a1b802": "Resuelve los conflictos antes de crear un {{value0}}.", + "d2a8c4e703": "No hay cambios en esta rama para incluir en un {{value0}}.", + "e3b9d5f814": "No se puede crear un {{value0}} desde la rama predeterminada.", + "f4c0e6a925": "Haz commit de los cambios antes de crear un {{value0}}.", + "a5d1f7b036": "Publica los commits antes de crear un {{value0}}.", + "b6e2a8c147": "Haz push de los commits antes de crear un {{value0}}.", + "c7f3b9d258": "Sincroniza esta rama antes de crear un {{value0}}.", + "d8a4c0e369": "Autentícate antes de crear un {{value0}}.", + "e9b5d1f470": "Haz checkout de una rama antes de crear un {{value0}}.", + "f0c6e2a581": "Esta rama aún no está lista para un {{value0}}.", + "8f9a0b1c2d": "No hay nada para hacer commit. La rama está actualizada.", + "h3i4j5k607": "Comprobando si esta rama puede crear un {{value0}}…" } } } @@ -9517,14 +9517,14 @@ } }, "useFileDeletion": { - "72691dfebc": "No se pudo realizar {{value0}} '{{value1}}'.", + "72691dfebc": "No se pudo {{value0}} '{{value1}}'.", "96affe1302": "'{{value0}}' se movió a {{value1}}", "74727df633": "'{{value0}}' eliminado", - "d979a4fbb5": "¿Eliminar permanentemente '{{value0}}'?", + "d979a4fbb5": "¿Eliminar '{{value0}}' permanentemente?", "a76c74f105": "destructivo", - "92276aceb7": "Borrar", - "7fb9435c86": "This permanently deletes the directory and its contents on the remote host. This cannot be undone.", - "23e98f192f": "This permanently deletes the file on the remote host. This cannot be undone." + "92276aceb7": "Eliminar", + "7fb9435c86": "Esto elimina permanentemente el directorio y su contenido en el host remoto. Esto no se puede deshacer.", + "23e98f192f": "Esto elimina permanentemente el archivo en el host remoto. Esto no se puede deshacer." }, "useFileExplorerHandlers": { "32cd9fd991": "No se puede abrir el destino del enlace simbólico" @@ -9544,62 +9544,62 @@ "request": { "review": { "copy": { - "a1f8c3d2e4": "La inserción se realizó correctamente, pero la creación de {{value0}} falló: {{value1}}" + "a1f8c3d2e4": "El push se realizó correctamente, pero falló la creación de {{value0}}: {{value1}}" } } } } }, "AiVaultPanel": { - "resumeCommandCopied": "Resume command copied", + "resumeCommandCopied": "Comando para reanudar copiado", "valueCopied": "{{value0}} copiado", "valueCopyFailed": "No se pudo copiar {{value0}}", - "openWorkspaceBeforeResuming": "Open a workspace before resuming a session.", + "openWorkspaceBeforeResuming": "Abre un workspace antes de reanudar una sesión.", "localWorkspacesOnly": "Reanudar desde el historial solo está disponible en espacios de trabajo locales.", - "agentSessionQueued": "{{value0}} session queued", - "sessionHistory": "Agent Session History", - "shownRecent": "{{value0}} shown · {{value1}} recent", - "resumePastSessions": "Resume past sessions", - "refreshSessionHistory": "Refresh Session History", - "searchSessions": "Search sessions", - "clearSearch": "Clear search", + "agentSessionQueued": "Sesión de {{value0}} en cola", + "sessionHistory": "Historial de sesiones de agentes", + "shownRecent": "{{value0}} mostradas · {{value1}} recientes", + "resumePastSessions": "Reanudar sesiones anteriores", + "refreshSessionHistory": "Actualizar historial de sesiones", + "searchSessions": "Buscar sesiones", + "clearSearch": "Limpiar búsqueda", "remoteBrowseLocalHistory": "Remote workspaces can browse local history. Resume actions run from local workspaces.", - "transcriptsSkipped": "{{count}} transcript skipped", - "noAgentSessionsFound": "No agent sessions found", - "noSessionsMatchFilters": "No sessions match the current filters", + "transcriptsSkipped": "{{count}} transcripciones omitidas", + "noAgentSessionsFound": "No se encontraron sesiones de agentes", + "noSessionsMatchFilters": "Ninguna sesión coincide con los filtros actuales", "sessionId": "ID de sesión", - "logPath": "Ruta del registro", + "logPath": "Ruta del log", "agents": "Agentes", "sessionsShownCompact": "{{value0}} mostradas", - "originalPaneUnavailable": "Original pane is no longer available.", - "worktreeUnavailable": "Worktree is no longer available.", - "runtimeBrowseLocalHistory": "Runtime-hosted workspaces can browse local history. Resume actions are available in local and SSH workspaces.", - "runtimeWorkspacesUnsupported": "Resume from history is not available in runtime-hosted workspaces.", - "openSupportedWorkspace": "Open a local or SSH workspace before resuming a session.", - "localSessionSshWorkspaceUnsupported": "El historial de esta sesión está almacenado en este equipo, por lo que no se puede reanudar en un espacio de trabajo SSH. En su lugar, abre un espacio de trabajo local." + "originalPaneUnavailable": "El panel original ya no está disponible.", + "worktreeUnavailable": "El worktree ya no está disponible.", + "runtimeBrowseLocalHistory": "Los workspaces alojados en runtime pueden explorar el historial local. Las acciones de reanudación están disponibles en workspaces locales y SSH.", + "runtimeWorkspacesUnsupported": "Reanudar desde el historial no está disponible en workspaces alojados en runtime.", + "openSupportedWorkspace": "Abre un workspace local o SSH antes de reanudar una sesión.", + "localSessionSshWorkspaceUnsupported": "El historial de esta sesión solo existe en este equipo. Para reanudarla, abre un workspace local. Los workspaces SSH no tienen acceso a ese historial." }, "AiVaultPanelControls": { - "scanningSessions": "Scanning sessions", - "scopeAriaLabel": "Session History scope: {{value0}}", - "currentWorkspaceLower": "current workspace", + "scanningSessions": "Buscando sesiones", + "scopeAriaLabel": "Ámbito del historial de sesiones: {{value0}}", + "currentWorkspaceLower": "workspace actual", "currentWorktreeLower": "current worktree", - "allSessionsLower": "all sessions", + "allSessionsLower": "todas las sesiones", "thisScope": "This", - "allScope": "All", + "allScope": "Todo", "scope": "Scope", "currentWorkspace": "Current workspace", "allSessions": "All sessions", - "viewOptionsAriaLabel": "Session History view options", - "viewOptions": "View options", + "viewOptionsAriaLabel": "Opciones de vista del historial de sesiones", + "viewOptions": "Opciones de vista", "agents": "Agentes", - "sort": "Sort", - "lastUpdated": "Last updated", - "created": "Created", - "group": "Group", + "sort": "Ordenar", + "lastUpdated": "Última actualización", + "created": "Creado", + "group": "Agrupar", "folder": "Carpeta", "agent": "Agente", - "resetView": "Reset view", - "hideEmptySessions": "Hide empty sessions", + "resetView": "Restablecer vista", + "hideEmptySessions": "Ocultar sesiones vacías", "workspaceScope": "Workspace", "worktreeScope": "Worktree", "globalScope": "Global", @@ -9624,8 +9624,8 @@ "resumeCommand": "Comando para reanudar", "sessionActions": "Acciones de sesión {{value0}}", "resumeInNewTab": "Reanudar en nueva pestaña", - "resumeInWorktree": "Reanudar en árbol de trabajo", - "viewLog": "Ver registro", + "resumeInWorktree": "Reanudar en worktree", + "viewLog": "Ver log", "copyResumeCommand": "Copiar comando para reanudar", "openLog": "Abrir registro", "revealLog": "Mostrar registro", @@ -9656,19 +9656,19 @@ "systemRole": "Sistema", "sessionRole": "Sesión", "jumpToOriginalPane": "Saltar al panel original", - "worktree": "Árbol de trabajo", - "jumpToWorktree": "Saltar al árbol de trabajo" + "worktree": "Worktree", + "jumpToWorktree": "Saltar al worktree" }, "AiVaultSessionRow": { - "resumeAgentSession": "Reanudar sesión {{value0}}", + "resumeAgentSession": "Reanudar sesión de {{value0}}", "resumeInNewTab": "Reanudar en nueva pestaña", "copyResumeCommand": "Copiar comando para reanudar", "openLog": "Abrir registro", - "revealLog": "Mostrar registro", + "revealLog": "Mostrar registro en carpeta", "openWorkingDirectory": "Abrir directorio de trabajo", "copySessionId": "Copiar ID de sesión", "copyLogPath": "Copiar ruta de registro", - "messageCount": "{{value0}} msjs", + "messageCount": "{{value0}} mens.", "tokenCount": "{{value0}} tok", "toggleSessionDetails": "Detalles de sesión de {{value0}}", "hideDetails": "Ocultar detalles", @@ -9683,7 +9683,7 @@ "systemRole": "Sistema", "sessionRole": "Sesión", "jumpToOriginalPane": "Saltar al panel original", - "jumpToWorktree": "Saltar al árbol de trabajo" + "jumpToWorktree": "Saltar al worktree" }, "FileExplorerNameFilter": { "26fb73c6e3": "Buscar archivos", @@ -9713,15 +9713,15 @@ "pull": { "policy": { "notice": { - "merge": "Fusionar (Merge)", - "mergeDescription": "Crear un commit de fusión cuando tanto el repositorio local como el remoto hayan cambiado.", - "rebase": "Reorganizar (Rebase)", - "rebaseDescription": "Volver a aplicar los commits locales sobre la rama remota.", - "fastForwardOnly": "Solo avance rápido (Fast-forward)", - "fastForwardOnlyDescription": "Solo realizar pull cuando no se requiera fusión ni rebase.", + "merge": "Merge", + "mergeDescription": "Crea un commit de merge cuando hayan cambiado tanto la rama local como la remota.", + "rebase": "Rebase", + "rebaseDescription": "Reaplica los commits locales sobre la rama remota.", + "fastForwardOnly": "Solo fast-forward", + "fastForwardOnlyDescription": "Solo permite pull cuando no se necesita merge ni rebase.", "title": "El pull requiere una política", - "diverged": "Divergido", - "body": "Esta rama tiene commits locales y remotos. Ejecute un comando en este árbol de trabajo o en el host SSH, luego intente Pull o Sincronizar de nuevo.", + "diverged": "Divergida", + "body": "Esta rama tiene commits locales y remotos. Ejecuta un comando en este worktree o en el host SSH y luego intenta Pull o Sync de nuevo.", "copyAria": "Copiar el comando de política de pull {{value0}}", "copied": "Copiado", "copyCommand": "Copiar comando" @@ -9729,15 +9729,15 @@ } }, "AiVaultSessionWorktree": { - "currentWorktree": "Árbol de trabajo actual", - "activeWorktree": "Árbol de trabajo activo", - "archivedWorktree": "Árbol de trabajo archivado", - "unavailableWorktree": "Árbol de trabajo no disponible", - "jumpToWorktree": "Saltar al árbol de trabajo", - "noRecordedWorktree": "No se registró ningún árbol de trabajo para esta sesión.", - "archivedJumpUnavailable": "Esta sesión se encuentra en un árbol de trabajo archivado.", - "noActiveWorktreeMatch": "Ningún árbol de trabajo activo coincide con esta sesión.", - "noActiveWorktreeTarget": "No hay ningún árbol de trabajo activo disponible." + "currentWorktree": "Worktree actual", + "activeWorktree": "Worktree activo", + "archivedWorktree": "Worktree archivado", + "unavailableWorktree": "Worktree no disponible", + "jumpToWorktree": "Saltar al worktree", + "noRecordedWorktree": "No se registró ningún worktree para esta sesión.", + "archivedJumpUnavailable": "Esta sesión está en un worktree archivado.", + "noActiveWorktreeMatch": "Ningún worktree activo coincide con esta sesión.", + "noActiveWorktreeTarget": "No hay ningún worktree activo disponible." } } }, @@ -9749,32 +9749,32 @@ "929734aea5": "Deseleccionar todo" }, "NestedRepoScanLimitNotice": { - "642a43c139": "Límites de análisis del repositorio anidado", - "574eb5408b": "Mostrando resultados de escaneo parciales.", + "642a43c139": "Límites de escaneo de repositorios anidados", + "574eb5408b": "Mostrando resultados parciales del escaneo.", "03e9beab7b": "El escaneo se detuvo antes de tiempo." }, "RepoCombobox": { "b3e15f4525": "Agregar proyecto", - "b4a235e886": "Agregar proyecto", + "b4a235e886": "Agregando proyecto", "3639fd9da2": "SSH", - "e7ed739236": "Ningún proyecto/carpeta coincide con su búsqueda.", + "e7ed739236": "Ningún proyecto o carpeta coincide con tu búsqueda.", "a0c48f5f29": "Buscar proyectos/carpetas...", "116812151a": "Agregando proyecto…" }, "repo": { "icon": { "0ad395d475": "Caja", - "857977b901": "formas", - "b1b8d99fc4": "AI", - "137bdb1856": "Métrica", + "857977b901": "Formas", + "b1b8d99fc4": "IA", + "137bdb1856": "Métricas", "d202c659a3": "Diseño", - "c4fd14299d": "Compañía", - "4ab9433660": "Trabajar", + "c4fd14299d": "Empresa", + "4ab9433660": "Trabajo", "febfbe0cd5": "Herramientas", "ecf63ec3ef": "Lanzamiento", "31826b712e": "API", - "70bef15d40": "capas", - "b5fac337aa": "Calcular", + "70bef15d40": "Capas", + "b5fac337aa": "Cómputo", "d37b4e2641": "Servidor", "3c5a593bc8": "Web", "477b28c948": "Base de datos", @@ -9789,68 +9789,68 @@ "pet": { "pet": { "models": { - "7433516faf": "Duendecillo", + "7433516faf": "Gremlin", "a84d5677ff": "OpenCode", - "2528586aa7": "Claudio" + "2528586aa7": "Claudino" } }, "PetOverlay": { - "de932b0e8f": "@keyframes pet-bob { 0%,100% { transformar: traducirY(0); } 50% { transformar: traducirY(-4px); } }", - "4712d196c6": "@keyframes pet-{{value0}} { desde { posición-fondo: {{value1}}px {{value2}}px; } a { posición de fondo: {{value3}}px {{value4}}px; } }" + "de932b0e8f": "@keyframes pet-bob { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-4px); } }", + "4712d196c6": "@keyframes pet-{{value0}} { from { background-position: {{value1}}px {{value2}}px; } to { background-position: {{value3}}px {{value4}}px; } }" } }, "onboarding": { "AgentStep": { "e6a369bd04": "Agentes populares", - "d7b3ef168b": "Detectado en su sistema", + "d7b3ef168b": "Detectados en tu sistema", "9c163bb0e0": "Instrucciones de instalación", - "69af7e9c1c": "aún no está en tu RUTA. Orca lo configurará como predeterminado y podrás instalarlo en cualquier momento.", - "1eee1c7bd8": "No se detectaron agents en su RUTA. Elija uno para instalarlo más tarde o continúe con un terminal en blanco.", - "hideAgents": "Ocultar agents", - "showMoreAgents": "Mostrar {{value0}} más agents→", - "yoloPermissionsLabel": "Yolo / Dangerously skip permissions", - "yoloPermissionsInfo": "Agent permission info", - "yoloPermissionsTooltip": "Skip permission checks for agents for less interruptions" + "69af7e9c1c": "aún no está en tu PATH. Orca lo configurará como predeterminado y podrás instalarlo en cualquier momento.", + "1eee1c7bd8": "No se detectaron agentes en tu PATH. Elige uno para instalarlo más tarde o continúa con una terminal en blanco.", + "hideAgents": "Ocultar agentes", + "showMoreAgents": "Mostrar {{value0}} agentes más→", + "yoloPermissionsLabel": "Yolo / Omitir permisos de forma peligrosa", + "yoloPermissionsInfo": "Información de permisos del agente", + "yoloPermissionsTooltip": "Omite las comprobaciones de permisos de los agentes para reducir interrupciones" }, "FeatureSetupChecklist": { - "77f74946f5": "Los Agents pueden enviarse mensajes entre sí, realizar tareas y coordinar traspasos.", - "399cf885c0": "Orquestación de Agents", - "c5292c409d": "Los Agents pueden inspeccionar las ventanas de las aplicaciones y operar aplicaciones locales cuando usted lo solicite.", + "77f74946f5": "Los agentes pueden enviarse mensajes entre sí, tomar tareas y coordinar handoffs.", + "399cf885c0": "Orquestación de agentes", + "c5292c409d": "Los agentes pueden inspeccionar ventanas de apps y operar apps locales cuando se lo pidas.", "1ecfb490ac": "Uso de la computadora", - "01426f3a23": "Los Agents pueden navegar por sitios, inspeccionar páginas y realizar tareas del navegador.", - "ea85d9e628": "Uso del navegador del Agent", - "linearTicketsTitle": "Habilidad de Agent de Linear", - "linearTicketsDescription": "Los Agents pueden usar tareas Linear enlazadas para traspasos más completos y conscientes del ticket.", - "linearTicketsSetupSummary": "Recomendado para espacios de trabajo Linear; no afecta la configuración de conexión de Linear." + "01426f3a23": "Los agentes pueden navegar por sitios, inspeccionar páginas y trabajar en tareas del navegador.", + "ea85d9e628": "Uso del navegador por agentes", + "linearTicketsTitle": "Skill de agente de Linear", + "linearTicketsDescription": "Los agentes pueden usar tareas de Linear enlazadas para handoffs con más contexto de ticket.", + "linearTicketsSetupSummary": "Recomendado para espacios de trabajo de Linear; no afecta la configuración de conexión de Linear." }, "FeatureSetupInlineTerminal": { - "789b59936e": "Presione Enter para ejecutar el comando y confirme npx si se le solicita. También puedes configurar esto más tarde en Configuración.", - "47fc6cc6dc": "Comando de configuración de habilidades", - "c767ab7061": "Configuración de habilidades" + "789b59936e": "Pulsa Enter para ejecutar el comando y confirma npx si se te solicita. También puedes configurarlo más tarde en Configuración.", + "47fc6cc6dc": "Comando de configuración de skills", + "c767ab7061": "Configuración de skills" }, "IntegrationsStep": { "277f30eb34": "Linear, GitLab, Bitbucket, Azure DevOps, Gitea y Jira se encuentran en Configuración > Integraciones.", "3a3e360289": "Más fuentes de tareas", - "80e3ce0bc9": "Vuelva a comprobar", + "80e3ce0bc9": "Volver a comprobar", "04ef416712": "Agregar acceso Linear", "dd9c186a8b": "Agregar acceso al espacio de trabajo", "c91a5782f1": "Conectado", "27743304b1": "Linear", - "af69f42372": "Presione Entrar para ejecutar la autenticación CLI de GitHub. Vuelva a verificar GitHub después de que finalice el flujo del navegador o dispositivo.", + "af69f42372": "Pulsa Enter para ejecutar la autenticación de la CLI de GitHub. Vuelve a comprobar GitHub cuando termine el flujo del navegador o del dispositivo.", "f9d2e12d17": "Comando de inicio de sesión de GitHub", "6d469169f2": "Configuración de GitHub", "bd5d976fb2": "Instalar gh", - "50db38cf4b": "Solicitudes de extracción, problemas y estado de verificación.", - "c1547656f0": "De cheques…", - "8405043962": "Iniciar sesión es necesario", - "5c115cb713": "CLI no instalado", + "50db38cf4b": "Pull requests, issues y estado de checks.", + "c1547656f0": "Comprobando…", + "8405043962": "Se requiere iniciar sesión", + "5c115cb713": "CLI no instalada", "217beb0658": "GitHub", "4983ae7433": "Agregue acceso Linear con una clave API personal. Las claves de acceso total pueden mostrar todos los equipos a los que puede acceder el propietario de la clave.", - "b08a6ac93c": "{{value0}} espacio de trabajo{{value1}} vinculado. Agregue otro espacio de trabajo o reemplace una clave restringida en cualquier momento.", + "b08a6ac93c": "{{value0}} workspace{{value1}} vinculado. Agrega otro workspace o reemplaza una clave restringida en cualquier momento.", "93f0c49ad1": "no autenticado", "a3bcf13694": "conectado", "d6e5dba05a": "Iniciar sesión", - "0b4a7d23ab": "Iniciar sesión", + "0b4a7d23ab": "Iniciando sesión", "a74c6d6b18": "no instalado" }, "NotificationStep": { @@ -9876,11 +9876,11 @@ "277ba45540": "Incorporación de Orca", "97c42cda00": "Instale la CLI de GitHub para:", "ae3b00ca82": "Configurar tareas de GitHub", - "ff92d15436": "Orca le notificará cuando los agents hayan terminado o necesiten ayuda.", + "ff92d15436": "Orca te notificará cuando los agentes terminen o necesiten ayuda.", "b054332836": "Configurar notificaciones", - "04ae28d8ca": "Elige el look que quieras mirar durante horas.", + "04ae28d8ca": "Elige la apariencia que quieras ver durante horas.", "f396db9f20": "Haz que te sientas como en casa", - "322fc50a18": "Orca trabaja con todos los agentes CLI. Elija el que alcanzará más. Cambie en cualquier momento.", + "322fc50a18": "Orca funciona con cualquier agente CLI. Elige el que más usarás. Puedes cambiarlo en cualquier momento.", "198b148b3c": "Elija su agente predeterminado", "a5e5da02f7": "integraciones", "35bbaf5ae0": "notificaciones", @@ -9894,7 +9894,7 @@ "111d3f8d92": "Saltar a la configuración del proyecto" }, "OnboardingInlineCommandTerminal": { - "4123609efd": "Terminal de arranque..." + "4123609efd": "Iniciando terminal..." }, "OnboardingSkipConfirmationDialog": { "9f47f345a4": "¡No tardará mucho!", @@ -9906,30 +9906,30 @@ }, "RepoStep": { "e8fdb36338": "Escaneo de repositorios. Haga clic para detener.", - "b7c4da0504": "SSH? Configurar hosts en Configuración", - "c33b190ca3": "Sólo rutas de servidor", + "b7c4da0504": "¿SSH? Configura hosts en Configuración", + "c33b190ca3": "Solo rutas del host", "7b679207e4": "Espacio de trabajo", - "24c7c8696c": "Clonar en la ruta del servidor", - "7932e95f68": "Clon", + "24c7c8696c": "Clonar en ruta del host", + "7932e95f68": "Clonar", "955134915e": "git@github.com:org/repo.git", "288d8444b7": "Pegue una URL HTTPS o SSH.", - "132425a3e3": "Clonar un repo", - "6558d50c69": "¿Quieres importar muchos repos a la vez? Seleccione la carpeta principal.", - "831524961f": "Elija cualquier directorio local, repo git o no.", + "132425a3e3": "Clonar un repositorio", + "6558d50c69": "¿Quieres importar muchos repositorios a la vez? Seleccione la carpeta principal.", + "831524961f": "Elija cualquier directorio local, repositorio git o no.", "f4e9c8dcf8": "Buscar una carpeta", "e8214aa632": "Abrir como carpeta", "3863747c56": "Agregar proyecto Git", - "2ebbc26343": "/inicio/usuario/proyecto", - "466108ab89": "Introduzca una ruta que exista en el servidor de ejecución.", - "8cab104e3c": "Abrir un proyecto de servidor", + "2ebbc26343": "/home/user/project", + "466108ab89": "Introduce una ruta que exista en el host seleccionado.", + "8cab104e3c": "Abrir un proyecto en este host", "2d20200346": "Importar repositorios", "27ca610db1": "Atrás", "cecd6593fa": "Carpeta escaneada:", "c7af322fc3": "Dejar de escanear", "c3d9d44ca2": "Detener escaneo", - "cf23006ba7": "Servidor de ejecución", - "7ec3f48820": "/casa/usuario", - "2e6438dd34": "{{value0}}Encontrado {{value1}} {{value2}} en esta carpeta." + "cf23006ba7": "Host seleccionado", + "7ec3f48820": "/home/user", + "2e6438dd34": "{{value0}}Se encontraron {{value1}} {{value2}} en esta carpeta." }, "ThemeStep": { "a4b254779d": "Tecla de opción de macOS", @@ -9937,22 +9937,22 @@ "8ca01945f2": "Divisores", "b3a99a2d29": "Ventana", "86c0f1caa2": "Relleno", - "06a24f4f2d": "Bandera", + "06a24f4f2d": "Colores", "c021e9dddd": "paleta temática", "ab2a583a97": "Cursor", "cc1858e19e": "Fuente", "248c812283": "Importar", - "7ee9234e54": "Se detectó una configuración fantasmal.", + "7ee9234e54": "Se detectó una configuración de Ghostty.", "78b6386140": "Importado de Ghostty.", "2c3aa538f8": "Buscando una configuración de Ghostty...", "94b9dc561d": "Configuración → Terminal", "dd5c16ad1b": "Más opciones de terminal, incluyendo fuente, cursor y paleta, en", - "ad192706e6": "Luz", + "ad192706e6": "Claro", "fa7b673ea9": "Oscuro", "827ea7b4a2": "Sistema", "699ddf83c2": "No se pudo importar la configuración de Ghostty", "16a9f0446a": "No se encontraron configuraciones de Ghostty para importar", - "ad19e5c916": "Importador…", + "ad19e5c916": "Importando…", "906c4373fe": "ajustes" }, "use": { @@ -9965,7 +9965,7 @@ } }, "AgentFeatureSetupStep": { - "97dcdc010f": "Instalar CLI y habilidades" + "97dcdc010f": "Activar capacidades" }, "WindowsTerminalStep": { "powerShell": "PowerShell", @@ -9997,7 +9997,7 @@ "new": { "workspace": { "SmartWorkspaceNameField": { - "2a0d535f69": "Crear nueva sucursal", + "2a0d535f69": "Crear nueva rama", "a44229ce4d": "como nombre del espacio de trabajo", "766083a596": "\"", "34ca97bce3": "\"", @@ -10009,7 +10009,7 @@ "9ef1a7c4b0": ", que es diferente del proyecto seleccionado.", "ad188067ae": "La URL de GitHub apunta a", "4bd98f1091": "¿Cambiar de proyecto?", - "0c9e668e3a": "Claro", + "0c9e668e3a": "Borrar", "7199ff19c7": "Borrar fuente seleccionada", "370a1faf67": "Abrir en el navegador", "2c69728c2a": "Abrir enlace en el navegador", @@ -10018,13 +10018,13 @@ "2cfc6be192": "GitLab", "7a47af0565": "Linear", "0a180280bd": "GitHub", - "b3c60c2b7c": "Elegante", + "b3c60c2b7c": "Smart", "26824f60dd": "Todo", "6fad211c66": "Cerrado", "2319d87718": "Fusionado", "622864b52a": "Abierto", "fda67f0b61": "proyecto actual", - "3e8bb1176a": "Conecte Linear en Configuración para buscar problemas.", + "3e8bb1176a": "Conecta Linear en Configuración para buscar issues.", "69ce292138": "lineal", "9c004911c3": "gitlab", "switchTaskSourceTitle": "¿Cambiar el origen de tareas?", @@ -10037,62 +10037,62 @@ "placeholderSmartWithBranchGitLab": "Escribe un nombre, #1234, rama, GitHub o URL de GitLab", "placeholderSmartGitLab": "Escribe un nombre, #1234, GitHub o URL de GitLab", "unavailable": "No disponible", - "searchGitHub": "Buscar PR e incidencias de GitHub", - "searchGitLab": "Buscar MR e incidencias de GitLab", + "searchGitHub": "Buscar PRs e issues de GitHub", + "searchGitLab": "Buscar MRs e issues de GitLab", "searchBranches": "Buscar ramas", - "searchLinear": "Buscar incidencias de Linear", + "searchLinear": "Buscar issues de Linear", "workspaceName": "Nombre del espacio de trabajo" }, "ProjectHostSetupCombobox": { - "empty": "No hosts are ready for this project.", - "placeholder": "Choose host" + "empty": "No hay hosts listos para este proyecto.", + "placeholder": "Elegir host" }, "ProjectCombobox": { - "search": "Search projects...", - "empty": "No projects match your search." + "search": "Buscar proyectos...", + "empty": "Ningún proyecto coincide con tu búsqueda." } } }, "mobile": { "MobileHero": { - "a8fb43cf1c": "Continue", + "a8fb43cf1c": "Continuar", "3f90dbd274": "Hecho", "b622eba64d": "Atrás", "65b3f2e8bc": "Generando…", - "27735e5f4e": "Emparejamiento QR", - "bb0074ce11": "Emparejamiento de código QR", + "27735e5f4e": "QR de emparejamiento", + "bb0074ce11": "Código QR de emparejamiento", "010dddcf27": "Copiar código de emparejamiento", "4c1df4eba7": "¿No puedes escanear?", "85067b9e06": "Actualizar interfaces de red", "ca85e595a7": "No se encontraron interfaces", "79d2f480da": "Interfaz de red para anunciar", "dfd2aa9d5d": "Red", - "2f077ef4eb": "y escanea el código.", + "2f077ef4eb": ", y escanea el código.", "3aa7bb2d8b": "Emparejar escritorio", - "d1495e5e64": "Abra Orca Mobile, toque", - "901c98bb93": "Empareja esto", + "d1495e5e64": "Abre Orca Mobile, toca", + "901c98bb93": "Empareja este", "3960f5c339": "Paso 2 de 2", - "3241f3c26a": "Instalar QR", - "7af266b80d": "Instalar código QR", + "3241f3c26a": "QR de instalación", + "7af266b80d": "Código QR de instalación", "aa97420ba4": "Copiar enlace de instalación", - "ac1eb64952": "Androide", + "ac1eb64952": "Android", "711e6f4b47": "iOS", - "e75647ace0": "Escanee el QR con su teléfono o abra el enlace de instalación para obtener Orca Mobile.", + "e75647ace0": "Escanea el código QR con tu teléfono o abre el enlace de instalación para obtener Orca Mobile.", "0d9b33299e": "Obtén la aplicación.", "92ddfdfa1f": "Paso 1 de 2", "ff48d9d520": "Emparejar otro dispositivo", "f9cbf4bb53": "Revocar dispositivo", "34f878d04f": "Revocar {{value0}}", - "94829abdb1": "emparejado", - "266c18c105": "Abre Orca Mobile para continuar donde lo dejaste o vincular otro dispositivo.", - "5410d55d79": "Orca Móvil", - "10d27b4cba": "empezar", + "94829abdb1": "Emparejado", + "266c18c105": "Abre Orca Mobile para continuar donde lo dejaste o emparejar otro dispositivo.", + "5410d55d79": "Orca Mobile", + "10d27b4cba": "Empezar", "da1d5e5ed0": "Disponible en", "ec0607bf66": "Plataformas móviles compatibles", - "b4ccce5cb7": "Controla Orca desde tu teléfono. Consulte a los agents, revise los cambios e inicie tareas mientras no está en su escritorio.", + "b4ccce5cb7": "Controla Orca desde tu teléfono. Consulta el estado de los agentes, revisa los cambios e inicia tareas cuando estés lejos de tu escritorio.", "cd4e5e816f": "Tus espacios de trabajo, en tu bolsillo.", "a6cffbbb0b": "Generar código", - "e59a252eca": "regenerar código", + "e59a252eca": "Regenerar código", "d0b52871ce": "Tus teléfonos están emparejados.", "051978a785": "Tu teléfono está emparejado.", "channel": { @@ -10108,7 +10108,7 @@ "6a66e38943": "No se pudo copiar el código de emparejamiento", "3c1f7168bb": "Código de emparejamiento copiado", "4c8bd11c1a": "No se pudo generar el código de emparejamiento", - "b353e18de1": "El transporte de WebSocket no se está ejecutando", + "b353e18de1": "El transporte WebSocket no está en ejecución", "4e1eb5d55c": "No se pudo revocar el dispositivo", "255372e6e8": "Dispositivo revocado", "1b4509a8a1": "emparejado", @@ -10116,24 +10116,24 @@ }, "MobilePageToolbar": { "ad2284a9e2": "Cerrar · Esc", - "9883b58693": "Cerrar Orca Móvil", + "9883b58693": "Cerrar Orca Mobile", "fb5f28330e": "Mostrar en la barra lateral", "c669abcf8f": "Ocultar de la barra lateral", - "e1c7b4a92d": "Configure in Settings > Mobile.", + "e1c7b4a92d": "Configúralo en Ajustes > Móvil.", "a4f8c2d91e": "Remove Orca Mobile from left sidebar", "b7e3d1c84a": "Show Orca Mobile in left sidebar", - "f3d8e5b71a": "Adds the shortcut back to the left sidebar." + "f3d8e5b71a": "Vuelve a añadir el acceso directo a la barra lateral." }, "PhoneCarousel": { "96d651cb87": "Sesión terminal", - "93217b41c1": "Lista de árbol de trabajo", + "93217b41c1": "Lista de worktrees", "89c7713645": "Pantalla de inicio de Orca Móvil" }, "mobile": { "platform": { "copy": { - "2a532d6fd7": "Escanee con su cámara Android para descargar el último APK de GitHub Releases.", - "432db52b73": "Escanee con la cámara de su iPhone para abrir la App Store.", + "2a532d6fd7": "Escanea con la cámara de tu Android para descargar el APK más reciente desde GitHub Releases.", + "432db52b73": "Escanea con la cámara de tu iPhone para abrir la App Store.", "preview": { "tagline": "Las funciones más recientes, actualizadas a diario." }, @@ -10154,17 +10154,17 @@ "0bad5b07c8": "GitHub y Linear", "d047197480": "GitHub · Linear", "a4c3f7b7aa": "Tareas", - "d33d7a9c29": "orca  ·  feat/página móvil", - "25d6e8a491": "hazaña/página móvil", + "d33d7a9c29": "orca  ·  feat/mobile-page", + "25d6e8a491": "feat/mobile-page", "c791677f2f": "Reanudar", "cf3f98fa3f": "Desconectado", - "091355da3d": "M1 Mini · inicio", - "0bc1881bc4": "Conectados · 40 árboles de trabajo · 5 activos", - "19c212e25e": "macbook pro", + "091355da3d": "M1 Mini · home", + "0bc1881bc4": "Conectado · 40 worktrees · 5 activos", + "19c212e25e": "MacBook Pro", "2f1a1d10c4": "Escritorios", - "156db8a68a": "RP creados", - "4a40af029b": "tiempo del Agent", - "00a6903322": "Agents generados", + "156db8a68a": "PRs creados", + "4a40af029b": "Tiempo de agentes", + "00a6903322": "Agentes iniciados", "c0e2e9dcd9": "Bienvenido de nuevo", "af761a0c0d": "Ajustes", "5d94e8ddcc": "Orca" @@ -10172,44 +10172,44 @@ "TerminalSlide": { "0bb39f8fe6": "Enviar", "69334b4b10": "Dictado de voz", - "29f2d13839": "Escribe un comando...", + "29f2d13839": "Escribe un comando…", "817090af40": "Ctrl+C", - "53ff909568": "Pestaña", - "4930eaaae7": "ESC", - "fa22927f13": "Pasta", + "53ff909568": "Tab", + "4930eaaae7": "Esc", + "fa22927f13": "Pegar", "985373052e": "Cambiar al modo teléfono", - "58a9ee6003": "formato de llamada de herramienta. ¿Quieres que agregue la diferencia a continuación?", + "58a9ee6003": "formato de tool-call. ¿Quieres que agregue el diff a continuación?", "aa64b519c6": "pantalla del terminal. Paleta Tokyonight, Menlo, real claude", - "e75112c834": "Reemplacé la diapositiva de escaneo en pares con una de alta fidelidad.", - "3ce3e8c892": "14 aprobados, 1 saltado (1,8s)", + "e75112c834": "Reemplacé la diapositiva de escaneo de emparejamiento por una de alta fidelidad", + "3ce3e8c892": "14 aprobados, 1 omitido (1,8 s)", "4b3666f9a9": "src/cache/worktree-cache.test.ts", - "1d448b69f7": "APROBAR", + "1d448b69f7": "PASS", "d39445686a": "src/transport/host-store.test.ts", - "a6e7cdc688": "prueba pnpm --filtro móvil", - "21b67dfc92": "Intento", - "d6d1041a1c": "⎿ Se reemplazó la diapositiva de escaneo de pares con sesión de terminal", - "336c0e070e": "móvil/orca-mobile-sidebar-mock-v3.html", + "a6e7cdc688": "pnpm test --filter mobile", + "21b67dfc92": "Bash", + "d6d1041a1c": "⎿ Se reemplazó la diapositiva de escaneo de emparejamiento por una sesión de terminal", + "336c0e070e": "mobile/orca-mobile-sidebar-mock-v3.html", "6d4ebd5833": "Editar", - "fc83e0d5ef": "⎿ Leer 2103 líneas", + "fc83e0d5ef": "⎿ Leídas 2103 líneas", "80cc356591": "Leer", "2c10d43745": "claude", "e0f98be657": "orca/feat-mobile-page", - "2defc05141": "desarrollador@mac", + "2defc05141": "dev@mac", "da121ba48d": "PLAN.md", - "e4befee569": "caparazón", + "e4befee569": "shell", "606aa93192": "Archivos", - "94febb0976": "control de fuente", + "94febb0976": "Control de código fuente", "8d6516312d": "2 terminales · claude activo", - "8432787c4e": "hazaña/página móvil", + "8432787c4e": "feat/mobile-page", "8fd998acd3": "Atrás" }, "WorktreeListSlide": { "357a519567": "Activo", "79a24ff530": "Fijado", - "22971156df": "Repo", + "22971156df": "Repositorio", "17f9e0d226": "Reciente", "0e3e809a4b": "Filtrar", - "b4271864bd": "macbook pro", + "b4271864bd": "MacBook Pro", "cefd048225": "Atrás", "c5ad56786d": "hilandero" } @@ -10234,16 +10234,16 @@ "rate": { "limit": { "display": { - "ebc0e8ecf1": "Cargando presupuesto de API de GitLab...", - "a2d3d1fdde": "El presupuesto de la API de GitLab no está disponible.", - "a2f68645ac": "Actualizar el presupuesto de la API de GitLab", + "ebc0e8ecf1": "Cargando cuota de la API de GitLab...", + "a2d3d1fdde": "La cuota de la API de GitLab no está disponible.", + "a2f68645ac": "Actualizar la cuota de la API de GitLab", "2f9c16d6c3": "Orca usa REST a través de la CLI de GitLab.", - "14e144f7a7": "Presupuesto de la API de GitLab", - "3e2c982cfa": "izquierda, se reinicia en", + "14e144f7a7": "Cuota de la API de GitLab", + "3e2c982cfa": "restantes, se restablece en", "ea8ad0bae8": "de", - "0a891e8935": "API DESCANSO", - "953f7c6062": "Este host de GitLab no devolvió encabezados de límite de velocidad.", - "budget_scope_prefix": "Ámbito del presupuesto" + "0a891e8935": "API REST", + "953f7c6062": "Este host de GitLab no devolvió cabeceras de rate limit.", + "budget_scope_prefix": "Ámbito de la cuota" } } } @@ -10257,27 +10257,27 @@ "0ee79e0674": "Mover a la barra de estado" }, "FloatingTerminalOrchestrationDialog": { - "f726054620": "Permite a los agents traspasar contexto y coordinar el trabajo a través de Orca.", - "1cd3f8af64": "Habilidad de orquestación", - "6f0aed26b8": "Instale la CLI de Orca y la habilidad de orquestación para que los agents puedan coordinarse a través de Orca.", + "f726054620": "Permite a los agentes transferir contexto y coordinar el trabajo a través de Orca.", + "1cd3f8af64": "Skill de orquestación", + "6f0aed26b8": "Instala la CLI de Orca y el skill de orquestación para que los agentes puedan coordinarse a través de Orca.", "05d7aabc20": "No instalado", "630c0ac8c8": "Instalado", - "dfd021ce46": "De cheques...", + "dfd021ce46": "Comprobando...", "543f325a14": "Habilitar orquestación" }, "FloatingTerminalPanel": { "fc1042e92b": "Minimizar", "8b07759314": "Nuevo navegador", - "88ffb502e5": "Abrir nota de Markdowns", - "629528690b": "Nueva nota de Markdowns", - "3215fc73e9": "Nueva Terminal", - "da508bd7f5": "Ahorrar", + "88ffb502e5": "Abrir nota Markdown", + "629528690b": "Nueva nota Markdown", + "3215fc73e9": "Nueva terminal", + "da508bd7f5": "Guardar", "918c2139f3": "No guardar", "e7bf09d4d4": "Cancelar", "690b6fb98a": "Cambios no guardados", - "bbc177f98f": "Permitir", - "adc281394d": "Despedir", - "8cf80db43b": "Configure la CLI de Orca y la habilidad del agente para que los agents puedan coordinarse a través de Orca.", + "bbc177f98f": "Habilitar", + "adc281394d": "Descartar", + "8cf80db43b": "Configura la CLI de Orca y el skill de agente para que los agentes puedan coordinarse a través de Orca.", "2a3c5ddf5e": "Habilitar orquestación", "d6b563ae24": "Cargando editor...", "8b14ba6c17": "Nueva pestaña del navegador", @@ -10295,11 +10295,11 @@ "1bbaa0302f": "Minimizar el espacio de trabajo flotante", "3f4ca29961": "Maximizar el espacio de trabajo flotante", "1c79cba25d": "Restaurar el espacio de trabajo flotante", - "648352c51f": "Abra {{value0}} en el espacio de trabajo flotante", - "82da3701e7": "No se pudo generar el comando de inicio para {{value0}}.", + "648352c51f": "Abrir {{value0}} en el espacio de trabajo flotante", + "82da3701e7": "No se pudo crear el comando de inicio para {{value0}}.", "109870e023": "Maximizar", "b5686fee1e": "Restaurar", - "1e502f1284": "Abierto" + "1e502f1284": "Abrir" } } }, @@ -10307,66 +10307,66 @@ "wall": { "AgentCapabilitiesSetupAction": { "b8dc9dd8a2": "Instalado", - "1b51644c2d": "Deje que los agents controlen el escritorio, muevan el cursor, hagan clic y escriban en cualquier aplicación.", - "362a07517d": "Uso de la computadora", - "5e8fe5a72d": "Ofrezca a los agents acceso directo al navegador de Orca para que puedan probar páginas, realizar capturas de pantalla y actuar en función de lo que ven.", - "e638da007a": "Uso del navegador del Agent", - "c61c91e642": "Permita que los agents se coordinen a través de Orca para que las tareas grandes de varios pasos avancen hasta su finalización.", - "ac07f8887f": "Orquestación de Agents", + "1b51644c2d": "Permite que los agentes controlen el escritorio, muevan el cursor, hagan clic y escriban en cualquier app.", + "362a07517d": "Uso de computadora", + "5e8fe5a72d": "Da a los agentes acceso directo al navegador de Orca para que puedan probar páginas, hacer capturas de pantalla y actuar según lo que ven.", + "e638da007a": "Uso del navegador por agentes", + "c61c91e642": "Permite que los agentes se coordinen a través de Orca para mantener en marcha tareas grandes de varios pasos hasta completarlas.", + "ac07f8887f": "Orquestación de agentes", "e9eb197e12": "Permisos de uso de computadora abiertos", - "3a59452a67": "Comando de habilidad copiado e insertado a continuación para su revisión.", - "c605f51f2b": "Configuración de capacidad lista", - "1aa657d8f4": "Alguna configuración de capacidad necesita atención", - "c89534cbe9": "Instalar CLI y habilidades" + "3a59452a67": "Comando de skill copiado e insertado abajo para revisarlo.", + "c605f51f2b": "Configuración de capacidades lista", + "1aa657d8f4": "La configuración de algunas capacidades necesita atención", + "c89534cbe9": "Instalar CLI y skills" }, "AiCommitPrSettingsCard": { - "8d4152701a": "p.ej. ollama corre llama3.1 {{value0}}", + "8d4152701a": "p. ej. ollama run llama3.1 {{value0}}", "9ee54037a4": "Comando personalizado", - "4b2fc4b80c": "esfuerzo de pensamiento", + "4b2fc4b80c": "Esfuerzo de razonamiento", "be8917699e": "Modelo", - "4d9b6d84df": "sin soporte. Elija Claude, Codex o Personalizado.", - "560d4feb00": "Costumbre", + "4d9b6d84df": "no compatible. Elige Claude, Codex o Personalizado.", + "560d4feb00": "Personalizado", "29d119fe95": "Agente", "f9382b48a1": "Habilitar autor de IA", - "1c0cb4fabb": "autor de IA", + "1c0cb4fabb": "Autor de IA", "bd14e9c42a": "No configurado", "1f9468c5c9": "{{value0}} no compatible" }, "BrowserAnimatedVisual": { "46df009982": "Comienza tu prueba gratuita", "25f15c2219": "Pro", - "59ae327405": "Motor de arranque", + "59ae327405": "Starter", "9e0f530390": "Precios", - "0ce7c24b4d": "Laboral…", + "0ce7c24b4d": "Trabajando…", "f2034c4930": ">", "6e4616d039": "Claude", "0f8481e1a7": "Enviar a Claude", "3d2352f94b": "Describe el cambio...", "d8856b604a": "div.pricing-grid > div.card.starter:nth-of-type(1) > a.cta", - "7da6eed7bf": "servidor local: 3000", + "7da6eed7bf": "localhost:3000", "0a2bd01c02": "Nueva pestaña del navegador", "04096318ab": "Terminal 1", "eb88125c6f": "✓ Verificado: la prueba gratuita aún funciona.", - "051c97d15a": ".pp-card[data-card=\"inicio\"] .pp-cta", + "051c97d15a": ".pp-card[data-card=\"starter\"] .pp-cta", "4fa59ca545": "✓ Actualizado", - "1bec24acc1": "@keyframes browserFlash { 0% { opacidad: 0; } 20% { opacidad: 0,85; } 100% { opacidad: 0; } } @keyframes browserTabIn { de { opacidad: 0; transformar: traducirY(-2px); } a {opacidad: 1; transformar: ninguno; } } @keyframes browserViewIn { de { opacidad: 0; transformar: traducirY(4px); } a {opacidad: 1; transformar: ninguno; } }", - "73bbb46073": "/precios", - "f39be6ca14": "/inscribirse" + "1bec24acc1": "@keyframes browserFlash { 0% { opacity: 0; } 20% { opacity: 0.85; } 100% { opacity: 0; } } @keyframes browserTabIn { from { opacity: 0; transform: translateY(-2px); } to { opacity: 1; transform: none; } } @keyframes browserViewIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }", + "73bbb46073": "/pricing", + "f39be6ca14": "/signup" }, "BrowserUseSkillSetupCard": { - "cbc45022d4": "Permite a los agents navegar y verificar páginas en el navegador de Orca.", - "d5bb1cd4ba": "Habilidad de uso del navegador" + "cbc45022d4": "Permite a los agentes navegar y verificar páginas en el navegador de Orca.", + "d5bb1cd4ba": "Skill Browser Use" }, "ComputerUseAnimatedVisual": { "d8401975b1": "aprobado", "f27676a92c": "estado:", - "6804cb356f": "haga clic en enviado", + "6804cb356f": "clic enviado", "1719b28a81": "encontrado \"Aprobar\"", "79445f7512": "aprobar la nota en mi aplicación", "99a8624bcb": ">", "2adb561b44": "Sesión de Claude Code iniciada", - "94787f01f8": "Código Claude", - "9cddfe96b2": "aplicación local", + "94787f01f8": "Claude Code", + "9cddfe96b2": "app local", "9634d870d1": "Aprobar", "3cc2df3671": "Hecho", "bdd5312213": "Pendiente", @@ -10374,47 +10374,47 @@ }, "EditorAnimatedVisual": { "7a763daf2f": "itálico", - "8521536429": "atrevido ·", + "8521536429": "negrita ·", "8341391520": "para bloques ·", "3fe42a1da0": "Tipo", "8268b2376b": "Bloque de código", "37fa4948ce": "Lista de viñetas", "f25687c588": "Cita", "abbdeea15d": "Bloques básicos", - "a26a68d30c": "Título 2", - "722170663a": "Título 1", - "1fb29ad710": "Encabezamientos", + "a26a68d30c": "Encabezado 2", + "722170663a": "Encabezado 1", + "1fb29ad710": "Encabezados", "4426aab46f": "Actualice el índice de documentos una vez que aparezca el nuevo mosaico.", - "95f0c3a46f": "Pruebe con humo el flujo de instalación en una máquina nueva.", + "95f0c3a46f": "Haz un smoke test del flujo de instalación en una máquina nueva.", "22ae7b4d9d": "Una breve nota para el equipo: reunir lo que queda antes del envío.", - "5a55c00a81": "plan de lanzamiento", + "5a55c00a81": "Plan de lanzamiento", "218503f9f3": "guardado automáticamente", - "cda56c5915": "notas / plan de lanzamiento.md", - "e16479c1c5": "[menú-barra-datos] [fila-barra-datos].barra-activa { fondo: rgba(24,24,27,0.07); sombra de cuadro: inserción 0 0 0 1px rgba(24,24,27,0.06); } [data-md-active-line][data-role=\"activo\"] { color: rgb(113 113 122); familia de fuentes: ui-monospace, SFMono-Regular, Menlo, monospace; tamaño de fuente: 12,5 px; } [data-md-active-line][data-role=\"h1\"] { color: heredar; familia de fuentes: heredar; tamaño de fuente: 18px; peso de fuente: 700; espacio entre letras: -0,01 em; altura de línea: 1,2; margen superior: 6px; } [datos-md-caret] { mostrar: bloque en línea; ancho: 1,5 px; altura: 1em; fondo: color actual; alineación vertical: -2px; margen izquierdo: 1px; animación: md-caret-blink 1.05s pasos(1) infinito; } @keyframes md-caret-blink { 0%, 50% { opacidad: 1 } 51%, 100% { opacidad: 0 } } @keyframes md-block-in { de { opacidad: 0; transformar: traducirY(-2px); } a {opacidad: 1; transformar: ninguno; } } @keyframes md-cursor-ripple { 0% { transformar: escala (0.4); opacidad: 0,9; } 100% { transformar: escala(1.4); opacidad: 0; } } [data-clicking=\"1\"] [data-cursor-ripple] { animación: md-cursor-ripple 460ms de avance gradual; }" + "cda56c5915": "notes / launch-plan.md", + "e16479c1c5": "[data-slash-menu] [data-slash-row].slash-active { background: rgba(24,24,27,0.07); box-shadow: inset 0 0 0 1px rgba(24,24,27,0.06); } [data-md-active-line][data-role=\"active\"] { color: rgb(113 113 122); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12.5px; } [data-md-active-line][data-role=\"h1\"] { color: inherit; font-family: inherit; font-size: 18px; font-weight: 700; letter-spacing: -0.01em; line-height: 1.2; margin-top: 6px; } [data-md-caret] { display: inline-block; width: 1.5px; height: 1em; background: currentColor; vertical-align: -2px; margin-left: 1px; animation: md-caret-blink 1.05s steps(1) infinite; } @keyframes md-caret-blink { 0%, 50% { opacity: 1 } 51%, 100% { opacity: 0 } } @keyframes md-block-in { from { opacity: 0; transform: translateY(-2px); } to { opacity: 1; transform: none; } } @keyframes md-cursor-ripple { 0% { transform: scale(0.4); opacity: 0.9; } 100% { transform: scale(1.4); opacity: 0; } } [data-clicking=\"1\"] [data-cursor-ripple] { animation: md-cursor-ripple 460ms ease-out forwards; }" }, "FeatureTourPreview": { "1170621527": ">", - "304ad0dfc1": "Pensamiento...", - "ef8b164dd1": "revisar src/autenticación", + "304ad0dfc1": "Pensando...", + "ef8b164dd1": "revisar src/auth", "952d3ddd9a": "sesión iniciada", "771d8881c2": "claude", - "6ed43cb0e0": "tablero.spec.ts", - "24fedd5a52": "iniciar sesión.spec.ts", + "6ed43cb0e0": "dashboard.spec.ts", + "24fedd5a52": "login.spec.ts", "8279e9d95b": "Ejecutando 12 pruebas", - "6218a9014d": "prueba de dramaturgo pnpm", + "6218a9014d": "pnpm playwright test", "04d54d50ec": "orca · zsh", - "1aa8a9a24a": "Terminal divisible", + "1aa8a9a24a": "Terminal dividible", "2a7cfc82c8": "Vinculado a GH #1842", - "3822d8d14b": "fix/worktree-picker-trunca", + "3822d8d14b": "fix/worktree-picker-truncates", "d54aefe09e": "LIN-329", "40bbd92ef4": "Comenzar", - "c1f28c03b2": "El selector de árbol de trabajo se trunca", - "fc0cc0b267": "#1842", + "c1f28c03b2": "El selector de worktree se trunca", + "fc0cc0b267": "GH #1842", "0688842445": "GH #1799", - "bee6b4088d": "GitHub y tareas Lineares", - "5171768676": "orquestando 3 agents", + "bee6b4088d": "Tareas de GitHub y Linear", + "5171768676": "orquestando 3 agentes", "cebc7769cd": "rediseñar el flujo de autenticación", - "e44269e97d": "Orquestación de Agents", + "e44269e97d": "Orquestación de agentes", "ec4a73f5e6": "PR 3/3", "cfdfd4d6b4": "PR 2/3", "b1f17bcc74": "PR 1/3", @@ -10422,12 +10422,12 @@ "9c812e0d7c": "acelerar el proceso de CI", "3c4adfd821": "arreglar la condición de carrera de inicio de sesión", "56a0271428": "Espacios de trabajo aislados", - "ef737dcee1": "GitHub y tareas Lineares", + "ef737dcee1": "Tareas de GitHub y Linear", "ac51c061e2": "codex", - "47f16ecf34": "Ship several things at once. Each workspace keeps its branch, terminal, and agent activity together.", - "70aa182266": "Hand off a goal and walk away. A coordinator agent fans out and ships parallel PRs.", - "f10c14dd9d": "Skip the tab-switching. Pick from your GitHub or Linear backlog and start a workspace in one click.", - "5d6ee181b6": "Open any workspace to return to its terminal, then split panes for tests, logs, and agents." + "47f16ecf34": "Avanza en varias cosas a la vez. Cada workspace mantiene juntos su rama, terminal y actividad de agentes.", + "70aa182266": "Delega un objetivo y sigue con lo tuyo. Un agente coordinador se despliega y publica PRs en paralelo.", + "f10c14dd9d": "Evita cambiar de pestaña. Elige desde tu backlog de GitHub o Linear e inicia un workspace con un clic.", + "5d6ee181b6": "Abre cualquier workspace para volver a su terminal y luego divide paneles para tests, logs y agentes." }, "FeatureWallBody": { "25ec5356d6": "Configuración" @@ -10451,11 +10451,11 @@ "13294d3405": "Hecho" }, "FeatureWallSetupWorkflowActions": { - "486c2f4d8d": "Primero agregue un proyecto git, luego configure el script de configuración para ese repositorio.", + "486c2f4d8d": "Agrega primero un proyecto git y luego configura el script de configuración para ese repositorio.", "00078a6134": "Ver en configuración", - "14327073cc": "Ahorrar", - "88469e926b": "Guión de configuración", - "5c5b65044e": "instalación pnpm", + "14327073cc": "Guardar", + "88469e926b": "Script de configuración", + "5c5b65044e": "pnpm install", "a7463915b6": "No se pudo guardar el script de configuración", "6299297dac": "Script de configuración guardado", "f0bbf7da77": "Pruébalo", @@ -10469,48 +10469,48 @@ }, "ReviewNotesAnimatedVisual": { "5dbd27c4c2": "Codex", - "09094f25e2": "Código Claude", - "294aaff104": "enviar notas a", + "09094f25e2": "Claude Code", + "294aaff104": "Enviar notas a", "ea4e45b71b": "Agregar nota", "271ea0cbf3": "Cancelar", "a7a89d8f94": "Línea", - "5cb213f967": "notas de IA", - "1eee3a397e": "src/server/migrate.ts (diferencia)" + "5cb213f967": "Notas de IA", + "1eee3a397e": "src/server/migrate.ts (diff)" }, "ReviewPRViewAnimatedVisual": { - "7c2808ecff": "truncamiento antes de la fusión.", + "7c2808ecff": "truncamiento antes del merge.", "c2062da7ec": "stderr", "6f4c2d7cb7": "Agregar un caso de cobertura para", "71828fba75": "¿Podemos incluir el comando defectuoso en la carga útil de diagnóstico?", "fb1a856b6d": "abierto", "7a8b896e11": "Comentarios", "ca36f7b27c": "Aprobado", - "25f6838e43": "hilas", - "2ef0b97954": "verificación de tipo", - "8ed213397c": "Correr", - "d340c052fb": "verificar", + "25f6838e43": "lint", + "2ef0b97954": "typecheck", + "8ed213397c": "En ejecución", + "d340c052fb": "verify", "9a097cae12": "1 pendiente", - "2f37142229": "Aplastar y fusionar", + "2f37142229": "Squash and merge", "0aab7ab84a": "Agregar seguimiento de errores de diagnóstico local", "dfe313e0c9": "ABIERTO", "6e3f5223c5": "Explorador", - "ab2901bce6": "cheques", + "ab2901bce6": "Checks", "d7f80060ca": "Control de fuente", "8e715588e4": "Buscar", - "a6c8b9e32f": "Checks passed", + "a6c8b9e32f": "Checks aprobados", "f4d5e1a7b2": "3 checks" }, "ReviewShipAnimatedVisual": { "4d99496b8c": "Crear PR", "62544e0852": "Cancelar", - "bcd5cae3c4": "Descripción de la solicitud de extracción", + "bcd5cae3c4": "Descripción del pull request", "3774b80eae": "Descripción", - "07da9245cc": "Título de la solicitud de extracción", + "07da9245cc": "Título del pull request", "54a093c52d": "Título", - "3b9b96d6a6": "principal", - "ce7d5d3a18": "sucursal base", + "3b9b96d6a6": "main", + "ce7d5d3a18": "Rama base", "e4473d438f": "Generar con IA", - "c30cd930ff": "Crear solicitud de extracción", + "c30cd930ff": "Crear pull request", "ea0100dd15": "Ver todo", "e725000cd7": "Cambios", "a079083a6c": "Commit", @@ -10519,35 +10519,35 @@ "cd8a3a39d7": "3 commits por delante" }, "TasksAnimatedVisual": { - "efba6f77eb": "Número de lectura #", - "b68c92fbdc": "Iniciar espacio de trabajo", + "efba6f77eb": "Leyendo issue #", + "b68c92fbdc": "Iniciar workspace", "4331c4d0f8": "Abierto", - "b13375617e": "El selector de árbol de trabajo trunca los nombres", - "72f9e516a3": "prensado", - "fe47c9c9e8": "Espacio de trabajo listo", - "61ffda7601": "Creando espacio de trabajo" + "b13375617e": "El selector de worktree trunca los nombres", + "72f9e516a3": "pulsando", + "fe47c9c9e8": "Workspace listo", + "61ffda7601": "Creando workspace" }, "WorkbenchAnimatedVisual": { - "633a91e358": "Pensamiento…", + "633a91e358": "Pensando…", "932c4b3a97": ">", - "ca2cfbf188": "Terminal dividido hacia abajo", - "e370fa8c2b": "Terminal dividido a la derecha", + "ca2cfbf188": "Dividir terminal abajo", + "e370fa8c2b": "Dividir terminal a la derecha", "b85eab49dd": "src/auth/session.ts", "99f5224f1e": "Editar", - "0d93c298a7": "lanzar src/auth", - "17cfdc3344": "grep", - "9923847785": "Leer", + "0d93c298a7": "throw src/auth", + "17cfdc3344": "Grep", + "9923847785": "Read", "c0eb94125e": "revisar casos extremos de autenticación", "431ca9842a": "Sesión de Claude Code iniciada", "000106adfe": "claude", - "7d9f1d5f7d": "(0,8s)", - "944199e54a": "› actualizaciones totales del carrito", - "623881d72e": "pago.spec.ts", - "5c5cbd783f": "(1,2s)", - "3261c6853b": "› puede iniciar sesión", - "defe550fe2": "iniciar sesión.spec.ts", + "7d9f1d5f7d": "(0.8s)", + "944199e54a": "› cart total updates", + "623881d72e": "checkout.spec.ts", + "5c5cbd783f": "(1.2s)", + "3261c6853b": "› can sign in", + "defe550fe2": "login.spec.ts", "0b20782e0f": "Ejecutando 12 pruebas con 4 trabajadores", - "4371cc9931": "prueba de dramaturgo pnpm", + "4371cc9931": "pnpm playwright test", "16877e038d": "se divide", "a2b114dad0": "se divide a la derecha ·", "0bc9ad0cd1": "Mismo panel:", @@ -10558,12 +10558,12 @@ "setup": { "status": { "8eccfcb314": "Instalado", - "21d4f79c93": "haga clic en Instalar CLI y habilidades para abrir la configuración de acceso de macOS", - "5c9293e51a": "comprobando el acceso a la aplicación", - "aae94eeb52": "Haga clic en Instalar CLI y habilidades", - "aa8e143a2f": "No se pudo verificar la instalación", + "21d4f79c93": "haz clic en Instalar CLI y skills para abrir la configuración de acceso de macOS", + "5c9293e51a": "comprobando acceso a la app", + "aae94eeb52": "Haz clic en Instalar CLI y skills", + "aa8e143a2f": "No se pudo comprobar la instalación", "9b33e7fb13": "Comprobando instalación", - "4c8e1f92a7": "abre Orca Desktop en esta Mac", + "4c8e1f92a7": "abre Orca Desktop en este Mac", "6d2b0a84e1": "No disponible en esta compilación" } } @@ -10589,19 +10589,19 @@ }, "notes": { "styles": { - "db6691aa0a": ".ravs-window { posición: absoluta; recuadro: 0; --ravs-soft-surface: color-mix(en srgb, var(--primer plano) 2%, var(--card)); --ravs-soft-fill: mezcla de colores (en srgb, var(--primer plano) 6%, transparente); --ravs-panel-border: mezcla de colores (en srgb, var(--primer plano) 18%, var(--border)); --ravs-emphasis-border: color-mix(en srgb, var(--primer plano) 44%, var(--border)); --ravs-sombra-flotante: 0 14px 30px rgb(0 0 0 / 0.22), 0 2px 6px rgb(0 0 0 / 0.12); fondo: var(--tarjeta); borde: 1px var sólido (--borde); radio del borde: 10px; desbordamiento: oculto; pantalla: flexible; dirección flexible: columna; sombra de cuadro: 0 1px 2px rgb(0 0 0 / 0.08); } .ravs-difftoolbar { mostrar: flex; alinear elementos: centro; espacio: 8px; relleno: 6px 10px; borde inferior: var sólido de 1 px (--borde); fondo: var(--ravs-soft-surface); tamaño de fuente: 11px; color: var(--primer plano silenciado); } .ravs-diff-path { ancho mínimo: 0; desbordamiento: oculto; desbordamiento de texto: puntos suspensivos; espacio en blanco: nowrap; familia de fuentes: ui-monospace, SFMono-Regular, Menlo, monospace; color: var(--primer plano); } .ravs-ai-chip { margen izquierdo: automático; pantalla: flexible en línea; alinear elementos: estirar; desbordamiento: oculto; radio del borde: 6px; borde: 1px var sólido (--borde); fondo: var(--ravs-soft-surface); opacidad: 0; transformar: traducirY(-2px); transición: opacidad 320 ms facilidad, transformación 320 ms facilidad; } .ravs-ai-chip.is-visible { opacidad: 1; transformar: ninguno; } .ravs-ai-chip .ravs-count-btn, .ravs-ai-chip .ravs-send-btn { pantalla: inline-flex; alinear elementos: centro; espacio: 5px; relleno: 3px 8px; tamaño de fuente: 11px; color: var(--primer plano silenciado); fondo: transparente; altura de línea: 1; } .ravs-ai-chip .ravs-count-btn { borde-derecha: 1px solid var(--border); } .ravs-ai-chip .ravs-send-btn { relleno: 3px 7px; posición: relativa; } .ravs-send-glow { posición: absoluta; recuadro: 0; fondo: rgba(34, 197, 94, 0,18); opacidad: 0; transición: opacidad 280 ms facilidad; eventos de puntero: ninguno; } .ravs-ai-chip .ravs-send-btn.is-flash .ravs-send-glow { opacidad: 1; } .ravs-count-num { familia de fuentes: ui-monospace, SFMono-Regular, Menlo, monospace; color: var(--primer plano); peso de fuente: 600; } .ravs-diffbody { flexionar: 1; altura mínima: 0; posición: relativa; fondo: var(--editor-surface, var(--card)); } .ravs-diffscroll { posición: absoluta; recuadro: 0; desbordamiento: oculto; familia de fuentes: ui-monospace, SFMono-Regular, Menlo, monospace; tamaño de fuente: 11,5 px; altura de línea: 1,55; color: var(--primer plano); relleno: 4px 0 8px; transición: opacidad 240 ms facilidad; } .ravs-diffscroll.is-hidden { opacidad: 0; eventos de puntero: ninguno; } .ravs-term { posición: absoluta; recuadro: 0; fondo: var(--editor-surface, var(--card)); color: var(--primer plano); familia de fuentes: ui-monospace, SFMono-Regular, Menlo, monospace; tamaño de fuente: 11px; altura de línea: 1,45; desbordamiento: oculto; pantalla: flexible; dirección flexible: columna; opacidad: 0; eventos de puntero: ninguno; transición: opacidad 240 ms facilidad; índice z: 4; } .ravs-term.is-visible { opacidad: 1; } .ravs-term-body { flex: 1; altura mínima: 0; relleno: 10px 12px; desbordamiento: oculto; pantalla: flexible; dirección flexible: columna; espacio: 6px; } .ravs-term-line { espacio en blanco: pre-envoltura; salto de palabra: salto de palabra; altura de línea: 1,45; } .ravs-term-muted { color: var(--silenciado-primer plano); } .ravs-term-glifo { color: rgb(217 119 6); margen derecho: 6px; } .ravs-term-check { color: rgb(16 185 129); peso de fuente: 700; margen derecho: 6px; } .ravs-term-spinner { mostrar: bloque en línea; ancho: 8px; altura: 8px; margen derecho: 6px; radio del borde: 999px; borde: mezcla de colores sólidos de 1,5 px (en srgb, var (--primer plano) 20%, transparente); color-superior-borde: var(--primer plano); alineación vertical: -1px; animación: ravs-term-spin 0,9 s lineal infinito; } @keyframes ravs-term-spin { a { transformar: rotar (360 grados) } } .ravs-hunk-header { display: grid; columnas-plantilla-cuadrícula: 36px 36px 16px minmax(0,1fr); alinear elementos: centro; relleno: 1px 8px 1px 0; fondo: rgba(99, 102, 241, 0,06); color: var(--primer plano silenciado); tamaño de fuente: 10,5 px; borde superior: var sólido de 1 px (--borde); borde inferior: var sólido de 1 px (--borde); } .ravs-hunk-header .ravs-text { columna-cuadrícula: 4 / -1; espacio en blanco: nowrap; desbordamiento: oculto; desbordamiento de texto: puntos suspensivos; color: rgb(99102241); tamaño de fuente: 10,5 px; } .ravs-diff-line { mostrar: cuadrícula; columnas-plantilla-cuadrícula: 36px 36px 16px minmax(0,1fr); alinear elementos: estirar; posición: relativa; } .ravs-ln { text-align: derecha; relleno: 0 6px 0 0; color: var(--primer plano silenciado); tamaño de fuente: 10,5 px; selección de usuario: ninguna; opacidad: 0,85; } .ravs-marker { text-align: centro; color: var(--primer plano silenciado); peso de fuente: 700; opacidad: 0,7; } .ravs-text-cell { padding-right: 8px; espacio en blanco: pre; desbordamiento: oculto; } .ravs-tok-kw { color: #a855f7; } .ravs-tok-id { color: #2563eb; } .ravs-tok-str { color: #16a34a; } .ravs-diff-line.is-add { fondo: color-mix(en srgb, var(--git-decoration-added) 14%, transparente); } .ravs-diff-line.is-add .ravs-marker { color: color-mix(en srgb, var(--git-decoration-added) 72%, transparente); opacidad: 1; } .ravs-diff-line.is-rem { fondo: color-mix(en srgb, var(--git-decoration-deleted) 14%, transparente); } .ravs-diff-line.is-rem .ravs-marker { color: color-mix(en srgb, var(--git-decoration-deleted) 72%, transparente); opacidad: 1; } .ravs-add-note-btn { posición: absoluta; izquierda: 4px; ancho: 18px; altura: 18px; pantalla: flexible en línea; alinear elementos: centro; justificar-contenido: centro; relleno: 0; borde: mezcla de colores sólidos de 1 px (en srgb, color actual 22%, var (--borde)); radio del borde: 4px; fondo: var(--ravs-soft-fill); color: var(--primer plano); índice z: 5; opacidad: 0; sombra de cuadro: 0 1px 2px rgb(0 0 0 / 0.14); eventos de puntero: ninguno; transición: opacidad 160 ms facilidad; } .ravs-add-note-btn.is-visible { opacidad: 1; } .ravs-note-row { relleno: 4px 8px 4px 0; altura máxima: 0; desbordamiento: oculto; opacidad: 0; transición: altura máxima 360 ms cúbico-bezier (.4,0,.2,1), opacidad 280 ms facilidad 60 ms, relleno 360 ms cúbico-bezier (.4,0,.2,1); } .ravs-note-row.is-visible { altura máxima: 90px; opacidad: 1; } .ravs-note-card { margen: 0 12px; posición: relativa; borde: 1px solid var(--ravs-panel-border); borde izquierdo: 3px solid var(--ravs-emphasis-border); radio del borde: 6px; color de fondo: var(--card); relleno: 5px 8px 5px 10px; sombra de cuadro: 0 1px 2px rgb(0 0 0 / 0.16); } .ravs-note-meta { tamaño de fuente: 9.5px; peso de fuente: 600; transformación de texto: mayúsculas; espacio entre letras: 0,04 em; color: var(--primer plano silenciado); } .ravs-note-body { tamaño de fuente: 11,5 px; color: var(--primer plano); altura de línea: 1,35; margen superior: 2px; } .ravs-popover { posición: absoluta; izquierda: 12px; derecha: 12px; ancho máximo: ninguno; índice z: 20; relleno: 8px 10px; borde: 1px solid var(--ravs-panel-border); borde izquierdo: 3px solid var(--ravs-emphasis-border); radio del borde: 6px; color de fondo: var(--card); color: var(--primer plano); sombra-cuadro: var(--ravs-sombra-flotante); pantalla: flexible; dirección flexible: columna; espacio: 6px; opacidad: 0; transformar: traducirY(-4px) escala(0.985); eventos de puntero: ninguno; transición: opacidad 180 ms facilidad, transformación 180 ms facilidad; } .ravs-popover.is-visible { opacidad: 1; transformar: ninguno; eventos de puntero: automático; } .ravs-pop-label { tamaño de fuente: 10px; peso de fuente: 600; transformación de texto: mayúsculas; espacio entre letras: 0,04 em; color: var(--primer plano silenciado); } .ravs-pop-input { altura mínima: 38px; altura máxima: 80 px; relleno: 6px 8px; borde: 1px var sólido (--borde); radio del borde: 4px; fondo: var(--editor-surface, var(--card)); tamaño de fuente: 12px; altura de línea: 1,4; color: var(--primer plano); espacio en blanco: preenvoltura; salto de palabra: salto de palabra; desbordamiento: oculto; } .ravs-pop-footer { pantalla: flex; justificar contenido: extremo flexible; espacio: 6px; } .ravs-pop-btn { tamaño de fuente: 11px; peso de fuente: 500; relleno: 4px 9px; radio del borde: 5px; altura de línea: 1; borde: 1px sólido transparente; pantalla: flexible en línea; alinear elementos: centro; espacio: 5px; } .ravs-pop-btn.is-cancel { color: var(--silenciado-primer plano); fondo: transparente; } .ravs-pop-btn.is-add { color: var(--primer-primer plano); fondo: var(--primario); } .ravs-send-menu { posición: absoluta; índice z: 30; derecha: 8px; arriba: 6px; ancho mínimo: 200px; fondo: var(--popover); color: var(--popover-primer plano); borde: 1px var sólido (--borde); radio del borde: 8px; relleno: 4px; sombra-cuadro: var(--ravs-sombra-flotante); opacidad: 0; transformar: traducirY(-4px) escala(0.985); eventos de puntero: ninguno; transición: opacidad 180 ms facilidad, transformación 180 ms facilidad; } .ravs-send-menu.is-visible { opacidad: 1; transformar: ninguno; eventos de puntero: automático; } .ravs-menu-section { relleno: 4px 8px 2px; tamaño de fuente: 9,5 px; peso de fuente: 700; transformación de texto: mayúsculas; espacio entre letras: 0,06 em; color: var(--primer plano silenciado); } .ravs-menu-row { mostrar: cuadrícula; columnas-plantilla-cuadrícula: 16px minmax(0,1fr); alinear elementos: centro; espacio: 8px; relleno: 6px 8px; radio del borde: 5px; tamaño de fuente: 12px; color: var(--popover-primer plano); } .ravs-menu-row.is-hot { fondo: var(--accent); sombra de cuadro: inserción 0 0 0 1px var (--borde); } .ravs-cursor { posición: absoluta; índice z: 40; eventos de puntero: ninguno; transición: transformar 600 ms cúbico-bezier (.45, .05, .2,1), opacidad 200 ms facilidad; transformar: traducir (-30px, 220px); opacidad: 0; } .ravs-cursor.is-visible { opacidad: 1; } .ravs-cursor .ravs-ripple { posición: absoluta; izquierda: -6px; arriba: -6px; ancho: 28px; altura: 28 píxeles; radio del borde: 999px; borde: mezcla de colores sólidos de 2px (en srgb, var(--primer plano) 52%, transparente); opacidad: 0; } .ravs-cursor.is-clicking .ravs-ripple { animación: ravs-ripple 460 ms hacia adelante; } @keyframes ravs-ripple { 0% { transformar: escala(0.4); opacidad: 0,9; } 100% { transformar: escala(1.4); opacidad: 0; } } .ravs-caret { mostrar: bloque en línea; ancho: 1,5 px; altura: 1em; fondo: color actual; alineación vertical: -2px; margen izquierdo: 1px; animación: ravs-caret-blink 1,05s pasos(1) infinito; } @keyframes ravs-caret-blink { 0%, 50% { opacidad: 1 } 51%, 100% { opacidad: 0 } }" + "db6691aa0a": ".ravs-window { position: absolute; inset: 0; --ravs-soft-surface: color-mix(in srgb, var(--foreground) 2%, var(--card)); --ravs-soft-fill: color-mix(in srgb, var(--foreground) 6%, transparent); --ravs-panel-border: color-mix(in srgb, var(--foreground) 18%, var(--border)); --ravs-emphasis-border: color-mix(in srgb, var(--foreground) 44%, var(--border)); --ravs-floating-shadow: 0 14px 30px rgb(0 0 0 / 0.22), 0 2px 6px rgb(0 0 0 / 0.12); background: var(--card); border: 1px solid var(--border); border-radius: 10px; overflow: hidden; display: flex; flex-direction: column; box-shadow: 0 1px 2px rgb(0 0 0 / 0.08); } .ravs-difftoolbar { display: flex; align-items: center; gap: 8px; padding: 6px 10px; border-bottom: 1px solid var(--border); background: var(--ravs-soft-surface); font-size: 11px; color: var(--muted-foreground); } .ravs-diff-path { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; color: var(--foreground); } .ravs-ai-chip { margin-left: auto; display: inline-flex; align-items: stretch; overflow: hidden; border-radius: 6px; border: 1px solid var(--border); background: var(--ravs-soft-surface); opacity: 0; transform: translateY(-2px); transition: opacity 320ms ease, transform 320ms ease; } .ravs-ai-chip.is-visible { opacity: 1; transform: none; } .ravs-ai-chip .ravs-count-btn, .ravs-ai-chip .ravs-send-btn { display: inline-flex; align-items: center; gap: 5px; padding: 3px 8px; font-size: 11px; color: var(--muted-foreground); background: transparent; line-height: 1; } .ravs-ai-chip .ravs-count-btn { border-right: 1px solid var(--border); } .ravs-ai-chip .ravs-send-btn { padding: 3px 7px; position: relative; } .ravs-send-glow { position: absolute; inset: 0; background: rgba(34, 197, 94, 0.18); opacity: 0; transition: opacity 280ms ease; pointer-events: none; } .ravs-ai-chip .ravs-send-btn.is-flash .ravs-send-glow { opacity: 1; } .ravs-count-num { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; color: var(--foreground); font-weight: 600; } .ravs-diffbody { flex: 1; min-height: 0; position: relative; background: var(--editor-surface, var(--card)); } .ravs-diffscroll { position: absolute; inset: 0; overflow: hidden; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11.5px; line-height: 1.55; color: var(--foreground); padding: 4px 0 8px; transition: opacity 240ms ease; } .ravs-diffscroll.is-hidden { opacity: 0; pointer-events: none; } .ravs-term { position: absolute; inset: 0; background: var(--editor-surface, var(--card)); color: var(--foreground); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11px; line-height: 1.45; overflow: hidden; display: flex; flex-direction: column; opacity: 0; pointer-events: none; transition: opacity 240ms ease; z-index: 4; } .ravs-term.is-visible { opacity: 1; } .ravs-term-body { flex: 1; min-height: 0; padding: 10px 12px; overflow: hidden; display: flex; flex-direction: column; gap: 6px; } .ravs-term-line { white-space: pre-wrap; word-break: break-word; line-height: 1.45; } .ravs-term-muted { color: var(--muted-foreground); } .ravs-term-glyph { color: rgb(217 119 6); margin-right: 6px; } .ravs-term-check { color: rgb(16 185 129); font-weight: 700; margin-right: 6px; } .ravs-term-spinner { display: inline-block; width: 8px; height: 8px; margin-right: 6px; border-radius: 999px; border: 1.5px solid color-mix(in srgb, var(--foreground) 20%, transparent); border-top-color: var(--foreground); vertical-align: -1px; animation: ravs-term-spin 0.9s linear infinite; } @keyframes ravs-term-spin { to { transform: rotate(360deg) } } .ravs-hunk-header { display: grid; grid-template-columns: 36px 36px 16px minmax(0,1fr); align-items: center; padding: 1px 8px 1px 0; background: rgba(99, 102, 241, 0.06); color: var(--muted-foreground); font-size: 10.5px; border-top: 1px solid var(--border); border-bottom: 1px solid var(--border); } .ravs-hunk-header .ravs-text { grid-column: 4 / -1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: rgb(99 102 241); font-size: 10.5px; } .ravs-diff-line { display: grid; grid-template-columns: 36px 36px 16px minmax(0,1fr); align-items: stretch; position: relative; } .ravs-ln { text-align: right; padding: 0 6px 0 0; color: var(--muted-foreground); font-size: 10.5px; user-select: none; opacity: 0.85; } .ravs-marker { text-align: center; color: var(--muted-foreground); font-weight: 700; opacity: 0.7; } .ravs-text-cell { padding-right: 8px; white-space: pre; overflow: hidden; } .ravs-tok-kw { color: #a855f7; } .ravs-tok-id { color: #2563eb; } .ravs-tok-str { color: #16a34a; } .ravs-diff-line.is-add { background: color-mix(in srgb, var(--git-decoration-added) 14%, transparent); } .ravs-diff-line.is-add .ravs-marker { color: color-mix(in srgb, var(--git-decoration-added) 72%, transparent); opacity: 1; } .ravs-diff-line.is-rem { background: color-mix(in srgb, var(--git-decoration-deleted) 14%, transparent); } .ravs-diff-line.is-rem .ravs-marker { color: color-mix(in srgb, var(--git-decoration-deleted) 72%, transparent); opacity: 1; } .ravs-add-note-btn { position: absolute; left: 4px; width: 18px; height: 18px; display: inline-flex; align-items: center; justify-content: center; padding: 0; border: 1px solid color-mix(in srgb, currentColor 22%, var(--border)); border-radius: 4px; background: var(--ravs-soft-fill); color: var(--foreground); z-index: 5; opacity: 0; box-shadow: 0 1px 2px rgb(0 0 0 / 0.14); pointer-events: none; transition: opacity 160ms ease; } .ravs-add-note-btn.is-visible { opacity: 1; } .ravs-note-row { padding: 4px 8px 4px 0; max-height: 0; overflow: hidden; opacity: 0; transition: max-height 360ms cubic-bezier(.4,0,.2,1), opacity 280ms ease 60ms, padding 360ms cubic-bezier(.4,0,.2,1); } .ravs-note-row.is-visible { max-height: 90px; opacity: 1; } .ravs-note-card { margin: 0 12px; position: relative; border: 1px solid var(--ravs-panel-border); border-left: 3px solid var(--ravs-emphasis-border); border-radius: 6px; background-color: var(--card); padding: 5px 8px 5px 10px; box-shadow: 0 1px 2px rgb(0 0 0 / 0.16); } .ravs-note-meta { font-size: 9.5px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; color: var(--muted-foreground); } .ravs-note-body { font-size: 11.5px; color: var(--foreground); line-height: 1.35; margin-top: 2px; } .ravs-popover { position: absolute; left: 12px; right: 12px; max-width: none; z-index: 20; padding: 8px 10px; border: 1px solid var(--ravs-panel-border); border-left: 3px solid var(--ravs-emphasis-border); border-radius: 6px; background-color: var(--card); color: var(--foreground); box-shadow: var(--ravs-floating-shadow); display: flex; flex-direction: column; gap: 6px; opacity: 0; transform: translateY(-4px) scale(0.985); pointer-events: none; transition: opacity 180ms ease, transform 180ms ease; } .ravs-popover.is-visible { opacity: 1; transform: none; pointer-events: auto; } .ravs-pop-label { font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; color: var(--muted-foreground); } .ravs-pop-input { min-height: 38px; max-height: 80px; padding: 6px 8px; border: 1px solid var(--border); border-radius: 4px; background: var(--editor-surface, var(--card)); font-size: 12px; line-height: 1.4; color: var(--foreground); white-space: pre-wrap; word-break: break-word; overflow: hidden; } .ravs-pop-footer { display: flex; justify-content: flex-end; gap: 6px; } .ravs-pop-btn { font-size: 11px; font-weight: 500; padding: 4px 9px; border-radius: 5px; line-height: 1; border: 1px solid transparent; display: inline-flex; align-items: center; gap: 5px; } .ravs-pop-btn.is-cancel { color: var(--muted-foreground); background: transparent; } .ravs-pop-btn.is-add { color: var(--primary-foreground); background: var(--primary); } .ravs-send-menu { position: absolute; z-index: 30; right: 8px; top: 6px; min-width: 200px; background: var(--popover); color: var(--popover-foreground); border: 1px solid var(--border); border-radius: 8px; padding: 4px; box-shadow: var(--ravs-floating-shadow); opacity: 0; transform: translateY(-4px) scale(0.985); pointer-events: none; transition: opacity 180ms ease, transform 180ms ease; } .ravs-send-menu.is-visible { opacity: 1; transform: none; pointer-events: auto; } .ravs-menu-section { padding: 4px 8px 2px; font-size: 9.5px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted-foreground); } .ravs-menu-row { display: grid; grid-template-columns: 16px minmax(0,1fr); align-items: center; gap: 8px; padding: 6px 8px; border-radius: 5px; font-size: 12px; color: var(--popover-foreground); } .ravs-menu-row.is-hot { background: var(--accent); box-shadow: inset 0 0 0 1px var(--border); } .ravs-cursor { position: absolute; z-index: 40; pointer-events: none; transition: transform 600ms cubic-bezier(.45,.05,.2,1), opacity 200ms ease; transform: translate(-30px, 220px); opacity: 0; } .ravs-cursor.is-visible { opacity: 1; } .ravs-cursor .ravs-ripple { position: absolute; left: -6px; top: -6px; width: 28px; height: 28px; border-radius: 999px; border: 2px solid color-mix(in srgb, var(--foreground) 52%, transparent); opacity: 0; } .ravs-cursor.is-clicking .ravs-ripple { animation: ravs-ripple 460ms ease-out forwards; } @keyframes ravs-ripple { 0% { transform: scale(0.4); opacity: 0.9; } 100% { transform: scale(1.4); opacity: 0; } } .ravs-caret { display: inline-block; width: 1.5px; height: 1em; background: currentColor; vertical-align: -2px; margin-left: 1px; animation: ravs-caret-blink 1.05s steps(1) infinite; } @keyframes ravs-caret-blink { 0%, 50% { opacity: 1 } 51%, 100% { opacity: 0 } }" } }, "pr": { "view": { "styles": { - "fc9a23c83d": ".ravpr-stage { posición: absoluta; recuadro: 0; desbordamiento: oculto; } .ravpr-stack { posición: absoluta; recuadro: 0; pantalla: flexible; justificar contenido: extremo flexible; relleno: 4px 34px 4px 2px; desbordamiento: oculto; } .ravpr-sidebar, .ravpr-card { posición: absoluta; arriba: 4px; derecha: 2px; ancho: 464px; altura: calc(100% - 8px); fondo: var(--card, #fff); borde: 1px var sólido (--borde); radio del borde: 10px; color: var(--primer plano, #18181b); desbordamiento: oculto; sombra de cuadro: 0 1px 2px rgba(24,24,27,0.04); } .ravpr-sidebar { opacidad: 0; transición: opacidad 220 ms facilidad; } .ravpr-sidebar.is-visible { opacidad: 1; } .ravpr-sidebar.is-hiding { opacidad: 0; } .ravpr-card { pantalla: flex; dirección flexible: columna; ancho mínimo: 0; opacidad: 0; transición: opacidad 260 ms facilidad; } .ravpr-card.is-visible { opacidad: 1; } .ravpr-tabs { posición: relativa; pantalla: flexible; alinear elementos: centro; espacio: 14px; altura: 36px; relleno: 0 14px; fondo: rgba(24,24,27,0.015); color: var(--primer plano silenciado, #71717a); } .ravpr-tab { posición: relativa; ancho: 18px; altura: 18px; pantalla: flexible en línea; alinear elementos: centro; justificar-contenido: centro; color: var(--primer plano silenciado, #71717a); } .ravpr-tab.is-active, .ravpr-tab.is-hovered { color: var(--primer plano, #18181b); } .ravpr-tab.is-active::después { contenido: ''; posición: absoluta; izquierda: -5px; derecha: -5px; abajo: -10px; altura: 1px; fondo: var(--primer plano, #18181b); } .ravpr-tooltip { posición: absoluta; arriba: 34px; izquierda: 106px; índice z: 6; relleno: 7px 11px; radio del borde: 8px; fondo: var(--card, #fff); color: var(--primer plano, #18181b); tamaño de fuente: 12px; altura de línea: 1; sombra de cuadro: 0 8px 22px rgba(0,0,0,0.22); opacidad: 0; transformar: traducirY(-3px); eventos de puntero: ninguno; transición: opacidad 160 ms facilidad, transformación 160 ms facilidad; } .ravpr-tooltip.is-visible { opacidad: 1; transformar: traducirY(0); } .ravpr-explorer { relleno: 10px 12px 12px; } .ravpr-heading { color: var(--primer plano silenciado, #71717a); tamaño de fuente: 10px; peso de fuente: 600; espacio entre letras: 0,05 em; transformación de texto: mayúsculas; } .ravpr-file-list { margen superior: 8px; pantalla: flexible; dirección flexible: columna; espacio: 2px; } .ravpr-file { pantalla: cuadrícula; columnas-plantilla-cuadrícula: 14px minmax(0,1fr) 22px; alinear elementos: centro; espacio: 8px; altura mínima: 28px; relleno: 4px 6px; radio del borde: 6px; } .ravpr-file.is-active { fondo: rgba(24,24,27,0.06); sombra de cuadro: inserción 0 0 0 1px rgba(24,24,27,0.06); } .ravpr-file-icon { ancho: 12px; altura: 12px; radio del borde: 3px; fondo: rgba(24,24,27,0.14); } .ravpr-nombre-archivo { altura: 8px; radio del borde: 999px; fondo: rgba(24,24,27,0.14); } .ravpr-file-status { ancho: 14px; altura: 8px; radio del borde: 999px; fondo: rgba(24,24,27,0.12); } .ravpr-body { relleno: 10px 12px 18px; pantalla: flexible; dirección flexible: columna; espacio: 5px; altura mínima: 0; } .ravpr-número-fila { mostrar: flex; alinear elementos: centro; espacio: 7px; } .ravpr-number { familia de fuentes: ui-monospace, SFMono-Regular, Menlo, monospace; tamaño de fuente: 12px; peso de fuente: 700; } .ravpr-open { pantalla: inline-flex; alinear elementos: centro; justificar-contenido: centro; altura: 18px; relleno: 0 7px; radio del borde: 5px; fondo: rgba(16.185.129,0,10); borde: 1px rgba sólido (16,185,129,0.28); color: rgb(4 120 87); tamaño de fuente: 9px; peso de fuente: 700; altura de línea: 1; } .ravpr-title { tamaño de fuente: 12px; peso de fuente: 600; altura de línea: 1,35; color: var(--primer plano, #18181b); margen inferior: 2px; } .ravpr-merge { mostrar: inline-flex; alinear elementos: centro; justificar-contenido: centro; espacio: 6px; altura: 30 píxeles; altura mínima: 30px; flexionar: 0 0 30px; radio del borde: 7px; fondo: rgb(22 163 74); color: #fff; tamaño de fuente: 11,5 px; peso de fuente: 700; margen inferior: 2px; sombra de cuadro: 0 1px 2px rgba(22,163,74,0.18); transición: caja-sombra 220 ms de facilidad, filtro 220 ms de facilidad; } .ravpr-merge.is-ready { box-shadow: 0 0 0 3px rgba(34,197,94,0.22), 0 1px 2px rgba(22,163,74,0.18); } .ravpr-section-row, .ravpr-check-row { mostrar: cuadrícula; columnas-plantilla-cuadrícula: 18px minmax(0,1fr) auto; alinear elementos: centro; espacio: 7px; relleno: 5px 0; tamaño de fuente: 11,5 px; color: var(--primer plano, #18181b); } .ravpr-check-row { relleno: 5px 7px; tamaño de fuente: 10,5 px; } .ravpr-label { espacio en blanco: nowrap; desbordamiento: oculto; desbordamiento de texto: puntos suspensivos; } .ravpr-meta, .ravpr-check-state { color: var(--primer plano silenciado, #71717a); tamaño de fuente: 10,5 px; } .ravpr-check-state { tamaño de fuente: 10px; } .ravpr-ring { mostrar: bloque en línea; ancho: 14px; altura: 14px; radio del borde: 999px; borde: 2px rgba sólido (245,158,11,0.35); color del borde superior: rgb(245 158 11); animación: ravpr-spin 1.1s lineal infinito; } .ravpr-check { ancho: 15px; altura: 15 píxeles; radio del borde: 999px; pantalla: ninguna; alinear elementos: centro; justificar-contenido: centro; fondo: rgba(34,197,94,0.14); color: rgb(22 163 74); } .ravpr-section-row.is-done .ravpr-ring, .ravpr-check-row.is-done .ravpr-ring { pantalla: ninguna; } .ravpr-section-row.is-done .ravpr-check, .ravpr-check-row.is-done .ravpr-check { display: inline-flex; } .ravpr-reveal { mostrar: flex; dirección flexible: columna; espacio: 3px; opacidad: 0; transformar: traducirY(4px); transición: opacidad 260 ms facilidad, transformación 260 ms facilidad; eventos de puntero: ninguno; } .ravpr-reveal.is-visible { opacidad: 1; transformar: traducirY(0); eventos de puntero: automático; } .ravpr-check-list, .ravpr-comment-list { mostrar: flex; dirección flexible: columna; espacio: 4px; altura mínima: 0; } .ravpr-comment-card { borde: var sólida de 1px (--borde); radio del borde: 8px; fondo: var(--card, #fff); desbordamiento: oculto; opacidad: 0; transformar: traducirY(4px); transición: opacidad 260 ms facilidad, transformación 260 ms facilidad; } .ravpr-comment-card.is-visible { opacidad: 1; transformar: traducirY(0); } .ravpr-comment-head { mostrar: cuadrícula; columnas-plantilla-cuadrícula: 18px minmax(0,1fr) auto; alinear elementos: centro; espacio: 7px; relleno: 5px 7px; fondo: rgba(24,24,27,0.015); } .ravpr-avatar { ancho: 16px; altura: 16px; radio del borde: 999px; fondo: rgba(24,24,27,0.16); } .ravpr-autor { ancho: 78px; altura: 8px; radio del borde: 999px; fondo: rgba(24,24,27,0.18); } .ravpr-comment-path { familia de fuentes: ui-monospace, SFMono-Regular, Menlo, monospace; tamaño de fuente: 9,5 px; color: var(--primer plano silenciado, #71717a); } .ravpr-comment-body { relleno: 5px 7px 6px; tamaño de fuente: 11px; altura de línea: 1,32; color: var(--primer plano, #18181b); } .ravpr-comment-body code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; tamaño de fuente: 10,5 px; relleno: 1px 4px; radio del borde: 4px; fondo: rgba(24,24,27,0.06); } .ravpr-cursor { posición: absoluta; índice z: 40; eventos de puntero: ninguno; transición: transformar 600 ms cúbico-bezier (.45, .05, .2,1), opacidad 200 ms facilidad; transformar: traducir (-30px, 220px); opacidad: 0; } .ravpr-cursor.is-visible { opacidad: 1; } .ravpr-ripple { posición: absoluta; izquierda: -6px; arriba: -6px; ancho: 28px; altura: 28 píxeles; radio del borde: 999px; borde: 2px rgba sólido (24,24,27,0.5); opacidad: 0; } .ravpr-cursor.is-clicking .ravpr-ripple { animación: ravpr-ripple 460 ms hacia adelante; } @keyframes ravpr-ripple { 0% { transformar: escala (0.4); opacidad: 0,9; } 100% { transformar: escala(1.4); opacidad: 0; } } @keyframes ravpr-spin { a { transformar: rotar (360 grados); } }" + "fc9a23c83d": ".ravpr-stage { position: absolute; inset: 0; overflow: hidden; } .ravpr-stack { position: absolute; inset: 0; display: flex; justify-content: flex-end; padding: 4px 34px 4px 2px; overflow: hidden; } .ravpr-sidebar, .ravpr-card { position: absolute; top: 4px; right: 2px; width: 464px; height: calc(100% - 8px); background: var(--card, #fff); border: 1px solid var(--border); border-radius: 10px; color: var(--foreground, #18181b); overflow: hidden; box-shadow: 0 1px 2px rgba(24,24,27,0.04); } .ravpr-sidebar { opacity: 0; transition: opacity 220ms ease; } .ravpr-sidebar.is-visible { opacity: 1; } .ravpr-sidebar.is-hiding { opacity: 0; } .ravpr-card { display: flex; flex-direction: column; min-width: 0; opacity: 0; transition: opacity 260ms ease; } .ravpr-card.is-visible { opacity: 1; } .ravpr-tabs { position: relative; display: flex; align-items: center; gap: 14px; height: 36px; padding: 0 14px; background: rgba(24,24,27,0.015); color: var(--muted-foreground, #71717a); } .ravpr-tab { position: relative; width: 18px; height: 18px; display: inline-flex; align-items: center; justify-content: center; color: var(--muted-foreground, #71717a); } .ravpr-tab.is-active, .ravpr-tab.is-hovered { color: var(--foreground, #18181b); } .ravpr-tab.is-active::after { content: ''; position: absolute; left: -5px; right: -5px; bottom: -10px; height: 1px; background: var(--foreground, #18181b); } .ravpr-tooltip { position: absolute; top: 34px; left: 106px; z-index: 6; padding: 7px 11px; border-radius: 8px; background: var(--card, #fff); color: var(--foreground, #18181b); font-size: 12px; line-height: 1; box-shadow: 0 8px 22px rgba(0,0,0,0.22); opacity: 0; transform: translateY(-3px); pointer-events: none; transition: opacity 160ms ease, transform 160ms ease; } .ravpr-tooltip.is-visible { opacity: 1; transform: translateY(0); } .ravpr-explorer { padding: 10px 12px 12px; } .ravpr-heading { color: var(--muted-foreground, #71717a); font-size: 10px; font-weight: 600; letter-spacing: 0.05em; text-transform: uppercase; } .ravpr-file-list { margin-top: 8px; display: flex; flex-direction: column; gap: 2px; } .ravpr-file { display: grid; grid-template-columns: 14px minmax(0,1fr) 22px; align-items: center; gap: 8px; min-height: 28px; padding: 4px 6px; border-radius: 6px; } .ravpr-file.is-active { background: rgba(24,24,27,0.06); box-shadow: inset 0 0 0 1px rgba(24,24,27,0.06); } .ravpr-file-icon { width: 12px; height: 12px; border-radius: 3px; background: rgba(24,24,27,0.14); } .ravpr-file-name { height: 8px; border-radius: 999px; background: rgba(24,24,27,0.14); } .ravpr-file-status { width: 14px; height: 8px; border-radius: 999px; background: rgba(24,24,27,0.12); } .ravpr-body { padding: 10px 12px 18px; display: flex; flex-direction: column; gap: 5px; min-height: 0; } .ravpr-number-row { display: flex; align-items: center; gap: 7px; } .ravpr-number { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; font-weight: 700; } .ravpr-open { display: inline-flex; align-items: center; justify-content: center; height: 18px; padding: 0 7px; border-radius: 5px; background: rgba(16,185,129,0.10); border: 1px solid rgba(16,185,129,0.28); color: rgb(4 120 87); font-size: 9px; font-weight: 700; line-height: 1; } .ravpr-title { font-size: 12px; font-weight: 600; line-height: 1.35; color: var(--foreground, #18181b); margin-bottom: 2px; } .ravpr-merge { display: inline-flex; align-items: center; justify-content: center; gap: 6px; height: 30px; min-height: 30px; flex: 0 0 30px; border-radius: 7px; background: rgb(22 163 74); color: #fff; font-size: 11.5px; font-weight: 700; margin-bottom: 2px; box-shadow: 0 1px 2px rgba(22,163,74,0.18); transition: box-shadow 220ms ease, filter 220ms ease; } .ravpr-merge.is-ready { box-shadow: 0 0 0 3px rgba(34,197,94,0.22), 0 1px 2px rgba(22,163,74,0.18); } .ravpr-section-row, .ravpr-check-row { display: grid; grid-template-columns: 18px minmax(0,1fr) auto; align-items: center; gap: 7px; padding: 5px 0; font-size: 11.5px; color: var(--foreground, #18181b); } .ravpr-check-row { padding: 5px 7px; font-size: 10.5px; } .ravpr-label { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .ravpr-meta, .ravpr-check-state { color: var(--muted-foreground, #71717a); font-size: 10.5px; } .ravpr-check-state { font-size: 10px; } .ravpr-ring { display: inline-block; width: 14px; height: 14px; border-radius: 999px; border: 2px solid rgba(245,158,11,0.35); border-top-color: rgb(245 158 11); animation: ravpr-spin 1.1s linear infinite; } .ravpr-check { width: 15px; height: 15px; border-radius: 999px; display: none; align-items: center; justify-content: center; background: rgba(34,197,94,0.14); color: rgb(22 163 74); } .ravpr-section-row.is-done .ravpr-ring, .ravpr-check-row.is-done .ravpr-ring { display: none; } .ravpr-section-row.is-done .ravpr-check, .ravpr-check-row.is-done .ravpr-check { display: inline-flex; } .ravpr-reveal { display: flex; flex-direction: column; gap: 3px; opacity: 0; transform: translateY(4px); transition: opacity 260ms ease, transform 260ms ease; pointer-events: none; } .ravpr-reveal.is-visible { opacity: 1; transform: translateY(0); pointer-events: auto; } .ravpr-check-list, .ravpr-comment-list { display: flex; flex-direction: column; gap: 4px; min-height: 0; } .ravpr-comment-card { border: 1px solid var(--border); border-radius: 8px; background: var(--card, #fff); overflow: hidden; opacity: 0; transform: translateY(4px); transition: opacity 260ms ease, transform 260ms ease; } .ravpr-comment-card.is-visible { opacity: 1; transform: translateY(0); } .ravpr-comment-head { display: grid; grid-template-columns: 18px minmax(0,1fr) auto; align-items: center; gap: 7px; padding: 5px 7px; background: rgba(24,24,27,0.015); } .ravpr-avatar { width: 16px; height: 16px; border-radius: 999px; background: rgba(24,24,27,0.16); } .ravpr-author { width: 78px; height: 8px; border-radius: 999px; background: rgba(24,24,27,0.18); } .ravpr-comment-path { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 9.5px; color: var(--muted-foreground, #71717a); } .ravpr-comment-body { padding: 5px 7px 6px; font-size: 11px; line-height: 1.32; color: var(--foreground, #18181b); } .ravpr-comment-body code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 10.5px; padding: 1px 4px; border-radius: 4px; background: rgba(24,24,27,0.06); } .ravpr-cursor { position: absolute; z-index: 40; pointer-events: none; transition: transform 600ms cubic-bezier(.45,.05,.2,1), opacity 200ms ease; transform: translate(-30px, 220px); opacity: 0; } .ravpr-cursor.is-visible { opacity: 1; } .ravpr-ripple { position: absolute; left: -6px; top: -6px; width: 28px; height: 28px; border-radius: 999px; border: 2px solid rgba(24,24,27,0.5); opacity: 0; } .ravpr-cursor.is-clicking .ravpr-ripple { animation: ravpr-ripple 460ms ease-out forwards; } @keyframes ravpr-ripple { 0% { transform: scale(0.4); opacity: 0.9; } 100% { transform: scale(1.4); opacity: 0; } } @keyframes ravpr-spin { to { transform: rotate(360deg); } }" } } }, "ship": { "styles": { - "90cdcd2ecc": ".ravs-ship-root { posición: absoluta; recuadro: 0; } .ravs-ship-stack { posición: absoluta; recuadro: 0; pantalla: cuadrícula; columnas-plantilla-cuadrícula: 232px minmax(0,1fr); espacio: 14px; relleno: 4px 2px; /* Por qué: las tarjetas se ajustan al tamaño de su contenido en lugar de extenderse hasta la altura total del padre, para que las dos tarjetas no muestren espacios vacíos debajo de su contenido. */ alinear-elementos: inicio; } /* Minibarra lateral de control de código fuente: encabezado de recuento anticipado, área de texto de Commit + botón de Commit dividido, luego una sección de CAMBIOS con filas de archivos. Las filas del archivo son la superficie sobre la que se anima el pulso de \"lectura\". */ .ravs-sc-card { pantalla: flex; dirección flexible: columna; fondo: var(--card, #fff); borde: 1px var sólido (--borde); radio del borde: 10px; desbordamiento: oculto; sombra de cuadro: 0 1px 2px rgba(24,24,27,0.04); } /* Ambos encabezados de tarjetas comparten la misma altura fija, por lo que la tarjeta SC y el cuadro de diálogo PR se alinean en el borde superior independientemente del contenido del encabezado. */ .ravs-sc-header, .ravs-pr-head { altura: 36px; tamaño de caja: cuadro de borde; } .ravs-sc-header { pantalla: flex; alinear elementos: centro; justificar contenido: espacio entre; relleno: 0 10px; borde inferior: var sólido de 1 px (--borde); } .ravs-sc-ahead { mostrar: inline-flex; alinear elementos: centro; espacio: 5px; tamaño de fuente: 11px; peso de fuente: 500; color: var(--primer plano, #18181b); } .ravs-sc-ahead svg { color: var(--silenciado-primer plano, #71717a); } .ravs-sc-commit-area { pantalla: flex; dirección flexible: columna; espacio: 6px; relleno: 8px 10px; } .ravs-sc-textarea { posición: relativa; borde: 1px var sólido (--borde); radio del borde: 6px; fondo: var(--editor-surface, var(--card)); relleno: 6px 26px 6px 8px; altura mínima: 56px; tamaño de fuente: 12px; altura de línea: 1,45; color: var(--primer plano, #18181b); espacio en blanco: preenvoltura; salto de palabra: salto de palabra; desbordamiento: oculto; } .ravs-sc-textarea .ravs-placeholder { color: rgba(113,113,122,0.7); } .ravs-sc-sparkle { posición: absoluta; derecha: 6px; arriba: 6px; ancho: 20px; altura: 20 píxeles; pantalla: flexible en línea; alinear elementos: centro; justificar-contenido: centro; radio del borde: 4px; color: var(--primer plano silenciado, #71717a); fondo: transparente; transición: color con 160 ms de facilidad, fondo con 160 ms de facilidad; } .ravs-sc-sparkle.is-scanning { color: rgb(109 40 217); fondo: mezcla de colores (en srgb, rgb(139 92 246) 18%, transparente); } .ravs-sc-split { pantalla: flex; alinear elementos: estirar; } /* Por qué: Commit y crear PR rodean a Chrome; las posibilidades violetas de la IA son los puntos focales. Representelos como botones secundarios silenciosos para que no compitan con las señales de brillo/escaneo. */ .ravs-sc-split .ravs-primary { flex: 1; pantalla: flexible en línea; alinear elementos: centro; justificar-contenido: centro; espacio: 5px; relleno: 5px 10px; fondo: var(--secundario, #f5f5f5); color: var(--primer plano secundario, #171717); tamaño de fuente: 11px; peso de fuente: 500; radio de borde: 6px 0 0 6px; borde: 1px var sólido (--borde); transición: fondo con 240 ms de facilidad, color de borde con 240 ms de facilidad, color con 240 ms de facilidad; } .ravs-sc-split .ravs-chev { pantalla: inline-flex; alinear elementos: centro; justificar-contenido: centro; ancho: 22px; fondo: var(--secundario, #f5f5f5); color: var(--primer plano silenciado, #71717a); radio de borde: 0 6px 6px 0; borde: 1px var sólido (--borde); borde izquierdo: var sólido de 1 px (--borde); transición: fondo con 240 ms de facilidad, color de borde con 240 ms de facilidad, color con 240 ms de facilidad; } /* Por qué: cuando la IA haya completado el mensaje de Commit, tiñe el botón Commit de verde para indicar \"listo para confirmar\". Utiliza la misma familia de éxito verde que el flash PR para que los dos tiempos rimen. La mezcla es intencionalmente fuerte (~28%): al 14% desapareció junto al brillo violeta y el flash PR, por lo que los usuarios solo vieron el PR cambiar de color. */ .ravs-sc-split.is-ready .ravs-primary, .ravs-sc-split.is-ready .ravs-chev { fondo: mezcla de colores (en srgb, rgb(34 197 94) 28%, var(--secundario, #f5f5f5)); color del borde: rgb(34 197 94); color: rgb(21 128 61); transición: fondo con 220 ms de facilidad, color de borde con 220 ms de facilidad, color con 220 ms de facilidad; } .ravs-sc-split.is-ready .ravs-chev { borde-izquierdo-color: rgba(34, 197, 94, 0,55); } .ravs-sc-changes-header { pantalla: flex; alinear elementos: centro; justificar contenido: espacio entre; relleno: 8px 10px 4px; tamaño de fuente: 10px; peso de fuente: 600; transformación de texto: mayúsculas; espacio entre letras: 0,05 em; color: var(--primer plano silenciado, #71717a); } .ravs-sc-changes-count { color: var(--primer plano, #18181b); peso de fuente: 600; margen izquierdo: 2px; } .ravs-sc-view-all { tamaño de fuente: 10px; peso de fuente: 500; transformación de texto: ninguna; espacio entre letras: 0; color: var(--primer plano silenciado, #71717a); } .ravs-sc-files { mostrar: flex; dirección flexible: columna; relleno: 2px 6px 8px; flexión: 1; altura mínima: 0; desbordamiento: oculto; } .ravs-sc-file { mostrar: cuadrícula; columnas-plantilla-cuadrícula: 14px minmax(0,1fr) 12px; alinear elementos: centro; espacio: 6px; relleno: 3px 6px; radio del borde: 4px; tamaño de fuente: 11px; altura de línea: 1,35; color: var(--primer plano, #18181b); posición: relativa; transición: facilidad de fondo 220 ms; } .ravs-sc-ficon { color: rgb(180 83 9); pantalla: flexible en línea; } .ravs-sc-fname { ancho mínimo: 0; desbordamiento: oculto; desbordamiento de texto: puntos suspensivos; espacio en blanco: nowrap; } .ravs-sc-fmark { familia de fuentes: ui-monospace, SFMono-Regular, Menlo, monospace; tamaño de fuente: 10px; alineación de texto: derecha; color: rgb(180 83 9); } .ravs-sc-file.is-reading { fondo: color-mix(en srgb, rgb(139 92 246) 14%, transparente); sombra de cuadro: inserción 0 0 0 1px mezcla de colores (en srgb, rgb (139 92 246) 28%, transparente); } /* Cuadro de diálogo PR: coincide con el cromo de la tarjeta .ravs-sc (mismo borde, radio, elevación) para que las dos tarjetas se lean como un solo lenguaje de diseño. */ .ravs-pr-dialog { fondo: var(--card, #fff); borde: 1px var sólido (--borde); radio del borde: 10px; sombra de cuadro: 0 1px 2px rgba(24,24,27,0.04); pantalla: flexible; dirección flexible: columna; ancho mínimo: 0; desbordamiento: oculto; } .ravs-pr-head { pantalla: flex; alinear elementos: centro; justificar contenido: espacio entre; espacio: 6px; relleno: 0 10px; borde inferior: var sólido de 1 px (--borde); } .ravs-pr-title-text { tamaño de fuente: 11px; peso de fuente: 500; color: var(--primer plano, #18181b); } /* Chip de asistencia de IA de solo íconos: refleja .ravs-sc-sparkle para que la capacidad se lea de manera idéntica en ambas tarjetas. */ .ravs-pr-gen-btn { pantalla: inline-flex; alinear elementos: centro; justificar-contenido: centro; ancho: 22px; altura: 22 píxeles; relleno: 0; radio del borde: 4px; color: var(--primer plano silenciado, #71717a); fondo: transparente; borde: 0; cursor: puntero; transición: color con 160 ms de facilidad, fondo con 160 ms de facilidad; } .ravs-pr-gen-btn:hover { fondo: rgba(24,24,27,0.06); color: var(--primer plano, #18181b); } .ravs-pr-gen-btn.is-scanning { color: rgb(109 40 217); fondo: mezcla de colores (en srgb, rgb(139 92 246) 18%, transparente); } .ravs-pr-body { pantalla: flex; dirección flexible: columna; espacio: 8px; relleno: 10px; } .ravs-pr-field { pantalla: flex; dirección flexible: columna; espacio: 4px; } .ravs-pr-field-label { tamaño de fuente: 10px; peso de fuente: 600; transformación de texto: mayúsculas; espacio entre letras: 0,05 em; color: var(--primer plano silenciado, #71717a); } .ravs-pr-base { mostrar: inline-flex; alinear elementos: centro; espacio: 5px; relleno: 4px 9px; borde: 1px var sólido (--borde); radio del borde: 6px; tamaño de fuente: 11px; familia de fuentes: ui-monospace, SFMono-Regular, Menlo, monospace; color: var(--primer plano, #18181b); fondo: var(--editor-surface, var(--card)); alineación propia: inicio flexible; } .ravs-pr-base svg { color: var(--primer plano silenciado, #71717a); } .ravs-pr-input { posición: relativa; relleno: 6px 8px; borde: 1px var sólido (--borde); radio del borde: 6px; fondo: var(--editor-surface, var(--card)); altura mínima: 28px; tamaño de fuente: 12px; altura de línea: 1,45; color: var(--primer plano, #18181b); espacio en blanco: preenvoltura; salto de palabra: salto de palabra; desbordamiento: oculto; } .ravs-pr-input.is-body { altura mínima: 50px; tamaño de fuente: 11px; altura de línea: 1,4; } .ravs-pr-input .ravs-placeholder { color: rgba(113,113,122,0.7); } .ravs-pr-footer { pantalla: flex; alinear elementos: centro; espacio: 6px; justificar contenido: extremo flexible; margen superior: 2px; } .ravs-pr-btn { tamaño de fuente: 11px; peso de fuente: 500; relleno: 5px 10px; radio del borde: 6px; altura de línea: 1; borde: 1px sólido transparente; esquema: ninguno; } .ravs-pr-btn:focus, .ravs-pr-btn:focus-visible { esquema: ninguno; } /* Cancelar se lee como un botón fantasma silencioso para que no compita con la acción afirmativa Crear PR. */ .ravs-pr-btn.is-outline { fondo: transparente; color: var(--primer plano silenciado, #71717a); color del borde: transparente; } .ravs-pr-btn.is-outline:hover { fondo: rgba(24,24,27,0.05); color: var(--primer plano, #18181b); } /* Relleno secundario silencioso: consulte la nota .ravs-sc-split anterior. El anillo de destello todavía usa el color verde exitoso para que se lea el ritmo \"PR creado\". El botón Crear-PR es ligeramente más grande que Cancelar, por lo que la acción afirmativa sigue siendo el objetivo más importante. */ .ravs-pr-btn.is-solid { fondo: var(--secundario, #f5f5f5); color: var(--primer plano secundario, #171717); color del borde: var(--borde); tamaño de fuente: 12px; relleno: 7px 14px; transición: fondo con 220 ms de facilidad, color de borde con 220 ms de facilidad, color con 220 ms de facilidad, cuadro de sombra con 220 ms de facilidad; } .ravs-pr-btn.is-solid.is-ready { fondo: color-mix(en srgb, rgb(34 197 94) 28%, var(--secundario, #f5f5f5)); color del borde: rgb(34 197 94); color: rgb(21 128 61); } .ravs-pr-btn.is-solid.is-flash { box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.30); } .ravs-cursor { posición: absoluta; índice z: 40; eventos de puntero: ninguno; transición: transformar 600 ms cúbico-bezier (.45, .05, .2,1), opacidad 200 ms facilidad; transformar: traducir (-30px, 220px); opacidad: 0; } .ravs-cursor.is-visible { opacidad: 1; } .ravs-cursor .ravs-ripple { posición: absoluta; izquierda: -6px; arriba: -6px; ancho: 28px; altura: 28 píxeles; radio del borde: 999px; borde: 2px rgba sólido (24,24,27,0.5); opacidad: 0; } .ravs-cursor.is-clicking .ravs-ripple { animación: ravs-ripple 460 ms hacia adelante; } @keyframes ravs-ripple { 0% { transformar: escala(0.4); opacidad: 0,9; } 100% { transformar: escala(1.4); opacidad: 0; } } .ravs-caret { mostrar: bloque en línea; ancho: 1,5 px; altura: 1em; fondo: color actual; alineación vertical: -2px; margen izquierdo: 1px; animación: ravs-caret-blink 1,05s pasos(1) infinito; } @keyframes ravs-caret-blink { 0%, 50% { opacidad: 1 } 51%, 100% { opacidad: 0 } }" + "90cdcd2ecc": ".ravs-ship-root { position: absolute; inset: 0; } .ravs-ship-stack { position: absolute; inset: 0; display: grid; grid-template-columns: 232px minmax(0,1fr); gap: 14px; padding: 4px 2px; /* Why: cards size to their content rather than stretch to the parent's full height, so the two cards don't show empty space below their content. */ align-items: start; } /* Source Control mini-sidebar — ahead-count header, commit textarea + split Commit button, then a CHANGES section with file rows. The file rows are the surface the \"reading\" pulse animates over. */ .ravs-sc-card { display: flex; flex-direction: column; background: var(--card, #fff); border: 1px solid var(--border); border-radius: 10px; overflow: hidden; box-shadow: 0 1px 2px rgba(24,24,27,0.04); } /* Both card headers share the same fixed height so the SC card and PR dialog align across the top edge regardless of header content. */ .ravs-sc-header, .ravs-pr-head { height: 36px; box-sizing: border-box; } .ravs-sc-header { display: flex; align-items: center; justify-content: space-between; padding: 0 10px; border-bottom: 1px solid var(--border); } .ravs-sc-ahead { display: inline-flex; align-items: center; gap: 5px; font-size: 11px; font-weight: 500; color: var(--foreground, #18181b); } .ravs-sc-ahead svg { color: var(--muted-foreground, #71717a); } .ravs-sc-commit-area { display: flex; flex-direction: column; gap: 6px; padding: 8px 10px; } .ravs-sc-textarea { position: relative; border: 1px solid var(--border); border-radius: 6px; background: var(--editor-surface, var(--card)); padding: 6px 26px 6px 8px; min-height: 56px; font-size: 12px; line-height: 1.45; color: var(--foreground, #18181b); white-space: pre-wrap; word-break: break-word; overflow: hidden; } .ravs-sc-textarea .ravs-placeholder { color: rgba(113,113,122,0.7); } .ravs-sc-sparkle { position: absolute; right: 6px; top: 6px; width: 20px; height: 20px; display: inline-flex; align-items: center; justify-content: center; border-radius: 4px; color: var(--muted-foreground, #71717a); background: transparent; transition: color 160ms ease, background 160ms ease; } .ravs-sc-sparkle.is-scanning { color: rgb(109 40 217); background: color-mix(in srgb, rgb(139 92 246) 18%, transparent); } .ravs-sc-split { display: flex; align-items: stretch; } /* Why: Commit + Create PR are surrounding chrome — the violet AI affordances are the focal points. Render them as quiet secondary buttons so they don't compete with the sparkle/scan signals. */ .ravs-sc-split .ravs-primary { flex: 1; display: inline-flex; align-items: center; justify-content: center; gap: 5px; padding: 5px 10px; background: var(--secondary, #f5f5f5); color: var(--secondary-foreground, #171717); font-size: 11px; font-weight: 500; border-radius: 6px 0 0 6px; border: 1px solid var(--border); transition: background 240ms ease, border-color 240ms ease, color 240ms ease; } .ravs-sc-split .ravs-chev { display: inline-flex; align-items: center; justify-content: center; width: 22px; background: var(--secondary, #f5f5f5); color: var(--muted-foreground, #71717a); border-radius: 0 6px 6px 0; border: 1px solid var(--border); border-left: 1px solid var(--border); transition: background 240ms ease, border-color 240ms ease, color 240ms ease; } /* Why: when AI has filled the commit message, tint the Commit button green to signal \"ready to commit\". Uses the same success-green family as the PR flash so the two beats rhyme. Mix is intentionally strong (~28%) — at 14% it disappeared next to the violet sparkle and PR flash, so users only saw the PR change color. */ .ravs-sc-split.is-ready .ravs-primary, .ravs-sc-split.is-ready .ravs-chev { background: color-mix(in srgb, rgb(34 197 94) 28%, var(--secondary, #f5f5f5)); border-color: rgb(34 197 94); color: rgb(21 128 61); transition: background 220ms ease, border-color 220ms ease, color 220ms ease; } .ravs-sc-split.is-ready .ravs-chev { border-left-color: rgba(34, 197, 94, 0.55); } .ravs-sc-changes-header { display: flex; align-items: center; justify-content: space-between; padding: 8px 10px 4px; font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted-foreground, #71717a); } .ravs-sc-changes-count { color: var(--foreground, #18181b); font-weight: 600; margin-left: 2px; } .ravs-sc-view-all { font-size: 10px; font-weight: 500; text-transform: none; letter-spacing: 0; color: var(--muted-foreground, #71717a); } .ravs-sc-files { display: flex; flex-direction: column; padding: 2px 6px 8px; flex: 1; min-height: 0; overflow: hidden; } .ravs-sc-file { display: grid; grid-template-columns: 14px minmax(0,1fr) 12px; align-items: center; gap: 6px; padding: 3px 6px; border-radius: 4px; font-size: 11px; line-height: 1.35; color: var(--foreground, #18181b); position: relative; transition: background 220ms ease; } .ravs-sc-ficon { color: rgb(180 83 9); display: inline-flex; } .ravs-sc-fname { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .ravs-sc-fmark { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 10px; text-align: right; color: rgb(180 83 9); } .ravs-sc-file.is-reading { background: color-mix(in srgb, rgb(139 92 246) 14%, transparent); box-shadow: inset 0 0 0 1px color-mix(in srgb, rgb(139 92 246) 28%, transparent); } /* PR dialog — matches the .ravs-sc-card chrome (same border, radius, elevation) so the two cards read as one design language. */ .ravs-pr-dialog { background: var(--card, #fff); border: 1px solid var(--border); border-radius: 10px; box-shadow: 0 1px 2px rgba(24,24,27,0.04); display: flex; flex-direction: column; min-width: 0; overflow: hidden; } .ravs-pr-head { display: flex; align-items: center; justify-content: space-between; gap: 6px; padding: 0 10px; border-bottom: 1px solid var(--border); } .ravs-pr-title-text { font-size: 11px; font-weight: 500; color: var(--foreground, #18181b); } /* Icon-only AI-assist chip — mirrors .ravs-sc-sparkle so the affordance reads identically across both cards. */ .ravs-pr-gen-btn { display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 22px; padding: 0; border-radius: 4px; color: var(--muted-foreground, #71717a); background: transparent; border: 0; cursor: pointer; transition: color 160ms ease, background 160ms ease; } .ravs-pr-gen-btn:hover { background: rgba(24,24,27,0.06); color: var(--foreground, #18181b); } .ravs-pr-gen-btn.is-scanning { color: rgb(109 40 217); background: color-mix(in srgb, rgb(139 92 246) 18%, transparent); } .ravs-pr-body { display: flex; flex-direction: column; gap: 8px; padding: 10px; } .ravs-pr-field { display: flex; flex-direction: column; gap: 4px; } .ravs-pr-field-label { font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted-foreground, #71717a); } .ravs-pr-base { display: inline-flex; align-items: center; gap: 5px; padding: 4px 9px; border: 1px solid var(--border); border-radius: 6px; font-size: 11px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; color: var(--foreground, #18181b); background: var(--editor-surface, var(--card)); align-self: flex-start; } .ravs-pr-base svg { color: var(--muted-foreground, #71717a); } .ravs-pr-input { position: relative; padding: 6px 8px; border: 1px solid var(--border); border-radius: 6px; background: var(--editor-surface, var(--card)); min-height: 28px; font-size: 12px; line-height: 1.45; color: var(--foreground, #18181b); white-space: pre-wrap; word-break: break-word; overflow: hidden; } .ravs-pr-input.is-body { min-height: 50px; font-size: 11px; line-height: 1.4; } .ravs-pr-input .ravs-placeholder { color: rgba(113,113,122,0.7); } .ravs-pr-footer { display: flex; align-items: center; gap: 6px; justify-content: flex-end; margin-top: 2px; } .ravs-pr-btn { font-size: 11px; font-weight: 500; padding: 5px 10px; border-radius: 6px; line-height: 1; border: 1px solid transparent; outline: none; } .ravs-pr-btn:focus, .ravs-pr-btn:focus-visible { outline: none; } /* Cancel reads as a quiet ghost button so it doesn't compete with the affirmative Create PR action. */ .ravs-pr-btn.is-outline { background: transparent; color: var(--muted-foreground, #71717a); border-color: transparent; } .ravs-pr-btn.is-outline:hover { background: rgba(24,24,27,0.05); color: var(--foreground, #18181b); } /* Quiet secondary fill — see the .ravs-sc-split note above. The flash ring still uses success-green so the \"PR created\" beat reads. The Create-PR button is slightly larger than Cancel so the affirmative action remains the bigger target. */ .ravs-pr-btn.is-solid { background: var(--secondary, #f5f5f5); color: var(--secondary-foreground, #171717); border-color: var(--border); font-size: 12px; padding: 7px 14px; transition: background 220ms ease, border-color 220ms ease, color 220ms ease, box-shadow 220ms ease; } .ravs-pr-btn.is-solid.is-ready { background: color-mix(in srgb, rgb(34 197 94) 28%, var(--secondary, #f5f5f5)); border-color: rgb(34 197 94); color: rgb(21 128 61); } .ravs-pr-btn.is-solid.is-flash { box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.30); } .ravs-cursor { position: absolute; z-index: 40; pointer-events: none; transition: transform 600ms cubic-bezier(.45,.05,.2,1), opacity 200ms ease; transform: translate(-30px, 220px); opacity: 0; } .ravs-cursor.is-visible { opacity: 1; } .ravs-cursor .ravs-ripple { position: absolute; left: -6px; top: -6px; width: 28px; height: 28px; border-radius: 999px; border: 2px solid rgba(24,24,27,0.5); opacity: 0; } .ravs-cursor.is-clicking .ravs-ripple { animation: ravs-ripple 460ms ease-out forwards; } @keyframes ravs-ripple { 0% { transform: scale(0.4); opacity: 0.9; } 100% { transform: scale(1.4); opacity: 0; } } .ravs-caret { display: inline-block; width: 1.5px; height: 1em; background: currentColor; vertical-align: -2px; margin-left: 1px; animation: ravs-caret-blink 1.05s steps(1) infinite; } @keyframes ravs-caret-blink { 0%, 50% { opacity: 1 } 51%, 100% { opacity: 0 } }" } } } @@ -10617,35 +10617,35 @@ "agents": { "orchestration": { "OrchestrationPage": { - "30b509a467": "2 niños", - "862605d066": "2 espacios de trabajo infantiles" + "30b509a467": "2 secundarios", + "862605d066": "2 workspaces secundarios" }, "StatusesPage": { "2f549fc0ba": "src/auth/session.test.ts", "139e3d7458": "Actualizado", - "7b26349cb2": "pnpm migrar más reciente", - "78f0318ac1": "quiere correr", + "7b26349cb2": "pnpm migrate latest", + "78f0318ac1": "Quiere ejecutar", "79971d1539": "rediseñar el flujo de autenticación" }, "UsageAccountsCard": { - "6986b36708": "Límites de tasas superficiales y cuentas swap en línea.", - "d90d2e1f6d": "Seguimiento de la sesión y el uso semanal.", - "8919321417": "Error al iniciar sesión en el Codex.", - "c7b90c140b": "Se agregó la cuenta del Codex.", + "6986b36708": "Muestra límites de tasa y cambia cuentas aquí mismo.", + "d90d2e1f6d": "Controla el uso por sesión y semanal.", + "8919321417": "Error al iniciar sesión en Codex.", + "c7b90c140b": "Cuenta de Codex agregada.", "4e71d72912": "Error al iniciar sesión en Claude.", - "9ddeb558f9": "Se agregó la cuenta de Claude.", + "9ddeb558f9": "Cuenta de Claude agregada.", "29d0653961": "Iniciar sesión", - "945865332e": "Iniciar sesión" + "945865332e": "Iniciando sesión" }, "UsagePage": { "64265cb295": "71% 5h", "be5a165875": "Cambiar a", - "277a9c65a9": "Cuenta del Codex", - "4dce5ca3aa": "Se reinicia en 4d 3h", + "277a9c65a9": "Cuenta de Codex", + "4dce5ca3aa": "Se restablece en 4d 3h", "05ce4ecdd3": "62% restante", - "0470aaed99": "Semanalmente", + "0470aaed99": "Semanal", "f421abf962": "Sesión", - "5e45fb1238": "Actualizado hace 1m", + "5e45fb1238": "Actualizado hace 1 min", "6a4b1d3c38": "Codex" }, "orchestration": { @@ -10666,28 +10666,28 @@ }, "FeatureWallBrowserAction": { "5022c43a88": "El navegador no pudo abrir", - "c9eb68b474": "Aún no hay ningún grupo de espacio de trabajo disponible para este árbol de trabajo.", + "c9eb68b474": "Aún no hay ningún grupo de workspace disponible para este worktree.", "c9728107c5": "Pruébalo", "25dd101f15": "La configuración del navegador necesita atención", "e02b11e6b0": "Configuración del navegador lista", - "d6d15077df": "Comando de habilidad copiado e insertado a continuación para su revisión.", + "d6d15077df": "Comando de skill copiado e insertado abajo para revisarlo.", "78e65f19d9": "La configuración del navegador falló", "b7345c18db": "Se produjo un error inesperado.", "5f97caf76b": "Instalando…", - "c2df599513": "Instalar CLI y habilidad" + "c2df599513": "Instalar CLI y skill" }, "ConnectIntegrationsList": { - "3dddb2d565": "connected for tasks", - "33b650af52": "Connect where your team tracks work. Orca starts workspaces with the issue title, link, and context already attached.", - "5b3577a492": "connected for review status", - "list_end": ", and ", - "list_pair": " and ", + "3dddb2d565": "conectado para tareas", + "33b650af52": "Conecta donde tu equipo hace seguimiento del trabajo. Orca inicia workspaces con el título del issue, el enlace y el contexto ya adjuntos.", + "5b3577a492": "conectado para estado de revisión", + "list_end": " y ", + "list_pair": " y ", "list_mid": ", ", - "code_host_tasks_summary": "issues available as tasks · add Linear or Jira if your team plans work there", - "code_host_tasks_caption": "Your code host's issues also work as tasks.", - "review_step_title": "See PR status while agents work", - "review_step_description": "Connect a review provider so Orca can show PR or MR status, checks, and reviews.", - "task_step_title": "Start agents on your tasks without leaving Orca" + "code_host_tasks_summary": "issues disponibles como tareas · agrega Linear o Jira si tu equipo planifica el trabajo allí", + "code_host_tasks_caption": "Los issues de tu proveedor de código también funcionan como tareas.", + "review_step_title": "Ver el estado de PR mientras trabajan los agentes", + "review_step_description": "Conecta un proveedor de revisión para que Orca pueda mostrar el estado de PR o MR, checks y revisiones.", + "task_step_title": "Iniciar agentes en tus tareas sin salir de Orca" }, "connect": { "integration": { @@ -10701,14 +10701,14 @@ }, "FullDiskAccessSetupPrompt": { "bbb3f1e404": "Comprobando", - "48d87edcd2": "Concedido", + "48d87edcd2": "Otorgado", "6db9a69f4e": "Recomendado", "fa809e8ada": "Privacidad y seguridad de macOS abiertas", "bfa3402305": "No se pudo solicitar permiso", - "c566bca278": "Acceso completo al disco", + "c566bca278": "Acceso total al disco", "0d6efe9cf4": "Recomendado en macOS cuando los proyectos o worktrees están en carpetas protegidas.", "dac08ec03e": "Abriendo...", - "6e3d62b816": "Abrir Acceso completo al disco" + "6e3d62b816": "Abrir Acceso total al disco" } }, "tips": { @@ -10717,17 +10717,17 @@ "22e62f3bab": "Sesión de Claude Code iniciada" }, "CliSkillSetupTerminal": { - "1953e90447": "Presione Entrar para instalar la habilidad de orquestación CLI de Orca para sus agents.", - "43b60ec5c3": "Terminal de instalación de habilidades de orquestación y CLI de Orca", - "84e9576dac": "Configuración de habilidades", - "5c3aee22c0": "comando copiar", - "5eca672aac": "Copiar comando de instalación de habilidad", - "6ff813fc1d": "No se pudo copiar el comando de habilidad.", - "b8ad063571": "Copié el comando de instalación de habilidades." + "1953e90447": "Presiona Enter para instalar el skill de orquestación de la CLI de Orca para tus agentes.", + "43b60ec5c3": "Terminal de instalación de CLI de Orca y skill de orquestación", + "84e9576dac": "Configuración de skill", + "5c3aee22c0": "Copiar comando", + "5eca672aac": "Copiar comando de instalación de skill", + "6ff813fc1d": "No se pudo copiar el comando de skill.", + "b8ad063571": "Comando de instalación de skill copiado." }, "CmdJPaletteTipDialog": { "c0bb9f869b": "Configuración → Atajos", - "8241897205": "Vuelva a vincular el acceso directo en cualquier momento en" + "8241897205": "Puedes reasignar el atajo en cualquier momento en" }, "FeatureTipActions": { "eb04abece8": "Quizás más tarde" @@ -10735,21 +10735,21 @@ "FeatureTipsModal": { "c169298e4d": "Hecho", "3c6c478462": "X termina, envíale la tarea de revisión”.", - "298301b7a0": "árbol de trabajo", + "298301b7a0": "worktree", "864e2db28f": "“Cuando el agente en", "7fc6f02099": "y crear PR para cada uno”.", - "27c567a89c": "árboles de trabajo", + "27c567a89c": "worktrees", "55846c7f95": "“Divide este PR en dos", - "4795ac2d4a": "Intenta preguntar:", - "53905bd076": "Vista previa del desarrollo: apertura del terminal de configuración de habilidades.", + "4795ac2d4a": "Prueba a pedir:", + "53905bd076": "Vista previa de desarrollo: abriendo la terminal de configuración de skills.", "1da82af45b": "Orca CLI necesita atención", - "ce13a742d0": "Orca registrada en PATH.", - "d1a86c7eb5": "Abra Configuración para finalizar la configuración de CLI." + "ce13a742d0": "`orca` registrado en PATH.", + "d1a86c7eb5": "Abre Configuración para finalizar la configuración de la CLI." }, "CmdJPaletteFeatureTipVisual": { - "ab94e16d44": "Crear árbol de trabajo \"{{value0}}\"", + "ab94e16d44": "Crear worktree \"{{value0}}\"", "d20ccf1e61": "hecho", - "379d776971": "mecanografía", + "379d776971": "escribiendo", "0418f9becc": "abierto" } } @@ -10757,9 +10757,9 @@ "error": { "boundaries": { "RecoverableRenderErrorBoundary": { - "55001880db": "Rever", - "34a189ae0f": "El resto de la aplicación sigue ejecutándose. Vuelva a intentar esta superficie o cambie y regrese.", - "ab855c11f4": "Esta parte de Orca tuvo un error." + "55001880db": "Reintentar", + "34a189ae0f": "El resto de la app sigue ejecutándose. Reintenta esta vista o cambia a otra y vuelve.", + "ab855c11f4": "Esta parte de Orca encontró un error." } } }, @@ -10772,18 +10772,18 @@ "device": { "frame": { "9406c15775": "Pantalla del emulador", - "8f25ffaf8a": "Pantalla del emulador, teclado capturado. Presione Escape para liberar.", + "8f25ffaf8a": "Pantalla del emulador, teclado capturado. Presiona Escape para liberar.", "0022420df0": "teléfono" } }, "pane": { "toolbar": { "06e10d7356": "Apagar el emulador", - "e7a0d1897e": "Hogar", + "e7a0d1897e": "Inicio", "6bd8dff42a": "Girar", - "3d836b879c": "Elige emulador", + "3d836b879c": "Elegir emulador", "81b3571a07": "Conectar", - "868c0f2938": "Laboral…" + "868c0f2938": "Procesando…" } }, "screen": { @@ -10798,7 +10798,7 @@ }, "unavailable": { "pane": { - "f630b9ca9f": "Mobile Emulator requiere una Mac con Xcode y el tiempo de ejecución del simulador de iOS. En Linux o Windows, utilice un dispositivo físico o un host de compilación Mac remoto.", + "f630b9ca9f": "Mobile Emulator requiere una Mac con Xcode y el runtime de iOS Simulator. En Linux o Windows, usa un dispositivo físico o un host de compilación Mac remoto.", "b2c268a0b9": "Mobile Emulator es solo macOS" } } @@ -10816,17 +10816,17 @@ "agent": { "setup": { "state": { - "fdcca1ec75": "Registrando...", + "fdcca1ec75": "Registrando…", "69fb2c2289": "Habilitado", "c6705092ba": "Corregir PATH", "7c1b6bdb1e": "Habilitar", - "51074ccb05": "Error al cargar el estado del CLI.", - "35dea1ae12": "El control del agent está listo.", - "9dff3a6338": "La skill está instalada. Habilita el CLI de Orca para completar la configuración.", - "15986a1080": "El CLI de Orca está listo. Instala la skill para completar la configuración.", - "4c26913def": "Aún no está configurado. Completa ambos pasos para habilitar el control del agent.", + "51074ccb05": "No se pudo cargar el estado de la CLI.", + "35dea1ae12": "El control de agentes está listo.", + "9dff3a6338": "La skill está instalada. Habilita la CLI de Orca para completar la configuración.", + "15986a1080": "La CLI de Orca está lista. Instala la skill para completar la configuración.", + "4c26913def": "Aún no está configurado. Completa ambos pasos para habilitar el control de agentes.", "c94ff11e91": "No se pudo volver a verificar el estado de la configuración.", - "2b519eed94": "Se registró el CLI de Orca en PATH." + "2b519eed94": "Se registró la CLI de Orca en PATH." } } }, @@ -10841,10 +10841,10 @@ } }, "MobileEmulatorAgentSetupGuide": { - "2fda9ff015": "Configurar control del agent", - "0ac0fef514": "El control del agent está listo.", - "2bdfff8763": "Control del agent (opcional).", - "72736b051f": "Configura el CLI de Orca + skill cuando quieras que los agents controlen este simulador.", + "2fda9ff015": "Configurar control de agentes", + "0ac0fef514": "El control de agentes está listo.", + "2bdfff8763": "Control de agentes (opcional).", + "72736b051f": "Configura la CLI de Orca + skill cuando quieras que los agentes controlen este simulador.", "d10ae98046": "Listo", "3756cbeca7": "Ahora no", "6d950431d2": "Ocultar", @@ -10852,19 +10852,19 @@ "3f003507f4": "Abrir configuración completa en Ajustes" }, "MobileEmulatorAgentSetupGuideSteps": { - "9b49d892e3": "Habilitar CLI de Orca", - "3d8dc52c93": "Registra el comando orca para control del emulador en los shells de los agents.", + "9b49d892e3": "Habilitar la CLI de Orca", + "3d8dc52c93": "Registra el comando orca para controlar el emulador en los shells de los agentes.", "21f5687c07": "Skill del CLI de Orca", - "64fb057667": "Enseña a los agents los comandos del emulador orca para este worktree.", - "5c59ea96ca": "Mobile emulator Orca CLI skill setup", - "bff5341ac3": "Mobile emulator Orca CLI skill install terminal", + "64fb057667": "Enseña a los agentes los comandos orca del emulador para este worktree.", + "5c59ea96ca": "Configuración de la skill de Orca CLI para Mobile Emulator", + "bff5341ac3": "Terminal de instalación de la skill de Orca CLI para Mobile Emulator", "3d34423e88": "Registrando la CLI de Orca", - "3be27641c9": "para que los comandos del emulador se puedan ejecutar desde shells de agents.", - "3941719a56": "Comprobando la CLI de Orca antes de abrir la configuración de la skill." + "3be27641c9": "para que los comandos del emulador se puedan ejecutar desde shells de agentes.", + "3941719a56": "Verificando la CLI de Orca antes de abrir la configuración de la skill." }, "MobileEmulatorTabIntroCallout": { "1924982130": "Descartar", - "5789936d9a": "Vista previa de simuladores iOS mientras los agents controlan la pantalla.", + "5789936d9a": "Vista previa de simuladores de iOS mientras los agentes controlan la pantalla.", "8014b4b80b": "Mantener", "6e051a40b7": "Ocultar" }, @@ -10872,29 +10872,29 @@ "emulator": { "hidden": { "toast": { - "e8f098a870": "Emulador Móvil oculto", - "c46c979c1d": "Vuelve a habilitar el Emulador Móvil en cualquier momento en", - "600f9a745a": "Ajustes › Emulador Móvil" + "e8f098a870": "Mobile Emulator oculto", + "c46c979c1d": "Vuelve a habilitar Mobile Emulator en cualquier momento en", + "600f9a745a": "Ajustes › Mobile Emulator" } } } }, "useEmulatorScreenKeyboard": { - "pasteTooLarge": "Paste is too large for emulator keyboard input.", - "unsupportedPasteText": "Emulator keyboard paste supports US keyboard text only.", - "pasteTargetUnavailable": "Emulator keyboard paste failed because the device is not ready." + "pasteTooLarge": "El texto pegado es demasiado grande para la entrada de teclado del emulador.", + "unsupportedPasteText": "Pegar en el teclado del emulador solo admite texto de teclado de EE. UU.", + "pasteTargetUnavailable": "No se pudo pegar en el teclado del emulador porque el dispositivo no está listo." } } }, "editor": { "ChangesModeView": { "ef25ae2d09": "No hay cambios no confirmados.", - "052c184f24": "La diferenciación de texto no está disponible para este archivo.", - "7dffb0f563": "archivo binario", - "54e0035b15": "Cargando diferencias..." + "052c184f24": "El diff de texto no está disponible para este archivo.", + "7dffb0f563": "Archivo binario", + "54e0035b15": "Cargando diff…" }, "CodeBlockCopyButton": { - "28921f5bf9": "copiado", + "28921f5bf9": "Copiado", "1f9f4def45": "Copiar código" }, "CombinedDiffFileTree": { @@ -10902,87 +10902,87 @@ "eafe1aeb53": "Restablecer filtros", "be119cb9d1": "Archivos vistos", "c00020f081": "Extensiones de archivo", - "cd0e0ed79e": "Filtrar archivos de diferencias", + "cd0e0ed79e": "Filtrar archivos del diff", "4cc7b83ffe": "Filtrar archivos...", "21783df79f": "Contraer árbol de archivos", "481e63ca52": "Archivos", - "d5ac717d65": "no comprometido", - "39b6b9e4e4": "Committed on Branch" + "d5ac717d65": "sin commit", + "39b6b9e4e4": "Commits en la rama" }, "CombinedDiffViewer": { - "35cc27aeb2": "en control de fuente", + "35cc27aeb2": "en Control de código fuente", "e3b9a6ce02": "más", "1da745c551": "Enviado", - "84898c548d": "Claro", + "84898c548d": "Borrar ", "88b70d0ef5": "Copiar", - "bb84b4c374": "notas de IA", + "bb84b4c374": "Notas de IA", "948a5fd6c8": "Borrar notas", "0f806a2ab1": "Cancelar", - "80a286d8f5": "de este árbol de trabajo?", + "80a286d8f5": "de este worktree?", "7e7ca60816": "archivos cambiados", "b6c3b84476": "Mostrar árbol de archivos", "39f8007549": "Revisar conflictos", - "39e73e7181": "fueron excluidos de esta vista diferente.", + "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 diferencias excluye los conflictos no resueltos porque la canalización de diferencias bidireccional normal no es segura para conflictos.", + "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.", "45cf23b418": "No se pudieron borrar las notas.", "0fb870a0fe": "notas", "8ab3248fd8": "nota", - "ec5053c7f5": "Juntos", + "ec5053c7f5": "Lado a lado", "f786fd54e1": "En línea", "ea08dae15b": "Contraer todo", "19c45cfdc0": "Expandir todo", - "982d14bfa5": "Abrir diferencia no confirmada", - "3d909843bb": "Diferencia de rama abierta", + "982d14bfa5": "Abrir todos los cambios", + "3d909843bb": "Abrir diff de rama", "8368d256ec": "rama combinada", "8f68ad9ca9": "Mostrar {{value0}} AI {{value1}}", "724a13568d": "en {{value0}}", "6094135eec": "frente a {{value0}}", - "a4420ca1f7": "Envolver", - "dde325ddfe": "Envolver" + "a4420ca1f7": "Ajuste activado", + "dde325ddfe": "Ajuste desactivado" }, "ConflictComponents": { - "f338288514": "Cargando contenidos conflictivos...", - "90d576adb2": "Refrescar", - "a1ce36f77d": "Instantánea capturada en", + "f338288514": "Cargando contenido del conflicto…", + "90d576adb2": "Actualizar", + "a1ce36f77d": "Instantánea capturada a las ", "4be41eaafc": "conflicto no resuelto", "c8ca989aea": "Mostrar árbol de archivos", - "58ad5ad431": "Despedir", - "28e7db4a90": "Control de fuente", + "58ad5ad431": "Descartar", + "28e7db4a90": "Control de código fuente", "31931dec46": "Esta instantánea de revisión ya no tiene conflictos activos sin resolver.", "992145ff5a": "Todos los conflictos resueltos", - "d5edd81755": "Renombrado de", - "6e459867ad": "Estado de continuidad local de sesión. Git ya no informa que este archivo no está fusionado.", - "9c2901ef8a": "Próximo conflicto", + "d5edd81755": "Renombrado desde ", + "6e459867ad": "Estado de continuidad local de la sesión. Git ya no informa que este archivo está sin merge.", + "9c2901ef8a": "Siguiente conflicto", "41d9af2e7a": "Conflicto anterior", "55d61a0ccd": "conflicto ·", - "da539359b6": "No hay ningún archivo de árbol de trabajo disponible para editar para este conflicto." + "da539359b6": "No hay ningún archivo del worktree disponible para editar este conflicto." }, "ConflictReviewFileTree": { "3449521a8c": "No hay conflictos en esta instantánea.", "a54551c5a6": "Contraer árbol de archivos", "99496bab6e": "Archivos", - "496e28a932": "Desaparecido", + "496e28a932": "No existe", "8528a5eaf5": "Resuelto", - "69d4e210bb": "Irresoluto" + "69d4e210bb": "Sin resolver" }, "CsvViewer": { "eedd0d37a7": "columnas", "ac31d2cd60": "filas", - "a233d55b77": "archivo vacio" + "a233d55b77": "Archivo vacío" }, "DiffNotesSendMenu": { - "f1aa04b5cf": "este archivo", + "f1aa04b5cf": "Este archivo", "8b87612461": "Todas las notas no enviadas" }, "DiffSectionBody": { "35d6afb5be": "Archivo binario cambiado", - "cef4cf0ff5": "Rever", - "f5cf81cec2": "Cargando diferencias...", - "72f71f52eb": "La diferenciación de texto no está disponible para este archivo.", - "7ce8436458": "La diferencia de texto no está disponible para este archivo en la comparación de ramas.", + "cef4cf0ff5": "Reintentar", + "f5cf81cec2": "Cargando diff…", + "72f71f52eb": "El diff de texto no está disponible para este archivo.", + "7ce8436458": "El diff de texto no está disponible para este archivo en la comparación de ramas.", "bdbf02d5df": "binario", "b5675b0694": "Guardar", "593f2193f6": "Este borrador superó el límite seguro de visualización, pero aún se puede guardar." @@ -10992,58 +10992,58 @@ }, "EditorContent": { "56dba34e1a": "(editar en modo fuente)", - "e4b074749d": "Asunto frontal", - "9640d1d3db": "Vista previa de la versión modificada de esta diferencia. Cambie al modo fuente para inspeccionar los cambios.", + "e4b074749d": "Front matter", + "9640d1d3db": "Vista previa de la versión modificada de este diff. Cambia al modo de código fuente para inspeccionar los cambios.", "78541e254e": "Archivo binario cambiado", - "c88c73a0d3": "Cargando diferencias...", - "b9de81ba52": "Archivo binario: no se puede mostrar", - "b2735221f5": "Cargando...", + "c88c73a0d3": "Cargando diff…", + "b9de81ba52": "Archivo binario — no se puede mostrar", + "b2735221f5": "Cargando…", "8608ce4cb1": "La vista previa de Markdown no está disponible para archivos binarios.", - "37a0e81fa6": "Cargando vista previa...", - "8b1a605bae": "Este archivo se encuentra en estado de conflicto, pero no hay ningún archivo de árbol de trabajo disponible para editar.", - "2a512bb46a": "Rever", + "37a0e81fa6": "Cargando vista previa…", + "8b1a605bae": "Este archivo está en estado de conflicto, pero no hay ningún archivo del worktree disponible para editar.", + "2a512bb46a": "Reintentar", "39f018b052": "No se puede cargar el archivo", - "8a0898ae4c": "La diferenciación de texto no está disponible para este archivo.", - "3c6e71df22": "La diferencia de texto no está disponible para este archivo en la comparación de ramas.", + "8a0898ae4c": "El diff de texto no está disponible para este archivo.", + "3c6e71df22": "El diff de texto no está disponible para este archivo en la comparación de ramas.", "d07e4b8553": "rama", "d16e037f40": "rico", - "6c4f1a8d2e": "Los detalles de la comprobación no están disponibles." + "6c4f1a8d2e": "Los detalles del check no están disponibles." }, "EditorPanelHeader": { "fb8331694e": "Abrir vista previa al lado", - "4157f3cbf3": "Abrir vista previa de Markdowns", + "4157f3cbf3": "Abrir vista previa de Markdown", "269ce4842b": "Copiar ruta relativa", "7c08a1f990": "Copiar ruta", - "84cdc0794b": "Rebautizar", + "84cdc0794b": "Renombrar", "1bb1e226ec": "Cambiar el nombre del archivo {{value0}}", "5447c4f68f": "Tabla de contenido", "146cb5473c": "La tabla de contenidos está disponible en modo enriquecido o de vista previa.", - "e836faacfa": "Cambiar a diferenciación lado a lado", - "94756f08ba": "Cambiar a diferencia en línea", - "c98ce191da": "Esta diferenciación no tiene ningún archivo del lado modificado para abrir", + "e836faacfa": "Cambiar a diff lado a lado", + "94756f08ba": "Cambiar a diff inline", + "c98ce191da": "Este diff no tiene ningún archivo del lado modificado para abrir", "9b80bbe1de": "Abrir pestaña de archivo", - "f0fd4174b5": "Abra la pestaña de archivo para utilizar la edición de markdowns enriquecida", + "f0fd4174b5": "Abre la pestaña de archivo para usar la edición enriquecida de Markdown", "a10d9b8337": "Abrir archivo" }, "EditorPanelMarkdownActionsMenu": { "3e0ce48c24": "Exportar como PDF", "561251019a": "Más acciones", - "8c8b7f5ff5": "Mostrar portada", - "10c39d58c1": "Ocultar materia preliminar", - "1eef809708": "Ajuste de palabras" + "8c8b7f5ff5": "Mostrar front matter", + "10c39d58c1": "Ocultar front matter", + "1eef809708": "Ajuste de línea" }, "EditorPanelShell": { - "e2c4dec350": "Cargando editor..." + "e2c4dec350": "Cargando editor…" }, "EditorViewToggle": { - "b3410cd5e0": "Computadora portátil", - "e408aa9cd5": "Mesa", + "b3410cd5e0": "Notebook", + "e408aa9cd5": "Tabla", "167f45888c": "Cambios no confirmados", "4837f3f578": "Cambios", "ac3bb87913": "Editar", - "0d193dc03c": "Avance", - "aff15f94f5": "Editor rico", - "4d6ccb7ba6": "Fuente" + "0d193dc03c": "Vista previa", + "aff15f94f5": "Editor enriquecido", + "4d6ccb7ba6": "Código fuente" }, "ImageDiffViewer": { "a651be62b0": "Modificado", @@ -11051,38 +11051,38 @@ "fb0ae4f3c0": "Sin vista previa" }, "ImageViewer": { - "3c9217f5a6": "Dar un golpe de zoom", + "3c9217f5a6": "Acercar", "6c89c73d9f": "Restablecer zoom", - "be27304574": "alejar", + "be27304574": "Alejar", "77bfc9b35a": "Abrir imagen en ventana emergente", "3ef9551ba2": "Cargando vista previa...", "d9d2944855": "No se pudo cargar la vista previa del archivo" }, "ImageViewerPopup": { "0ef78475e7": "Presione Esc para cerrar", - "535f4e2b56": "Cerca", + "535f4e2b56": "Cerrar", "9e27b2ecaf": "Vista previa de imagen a tamaño completo" }, "IpynbViewer": { "859bf9fc21": "Ejecutar celda", "7f0d7077c6": "Cancelar", - "10ed04a685": "Las celdas del cuaderno ejecutan Python local en esta máquina desde la carpeta del cuaderno. Ejecute únicamente celdas de archivos en los que confíe.", - "9e06ae5d36": "¿Ejecutar el código del cuaderno?", - "d6f37a640b": "cuaderno vacio", - "8c3b21369a": "formato nb", + "10ed04a685": "Las celdas del notebook ejecutan Python localmente en esta máquina desde la carpeta del notebook. Ejecuta solo celdas de archivos en los que confíes.", + "9e06ae5d36": "¿Ejecutar código del notebook?", + "d6f37a640b": "Notebook vacío", + "8c3b21369a": "nbformat", "329764e9fc": "BETA", - "15ec40a735": "guardar cuaderno", - "07e7d96612": "células", + "15ec40a735": "Guardar notebook", + "07e7d96612": "celdas", "c1601b23b2": "No se puede renderizar el cuaderno", - "66a3f7d330": "Salida HTML del cuaderno", + "66a3f7d330": "Salida HTML del notebook", "781abd6926": "Eliminar celda", - "b42f6a9547": "Insertar celda de markdowns a continuación", - "ffc1ac2699": "Insertar celda de markdowns arriba", - "b4208cad7e": "Inserte la celda de código a continuación", - "53b839b8a0": "Insertar celda de código arriba", + "b42f6a9547": "Insertar celda de Markdown debajo", + "ffc1ac2699": "Insertar celda de Markdown encima", + "b4208cad7e": "Insertar celda de código debajo", + "53b839b8a0": "Insertar celda de código encima", "27e064e2db": "Mover celda hacia abajo", "fd8ac707bc": "Mover celda hacia arriba", - "3e4cbf15ea": "Crudo", + "3e4cbf15ea": "Raw", "1833dbbc43": "Markdown", "7005960d73": "Código", "59b6cd874b": "código", @@ -11092,28 +11092,28 @@ "e4683f70c4": "Cancelar", "d737791433": "Agregar nota para la IA", "b1bfc04034": "Texto seleccionado", - "f37b98999e": "esta nota", - "2b2b31382c": "Asunto frontal", + "f37b98999e": "Esta nota", + "2b2b31382c": "Front matter", "bb629de58a": "Copiar notas para el agente", "322afab6ff": "Notas de revisión", "0f9969a159": "Saltar a la primera nota de revisión", "12052c639c": "Cerrar búsqueda", - "b42c41bd0d": "Próximo partido", - "1febd97f5c": "Partido anterior", - "ec77985138": "Buscar en vista previa de markdowns", + "b42c41bd0d": "Coincidencia siguiente", + "1febd97f5c": "Coincidencia anterior", + "ec77985138": "Buscar en vista previa de Markdown", "517aea303b": "Buscar en vista previa", "f961e94057": "Copiar nota para el agente", - "94b520a96a": "nota copiada", + "94b520a96a": "Nota copiada", "13f94d760c": "Agregar nota", "ddf087d12e": "Todas las notas no enviadas", - "d652c87c91": "Ahorro…", + "d652c87c91": "Guardando…", "c5dc92cfe3": "Sin resultados", "6c043947ae": "Archivo no encontrado: {{value0}}", "759463a221": "No se puede abrir el directorio: {{value0}}" }, "MarkdownTableOfContentsPanel": { "de3928b6e4": "Sin encabezados", - "bbe8369097": "Cerrar tabla de contenidos", + "bbe8369097": "Cerrar tabla de contenido", "4680a4b808": "Contraer a H{{value0}}", "a5daadd68b": "Expandir todo", "111e66b85d": "Contraer al nivel de título {{value0}}", @@ -11123,15 +11123,15 @@ "27d0a9c49a": "Tabla de contenido", "65b036a6c8": "Expandir {{value0}}", "97ad46f11f": "Contraer {{value0}}", - "8f4d2c1a9b": "Resize table of contents" + "8f4d2c1a9b": "Redimensionar tabla de contenido" }, "MarkdownTemplatePicker": { "22cd94426f": "sin título.md", "6e2e6c04ad": "Markdown en blanco", - "df667919ca": "No hay plantillas coincidentes.", + "df667919ca": "No hay plantillas que coincidan.", "22fd4890ad": "Buscar plantillas...", "7b458e0b7f": "Elija una plantilla de Markdown.", - "1829437fce": "Nueva Markdown" + "1829437fce": "Nuevo Markdown" }, "MermaidBlock": { "dcc132e691": "Error de diagrama:" @@ -11143,17 +11143,17 @@ }, "MonacoGutterContextMenu": { "7b57b1b468": "Copiar URL remota", - "2e0b1cdc05": "Copiar Rel. Camino a la línea", - "4eaa991bde": "Copiar ruta a línea" + "2e0b1cdc05": "Copiar ruta relativa a la línea", + "4eaa991bde": "Copiar ruta a la línea" }, "NotesSendMenu": { - "44dc5e60a6": "enviar notas", + "44dc5e60a6": "Enviar notas", "433928cd9f": "Enviar {{value0}} a un agente" }, "PdfFind": { - "cd65b1d6b0": "Cerca", - "eeba2547a1": "Próximo partido", - "30de726ad0": "Partido anterior", + "cd65b1d6b0": "Cerrar", + "eeba2547a1": "Coincidencia siguiente", + "30de726ad0": "Coincidencia anterior", "2fc3ba0ea8": "Buscar en la página...", "d080ab37d6": "No hay coincidencias", "db56fcd6d2": "{{value0}} de {{value1}}" @@ -11161,14 +11161,14 @@ "PdfViewer": { "3e98d500d2": "Vista previa de PDF", "069ff59932": "Buscar en PDF ({{value0}})", - "2b6eb1ccd6": "Dar un golpe de zoom", + "2b6eb1ccd6": "Acercar", "c0119616d6": "Ajustar al ancho", - "fa5d096b00": "alejar" + "fa5d096b00": "Alejar" }, "ReviewNotesSendMenuContent": { "a49800405b": "Nuevo agente", "e84705f223": "Sesión de agente activo", - "03378aea75": "enviar notas a", + "03378aea75": "Enviar notas a", "f5096c6e4e": "No se pudieron enviar notas al agente activo.", "bb9c69a0c9": "Notas enviadas al agente activo.", "50f7e753ea": "Enviando notas al agente activo..." @@ -11178,56 +11178,56 @@ "6f2f3a6001": "Agregar nota de revisión" }, "RichMarkdownCodeBlock": { - "232d9ed853": "copiado", + "232d9ed853": "Copiado", "c72beafc0f": "Copiar código", "74eab1d9b2": "YAML", "5ef5605cb7": "XML", - "88d777bc07": "Mecanografiado", - "9e384d48dc": "Rápido", + "88d777bc07": "TypeScript", + "9e384d48dc": "Swift", "3009f722b9": "SQL", - "d01f55be57": "Caparazón", + "d01f55be57": "Shell", "5af8251002": "SCSS", - "e72e6b03f4": "Óxido", - "96182a2f64": "Rubí", - "2391f9cda9": "Pitón", - "89d6cc14fb": "Sirena", + "e72e6b03f4": "Rust", + "96182a2f64": "Ruby", + "2391f9cda9": "Python", + "89d6cc14fb": "Mermaid", "983b9576b4": "Markdown", "bcb236e2d8": "Kotlin", "78eba32de4": "JSON", - "a209c57063": "javascript", + "a209c57063": "JavaScript", "36536ad539": "Java", "8c4a3fa02d": "HTML", - "706fd85738": "GrafoQL", - "edfcc64182": "Ir", - "bf6ee5caaa": "diferencia", + "706fd85738": "GraphQL", + "edfcc64182": "Go", + "bf6ee5caaa": "Diff", "026653f21f": "CSS", "4daed43ae3": "C++", - "4227cf50fe": "Intento", + "4227cf50fe": "Bash", "13822cdfda": "Texto sin formato" }, "RichMarkdownDocLinkMenu": { "e17b987473": "↑↓ navegar  ↵ seleccionar  esc descartar", "90c5f0e1e4": "de", - "2aaf7d9678": "Demostración", + "2aaf7d9678": "Mostrando", "63ced7cb9b": "No se encontraron documentos", - "0e8489bc11": "Enlaces de documentos de Markdowns", + "0e8489bc11": "Enlaces a documentos Markdown", "142a7d51cd": "documento" }, "RichMarkdownErrorBoundary": { - "aad0998127": "Rever", + "aad0998127": "Reintentar", "4a5de9f2f0": "Cambie al modo fuente o haga clic en Reintentar para recargar la vista enriquecida.", - "dfdf1cacd4": "El editor de markdowns enriquecido sufrió un error inesperado y se restableció para que el resto de Orca siguiera respondiendo." + "dfdf1cacd4": "El editor Markdown enriquecido encontró un error inesperado y se restableció para que el resto de Orca siguiera respondiendo." }, "RichMarkdownLinkBubble": { "1c99b726e0": "Quitar enlace", "cdfe166f6f": "Editar enlace", "bfc813e909": "Abrir enlace", - "7b0b945fdc": "Pegue o escriba un enlace..." + "7b0b945fdc": "Pega o escribe un enlace…" }, "RichMarkdownReviewNoteLayer": { - "f3ef92952b": "esta nota", + "f3ef92952b": "Esta nota", "9cde7ad994": "Copiar nota para el agente", - "117432e2c6": "nota copiada", + "117432e2c6": "Nota copiada", "3ababd949d": "Notas de revisión" }, "RichMarkdownReviewRailActions": { @@ -11279,7 +11279,7 @@ "d1bbf9a835": "Sección plegable" }, "UntitledFileRenameDialog": { - "a7dd27b0bc": "Ahorrar", + "a7dd27b0bc": "Guardar", "949711deb4": "Cancelar", "725868c75d": "Explorar carpetas", "5e7f0d8a80": "Selector de carpetas no disponible para archivos remotos", @@ -11368,7 +11368,7 @@ }, "useLocalImagePick": { "175cb8b8ce": "No se pudo insertar la imagen.", - "91d835dc88": "Ruta del árbol de trabajo no disponible." + "91d835dc88": "Ruta del worktree no disponible." }, "useRichMarkdownReviewData": { "f9d2acd6b0": "Todas las notas no enviadas" @@ -11391,26 +11391,26 @@ "593f2193f6": "Este borrador superó el límite seguro de visualización, pero aún se puede guardar." }, "CheckRunDetailsPanel": { - "8f2d0f5a91": "Passed", - "4c8e1b2d73": "Failed", + "8f2d0f5a91": "Aprobado", + "4c8e1b2d73": "Falló", "91a4c7e2b0": "Cancelado", "2f6d8a1c45": "Tiempo agotado", "7b3e9d4f12": "Omitido", "5a1c8e3d67": "Neutral", - "3d9f2b8e14": "Pending", - "b7f5e2c91a": "Refresh", + "3d9f2b8e14": "Pendiente", + "b7f5e2c91a": "Actualizar", "a54ae21c6f": "Estado:", "fd46a70f1a": "Iniciado", - "00e1c1658a": "Completed", + "00e1c1658a": "Completado", "aa8494ae3c": "check #", "2dd5ddabc4": "workflow #", "1f2b980522": "Cargando detalles de la comprobación…", "d098e5529a": "Salida", "f2fe8a4e8f": "Anotaciones", - "cdbfda4dec": "Annotation", + "cdbfda4dec": "Anotación", "066fedd446": "Tareas fallidas", "49731703ea": "Tareas", - "ee07b33924": "unknown", + "ee07b33924": "desconocido", "07eccfa397": "No hay detalles disponibles para esta comprobación.", "a916648574": "Abrir detalles", "834cb3f23d": "Corregir con IA", @@ -11428,11 +11428,11 @@ "fix": { "with": { "ai": { - "1a8c4e2b90": "Select a workspace before launching an AI action.", - "4f2d9a8c17": "Select a repository before launching an AI action.", - "7c3e1b5d42": "Open a PR or MR before launching an AI fix.", - "9b2f6d4a81": "This check is not failing.", - "2ef90c9819": "Started an AI agent for this check." + "1a8c4e2b90": "Selecciona un workspace antes de lanzar una acción de IA.", + "4f2d9a8c17": "Selecciona un repositorio antes de lanzar una acción de IA.", + "7c3e1b5d42": "Abre un PR o MR antes de lanzar un fix con IA.", + "9b2f6d4a81": "Este check no está fallando.", + "2ef90c9819": "Se inició un agente de IA para este check." } } } @@ -11453,8 +11453,8 @@ "diff": { "comments": { "DiffCommentCard": { - "109a791e7b": "Ahorrar", - "bb0a55f856": "Ahorro…", + "109a791e7b": "Guardar", + "bb0a55f856": "Guardando…", "0203bed775": "Cancelar", "6978871a3d": "Abierto", "cce596969e": "Eliminar nota", @@ -11508,16 +11508,16 @@ "88fea8e84e": "No enviar", "50b00dc327": "Copiar detalles", "6d3ebe216a": "Texto de diagnóstico", - "835037edc9": "· Orcas", + "835037edc9": "· Orca", "56a3dfa283": "No se pudo enviar el informe de fallas.", "8e24fe4f75": "Informe de fallo enviado.", "8b8473c544": "Se copió el informe de fallos.", "b175e90213": "No hay ningún informe de fallo disponible.", "765591798d": "Comprobando informes de fallos...", - "b2e36f53a1": "Failed to send crash report. Diagnostic ticket {{value0}} was uploaded but not linked.", - "ead6fc0510": "No automatic crash report was captured. You can still send details and include recent diagnostic logs when available.", - "b082f27490": "Attach recent diagnostic logs", - "e59f0b9427": "Sends a capped redacted log bundle with the report." + "b2e36f53a1": "No se pudo enviar el informe de fallo. Se subió el ticket de diagnóstico {{value0}}, pero no se vinculó.", + "ead6fc0510": "No se capturó ningún informe de fallo automático. Aun así puedes enviar detalles e incluir logs de diagnóstico recientes cuando estén disponibles.", + "b082f27490": "Adjuntar logs de diagnóstico recientes", + "e59f0b9427": "Envía un paquete limitado de logs redactados junto con el informe." } } }, @@ -11553,39 +11553,39 @@ "j": { "quick": { "actions": { - "c884a6398e": "Cree un comando de terminal guardado.", + "c884a6398e": "Crea un comando de terminal guardado.", "a43ab56fc1": "Agregar comando rápido", - "54853d52a2": "Eliminar el árbol de trabajo actual.", - "9537b910fe": "Eliminar árbol de trabajo", - "0b1f25f796": "Inicie un nuevo árbol de trabajo.", - "52ac9da671": "Crear árbol de trabajo", - "f70812764a": "Abra una pestaña de terminal en el espacio de trabajo activo.", + "54853d52a2": "Elimina el worktree actual.", + "9537b910fe": "Eliminar worktree", + "0b1f25f796": "Inicia un nuevo worktree.", + "52ac9da671": "Crear worktree", + "f70812764a": "Abre una pestaña de terminal en el workspace activo.", "34980395d4": "Nueva pestaña de terminal", - "f2a1b33f8d": "Cree un archivo de markdowns sin título en el espacio de trabajo activo.", - "25349b66fc": "Nuevo archivo de Markdowns", - "784812ca24": "Abra una pestaña del navegador en el espacio de trabajo activo.", + "f2a1b33f8d": "Crea un archivo Markdown sin título en el workspace activo.", + "25349b66fc": "Nuevo archivo Markdown", + "784812ca24": "Abre una pestaña del navegador en el workspace activo.", "892bfa9339": "Nueva pestaña del navegador", "verbs": { "newBrowser": "nuevo navegador", "newBrowserTab": "nueva pestaña del navegador", "openBrowser": "abrir el navegador", "browserTab": "pestaña del navegador", - "newMarkdown": "nueva markdown", - "newMarkdownFile": "nuevo archivo de markdowns", - "newMark": "nueva marca", + "newMarkdown": "nuevo markdown", + "newMarkdownFile": "nuevo archivo markdown", + "newMark": "nuevo markdown", "newFile": "nuevo archivo", - "markdownFile": "archivo de markdowns", + "markdownFile": "archivo markdown", "newTerminal": "nueva terminal", "newTerminalTab": "nueva pestaña de terminal", - "newShell": "nuevo caparazón", - "terminalTab": "pestaña terminal", - "createWorktree": "crear árbol de trabajo", - "addWorktree": "agregar árbol de trabajo", - "newWorktree": "nuevo árbol de trabajo", - "deleteWorktree": "eliminar árbol de trabajo", - "deleteCurrentWorktree": "eliminar el árbol de trabajo actual", - "removeWorktree": "eliminar árbol de trabajo", - "trashWorktree": "árbol de trabajo de basura", + "newShell": "nuevo shell", + "terminalTab": "pestaña de terminal", + "createWorktree": "crear worktree", + "addWorktree": "agregar worktree", + "newWorktree": "nuevo worktree", + "deleteWorktree": "eliminar worktree", + "deleteCurrentWorktree": "eliminar el worktree actual", + "removeWorktree": "quitar worktree", + "trashWorktree": "mandar worktree a la papelera", "addQuickCommand": "agregar comando rápido", "newQuickCommand": "nuevo comando rápido" } @@ -11647,13 +11647,13 @@ "93be92f8d1": "Copiar dirección", "3c085f638d": "Copiar la URL de la página fallida", "c6be71329e": "Refrescar", - "781d6459ad": "Rever", - "2fdca7df09": "Despedir", - "8b6fab9ffa": "Ahorrar", + "781d6459ad": "Reintentar", + "2fdca7df09": "Descartar", + "8b6fab9ffa": "Guardar", "0f41bf80c7": "Abrir en el navegador predeterminado", - "ec75d0c412": "Abrir herramientas de desarrollo del navegador", - "fc9be38f6f": "Anotar elemento de página", - "fdfc7fe0ef": "Tomar elemento de página", + "ec75d0c412": "Abrir DevTools del navegador", + "fc9be38f6f": "Anotar elemento de la página", + "fdfc7fe0ef": "Capturar elemento de la página", "a8f37f70c3": "Inspeccionar página", "1b179ab561": "Copiar URL de página", "f7ab83f7ed": "Abrir página en el navegador predeterminado", @@ -11662,36 +11662,36 @@ "40edfa75cb": "Atrás", "efb0e8f7f3": "Copiar dirección de enlace", "8ce4f6b12e": "Abrir enlace en el navegador predeterminado", - "b5b87d6cbb": "Abrir enlace en el navegador Orca", + "b5b87d6cbb": "Abrir enlace en el navegador de Orca", "87eb75f7d2": "Introduzca una URL http(s) o localhost válida.", "27d863542c": "Anotaciones del navegador", "e48569ac6d": "No se pudo acceder a este sitio.", "bbe8f15e83": "Este panel se representa desde el servidor de ejecución activo.", "8b7e6d1f5a": "Las anotaciones del navegador solo están disponibles en las pestañas del navegador local.", - "deb5293610": "Las anotaciones del navegador no están disponibles en el tiempo de ejecución remoto", + "deb5293610": "Anotaciones del navegador no disponibles en runtime remoto", "90d021f2ad": "Agregar", "0cb3bd6221": "Intención de anotación", "8f87e6c2e5": "Intención", "532bac48c5": "Describe aquí lo que el agente debería cambiar...", - "d2a7092e6e": "comentario de anotación", + "d2a7092e6e": "Comentario de anotación", "b472c5fe03": "Agregar anotación del navegador", "b5ba6085de": "Pregunta", "143204e423": "Cambiar", - "e7ca5a098c": "éxito", + "e7ca5a098c": "correcto", "d51ef37351": "Copiar", - "6f4ab3592b": "copiado", + "6f4ab3592b": "Copiado", "b2856516e2": "No se puede cargar esta página", - "db325a7eeb": "No puedo comunicarme con {{value0}}", + "db325a7eeb": "No se puede acceder a {{value0}}", "499b31b84e": "Copiar todo", "e72dfa268a": "anotar", - "168350ae6a": "Haga clic o coloque el cursor sobre un elemento, luego presione C para copiar o S para hacer una captura de pantalla.", - "e852e20cea": "Copiado: presione S para hacer una captura de pantalla o seleccione otro elemento", + "168350ae6a": "Haz clic o pon el cursor sobre un elemento, luego presiona C para copiar o S para hacer una captura de pantalla.", + "e852e20cea": "Copiado — presiona S para hacer una captura de pantalla o selecciona otro elemento", "a5dcd0fd1d": "confirmando", - "777b5bc4ec": "Haga clic en un elemento para agregar comentarios para el agente.", - "b733a91bd9": "Agregue comentarios para el elemento seleccionado.", - "4328a0a062": "Grabación fallida: {{value0}}", + "777b5bc4ec": "Haz clic en un elemento para agregar comentarios para el agente.", + "b733a91bd9": "Agrega comentarios para el elemento seleccionado.", + "4328a0a062": "Captura fallida: {{value0}}", "26615e116b": "error", - "8aec5bc044": "idle", + "8aec5bc044": "inactivo", "759f32af29": "Descargando", "c8bc7f1f9e": "solicitado", "4300f38145": "Descargando desde {{value0}}{{value1}}", @@ -11704,15 +11704,15 @@ "c13693fe27": "{{value0}} anotaciones", "074f0ed10b": "{{value0}} anotación lista. Seleccione otro elemento o copie todos los comentarios.", "a2164a6e5a": "{{value0}} anotaciones listas. Seleccione otro elemento o copie todos los comentarios.", - "9f6f2e8c19": "The downloaded file path is unavailable.", - "0c79b7634d": "Could not open the downloaded file. It may have been moved or deleted.", - "397d9dc923": "Could not show the downloaded file. It may have been moved or deleted.", - "39c04fed61": "Downloading paused", - "5c3d530a68": "Downloaded", - "4bb7424d6b": "Canceled", - "6e776f9ef9": "Download failed", + "9f6f2e8c19": "La ruta del archivo descargado no está disponible.", + "0c79b7634d": "No se pudo abrir el archivo descargado. Puede haberse movido o eliminado.", + "397d9dc923": "No se pudo mostrar el archivo descargado. Puede haberse movido o eliminado.", + "39c04fed61": "Descarga pausada", + "5c3d530a68": "Descargado", + "4bb7424d6b": "Cancelado", + "6e776f9ef9": "Falló la descarga", "756bfc25c9": "Abrir", - "09a9489aa5": "Show", + "09a9489aa5": "Mostrar", "2a4c4b8e1f": "Copiar" }, "BrowserToolbarMenu": { @@ -11804,20 +11804,20 @@ "AutomationEditorDialog": { "fb1896a5e7": "Cancelar", "57b722cbba": "Agente", - "6ff66f9012": "Nueva carrera", - "a2e688226d": "árbol de trabajo", - "6f9610e667": "Worktree se ejecuta en el espacio de trabajo seleccionado. La nueva ejecución crea un espacio de trabajo nuevo a partir de la rama seleccionada cada vez.", - "2c3fd9bfa1": "Ayuda del modo espacio de trabajo", - "b28b140eaf": "Espacio de trabajo", + "6ff66f9012": "Nueva ejecución", + "a2e688226d": "Worktree", + "6f9610e667": "Worktree se ejecuta en el workspace seleccionado. Nueva ejecución crea un workspace nuevo desde la rama seleccionada cada vez.", + "2c3fd9bfa1": "Ayuda del modo workspace", + "b28b140eaf": "Workspace", "0d17f4ca8f": "Seleccionar proyecto", "02d351877e": "Proyecto", - "a4ac8fcc62": "/meta", - "827b25a81e": "Admite habilidades, rutas de archivos y comandos integrados como", + "a4ac8fcc62": "/goal", + "827b25a81e": "Admite skills, rutas de archivos y comandos integrados como", "6d778190b7": "Ejecute la auditoría de dependencia semanal y resuma los cambios riesgosos.", - "058c23cb3f": "Inmediato", - "c4b19094c2": "Cronograma", + "058c23cb3f": "Prompt", + "c4b19094c2": "Programación", "e46c1aa9ad": "Crear", - "a9d9dccf77": "Ahorrar", + "a9d9dccf77": "Guardar", "777548c2d6": "Guardar cambios", "ff5db28639": "existente" }, @@ -11826,7 +11826,7 @@ "7e35393632": "Hermes", "6f309eef8d": "Orca", "58f56b73d9": "Nombre de la automatización", - "1d9826933e": "Auditoría de repos entre semana", + "1d9826933e": "Auditoría de repositorios entre semana", "4133d33862": "Crear automatización", "0a75e5e2fa": "Crear automatización Hermes", "03142e7721": "Editar la automatización de Hermes", @@ -11955,18 +11955,18 @@ "7b2e285552": "Las conexiones SSH no están disponibles en este cliente.", "d441032f7e": "pausa", "5918020edc": "correr", - "a21f6c33ad": "Automation source refreshed.", - "53f06f0ad5": "Retry source", + "a21f6c33ad": "Origen de automatización actualizado.", + "53f06f0ad5": "Reintentar origen", "pendingAutomationMissing": "La automatización ya no está disponible.", "pendingAutomationRunMissing": "El historial de ejecuciones ya no está disponible." }, "CreateFromPicker": { - "f061f49e3f": "Buscar sucursales de repo...", - "dd3841b442": "Derivarse de", + "f061f49e3f": "Buscar ramas del repositorio...", + "dd3841b442": "Crear desde", "ef6d762538": "Valor predeterminado del proyecto", "e53d306056": "{{value0}} (predeterminado)", - "79512f22a7": "No se encontraron sucursales.", - "9ce96621f4": "Buscando sucursales..." + "79512f22a7": "No se encontraron ramas.", + "9ce96621f4": "Buscando ramas..." }, "ExternalAutomationManagers": { "e02f970595": "No se encontraron administradores de automatización externos.", @@ -12028,10 +12028,10 @@ "513401db93": "Prepare un resumen semanal del riesgo de liberación del estado actual del proyecto.", "39ed39280a": "Preparación para el lanzamiento", "a7fbd32ddb": "Verifique las dependencias, las pruebas fallidas y los cambios abiertos riesgosos todos los días de la semana.", - "b84757677d": "Auditoría de repos entre semana", + "b84757677d": "Auditoría de repositorios entre semana", "repoHealth": { - "category": "Salud del Repo", - "name": "Auditoría de repos entre semana", + "category": "Salud del Repositorio", + "name": "Auditoría de repositorios entre semana", "prompt": "Revisar el estado del repositorio. Verifique las actualizaciones de dependencia, las pruebas fallidas, el estado de pelusa/verificación de tipos y los cambios abiertos riesgosos. Resuma los hallazgos y sugiera la siguiente acción." }, "releasePrep": { @@ -12061,16 +12061,16 @@ } }, "AutomationProjectCombobox": { - "search": "Search projects/folders...", - "empty": "No projects/folders match your search.", - "chooseHost": "Choose automation host", - "adding": "Adding project…", - "addProject": "Add project" + "search": "Buscar proyectos/carpetas...", + "empty": "Ningún proyecto/carpeta coincide con tu búsqueda.", + "chooseHost": "Elegir host de automatización", + "adding": "Agregando proyecto…", + "addProject": "Agregar proyecto" }, "AutomationSetupDecisionField": { "5a7863909c": "Ejecutar la configuración para cada espacio de trabajo nuevo", - "18f000ad4e": "Advanced", - "874b72195b": "When this automation creates a workspace, prepare it the same way creating a worktree by hand does — run the project's setup and open its terminal tabs." + "18f000ad4e": "Avanzado", + "874b72195b": "Cuando esta automatización crea un workspace, lo prepara igual que al crear un worktree manualmente: ejecuta la configuración del proyecto y abre sus pestañas de terminal." } }, "agent": { @@ -12089,7 +12089,7 @@ }, "activity": { "ActivityPrototypePage": { - "cf780197a1": "Seleccione un agente para ver su actividad", + "cf780197a1": "Selecciona un agente para ver su actividad", "e3db9892f6": "Aún no hay actividad.", "1b633f5c1e": "Terminal de conexión...", "8de7c5beaa": "Terminal no disponible", @@ -12103,22 +12103,22 @@ "db8a1878b5": "Opciones de lista de hilos", "d1a88df9a8": "Mostrar solo hilos no leídos", "f6396e1f85": "Agente", - "b29191b3e0": "árbol de trabajo", + "b29191b3e0": "worktree", "8c3b621ddf": "Proyecto", "4a3986b200": "Estado", - "770d458144": "Actividad del agente de grupo por", + "770d458144": "Agrupar actividad del agente por", "795cbf26e2": "Filtrar...", - "4616ea39fd": "Saltar al espacio de trabajo", + "4616ea39fd": "Ir al workspace", "59b131fbd9": "Marcar hilo como no leído", "beb2c19173": "No leído", "5651b216c6": "Proyecto desconocido", "22b22034bc": "Terminal independiente no disponible en Actividad.", - "afdc2139a8": "Terminal de Agent cerrada. Abra una nueva terminal en este espacio de trabajo para continuar." + "afdc2139a8": "Terminal de Agent cerrada. Abre una nueva terminal en este workspace para continuar." }, "ActivityTitlebarControls": { "f915168c8e": "no leído", "d6a8de3934": "agentes", - "dc708f3eff": "Agentes cercanos" + "dc708f3eff": "Agents cercanos" } }, "confirmation": { @@ -12131,45 +12131,45 @@ "connect": { "dialog": { "63ce735809": "Conectar", - "4a2ab52781": "Verifying…", + "4a2ab52781": "Verificando…", "79e7aaed39": "Cancelar", - "fdd26d81cc": "Atlassian account settings", - "8090504a3e": "Create a token in", - "7b3967c12f": "Atlassian API token", + "fdd26d81cc": "Ajustes de cuenta de Atlassian", + "8090504a3e": "Crea un token en", + "7b3967c12f": "Token API de Atlassian", "3d81bf3ab3": "API token", "e91b9a4073": "you@example.com", - "2849ddb295": "Atlassian email", + "2849ddb295": "Email de Atlassian", "70fcd360c4": "https://example.atlassian.net", - "e176f9d0c5": "Jira Cloud site URL", - "d785c42b8b": "Use a Jira Cloud site URL, Atlassian email, and API token to browse issues.", - "8388bdea2b": "Connect Jira site" + "e176f9d0c5": "URL del sitio de Jira Cloud", + "d785c42b8b": "Usa una URL de Jira Cloud, email de Atlassian y token API para explorar issues.", + "8388bdea2b": "Conectar sitio de Jira" } } }, "rightSidebar": { "FolderWorkspaceWorktreesPanel": { - "unavailable": "Workspaces are only shown for folder workspaces.", + "unavailable": "Los workspaces solo se muestran para workspaces de carpeta.", "label": "Workspaces", - "description": "Shows worktrees attached to this folder workspace.", - "countOne": "1 attached worktree", - "countMany": "{{value0}} attached worktrees", - "emptyTitle": "No attached worktrees yet", - "emptyCopy": "Worktrees created from this workspace will show up here." + "description": "Muestra worktrees adjuntos a este workspace de carpeta.", + "countOne": "1 worktree adjunto", + "countMany": "{{value0}} worktrees adjuntos", + "emptyTitle": "Aún no hay worktrees adjuntos", + "emptyCopy": "Los worktrees creados desde este workspace aparecerán aquí." }, "FolderWorkspacePrChecksPanel": { "unavailable": "Las comprobaciones de PR solo se muestran en espacios de trabajo de carpeta.", "refresh": "Actualizar comprobaciones de PR", - "emptyTitle": "Aún no hay árboles de trabajo adjuntos", - "emptyCopy": "Las comprobaciones de PR aparecerán aquí cuando se adjunten árboles de trabajo a este espacio de trabajo de carpeta.", - "openChecksTab": "Abrir la pestaña Comprobaciones de {{value0}}", + "emptyTitle": "Aún no hay worktrees adjuntos", + "emptyCopy": "Los checks de PR aparecerán aquí cuando se adjunten worktrees a este workspace de carpeta.", + "openChecksTab": "Abrir pestaña Checks de {{value0}}", "openReviewExternally": "Abrir {{value0}} externamente", "summary": "{{value0}} adjuntos · {{value1}} con PR/MR · {{value2}} requieren atención · {{value3}} pendientes · {{value4}} correctos · {{value5}} sin PR · {{value6}} desconocidos", - "showDetails": "Mostrar detalles de comprobaciones de PR de {{value0}}", - "hideDetails": "Ocultar detalles de comprobaciones de PR de {{value0}}", - "reviewChecks": "Comprobaciones de revisión", - "allChecksPassing": "todas las comprobaciones correctas", - "oneWorktree": "1 árbol de trabajo", - "worktreeCount": "{{value0}} árboles de trabajo", + "showDetails": "Mostrar detalles de checks de PR de {{value0}}", + "hideDetails": "Ocultar detalles de checks de PR de {{value0}}", + "reviewChecks": "Checks de revisión", + "allChecksPassing": "todos los checks correctos", + "oneWorktree": "1 worktree", + "worktreeCount": "{{value0}} worktrees", "oneFailing": "1 fallando", "failingCount": "{{value0}} fallando", "onePending": "1 pendiente", @@ -12179,21 +12179,21 @@ "rowSummary": { "failingCount": "{{value0}} fallando", "pendingCount": "{{value0}} pendientes", - "checksFailing": "Comprobaciones fallando", - "mergeConflicts": "Conflictos de fusión", - "checksPending": "Comprobaciones pendientes", - "checksPassing": "Comprobaciones correctas", - "merged": "Fusionado", - "closedWithoutMerge": "Cerrado sin fusionar", + "checksFailing": "Checks fallando", + "mergeConflicts": "Conflictos de merge", + "checksPending": "Checks pendientes", + "checksPassing": "Checks correctos", + "merged": "Merged", + "closedWithoutMerge": "Cerrado sin merge", "draftReview": "Revisión en borrador", - "noCheckSignal": "Sin señal de comprobaciones", + "noCheckSignal": "Sin señal de checks", "reviewUnavailable": "Estado de revisión no disponible", "noPrLinked": "Sin PR vinculado", "detailsUnavailable": "Detalles de revisión no disponibles", "refreshFailed": "Error al actualizar", "checking": "Comprobando estado de revisión…", "notFetched": "Estado aún no obtenido", - "unavailableWorktree": "No disponible para este árbol de trabajo" + "unavailableWorktree": "No disponible para este worktree" }, "groups": { "needsAttention": "Requiere atención", @@ -12248,49 +12248,49 @@ "project": { "source": { "combobox": { - "noProjects": "No projects", - "allProjects": "All projects", + "noProjects": "Sin proyectos", + "allProjects": "Todos los proyectos", "hostCount": "{{value0}} hosts", - "searchProjects": "Search projects...", - "noMatches": "No projects match your search.", + "searchProjects": "Buscar proyectos...", + "noMatches": "Ningún proyecto coincide con tu búsqueda.", "chooseSource": "Elige el origen de tareas" } } } }, "taskPageEmptyState": { - "noProjectSourcesTitle": "No project sources selected", - "noProjectSourcesDescription": "Select at least one project source so Orca knows which host/account to fetch tasks from.", - "noMatchingGitHubWorkTitle": "No matching GitHub work", - "changeQueryDescription": "Change the query or clear it.", - "noGitLabIssuesTitle": "No GitLab issues", - "noGitLabIssuesDescription": "No GitLab issues match this filter.", - "noGitLabMrsTitle": "No GitLab merge requests", - "noGitLabMrsDescription": "No GitLab MRs match this filter.", - "noGitLabWorkTitle": "No GitLab work", - "noGitLabWorkDescription": "No GitLab work matches this filter." + "noProjectSourcesTitle": "No hay orígenes de proyecto seleccionados", + "noProjectSourcesDescription": "Selecciona al menos un origen de proyecto para que Orca sepa desde qué host/cuenta obtener tareas.", + "noMatchingGitHubWorkTitle": "No hay trabajo de GitHub coincidente", + "changeQueryDescription": "Cambia la consulta o bórrala.", + "noGitLabIssuesTitle": "No hay issues de GitLab", + "noGitLabIssuesDescription": "Ningún issue de GitLab coincide con este filtro.", + "noGitLabMrsTitle": "No hay merge requests de GitLab", + "noGitLabMrsDescription": "Ningún MR de GitLab coincide con este filtro.", + "noGitLabWorkTitle": "No hay trabajo de GitLab", + "noGitLabWorkDescription": "Ningún trabajo de GitLab coincide con este filtro." }, "taskSourceContextSummary": { - "sourceUnavailable": "{{value0}} source unavailable: {{value1}}", - "someSourceHostsUnavailable": "Some {{value0}} source hosts unavailable: {{value1}}", - "reconnectOrUpdateTitle": "Reconnect or update {{value0}} to load this source." + "sourceUnavailable": "Origen {{value0}} no disponible: {{value1}}", + "someSourceHostsUnavailable": "Algunos hosts de origen de {{value0}} no están disponibles: {{value1}}", + "reconnectOrUpdateTitle": "Reconecta o actualiza {{value0}} para cargar este origen." }, "ShortcutKeyCombo": { - "07eb4985a1": "Double-tap {{value0}}" + "07eb4985a1": "Doble toque en {{value0}}" }, "star": { "nag": { "StarNagToastHost": { - "starredThanks": "Starred — thank you!", - "githubOpened": "GitHub opened", - "opening": "Opening…", - "starring": "Starring…", - "openGithub": "Open GitHub", - "starOnGithub": "Star on GitHub", - "onboardingCompleted": "Onboarding completed!", - "body": "If you’re enjoying Orca so far, a GitHub star helps other developers discover it.", + "starredThanks": "Star enviada — ¡gracias!", + "githubOpened": "GitHub abierto", + "opening": "Abriendo…", + "starring": "Enviando star…", + "openGithub": "Abrir GitHub", + "starOnGithub": "Dar star en GitHub", + "onboardingCompleted": "¡Onboarding completado!", + "body": "Si Orca te está gustando, una star en GitHub ayuda a que otros desarrolladores lo descubran.", "dismiss": "Descartar", - "later": "Later" + "later": "Más tarde" } } }, @@ -12304,7 +12304,7 @@ "hostedReview": { "copy": { "f0a4b8c2d1": "PR", - "e9f3a7b1c0": "solicitud de extracción", + "e9f3a7b1c0": "pull request", "d8e2f6a0b9": "Solicitud de extracción", "c7d1e5f9a8": "GitHub", "c4e8f1a2b9": "SEÑOR", From ac8d365296e45e4e75641881acc13655ffac48e5 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Sat, 4 Jul 2026 13:37:06 -0700 Subject: [PATCH 19/45] Fix STA-1292: prevent Windows worktree-create hang (disable interactive credential prompt + timeout create-path git) (#7301) Co-authored-by: Orca --- src/main/git/runner.test.ts | 71 +++++++++++++++++++++ src/main/git/runner.ts | 48 ++++++++++++-- src/main/git/worktree.test.ts | 41 +++++++++--- src/main/git/worktree.ts | 13 +++- src/main/ipc/worktree-remote.ts | 8 ++- src/main/runtime/fetch-remote-cache.test.ts | 12 +++- src/main/runtime/orca-runtime.test.ts | 5 +- src/main/runtime/orca-runtime.ts | 14 +++- 8 files changed, 187 insertions(+), 25 deletions(-) diff --git a/src/main/git/runner.test.ts b/src/main/git/runner.test.ts index 24a031bbceb..57b04831acc 100644 --- a/src/main/git/runner.test.ts +++ b/src/main/git/runner.test.ts @@ -3,12 +3,30 @@ // and stderr extraction from execFile rejections (err.message is unreliable). import { afterEach, describe, expect, it, vi } from 'vitest' import { + appendGitConfigEnv, extractExecError, isTransientGhError, + nonInteractiveGitEnv, parseRetryAfterMs, + promptGuardGitEnv, redirectPortedHostnameToEnv } from './runner' +// Reads git config injected via the GIT_CONFIG_COUNT/KEY/VALUE env protocol +// back into a plain key→value map so tests can assert on it directly. +function readGitConfigEnv(env: NodeJS.ProcessEnv): Record { + const count = Number.parseInt(env.GIT_CONFIG_COUNT ?? '0', 10) + const config: Record = {} + for (let i = 0; i < count; i++) { + const key = env[`GIT_CONFIG_KEY_${i}`] + const value = env[`GIT_CONFIG_VALUE_${i}`] + if (key !== undefined && value !== undefined) { + config[key] = value + } + } + return config +} + describe('redirectPortedHostnameToEnv', () => { it('moves a ported --hostname into GITLAB_HOST and strips the flag', () => { const { args, options } = redirectPortedHostnameToEnv( @@ -147,3 +165,56 @@ describe('extractExecError', () => { }) }) }) + +describe('appendGitConfigEnv', () => { + it('injects entries starting at count 0 when none exist', () => { + const env = appendGitConfigEnv({ PATH: '/usr/bin' }, [['credential.interactive', 'false']]) + expect(env.GIT_CONFIG_COUNT).toBe('1') + expect(env.GIT_CONFIG_KEY_0).toBe('credential.interactive') + expect(env.GIT_CONFIG_VALUE_0).toBe('false') + expect(env.PATH).toBe('/usr/bin') + }) + + it('composes with an existing count instead of clobbering caller config', () => { + const env = appendGitConfigEnv( + { GIT_CONFIG_COUNT: '1', GIT_CONFIG_KEY_0: 'core.quotePath', GIT_CONFIG_VALUE_0: 'false' }, + [['credential.guiPrompt', 'false']] + ) + expect(env.GIT_CONFIG_COUNT).toBe('2') + // Existing entry preserved. + expect(env.GIT_CONFIG_KEY_0).toBe('core.quotePath') + expect(env.GIT_CONFIG_VALUE_0).toBe('false') + // New entry appended at the next index. + expect(env.GIT_CONFIG_KEY_1).toBe('credential.guiPrompt') + expect(env.GIT_CONFIG_VALUE_1).toBe('false') + }) +}) + +describe('promptGuardGitEnv credential-interactivity disable (STA-1292)', () => { + it('disables the GCM GUI prompt without nuking the credential helper', () => { + const env = promptGuardGitEnv({ PATH: '/usr/bin' }) + // GCM: never show the GUI, but still serve cached credentials. + expect(env.GCM_INTERACTIVE).toBe('never') + const config = readGitConfigEnv(env) + expect(config['credential.interactive']).toBe('false') + expect(config['credential.guiPrompt']).toBe('false') + // Regression guard: we must NOT clear the helper — that would break + // cached-credential auth for private repos. + expect(config['credential.helper']).toBeUndefined() + // Existing prompt guards remain intact. + expect(env.GIT_TERMINAL_PROMPT).toBe('0') + }) +}) + +describe('nonInteractiveGitEnv credential-interactivity disable (STA-1292)', () => { + it('carries the credential-interactivity disable through from promptGuardGitEnv', () => { + const env = nonInteractiveGitEnv({ PATH: '/usr/bin' }) + expect(env.GCM_INTERACTIVE).toBe('never') + const config = readGitConfigEnv(env) + expect(config['credential.interactive']).toBe('false') + expect(config['credential.guiPrompt']).toBe('false') + expect(config['credential.helper']).toBeUndefined() + // Its own BatchMode SSH guard is still applied and unaffected. + expect(env.GIT_SSH_COMMAND).toBe('ssh -o BatchMode=yes') + }) +}) diff --git a/src/main/git/runner.ts b/src/main/git/runner.ts index 325d92220e3..ec0d1d7b84d 100644 --- a/src/main/git/runner.ts +++ b/src/main/git/runner.ts @@ -526,13 +526,47 @@ export function gitOptionalLocksDisabledEnv( } } -function promptGuardGitEnv(env: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv { - return { - ...env, - GIT_TERMINAL_PROMPT: '0', - GIT_ASKPASS: env.GIT_ASKPASS ?? '', - SSH_ASKPASS: env.SSH_ASKPASS ?? '' - } +/** + * Append git config entries through the GIT_CONFIG_COUNT / GIT_CONFIG_KEY_n / + * GIT_CONFIG_VALUE_n env protocol (git >= 2.31), composing with any count + * already present in `env` so we never clobber config a caller injected the + * same way. + */ +export function appendGitConfigEnv( + env: NodeJS.ProcessEnv, + entries: readonly (readonly [key: string, value: string])[] +): NodeJS.ProcessEnv { + const parsed = Number.parseInt(env.GIT_CONFIG_COUNT ?? '', 10) + const base = Number.isInteger(parsed) && parsed > 0 ? parsed : 0 + const next = { ...env } + entries.forEach(([key, value], index) => { + next[`GIT_CONFIG_KEY_${base + index}`] = key + next[`GIT_CONFIG_VALUE_${base + index}`] = value + }) + next.GIT_CONFIG_COUNT = String(base + entries.length) + return next +} + +export function promptGuardGitEnv(env: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv { + return appendGitConfigEnv( + { + ...env, + GIT_TERMINAL_PROMPT: '0', + GIT_ASKPASS: env.GIT_ASKPASS ?? '', + SSH_ASKPASS: env.SSH_ASKPASS ?? '', + // Why: Git Credential Manager ignores GIT_TERMINAL_PROMPT / GIT_ASKPASS and + // pops a GUI on first auth — the Windows worktree-create hang (STA-1292). + // `never` suppresses the prompt while still serving cached credentials. + GCM_INTERACTIVE: 'never' + }, + // Why: disable only the *interactive* credential prompt, NOT the helper + // itself — an empty credential.helper would break cached-credential auth for + // private repos. Harmless on macOS/Linux (no GCM) and on the SSH path. + [ + ['credential.interactive', 'false'], + ['credential.guiPrompt', 'false'] + ] + ) } /** diff --git a/src/main/git/worktree.test.ts b/src/main/git/worktree.test.ts index 5bc272b98d1..43269a065a1 100644 --- a/src/main/git/worktree.test.ts +++ b/src/main/git/worktree.test.ts @@ -25,7 +25,8 @@ import { listWorktrees, moveWorktree, parseWorktreeList, - removeWorktree + removeWorktree, + WORKTREE_ADD_TIMEOUT_MS } from './worktree' describe('listWorktrees in-flight sharing', () => { @@ -482,7 +483,7 @@ describe('addWorktree', () => { '/repo-feature', 'refs/remotes/origin/main' ], - { cwd: '/repo' } + { cwd: '/repo', timeout: WORKTREE_ADD_TIMEOUT_MS } ], [ [ @@ -507,10 +508,30 @@ describe('addWorktree', () => { }) expect(gitExecFileAsyncMock.mock.calls).toEqual([ - [['worktree', 'add', '/repo-feature', 'feature/test'], { cwd: '/repo' }] + [ + ['worktree', 'add', '/repo-feature', 'feature/test'], + { cwd: '/repo', timeout: WORKTREE_ADD_TIMEOUT_MS } + ] ]) }) + it('bounds the worktree add call with a positive timeout (STA-1292 OneDrive stall guard)', async () => { + // Why: without a timeout, a OneDrive cloud-placeholder checkout can stall + // `git worktree add` for minutes. Assert the runner receives a non-zero + // timeout so a stuck create fails fast instead of hanging forever. + gitExecFileAsyncMock.mockResolvedValueOnce({ stdout: '' }) // worktree add + + await addWorktree('/repo', '/repo-feature', 'feature/test', 'feature/test', false, false, { + checkoutExistingBranch: true + }) + + const worktreeAddCall = gitExecFileAsyncMock.mock.calls.find( + ([argv]) => Array.isArray(argv) && argv[0] === 'worktree' && argv[1] === 'add' + ) + expect(worktreeAddCall?.[1]).toMatchObject({ timeout: WORKTREE_ADD_TIMEOUT_MS }) + expect(WORKTREE_ADD_TIMEOUT_MS).toBeGreaterThan(0) + }) + it('does not write branch base config when no base branch is provided', async () => { gitExecFileAsyncMock.mockResolvedValueOnce({ stdout: '' }) // worktree add gitExecFileAsyncMock.mockResolvedValueOnce({ stdout: 'true\n' }) // push.autoSetupRemote already set @@ -520,7 +541,7 @@ describe('addWorktree', () => { expect(gitExecFileAsyncMock.mock.calls).toEqual([ [ ['worktree', 'add', '--no-track', '-b', 'feature/no-base', '/repo-feature'], - { cwd: '/repo' } + { cwd: '/repo', timeout: WORKTREE_ADD_TIMEOUT_MS } ], [['config', '--get', 'push.autoSetupRemote'], { cwd: '/repo-feature' }] ]) @@ -602,7 +623,7 @@ describe('addWorktree', () => { '/repo-feature', 'refs/remotes/origin/main' ], - { cwd: '/repo' } + { cwd: '/repo', timeout: WORKTREE_ADD_TIMEOUT_MS } ], [ [ @@ -640,7 +661,7 @@ describe('addWorktree', () => { '/repo-feature', 'refs/remotes/origin/main' ], - { cwd: '/repo' } + { cwd: '/repo', timeout: WORKTREE_ADD_TIMEOUT_MS } ], [ [ @@ -679,7 +700,7 @@ describe('addWorktree', () => { '/repo-feature', 'refs/remotes/origin/main' ], - { cwd: '/repo' } + { cwd: '/repo', timeout: WORKTREE_ADD_TIMEOUT_MS } ], [ [ @@ -719,7 +740,7 @@ describe('addWorktree', () => { '/repo-feature', 'refs/remotes/origin/main' ], - { cwd: '/repo' } + { cwd: '/repo', timeout: WORKTREE_ADD_TIMEOUT_MS } ] ]) }) @@ -769,7 +790,7 @@ describe('addWorktree', () => { '/repo-feature', 'refs/remotes/origin/main' ], - { cwd: '/repo' } + { cwd: '/repo', timeout: WORKTREE_ADD_TIMEOUT_MS } ], [ [ @@ -1283,7 +1304,7 @@ describe('addWorktree', () => { '/repo-feature', 'refs/heads/main' ], - { cwd: '/repo' } + { cwd: '/repo', timeout: WORKTREE_ADD_TIMEOUT_MS } ]) }) diff --git a/src/main/git/worktree.ts b/src/main/git/worktree.ts index 5a2e875a460..eb30e13ad0a 100644 --- a/src/main/git/worktree.ts +++ b/src/main/git/worktree.ts @@ -68,6 +68,12 @@ type LocalBaseRefRefreshability = const SPARSE_CHECKOUT_DETECTION_CONCURRENCY = 8 +// Why: bound `git worktree add` so a OneDrive cloud-placeholder base path can't +// stall its checkout writes for minutes (STA-1292) — a stuck create then fails +// fast instead of spinning forever. Generous enough not to kill a legit large +// checkout (mirrors the SYNC runner's cloud-placeholder floor, #7225). +export const WORKTREE_ADD_TIMEOUT_MS = 180_000 + function gitExecOptions( cwd: string, options: GitWorktreeExecOptions = {} @@ -878,7 +884,12 @@ async function performAddWorktree( args.push(effectiveBase) } } - await gitExecFileAsync(args, gitExecOptions(repoPath, options)) + await gitExecFileAsync(args, { + ...gitExecOptions(repoPath, options), + // Why: bound the checkout so a OneDrive cloud-placeholder stall (STA-1292) + // fails fast rather than hanging worktree creation indefinitely. + timeout: WORKTREE_ADD_TIMEOUT_MS + }) if (options.checkoutExistingBranch) { return localBaseRefRefresh ? { localBaseRefRefresh } : {} diff --git a/src/main/ipc/worktree-remote.ts b/src/main/ipc/worktree-remote.ts index c1003a10651..8193d6d74c1 100644 --- a/src/main/ipc/worktree-remote.ts +++ b/src/main/ipc/worktree-remote.ts @@ -118,6 +118,9 @@ import { const SSH_WORKTREE_CREATE_FETCH_FRESHNESS_MS = 30_000 const SSH_WORKTREE_CREATE_FETCH_CACHE_MAX = 512 +// Why: bound the create-path fallback `git fetch origin` so a Windows +// credential-manager GUI hang (STA-1292) can't wedge worktree creation forever. +const CREATE_BASE_FALLBACK_FETCH_TIMEOUT_MS = 60_000 const sshWorktreeCreateFetchInflight = new Map>() const sshWorktreeCreateFetchCompletedAt = new Map() const sshWorktreeCreateFetchQueueTail = new Map>() @@ -1974,7 +1977,10 @@ export async function createLocalWorktree( } } else { if (!(await hasLocalCommitObjectWithOptions(repo.path, baseBranch, localWorktreeGitOptions))) { - legacyFetchPromise = gitExecFileAsync(['fetch', 'origin'], localGitExecOptions) + legacyFetchPromise = gitExecFileAsync(['fetch', 'origin'], { + ...localGitExecOptions, + timeout: CREATE_BASE_FALLBACK_FETCH_TIMEOUT_MS + }) .then(() => undefined) .catch(() => undefined) emitCreateWorktreeProgress(mainWindow, 'fetching', args.creationId) diff --git a/src/main/runtime/fetch-remote-cache.test.ts b/src/main/runtime/fetch-remote-cache.test.ts index 514594eb64c..fce2a5e6d59 100644 --- a/src/main/runtime/fetch-remote-cache.test.ts +++ b/src/main/runtime/fetch-remote-cache.test.ts @@ -36,6 +36,12 @@ function exactBaseRefreshOptions(cwd: string): { return { cwd, timeout: 60_000, useConfiguredSshCommandForNetwork: true } } +// Why (STA-1292): the broad create-path fetch must carry a timeout so a Windows +// credential-manager GUI hang can't wedge worktree creation forever. +function fullRemoteFetchOptions(cwd: string): { cwd: string; timeout: number } { + return { cwd, timeout: 60_000 } +} + function mockFetchResults(results: (Promise | unknown)[]): void { let fetchIndex = 0 gitExecFileAsyncMock.mockImplementation((argv: string[]) => { @@ -299,7 +305,7 @@ describe('OrcaRuntimeService.fetchRemoteWithCache', () => { ['fetch', '--no-tags', 'origin', '+refs/heads/main:refs/remotes/origin/main'], exactBaseRefreshOptions('/repo/h') ], - [['fetch', 'origin'], { cwd: '/repo/h' }] + [['fetch', 'origin'], fullRemoteFetchOptions('/repo/h')] ]) }) @@ -341,7 +347,7 @@ describe('OrcaRuntimeService.fetchRemoteWithCache', () => { ([argv]) => Array.isArray(argv) && argv[0] === 'fetch' ) expect(fetchCalls).toEqual([ - [['fetch', 'origin'], { cwd: '/repo/i' }], + [['fetch', 'origin'], fullRemoteFetchOptions('/repo/i')], [ ['fetch', '--no-tags', 'origin', '+refs/heads/main:refs/remotes/origin/main'], exactBaseRefreshOptions('/repo/i') @@ -387,7 +393,7 @@ describe('OrcaRuntimeService.fetchRemoteWithCache', () => { ([argv]) => Array.isArray(argv) && argv[0] === 'fetch' ) expect(fetchCalls).toEqual([ - [['fetch', 'origin'], { cwd: '/repo/i-fail' }], + [['fetch', 'origin'], fullRemoteFetchOptions('/repo/i-fail')], [ ['fetch', '--no-tags', 'origin', '+refs/heads/main:refs/remotes/origin/main'], exactBaseRefreshOptions('/repo/i-fail') diff --git a/src/main/runtime/orca-runtime.test.ts b/src/main/runtime/orca-runtime.test.ts index a005e79a814..a8ef74053a3 100644 --- a/src/main/runtime/orca-runtime.test.ts +++ b/src/main/runtime/orca-runtime.test.ts @@ -4880,7 +4880,10 @@ describe('OrcaRuntimeService', () => { ['rev-parse', '--path-format=absolute', '--git-common-dir'], wslGitOptions ) - expect(asyncGitSpy).toHaveBeenCalledWith(['fetch', 'origin'], wslGitOptions) + expect(asyncGitSpy).toHaveBeenCalledWith(['fetch', 'origin'], { + ...wslGitOptions, + timeout: 60_000 + }) expect(syncGitSpy).toHaveBeenCalledWith( ['rev-list', '--left-right', '--count', 'HEAD...origin/main'], { cwd: TEST_WORKTREE_PATH, wslDistro: 'Ubuntu' } diff --git a/src/main/runtime/orca-runtime.ts b/src/main/runtime/orca-runtime.ts index 21623519884..8c53bbd9964 100644 --- a/src/main/runtime/orca-runtime.ts +++ b/src/main/runtime/orca-runtime.ts @@ -14075,7 +14075,13 @@ export class OrcaRuntimeService { } const promise = this.enqueueRemoteFetch(key, () => - gitExecFileAsync(['fetch', remote], { cwd: repoPath, ...gitOptions }) + gitExecFileAsync(['fetch', remote], { + cwd: repoPath, + ...gitOptions, + // Why: cap the create-path base-ref fetch so a stuck first-auth on + // Windows (GCM prompt) fails fast instead of hanging creation (STA-1292). + timeout: REMOTE_FETCH_TIMEOUT_MS + }) .then((): RemoteFetchResult => { // Why (§3.3 Lifecycle): timestamp on success ONLY. Writing on rejection // would make the freshness cache lie about the last known remote state. @@ -14134,7 +14140,7 @@ export class OrcaRuntimeService { // Why: exact remote-base refresh is the network gate for worktree // creation, so honor repo SSH routing and bound custom wrappers. useConfiguredSshCommandForNetwork: true, - timeout: 60_000 + timeout: REMOTE_FETCH_TIMEOUT_MS } ) .then((): RemoteFetchResult => { @@ -22520,6 +22526,10 @@ const PTY_CONTROLLER_LIST_TIMEOUT_MS = 3000 // clicks and successive coordinator dispatches feel snappy, while still being // short enough that a genuinely-changed remote is observed on the next action. const FETCH_FRESHNESS_MS = 30_000 +// Why: bound create-path remote fetches so a Windows credential-manager GUI hang +// (STA-1292) can't wedge worktree creation forever; parity with the exact-base +// refresh sibling's timeout. +const REMOTE_FETCH_TIMEOUT_MS = 60_000 const REMOTE_FETCH_CACHE_MAX = 512 const DRIFT_PROBE_SUBJECT_LIMIT = 5 From a81b82f640646c7f61856bc7d07efeb4182350ad Mon Sep 17 00:00:00 2001 From: PP <128917720+huhuhulululu@users.noreply.github.com> Date: Sat, 4 Jul 2026 17:15:35 -0400 Subject: [PATCH 20/45] feat(ghostty): resolve theme references during config import (#7126) * feat(ghostty): resolve theme references during config import `theme = ` is how most Ghostty configs pick their colors, but the importer treated the key as unsupported and silently dropped the entire palette. Resolve the referenced theme file and merge its colors as defaults, mirroring Ghostty's own precedence: - Search user themes first (XDG, then the native macOS dir), then the bundled themes inside the Ghostty install, and parse the file with the existing parseGhosttyConfig (theme files are plain config subsets). - Explicit config keys win over theme values; config palette entries are appended after the theme's so per-index overrides apply naturally. - Only color-bearing keys may flow from a theme into the import, a theme file cannot smuggle font/window settings past the user's config. - Theme names containing path separators are rejected (no traversal), files over 256 KB are ignored, and light:/dark: pairs plus unresolvable names surface as annotated unsupportedKeys instead of vanishing. * fix(ghostty): align theme resolution with Ghostty lookup Co-authored-by: Orca * test: stabilize ghostty import CI coverage Co-authored-by: Orca --------- Co-authored-by: Jinwoo-H Co-authored-by: Orca --- src/main/ghostty/index.ts | 47 ++++- src/main/ghostty/theme-import.test.ts | 171 ++++++++++++++++++ src/main/ghostty/theme-resolution.test.ts | 171 ++++++++++++++++++ src/main/ghostty/theme-resolution.ts | 120 ++++++++++++ .../components/settings/AccountsPane.test.tsx | 4 +- 5 files changed, 510 insertions(+), 3 deletions(-) create mode 100644 src/main/ghostty/theme-import.test.ts create mode 100644 src/main/ghostty/theme-resolution.test.ts create mode 100644 src/main/ghostty/theme-resolution.ts diff --git a/src/main/ghostty/index.ts b/src/main/ghostty/index.ts index ac8917154a1..8557193fe9f 100644 --- a/src/main/ghostty/index.ts +++ b/src/main/ghostty/index.ts @@ -5,6 +5,7 @@ import type { Store } from '../persistence' import { findGhosttyConfigPaths } from './discovery' import { parseGhosttyConfig } from './parser' import { mapGhosttyToOrca } from './mapper' +import { resolveGhosttyThemeColors } from './theme-resolution' // Why: defensive upper bound on the Ghostty config size we're willing to read // into memory on the main process. Real configs are a few KB; anything past @@ -61,6 +62,44 @@ function mergeParsedConfig( } } +function usesConditionalThemePair(themeName: string): boolean { + return themeName + .split(',') + .map((part) => part.trim()) + .some((part) => part.startsWith('light:') || part.startsWith('dark:')) +} + +// Why: `theme = ` is how most Ghostty configs pick their colors, so +// dropping it silently loses the entire palette. Resolve the referenced theme +// file and merge its colors as defaults: explicit config keys win, and config +// palette entries are appended after the theme's so per-index overrides apply. +async function applyThemeReference(parsed: Record): Promise { + const rawTheme = parsed['theme'] + if (rawTheme === undefined) { + return [] + } + delete parsed['theme'] + + const themeName = (Array.isArray(rawTheme) ? (rawTheme.at(-1) ?? '') : rawTheme).trim() + if (usesConditionalThemePair(themeName)) { + return ['theme (light:/dark: pairs not supported)'] + } + + const themeColors = await resolveGhosttyThemeColors(themeName) + if (themeColors === null) { + return ['theme (theme file not found)'] + } + + for (const [key, value] of Object.entries(themeColors)) { + if (key === 'palette') { + parsed[key] = [...asArray(value), ...asArray(parsed[key])] + } else if (parsed[key] === undefined) { + parsed[key] = value + } + } + return [] +} + export async function previewGhosttyImport(store: Store): Promise { const configPaths = await findGhosttyConfigPaths() if (configPaths.length === 0) { @@ -93,7 +132,13 @@ export async function previewGhosttyImport(store: Store): Promise = {} diff --git a/src/main/ghostty/theme-import.test.ts b/src/main/ghostty/theme-import.test.ts new file mode 100644 index 00000000000..c2d2ec61ab9 --- /dev/null +++ b/src/main/ghostty/theme-import.test.ts @@ -0,0 +1,171 @@ +import type { Store } from '../persistence' +import type { GlobalSettings } from '../../shared/types' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +const { statMock, readFileMock } = vi.hoisted(() => ({ + statMock: vi.fn(), + readFileMock: vi.fn() +})) + +vi.mock('fs/promises', () => ({ + stat: statMock, + readFile: readFileMock +})) + +vi.mock('os', () => ({ + platform: vi.fn(() => 'darwin'), + homedir: vi.fn(() => '/Users/alice') +})) + +import { previewGhosttyImport } from './index' + +const originalXdgConfigHome = process.env.XDG_CONFIG_HOME +const originalGhosttyResourcesDir = process.env.GHOSTTY_RESOURCES_DIR + +beforeEach(() => { + delete process.env.XDG_CONFIG_HOME + delete process.env.GHOSTTY_RESOURCES_DIR +}) + +afterEach(() => { + vi.clearAllMocks() + if (originalXdgConfigHome !== undefined) { + process.env.XDG_CONFIG_HOME = originalXdgConfigHome + } else { + delete process.env.XDG_CONFIG_HOME + } + if (originalGhosttyResourcesDir !== undefined) { + process.env.GHOSTTY_RESOURCES_DIR = originalGhosttyResourcesDir + } else { + delete process.env.GHOSTTY_RESOURCES_DIR + } +}) + +function createStore(settings: Record = {}): Store { + return { + getSettings: () => settings as GlobalSettings + } as Store +} + +describe('previewGhosttyImport theme references', () => { + it('resolves a theme reference into color overrides', async () => { + const configPath = '/Users/alice/Library/Application Support/com.mitchellh.ghostty/config' + const themePath = '/Users/alice/.config/ghostty/themes/Tomorrow Night Bright' + statMock.mockImplementation(async (p: string) => { + if (p === configPath || p === themePath) { + return { isFile: () => true, size: 128 } + } + throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' }) + }) + readFileMock.mockImplementation(async (p: string) => { + if (p === themePath) { + return 'palette = 1=#d54e53\nbackground = #000000\nforeground = #eaeaea\n' + } + return 'theme = Tomorrow Night Bright\nfont-size = 14\n' + }) + + const result = await previewGhosttyImport(createStore()) + + expect(result.diff).toEqual({ + terminalFontSize: 14, + terminalColorOverrides: { + red: '#d54e53', + background: '#000000', + foreground: '#eaeaea' + } + }) + expect(result.unsupportedKeys).toEqual([]) + }) + + it('lets explicit config colors override theme colors', async () => { + const configPath = '/Users/alice/Library/Application Support/com.mitchellh.ghostty/config' + const themePath = '/Users/alice/.config/ghostty/themes/night' + statMock.mockImplementation(async (p: string) => { + if (p === configPath || p === themePath) { + return { isFile: () => true, size: 128 } + } + throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' }) + }) + readFileMock.mockImplementation(async (p: string) => { + if (p === themePath) { + return 'palette = 1=#d54e53\npalette = 2=#b9ca4a\nbackground = #000000\n' + } + return 'theme = night\nbackground = #101010\npalette = 1=#ff0000\n' + }) + + const result = await previewGhosttyImport(createStore()) + + expect(result.diff).toEqual({ + terminalColorOverrides: { + // Why: config's palette index 1 and background win; theme keeps index 2. + red: '#ff0000', + green: '#b9ca4a', + background: '#101010' + } + }) + expect(result.unsupportedKeys).toEqual([]) + }) + + it('resolves an absolute theme path without probing theme search dirs', async () => { + const configPath = '/Users/alice/Library/Application Support/com.mitchellh.ghostty/config' + const themePath = '/Users/alice/themes/work' + statMock.mockImplementation(async (p: string) => { + if (p === configPath || p === themePath) { + return { isFile: () => true, size: 128 } + } + throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' }) + }) + readFileMock.mockImplementation(async (p: string) => { + if (p === themePath) { + return 'background = #202020\nforeground = #f0f0f0\n' + } + return `theme = ${themePath}\n` + }) + + const result = await previewGhosttyImport(createStore()) + + expect(result.diff).toEqual({ + terminalColorOverrides: { + background: '#202020', + foreground: '#f0f0f0' + } + }) + expect(statMock).toHaveBeenCalledWith(themePath) + expect(statMock).not.toHaveBeenCalledWith( + '/Users/alice/.config/ghostty/themes/Users/alice/themes/work' + ) + expect(result.unsupportedKeys).toEqual([]) + }) + + it('marks an unresolvable theme as unsupported', async () => { + const configPath = '/Users/alice/Library/Application Support/com.mitchellh.ghostty/config' + statMock.mockImplementation(async (p: string) => { + if (p === configPath) { + return { isFile: () => true, size: 128 } + } + throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' }) + }) + readFileMock.mockResolvedValue('theme = Missing Theme\n') + + const result = await previewGhosttyImport(createStore()) + + expect(result.diff).toEqual({}) + expect(result.unsupportedKeys).toEqual(['theme (theme file not found)']) + }) + + it('marks light:/dark: theme pairs as unsupported', async () => { + const configPath = '/Users/alice/Library/Application Support/com.mitchellh.ghostty/config' + statMock.mockImplementation(async (p: string) => { + if (p === configPath) { + return { isFile: () => true, size: 128 } + } + throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' }) + }) + readFileMock.mockResolvedValue('theme = light:Tomorrow,dark:Tomorrow Night\n') + + const result = await previewGhosttyImport(createStore()) + + expect(result.diff).toEqual({}) + expect(result.unsupportedKeys).toEqual(['theme (light:/dark: pairs not supported)']) + }) +}) diff --git a/src/main/ghostty/theme-resolution.test.ts b/src/main/ghostty/theme-resolution.test.ts new file mode 100644 index 00000000000..607e69a39e8 --- /dev/null +++ b/src/main/ghostty/theme-resolution.test.ts @@ -0,0 +1,171 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +const { statMock, readFileMock } = vi.hoisted(() => ({ + statMock: vi.fn(), + readFileMock: vi.fn() +})) + +vi.mock('fs/promises', () => ({ + stat: statMock, + readFile: readFileMock +})) + +vi.mock('os', () => ({ + platform: vi.fn(() => 'darwin'), + homedir: vi.fn(() => '/Users/alice') +})) + +import { getGhosttyThemeSearchDirs, resolveGhosttyThemeColors } from './theme-resolution' + +const originalXdgConfigHome = process.env.XDG_CONFIG_HOME +const originalGhosttyResourcesDir = process.env.GHOSTTY_RESOURCES_DIR + +beforeEach(() => { + delete process.env.XDG_CONFIG_HOME + delete process.env.GHOSTTY_RESOURCES_DIR +}) + +afterEach(() => { + vi.clearAllMocks() + if (originalXdgConfigHome !== undefined) { + process.env.XDG_CONFIG_HOME = originalXdgConfigHome + } else { + delete process.env.XDG_CONFIG_HOME + } + if (originalGhosttyResourcesDir !== undefined) { + process.env.GHOSTTY_RESOURCES_DIR = originalGhosttyResourcesDir + } else { + delete process.env.GHOSTTY_RESOURCES_DIR + } +}) + +const THEME_FILE = ` +palette = 0=#000000 +palette = 1=#d54e53 +background = #000000 +foreground = #eaeaea +cursor-color = #eaeaea +selection-background = #424242 +selection-foreground = #eaeaea +` + +describe('getGhosttyThemeSearchDirs', () => { + it('probes XDG, then the Ghostty.app resources dir', () => { + delete process.env.XDG_CONFIG_HOME + expect(getGhosttyThemeSearchDirs()).toEqual([ + '/Users/alice/.config/ghostty/themes', + '/Applications/Ghostty.app/Contents/Resources/ghostty/themes' + ]) + }) + + it('honors XDG_CONFIG_HOME for the custom themes dir', () => { + process.env.XDG_CONFIG_HOME = '/xdg' + expect(getGhosttyThemeSearchDirs()[0]).toBe('/xdg/ghostty/themes') + }) + + it('honors GHOSTTY_RESOURCES_DIR for bundled themes', () => { + process.env.GHOSTTY_RESOURCES_DIR = '/opt/ghostty' + expect(getGhosttyThemeSearchDirs()).toEqual([ + '/Users/alice/.config/ghostty/themes', + '/opt/ghostty/themes' + ]) + }) +}) + +describe('resolveGhosttyThemeColors', () => { + it('reads the first matching theme file and returns only color keys', async () => { + statMock.mockImplementation(async (p: string) => { + if (p === '/Users/alice/.config/ghostty/themes/Tomorrow Night Bright') { + return { isFile: () => true, size: THEME_FILE.length } + } + throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' }) + }) + readFileMock.mockResolvedValue(THEME_FILE) + + const result = await resolveGhosttyThemeColors('Tomorrow Night Bright') + expect(result).toEqual({ + palette: ['0=#000000', '1=#d54e53'], + background: '#000000', + foreground: '#eaeaea', + 'cursor-color': '#eaeaea', + 'selection-background': '#424242', + 'selection-foreground': '#eaeaea' + }) + }) + + it('falls through to later dirs when earlier ones miss', async () => { + statMock.mockImplementation(async (p: string) => { + if (p === '/Applications/Ghostty.app/Contents/Resources/ghostty/themes/Tomorrow') { + return { isFile: () => true, size: THEME_FILE.length } + } + throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' }) + }) + readFileMock.mockResolvedValue('background = #1d1f21') + + const result = await resolveGhosttyThemeColors('Tomorrow') + expect(result).toEqual({ background: '#1d1f21' }) + }) + + it('resolves absolute theme paths without searching named theme dirs', async () => { + statMock.mockImplementation(async (p: string) => { + if (p === '/Users/alice/themes/work') { + return { isFile: () => true, size: 64 } + } + throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' }) + }) + readFileMock.mockResolvedValue('background = #1d1f21') + + const result = await resolveGhosttyThemeColors('/Users/alice/themes/work') + expect(result).toEqual({ background: '#1d1f21' }) + expect(statMock).toHaveBeenCalledTimes(1) + }) + + it('drops non-color keys a theme file might carry', async () => { + statMock.mockImplementation(async (p: string) => { + if (p === '/Users/alice/.config/ghostty/themes/custom') { + return { isFile: () => true, size: 64 } + } + throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' }) + }) + readFileMock.mockResolvedValue('background = #101010\nfont-size = 20') + + const result = await resolveGhosttyThemeColors('custom') + expect(result).toEqual({ background: '#101010' }) + }) + + it('returns null when the theme is not found anywhere', async () => { + statMock.mockRejectedValue(Object.assign(new Error('ENOENT'), { code: 'ENOENT' })) + expect(await resolveGhosttyThemeColors('Missing Theme')).toBeNull() + }) + + it('rejects names containing path separators or traversal', async () => { + expect(await resolveGhosttyThemeColors('../etc/passwd')).toBeNull() + expect(await resolveGhosttyThemeColors('a/b')).toBeNull() + expect(await resolveGhosttyThemeColors('a\\b')).toBeNull() + expect(await resolveGhosttyThemeColors('..')).toBeNull() + expect(statMock).not.toHaveBeenCalled() + }) + + it('rejects oversized theme files', async () => { + statMock.mockImplementation(async () => ({ isFile: () => true, size: 10_000_000 })) + expect(await resolveGhosttyThemeColors('huge')).toBeNull() + expect(readFileMock).not.toHaveBeenCalled() + expect(statMock).toHaveBeenCalledTimes(1) + }) + + it('does not fall through when a shadowing theme path is invalid', async () => { + statMock.mockImplementation(async (p: string) => { + if (p === '/Users/alice/.config/ghostty/themes/night') { + return { isFile: () => false, size: 128 } + } + if (p === '/Applications/Ghostty.app/Contents/Resources/ghostty/themes/night') { + return { isFile: () => true, size: 128 } + } + throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' }) + }) + + expect(await resolveGhosttyThemeColors('night')).toBeNull() + expect(readFileMock).not.toHaveBeenCalled() + expect(statMock).toHaveBeenCalledTimes(1) + }) +}) diff --git a/src/main/ghostty/theme-resolution.ts b/src/main/ghostty/theme-resolution.ts new file mode 100644 index 00000000000..eedb2c1592d --- /dev/null +++ b/src/main/ghostty/theme-resolution.ts @@ -0,0 +1,120 @@ +import { homedir, platform } from 'node:os' +import path from 'node:path' +import { readFile, stat } from 'node:fs/promises' +import { parseGhosttyConfig } from './parser' + +// Why: theme files ship a few dozen short lines; anything larger is not a +// Ghostty theme and should not be read into the main process. +const MAX_THEME_BYTES = 262_144 +type ThemeColors = Record +type ThemeReadResult = + | { status: 'found'; colors: ThemeColors } + | { status: 'missing' } + | { status: 'invalid' } + +// Why: only color-bearing keys may flow from a theme into the import — a theme +// file must not be able to smuggle font/window settings past the user's config. +const THEME_COLOR_KEYS = new Set([ + 'palette', + 'background', + 'foreground', + 'cursor-color', + 'cursor-text', + 'selection-background', + 'selection-foreground', + 'bold-color', + 'split-divider-color' +]) + +function xdgThemeDirs(home: string): string[] { + if (process.env.XDG_CONFIG_HOME) { + return [path.posix.join(process.env.XDG_CONFIG_HOME, 'ghostty', 'themes')] + } + return [path.posix.join(home, '.config', 'ghostty', 'themes')] +} + +function resourceThemeDirs(plat: 'darwin' | 'linux'): string[] { + if (process.env.GHOSTTY_RESOURCES_DIR) { + return [path.posix.join(process.env.GHOSTTY_RESOURCES_DIR, 'themes')] + } + + if (plat === 'darwin') { + return ['/Applications/Ghostty.app/Contents/Resources/ghostty/themes'] + } + return ['/usr/share/ghostty/themes', '/usr/local/share/ghostty/themes'] +} + +// Why: mirrors Ghostty's lookup — user themes shadow the bundled themes shipped +// inside the Ghostty resources directory. +export function getGhosttyThemeSearchDirs(): string[] { + const home = homedir() + const plat = platform() + if (plat !== 'darwin' && plat !== 'linux') { + // Why: Ghostty has no Windows build, so there are no named theme dirs to probe. + return [] + } + return [...xdgThemeDirs(home), ...resourceThemeDirs(plat)] +} + +function isMissingThemeError(err: unknown): boolean { + if (typeof err !== 'object' || err === null || !('code' in err)) { + return false + } + const code = (err as { code?: unknown }).code + return code === 'ENOENT' || code === 'ENOTDIR' +} + +function isRelativeThemeName(name: string): boolean { + return !name.includes('/') && !name.includes('\\') && name !== '..' && name !== '.' +} + +function themeColorsFromContent(content: string): ThemeColors { + const parsed = parseGhosttyConfig(content) + const colors: ThemeColors = {} + for (const [key, value] of Object.entries(parsed)) { + if (THEME_COLOR_KEYS.has(key)) { + colors[key] = value + } + } + return colors +} + +async function readThemeColors(themePath: string): Promise { + let content: string + try { + const info = await stat(themePath) + if (!info.isFile() || info.size > MAX_THEME_BYTES) { + return { status: 'invalid' } + } + content = await readFile(themePath, 'utf-8') + } catch (err) { + return { status: isMissingThemeError(err) ? 'missing' : 'invalid' } + } + + return { status: 'found', colors: themeColorsFromContent(content) } +} + +export async function resolveGhosttyThemeColors(name: string): Promise { + if (path.isAbsolute(name)) { + const result = await readThemeColors(name) + return result.status === 'found' ? result.colors : null + } + + // Why: relative theme names become filenames below; only absolute paths may + // contain separators, matching Ghostty's traversal guard. + if (!isRelativeThemeName(name)) { + return null + } + + for (const dir of getGhosttyThemeSearchDirs()) { + const themePath = path.posix.join(dir, name) + const result = await readThemeColors(themePath) + if (result.status === 'found') { + return result.colors + } + if (result.status === 'invalid') { + return null + } + } + return null +} diff --git a/src/renderer/src/components/settings/AccountsPane.test.tsx b/src/renderer/src/components/settings/AccountsPane.test.tsx index 0bf97d581ea..f30ed5440ec 100644 --- a/src/renderer/src/components/settings/AccountsPane.test.tsx +++ b/src/renderer/src/components/settings/AccountsPane.test.tsx @@ -65,8 +65,8 @@ describe('AccountsPane', () => { const markup = renderPane(getDefaultSettings('/tmp')) - expect(markup).toContain( - 'Mostrando cuentas para este dispositivo. Las nuevas cuentas se agregan allí.' + expect(markup).toMatch( + /Mostrando cuentas para [Ee]ste dispositivo\. Las nuevas cuentas se agregan allí\./ ) expect(markup).not.toContain('This device') }) From 2607ff110b1430c4f719ee39836265ad6b5cae4a Mon Sep 17 00:00:00 2001 From: PP <128917720+huhuhulululu@users.noreply.github.com> Date: Sat, 4 Jul 2026 17:19:00 -0400 Subject: [PATCH 21/45] feat(ghostty): import adjust-cell-height and dual-value window paddings (#7125) * feat(ghostty): import adjust-cell-height and dual-value window paddings Two Ghostty config forms previously landed in unsupportedKeys: - adjust-cell-height percentages (e.g. "35%") now map to terminalLineHeight (1.35), the direct xterm equivalent. Pixel values are still rejected because they depend on the rendered cell height. - window-padding-x/y "top,bottom" / "left,right" pairs now import as the rounded average, preserving the total padding along the axis. parseStrictInt moves to numeric-config-values.ts alongside the new pair parser to keep mapper.ts under the max-lines budget. * fix(ghostty): tighten import edge cases Co-authored-by: Orca * fix(settings): sentence-case account runtime copy Co-authored-by: Orca --------- Co-authored-by: Jinwoo-H Co-authored-by: Orca --- src/main/ghostty/mapper-extended.test.ts | 104 ++++++++++++++++++ src/main/ghostty/mapper.ts | 38 ++++--- src/main/ghostty/numeric-config-values.ts | 30 +++++ .../components/settings/AccountsPane.test.tsx | 4 +- .../src/components/settings/AccountsPane.tsx | 23 ++-- .../settings/GhosttyImportModal.test.ts | 19 ++++ .../src/components/settings/setting-labels.ts | 1 + 7 files changed, 192 insertions(+), 27 deletions(-) create mode 100644 src/main/ghostty/numeric-config-values.ts diff --git a/src/main/ghostty/mapper-extended.test.ts b/src/main/ghostty/mapper-extended.test.ts index 5c551139120..777110630c1 100644 --- a/src/main/ghostty/mapper-extended.test.ts +++ b/src/main/ghostty/mapper-extended.test.ts @@ -77,6 +77,110 @@ describe('mapGhosttyToOrca — window-padding', () => { expect(result.diff).toEqual({}) expect(result.unsupportedKeys).toEqual(['window-padding-x']) }) + + it('averages a dual-value window-padding-y', () => { + const result = mapGhosttyToOrca({ 'window-padding-y': '10,8' }) + expect(result.diff).toEqual({ terminalPaddingY: 9 }) + expect(result.unsupportedKeys).toEqual([]) + }) + + it('averages a dual-value window-padding-x with surrounding whitespace', () => { + const result = mapGhosttyToOrca({ 'window-padding-x': '16, 12' }) + expect(result.diff).toEqual({ terminalPaddingX: 14 }) + expect(result.unsupportedKeys).toEqual([]) + }) + + it('preserves odd-sum dual-value padding as a fractional average', () => { + const result = mapGhosttyToOrca({ 'window-padding-x': '1,2' }) + expect(result.diff).toEqual({ terminalPaddingX: 1.5 }) + expect(result.unsupportedKeys).toEqual([]) + }) + + it('rejects a dual-value padding with an invalid half', () => { + const result = mapGhosttyToOrca({ 'window-padding-y': '10,wide' }) + expect(result.diff).toEqual({}) + expect(result.unsupportedKeys).toEqual(['window-padding-y']) + }) + + it('rejects a dual-value padding with an out-of-range half', () => { + const result = mapGhosttyToOrca({ 'window-padding-y': '10,600' }) + expect(result.diff).toEqual({}) + expect(result.unsupportedKeys).toEqual(['window-padding-y']) + }) + + it('rejects paddings with more than two values', () => { + const result = mapGhosttyToOrca({ 'window-padding-y': '10,8,6' }) + expect(result.diff).toEqual({}) + expect(result.unsupportedKeys).toEqual(['window-padding-y']) + }) +}) + +describe('mapGhosttyToOrca — adjust-cell-height', () => { + it('maps a percentage to terminalLineHeight', () => { + const result = mapGhosttyToOrca({ 'adjust-cell-height': '35%' }) + expect(result.diff).toEqual({ terminalLineHeight: 1.35 }) + expect(result.unsupportedKeys).toEqual([]) + }) + + it('rounds the mapped line height to two decimals', () => { + const result = mapGhosttyToOrca({ 'adjust-cell-height': '33%' }) + expect(result.diff).toEqual({ terminalLineHeight: 1.33 }) + expect(result.unsupportedKeys).toEqual([]) + }) + + it('rounds half-boundary decimal percentages to the nearest hundredth', () => { + const result = mapGhosttyToOrca({ 'adjust-cell-height': '0.5%' }) + expect(result.diff).toEqual({ terminalLineHeight: 1.01 }) + expect(result.unsupportedKeys).toEqual([]) + }) + + it('rounds higher half-boundary decimal percentages to the nearest hundredth', () => { + const result = mapGhosttyToOrca({ 'adjust-cell-height': '1.5%' }) + expect(result.diff).toEqual({ terminalLineHeight: 1.02 }) + expect(result.unsupportedKeys).toEqual([]) + }) + + it('rounds high half-boundary decimal percentages without floating-point drift', () => { + const result = mapGhosttyToOrca({ 'adjust-cell-height': '101.5%' }) + expect(result.diff).toEqual({ terminalLineHeight: 2.02 }) + expect(result.unsupportedKeys).toEqual([]) + }) + + it('accepts the inclusive 1x line-height floor', () => { + const result = mapGhosttyToOrca({ 'adjust-cell-height': '0%' }) + expect(result.diff).toEqual({ terminalLineHeight: 1 }) + expect(result.unsupportedKeys).toEqual([]) + }) + + it('accepts the inclusive 3x line-height ceiling', () => { + const result = mapGhosttyToOrca({ 'adjust-cell-height': '200%' }) + expect(result.diff).toEqual({ terminalLineHeight: 3 }) + expect(result.unsupportedKeys).toEqual([]) + }) + + it('rejects a pixel value (not convertible to a line-height ratio)', () => { + const result = mapGhosttyToOrca({ 'adjust-cell-height': '2' }) + expect(result.diff).toEqual({}) + expect(result.unsupportedKeys).toEqual(['adjust-cell-height']) + }) + + it('rejects a negative percentage (below the 1x line-height floor)', () => { + const result = mapGhosttyToOrca({ 'adjust-cell-height': '-10%' }) + expect(result.diff).toEqual({}) + expect(result.unsupportedKeys).toEqual(['adjust-cell-height']) + }) + + it('rejects a percentage above the 3x line-height ceiling', () => { + const result = mapGhosttyToOrca({ 'adjust-cell-height': '250%' }) + expect(result.diff).toEqual({}) + expect(result.unsupportedKeys).toEqual(['adjust-cell-height']) + }) + + it('rejects values that only fall inside the ceiling after rounding', () => { + const result = mapGhosttyToOrca({ 'adjust-cell-height': '200.4%' }) + expect(result.diff).toEqual({}) + expect(result.unsupportedKeys).toEqual(['adjust-cell-height']) + }) }) describe('mapGhosttyToOrca — cursor-text', () => { diff --git a/src/main/ghostty/mapper.ts b/src/main/ghostty/mapper.ts index 69abcbff4d7..7c0588f4c2b 100644 --- a/src/main/ghostty/mapper.ts +++ b/src/main/ghostty/mapper.ts @@ -4,6 +4,7 @@ import type { GhosttyImportPreview } from '../../shared/types' import { HEX_COLOR_RE } from '../../shared/color-validation' +import { parsePaddingValue, parseStrictInt } from './numeric-config-values' const PALETTE_INDEX_MAP: Record = { 0: 'black', @@ -34,18 +35,6 @@ type FieldResult = const normalizeHex = (v: string): string => (v.startsWith('#') ? v : `#${v}`) -// Why: `Number("1e10")` succeeds and passes `Number.isInteger`, so a Ghostty -// config with `window-padding-x = 1e9` would sail through the mapper and land -// an absurd value in the store. Restrict to plain decimal integers. -const STRICT_INT_RE = /^-?\d+$/ -const parseStrictInt = (v: string): number | null => { - if (!STRICT_INT_RE.test(v)) { - return null - } - const num = Number(v) - return Number.isFinite(num) ? num : null -} - type FieldParser = (value: string, rawValue: string | string[]) => FieldResult export function mapGhosttyToOrca( @@ -171,21 +160,38 @@ export function mapGhosttyToOrca( }, 'window-padding-x': (v) => { - const num = parseStrictInt(v) - if (num === null || num < 0 || num > 512) { + const num = parsePaddingValue(v) + if (num === null) { return null } return { key: 'terminalPaddingX', value: num } }, 'window-padding-y': (v) => { - const num = parseStrictInt(v) - if (num === null || num < 0 || num > 512) { + const num = parsePaddingValue(v) + if (num === null) { return null } return { key: 'terminalPaddingY', value: num } }, + // Why: only percentages translate to xterm's line-height ratio; Ghostty's + // pixel form depends on the rendered cell height, which we can't know here. + // The settings UI clamps terminalLineHeight to [1, 3], so reject outside it. + 'adjust-cell-height': (v) => { + const match = /^\+?(\d+(?:\.\d+)?)%$/.exec(v.trim()) + if (!match) { + return null + } + const percent = Number(match[1]) + const rawLineHeight = 1 + percent / 100 + if (rawLineHeight > 3) { + return null + } + const lineHeight = Math.round(100 + percent) / 100 + return { key: 'terminalLineHeight', value: lineHeight } + }, + 'cursor-text': (v) => { if (!HEX_COLOR_RE.test(v)) { return null diff --git a/src/main/ghostty/numeric-config-values.ts b/src/main/ghostty/numeric-config-values.ts new file mode 100644 index 00000000000..f8a7ce7b468 --- /dev/null +++ b/src/main/ghostty/numeric-config-values.ts @@ -0,0 +1,30 @@ +// Why: `Number("1e10")` succeeds and passes `Number.isInteger`, so a Ghostty +// config with `window-padding-x = 1e9` would sail through the mapper and land +// an absurd value in the store. Restrict to plain decimal integers. +const STRICT_INT_RE = /^-?\d+$/ +export const parseStrictInt = (v: string): number | null => { + if (!STRICT_INT_RE.test(v)) { + return null + } + const num = Number(v) + return Number.isFinite(num) ? num : null +} + +// Why: Ghostty accepts "top,bottom" / "left,right" pairs for window paddings, +// but Orca stores a single value per axis — average the pair so the total +// padding along the axis stays the same. +export const parsePaddingValue = (v: string): number | null => { + const parts = v.split(',') + if (parts.length > 2) { + return null + } + const nums: number[] = [] + for (const part of parts) { + const num = parseStrictInt(part.trim()) + if (num === null || num < 0 || num > 512) { + return null + } + nums.push(num) + } + return nums.reduce((sum, num) => sum + num, 0) / nums.length +} diff --git a/src/renderer/src/components/settings/AccountsPane.test.tsx b/src/renderer/src/components/settings/AccountsPane.test.tsx index f30ed5440ec..3e247f88db5 100644 --- a/src/renderer/src/components/settings/AccountsPane.test.tsx +++ b/src/renderer/src/components/settings/AccountsPane.test.tsx @@ -54,8 +54,8 @@ describe('AccountsPane', () => { it('keeps the runtime label inside the localized account copy', () => { const markup = renderPane(getDefaultSettings('/tmp')) - expect(markup).toContain('Showing accounts for This device. New accounts are added there.') - expect(markup).toContain('authenticate with Google for This device. This uses credentials') + expect(markup).toContain('Showing accounts for this device. New accounts are added there.') + expect(markup).toContain('authenticate with Google for this device. This uses credentials') expect(markup).not.toContain('ShowingThis device') expect(markup).not.toContain('forThis device') }) diff --git a/src/renderer/src/components/settings/AccountsPane.tsx b/src/renderer/src/components/settings/AccountsPane.tsx index 947ec64c228..d50003b5122 100644 --- a/src/renderer/src/components/settings/AccountsPane.tsx +++ b/src/renderer/src/components/settings/AccountsPane.tsx @@ -264,6 +264,11 @@ export function AccountsPane({ wslDistros, wslCapabilitiesLoading ) + // Why: host runtime labels are standalone UI labels; interpolated prose needs sentence casing. + const accountRuntimeSentenceLabel = + accountRuntime.runtime === 'host' && !navigator.userAgent.includes('Windows') + ? `${accountRuntime.label.charAt(0).toLocaleLowerCase()}${accountRuntime.label.slice(1)}` + : accountRuntime.label const [codexAccounts, setCodexAccounts] = useState({ accounts: [], @@ -611,7 +616,7 @@ export function AccountsPane({ {translate( 'auto.components.settings.AccountsPane.c0a52abfc5', 'Showing accounts for {{value0}}. New accounts are added there.', - { value0: accountRuntime.label } + { value0: accountRuntimeSentenceLabel } )}

@@ -693,7 +698,7 @@ export function AccountsPane({ {translate( 'auto.components.settings.AccountsPane.e05d0ff737', 'Use your current {{value0}} Claude login.', - { value0: accountRuntime.label } + { value0: accountRuntimeSentenceLabel } )} @@ -703,7 +708,7 @@ export function AccountsPane({ {translate( 'auto.components.settings.AccountsPane.3fe7862418', "No managed Claude accounts for {{value0}}. Orca will use that environment's system default Claude login until you add one here.", - { value0: accountRuntime.label } + { value0: accountRuntimeSentenceLabel } )} ) : ( @@ -863,7 +868,7 @@ export function AccountsPane({ : translate( 'auto.components.settings.AccountsPane.e4a28e8894', 'Codex reported that the {{value0}} login needs a fresh sign-in. Sign in again before starting new Codex sessions.', - { value0: accountRuntime.label } + { value0: accountRuntimeSentenceLabel } )} @@ -877,7 +882,7 @@ export function AccountsPane({ {translate( 'auto.components.settings.AccountsPane.c0a52abfc5', 'Showing accounts for {{value0}}. New accounts are added there.', - { value0: accountRuntime.label } + { value0: accountRuntimeSentenceLabel } )}

@@ -964,12 +969,12 @@ export function AccountsPane({ ? translate( 'auto.components.settings.AccountsPane.fd62f37c24', 'Codex reported this {{value0}} login is out of date.', - { value0: accountRuntime.label } + { value0: accountRuntimeSentenceLabel } ) : translate( 'auto.components.settings.AccountsPane.fcc4093fc1', 'Use your current {{value0}} Codex login.', - { value0: accountRuntime.label } + { value0: accountRuntimeSentenceLabel } )} @@ -979,7 +984,7 @@ export function AccountsPane({ {translate( 'auto.components.settings.AccountsPane.b4c9450319', "No managed Codex accounts for {{value0}}. Orca will use that environment's system default Codex login until you add one here.", - { value0: accountRuntime.label } + { value0: accountRuntimeSentenceLabel } )} ) : ( @@ -1177,7 +1182,7 @@ export function AccountsPane({ {translate( 'auto.components.settings.AccountsPane.c2aee76420', 'Extracts OAuth credentials from your local Gemini CLI installation to authenticate with Google for {{value0}}. This uses credentials issued to the Gemini CLI app, not Orca. May break if Google updates the CLI. Use at your own risk.', - { value0: accountRuntime.label } + { value0: accountRuntimeSentenceLabel } )}

diff --git a/src/renderer/src/components/settings/GhosttyImportModal.test.ts b/src/renderer/src/components/settings/GhosttyImportModal.test.ts index 6806f9a2151..67590e710f2 100644 --- a/src/renderer/src/components/settings/GhosttyImportModal.test.ts +++ b/src/renderer/src/components/settings/GhosttyImportModal.test.ts @@ -99,6 +99,25 @@ describe('GhosttyImportModal', () => { expect(onOpenChange).toHaveBeenCalledWith(false) }) + it('labels Ghostty line-height imports with the settings display name', () => { + const element = GhosttyImportModal({ + open: true, + onOpenChange: () => {}, + preview: { + found: true, + configPath: '/Users/alice/.config/ghostty/config', + diff: { terminalLineHeight: 1.35 }, + unsupportedKeys: [] + }, + loading: false, + onApply: () => {}, + applied: false + }) + + expect(containsText(element, 'Line Height')).toBe(true) + expect(containsText(element, 'terminalLineHeight')).toBe(false) + }) + it('renders success summary with done button when applied', () => { const onOpenChange = vi.fn() const onApply = vi.fn() diff --git a/src/renderer/src/components/settings/setting-labels.ts b/src/renderer/src/components/settings/setting-labels.ts index cdc459d9790..0f108fadeae 100644 --- a/src/renderer/src/components/settings/setting-labels.ts +++ b/src/renderer/src/components/settings/setting-labels.ts @@ -4,6 +4,7 @@ export const SETTING_LABELS: Partial> = { terminalFontSize: 'Font Size', terminalFontFamily: 'Font Family', terminalFontWeight: 'Font Weight', + terminalLineHeight: 'Line Height', terminalScrollSensitivity: 'Normal Scroll Speed', terminalFastScrollSensitivity: 'Fast Scroll Speed', terminalTuiScrollSensitivity: 'TUI Scroll Speed', From fa3ca498c2609d381ca0b66fb8b9e9ee376c94df Mon Sep 17 00:00:00 2001 From: Vladislav Meshkorudnyj Date: Sat, 4 Jul 2026 23:38:05 +0200 Subject: [PATCH 22/45] fix(secure-file): re-harden on coarse-ctime filesystems Store POSIX mode bits in secure-file hardening cache entries so permission drift is detected even when ctime granularity is coarse. Also clears inherited HISTFILE in the local PTY test harness for hermetic WSL history assertions, and adds a deterministic coarse-ctime regression test for directory and credential-file mode drift. --- src/main/providers/local-pty-provider.test.ts | 9 ++ src/shared/secure-file-coarse-ctime.test.ts | 123 ++++++++++++++++++ src/shared/secure-file.ts | 11 ++ 3 files changed, 143 insertions(+) create mode 100644 src/shared/secure-file-coarse-ctime.test.ts diff --git a/src/main/providers/local-pty-provider.test.ts b/src/main/providers/local-pty-provider.test.ts index bc236e9f9bd..d37faa0ac57 100644 --- a/src/main/providers/local-pty-provider.test.ts +++ b/src/main/providers/local-pty-provider.test.ts @@ -98,6 +98,7 @@ describe('LocalPtyProvider', () => { let exitCb: ((info: { exitCode: number }) => void) | undefined let origShell: string | undefined let origPowerlevelWizardDisable: string | undefined + let origHistFile: string | undefined let origPlatform: PropertyDescriptor | undefined beforeEach(() => { @@ -105,8 +106,11 @@ describe('LocalPtyProvider', () => { Object.defineProperty(process, 'platform', { configurable: true, value: 'linux' }) origShell = process.env.SHELL origPowerlevelWizardDisable = process.env.POWERLEVEL9K_DISABLE_CONFIGURATION_WIZARD + origHistFile = process.env.HISTFILE process.env.SHELL = '/bin/zsh' delete process.env.POWERLEVEL9K_DISABLE_CONFIGURATION_WIZARD + // injectHistoryEnv preserves an inherited HISTFILE, so clear it for hermetic history assertions. + delete process.env.HISTFILE existsSyncMock.mockReturnValue(true) statSyncMock.mockReturnValue({ isDirectory: () => true, mode: 0o755 }) @@ -158,6 +162,11 @@ describe('LocalPtyProvider', () => { } else { process.env.POWERLEVEL9K_DISABLE_CONFIGURATION_WIZARD = origPowerlevelWizardDisable } + if (origHistFile === undefined) { + delete process.env.HISTFILE + } else { + process.env.HISTFILE = origHistFile + } }) describe('spawn', () => { diff --git a/src/shared/secure-file-coarse-ctime.test.ts b/src/shared/secure-file-coarse-ctime.test.ts new file mode 100644 index 00000000000..64c33ba36f1 --- /dev/null +++ b/src/shared/secure-file-coarse-ctime.test.ts @@ -0,0 +1,123 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +const { chmodSyncMock, existsSyncMock, paths, statSyncMock, state } = vi.hoisted(() => ({ + chmodSyncMock: vi.fn(), + existsSyncMock: vi.fn(), + paths: { + dir: '/secure', + file: '/secure/secret.json' + }, + statSyncMock: vi.fn(), + state: { + dirMode: 0o755, + fileMode: 0o600 + } +})) + +vi.mock('fs', () => ({ + chmodSync: chmodSyncMock, + existsSync: existsSyncMock, + mkdirSync: vi.fn(), + renameSync: vi.fn(), + rmSync: vi.fn(), + statSync: statSyncMock, + writeFileSync: vi.fn() +})) + +vi.mock('child_process', () => ({ + execFile: vi.fn(), + execFileSync: vi.fn() +})) + +import { + __resetSecureFileHardenedPathsForTests, + __resetSecureFileWindowsUserSidForTests, + hardenExistingSecureFile +} from './secure-file' + +describe('secure-file coarse-ctime mode drift', () => { + const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform') + + beforeEach(() => { + Object.defineProperty(process, 'platform', { configurable: true, value: 'linux' }) + state.dirMode = 0o755 + state.fileMode = 0o600 + existsSyncMock.mockReturnValue(true) + statSyncMock.mockImplementation((targetPath: string) => { + const path = String(targetPath) + if (path === paths.dir) { + return fakeStats(true, state.dirMode) + } + if (path === paths.file) { + return fakeStats(false, state.fileMode) + } + throw new Error(`unexpected stat path ${path}`) + }) + chmodSyncMock.mockImplementation((targetPath: string, mode: number) => { + const path = String(targetPath) + if (path === paths.dir) { + state.dirMode = mode & 0o777 + } else if (path === paths.file) { + state.fileMode = mode & 0o777 + } + }) + __resetSecureFileWindowsUserSidForTests() + __resetSecureFileHardenedPathsForTests() + }) + + afterEach(() => { + chmodSyncMock.mockReset() + existsSyncMock.mockReset() + statSyncMock.mockReset() + __resetSecureFileWindowsUserSidForTests() + __resetSecureFileHardenedPathsForTests() + if (originalPlatform) { + Object.defineProperty(process, 'platform', originalPlatform) + } + }) + + it('re-hardens a directory when only POSIX mode changes under stable timestamps', () => { + hardenExistingSecureFile(paths.file) + expect(chmodSyncMock.mock.calls).toEqual([ + [paths.dir, 0o700], + [paths.file, 0o600] + ]) + + chmodSyncMock.mockClear() + // Coarse-ctime filesystems can report identical timestamps after chmod; only mode changes. + state.dirMode = 0o755 + + hardenExistingSecureFile(paths.file) + + expect(chmodSyncMock.mock.calls).toEqual([[paths.dir, 0o700]]) + }) + + it('re-hardens a file when only POSIX mode changes under stable timestamps', () => { + hardenExistingSecureFile(paths.file) + expect(chmodSyncMock.mock.calls).toEqual([ + [paths.dir, 0o700], + [paths.file, 0o600] + ]) + + chmodSyncMock.mockClear() + // Coarse-ctime filesystems can report identical timestamps after chmod; only mode changes. + state.fileMode = 0o644 + + hardenExistingSecureFile(paths.file) + + expect(chmodSyncMock.mock.calls).toEqual([[paths.file, 0o600]]) + }) +}) + +function fakeStats(isDirectory: boolean, mode: number) { + return { + birthtimeMs: 10, + ctimeMs: 20, + dev: 30, + ino: isDirectory ? 40 : 41, + isDirectory: () => isDirectory, + mode, + mtimeMs: 50, + size: isDirectory ? 0 : 2 + } +} diff --git a/src/shared/secure-file.ts b/src/shared/secure-file.ts index 146c62b09e0..1ed451b5633 100644 --- a/src/shared/secure-file.ts +++ b/src/shared/secure-file.ts @@ -18,6 +18,7 @@ type HardenedPathCacheEntry = { dev: number ino: number size: number + mode: number ctimeMs: number mtimeMs: number birthtimeMs: number @@ -130,6 +131,7 @@ export function hardenExistingSecureFile(targetPath: string): void { } } +/** Applies the platform-appropriate permission restriction to a path once, bypassing the cache. */ export function hardenSecurePath( targetPath: string, options: { @@ -146,6 +148,7 @@ export function hardenSecurePath( ) } +/** Applies hardening; async Windows calls only report that best-effort ACL work was accepted. */ function applySecurePathRestriction( targetPath: string, isDirectory: boolean, @@ -170,6 +173,7 @@ function applySecurePathRestriction( return true } +/** Caches the current metadata snapshot for a just-hardened path, or clears it if the path is gone. */ function rememberHardenedPath(targetPath: string, isDirectory: boolean): void { const entry = getHardenedPathCacheEntry(targetPath, isDirectory) if (entry) { @@ -179,6 +183,10 @@ function rememberHardenedPath(targetPath: string, isDirectory: boolean): void { } } +/** + * Snapshots a path's identity, mode, and timestamps so later drift is detectable. + * Mode is tracked directly so a chmod is caught even where coarse ctime granularity hides it. + */ function getHardenedPathCacheEntry( targetPath: string, isDirectory: boolean @@ -193,6 +201,7 @@ function getHardenedPathCacheEntry( dev: stats.dev, ino: stats.ino, size: stats.size, + mode: stats.mode & 0o777, ctimeMs: stats.ctimeMs, mtimeMs: stats.mtimeMs, birthtimeMs: stats.birthtimeMs @@ -202,6 +211,7 @@ function getHardenedPathCacheEntry( } } +/** True when two snapshots describe the same unchanged path (identity, mode, timestamps). */ function hardenedPathCacheEntriesMatch( a: HardenedPathCacheEntry, b: HardenedPathCacheEntry @@ -211,6 +221,7 @@ function hardenedPathCacheEntriesMatch( a.dev === b.dev && a.ino === b.ino && a.size === b.size && + a.mode === b.mode && a.ctimeMs === b.ctimeMs && a.mtimeMs === b.mtimeMs && a.birthtimeMs === b.birthtimeMs From a3e34b66b8dc9ab0707fb17cdec453e83e49f919 Mon Sep 17 00:00:00 2001 From: Bevis Date: Sun, 5 Jul 2026 05:43:57 +0800 Subject: [PATCH 23/45] fix(pet): stop i18n from corrupting CSS keyframes Keep PetOverlay runtime keyframes out of localization so translated locales cannot invalidate sprite or bob animations.\n\nAlso adds regression coverage for translated-locale keyframe CSS and removes the stale generated locale entries. --- config/scripts/locale-ko-key-overrides.json | 6 - .../pet/PetOverlay.keyframes.test.tsx | 107 ++++++++++++++++++ .../src/components/pet/PetOverlay.tsx | 23 ++-- src/renderer/src/i18n/locales/en.json | 4 - src/renderer/src/i18n/locales/es.json | 4 - src/renderer/src/i18n/locales/ja.json | 4 - src/renderer/src/i18n/locales/ko.json | 4 - src/renderer/src/i18n/locales/zh.json | 4 - 8 files changed, 116 insertions(+), 40 deletions(-) create mode 100644 src/renderer/src/components/pet/PetOverlay.keyframes.test.tsx diff --git a/config/scripts/locale-ko-key-overrides.json b/config/scripts/locale-ko-key-overrides.json index 071a249e01c..953eb8a21fc 100644 --- a/config/scripts/locale-ko-key-overrides.json +++ b/config/scripts/locale-ko-key-overrides.json @@ -1559,12 +1559,6 @@ "auto.components.onboarding.ThemeStep.94b9dc561d": { "ko": "설정 -> Terminal" }, - "auto.components.pet.PetOverlay.4712d196c6": { - "ko": "@keyframes pet-{{value0}} { from { background-position: {{value1}}px {{value2}}px; } to { background-position: {{value3}}px {{value4}}px; } }" - }, - "auto.components.pet.PetOverlay.de932b0e8f": { - "ko": "@keyframes pet-bob { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-4px); } }" - }, "auto.components.repo.NestedRepoChecklist.ea54c7bf8f": { "ko": "/" }, diff --git a/src/renderer/src/components/pet/PetOverlay.keyframes.test.tsx b/src/renderer/src/components/pet/PetOverlay.keyframes.test.tsx new file mode 100644 index 00000000000..5e264ff06b6 --- /dev/null +++ b/src/renderer/src/components/pet/PetOverlay.keyframes.test.tsx @@ -0,0 +1,107 @@ +// @vitest-environment happy-dom + +import { act } from 'react' +import { createRoot, type Root } from 'react-dom/client' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +import { i18n } from '../../i18n/i18n' + +const storeState = vi.hoisted(() => ({ + agentStatusByPaneKey: {}, + agentStatusEpoch: 0, + retainedAgentsByPaneKey: {}, + petSize: 180 +})) + +vi.mock('../../store', () => ({ + useAppStore: Object.assign( + (selector: (state: typeof storeState) => T): T => { + return selector(storeState) + }, + { + getState: () => storeState + } + ) +})) + +vi.mock('./usePetUrl', () => ({ + usePetUrl: () => ({ + url: 'blob:custom-pet', + ready: true, + sprite: { + frameWidth: 32, + frameHeight: 24, + columns: 6, + rows: 1, + sheetWidth: 192, + sheetHeight: 24, + fps: 6, + defaultAnimation: 'idle', + animations: { + idle: { row: 0, frames: 6 } + } + }, + detected: null + }) +})) + +import { PetOverlay } from './PetOverlay' + +function renderPetOverlay(): { container: HTMLDivElement; root: Root } { + const container = document.createElement('div') + document.body.appendChild(container) + const root = createRoot(container) + act(() => { + root.render() + }) + return { container, root } +} + +function installLocalStorage(): void { + const values = new Map() + Object.defineProperty(window, 'localStorage', { + configurable: true, + value: { + clear: () => values.clear(), + getItem: (key: string) => values.get(key) ?? null, + removeItem: (key: string) => values.delete(key), + setItem: (key: string, value: string) => values.set(key, value) + } + }) +} + +describe('PetOverlay sprite keyframes', () => { + let root: Root | null = null + let container: HTMLDivElement | null = null + + beforeEach(async () => { + await i18n.changeLanguage('zh') + installLocalStorage() + }) + + afterEach(async () => { + if (root) { + act(() => root?.unmount()) + } + container?.remove() + root = null + container = null + await i18n.changeLanguage('en') + }) + + it('keeps custom spritesheet keyframes valid in translated locales', () => { + ;({ container, root } = renderPetOverlay()) + + const css = Array.from(container.querySelectorAll('style')) + .map((style) => style.textContent ?? '') + .join('\n') + + expect(css).toContain('@keyframes pet-bob') + expect(css).toContain('transform: translateY(0)') + expect(css).toContain('background-position:') + expect(css).toContain('to { background-position:') + expect(css).not.toContain('变换') + expect(css).not.toContain('背景位置') + expect(css).not.toContain('到 {') + }) +}) diff --git a/src/renderer/src/components/pet/PetOverlay.tsx b/src/renderer/src/components/pet/PetOverlay.tsx index e55004a4788..89fc81dc6da 100644 --- a/src/renderer/src/components/pet/PetOverlay.tsx +++ b/src/renderer/src/components/pet/PetOverlay.tsx @@ -6,7 +6,6 @@ import type { CustomPet } from '../../../../shared/types' import { useAppStore } from '../../store' import { AGENT_STATUS_STALE_AFTER_MS } from '../../../../shared/agent-status-types' import { selectPetAnimationName, type PetAnimationName } from './pet-agent-state' -import { translate } from '@/i18n/i18n' type Sprite = NonNullable @@ -66,15 +65,12 @@ function SpriteFrame({ const startY = -(row * sprite.frameHeight * scale) const endX = -(frames * sprite.frameWidth * scale) const duration = Math.max(0.1, frames / Math.max(0.1, sprite.fps)) + // Why: sprite keyframes are runtime CSS, not user-visible copy; translated + // CSS keywords make the browser discard the animation. + const keyframesCss = `@keyframes pet-${animKeyframesId} { from { background-position: ${startX}px ${startY}px; } to { background-position: ${endX}px ${startY}px; } }` return ( <> - +
- + {sprite ? ( Date: Sat, 4 Jul 2026 17:46:09 -0400 Subject: [PATCH 24/45] Fix missing space in import-cookies "From " menu labels (#6468) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix missing space in import-cookies "From " menu labels The Import Cookies dropdown rendered browser entries as "FromGoogle Chrome" and "FromSafari" — JSX collapses whitespace between the adjacent {translate(…, 'From')}{browser.label} expressions. Switch both settings menus to the 'From {{value0}}' interpolation pattern already used by the other import-cookies menus, so the label is a single translatable unit and renders with the correct space. Interpolation also lets each locale position the browser name correctly: ja/ko use postpositions ("から"/"에서") that belong after the name, which the old concatenation placed before it. Localized the new key in es/ja/ko/zh to match the existing catalog style. Adds a regression test. Co-Authored-By: Claude Opus 4.8 (1M context) * Fix browser use import source label Co-authored-by: Orca --------- Co-authored-by: Claude Opus 4.8 (1M context) Co-authored-by: Jinwoo-H Co-authored-by: Orca --- .../components/settings/BrowserProfileRow.tsx | 14 +++-- .../settings/BrowserUseCookieImportStep.tsx | 18 ++++--- .../browser-cookie-import-label.test.ts | 53 +++++++++++++++++++ src/renderer/src/i18n/locales/en.json | 8 +-- src/renderer/src/i18n/locales/es.json | 8 +-- src/renderer/src/i18n/locales/ja.json | 8 +-- src/renderer/src/i18n/locales/ko.json | 8 +-- src/renderer/src/i18n/locales/zh.json | 8 +-- 8 files changed, 100 insertions(+), 25 deletions(-) create mode 100644 src/renderer/src/components/settings/browser-cookie-import-label.test.ts diff --git a/src/renderer/src/components/settings/BrowserProfileRow.tsx b/src/renderer/src/components/settings/BrowserProfileRow.tsx index 6c3c06dd3df..0d1c377c45f 100644 --- a/src/renderer/src/components/settings/BrowserProfileRow.tsx +++ b/src/renderer/src/components/settings/BrowserProfileRow.tsx @@ -174,8 +174,11 @@ export function BrowserProfileRow({ browser.profiles.length > 1 ? ( - {translate('auto.components.settings.BrowserProfileRow.7df818977e', 'From')} - {browser.label} + {translate( + 'auto.components.settings.BrowserProfileRow.c5a273a809', + 'From {{value0}}', + { value0: browser.label } + )} @@ -197,8 +200,11 @@ export function BrowserProfileRow({ key={browser.family} onSelect={() => void handleImportFromBrowser(browser.family)} > - {translate('auto.components.settings.BrowserProfileRow.7df818977e', 'From')} - {browser.label} + {translate( + 'auto.components.settings.BrowserProfileRow.c5a273a809', + 'From {{value0}}', + { value0: browser.label } + )} ) )} diff --git a/src/renderer/src/components/settings/BrowserUseCookieImportStep.tsx b/src/renderer/src/components/settings/BrowserUseCookieImportStep.tsx index 5104ace581f..f73668fa908 100644 --- a/src/renderer/src/components/settings/BrowserUseCookieImportStep.tsx +++ b/src/renderer/src/components/settings/BrowserUseCookieImportStep.tsx @@ -116,9 +116,9 @@ export function BrowserUseCookieImportStep({

{translate( 'auto.components.settings.BrowserUsePane.112f70adc4', - 'Last imported from' + 'Last imported from {{value0}}', + { value0: sourceLabel } )} - {sourceLabel}

) : null} {onConfigureMoreBrowsers ? ( @@ -163,8 +163,11 @@ export function BrowserUseCookieImportStep({ browser.profiles.length > 1 ? ( - {translate('auto.components.settings.BrowserUsePane.e44c5d681e', 'From')} - {browser.label} + {translate( + 'auto.components.settings.BrowserUsePane.5301857d88', + 'From {{value0}}', + { value0: browser.label } + )} @@ -186,8 +189,11 @@ export function BrowserUseCookieImportStep({ key={browser.family} onSelect={() => void handleImportFromBrowser(browser.family)} > - {translate('auto.components.settings.BrowserUsePane.e44c5d681e', 'From')} - {browser.label} + {translate( + 'auto.components.settings.BrowserUsePane.5301857d88', + 'From {{value0}}', + { value0: browser.label } + )} ) )} diff --git a/src/renderer/src/components/settings/browser-cookie-import-label.test.ts b/src/renderer/src/components/settings/browser-cookie-import-label.test.ts new file mode 100644 index 00000000000..108a2e4c8d2 --- /dev/null +++ b/src/renderer/src/components/settings/browser-cookie-import-label.test.ts @@ -0,0 +1,53 @@ +import { beforeEach, describe, expect, it } from 'vitest' + +import en from '@/i18n/locales/en.json' +import { i18n, translate } from '@/i18n/i18n' + +// Keys for the "From " import-cookies menu entries. The label is built +// by interpolating the browser name into a single localized string; a regression +// where the name was concatenated directly onto "From" dropped the space and +// rendered e.g. "FromGoogle Chrome". +const IMPORT_FROM_KEYS = [ + 'auto.components.settings.BrowserProfileRow.c5a273a809', + 'auto.components.settings.BrowserUsePane.5301857d88' +] +const LAST_IMPORTED_FROM_KEY = 'auto.components.settings.BrowserUsePane.112f70adc4' + +function catalogEntry(key: string): unknown { + return key.split('.').reduce((node, part) => { + if (typeof node === 'object' && node !== null) { + return (node as Record)[part] + } + return undefined + }, en) +} + +describe('import-cookies "From " label', () => { + beforeEach(async () => { + await i18n.changeLanguage('en') + }) + + it('keeps a space between "From" and the browser name', () => { + for (const key of IMPORT_FROM_KEYS) { + expect(translate(key, 'From {{value0}}', { value0: 'Google Chrome' })).toBe( + 'From Google Chrome' + ) + expect(translate(key, 'From {{value0}}', { value0: 'Safari' })).toBe('From Safari') + } + }) + + it('stores the catalog entry with an interpolation placeholder', () => { + for (const key of IMPORT_FROM_KEYS) { + expect(catalogEntry(key)).toBe('From {{value0}}') + } + expect(catalogEntry(LAST_IMPORTED_FROM_KEY)).toBe('Last imported from {{value0}}') + }) + + it('keeps a space in the last-imported source label', () => { + expect( + translate(LAST_IMPORTED_FROM_KEY, 'Last imported from {{value0}}', { + value0: 'Google Chrome' + }) + ).toBe('Last imported from Google Chrome') + }) +}) diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index f952a80d2ba..ca2bbd195a2 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -4882,7 +4882,8 @@ "d420c43729": "Imported {{value0}} cookies from {{value1}}{{value2}} into {{value3}}.", "b4c167764d": "Imported {{value0}} cookies from file into {{value1}}.", "a3f8c2d1e0b4": "Imported {{value0}} cookies from {{value1}} ({{value2}}) into {{value3}}.", - "b4e9d3f2a1c5": "Imported {{value0}} cookies from {{value1}} into {{value2}}." + "b4e9d3f2a1c5": "Imported {{value0}} cookies from {{value1}} into {{value2}}.", + "c5a273a809": "From {{value0}}" }, "BrowserUseComputerUseNotice": { "15b5e680ba": "Open Computer Use", @@ -4906,7 +4907,7 @@ "be6df68384": "From File…", "e44c5d681e": "From", "67d9a53f47": "Manage profiles for separate logins", - "112f70adc4": "Last imported from", + "112f70adc4": "Last imported from {{value0}}", "72d4815523": "Bring your existing logins into Orca so agents can reach authenticated pages. Imports into the default profile.", "2eb906706c": "Import Browser Cookies", "af8c83ed61": "Import cookies from Chrome, Edge, or other browsers so agents can reuse your logins.", @@ -4928,7 +4929,8 @@ "ad8cb0ee22": "Fix PATH", "0289434ed6": "Enabled", "8b3054dac7": "Registering...", - "8f2675c2f3": "Imported {{value0}} cookies from file." + "8f2675c2f3": "Imported {{value0}} cookies from file.", + "5301857d88": "From {{value0}}" }, "BrowserUseSkillStep": { "0871b6998d": "Enables agents to navigate and verify pages in Orca's browser.", diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json index 23d826fc893..9d4cdf90b27 100644 --- a/src/renderer/src/i18n/locales/es.json +++ b/src/renderer/src/i18n/locales/es.json @@ -4882,7 +4882,8 @@ "d420c43729": "Se importaron {{value0}} cookies de {{value1}}{{value2}} a {{value3}}.", "b4c167764d": "Se importaron {{value0}} cookies desde un archivo a {{value1}}.", "a3f8c2d1e0b4": "Se importaron {{value0}} cookies de {{value1}} ({{value2}}) a {{value3}}.", - "b4e9d3f2a1c5": "Se importaron {{value0}} cookies de {{value1}} a {{value2}}." + "b4e9d3f2a1c5": "Se importaron {{value0}} cookies de {{value1}} a {{value2}}.", + "c5a273a809": "Desde {{value0}}" }, "BrowserUseComputerUseNotice": { "15b5e680ba": "Abrir Computer Use", @@ -4906,7 +4907,7 @@ "be6df68384": "Desde archivo…", "e44c5d681e": "Desde ", "67d9a53f47": "Gestionar perfiles para logins separados", - "112f70adc4": "Última importación desde ", + "112f70adc4": "Última importación desde {{value0}}", "72d4815523": "Trae tus logins existentes a Orca para que los agents puedan acceder a páginas autenticadas. Importa al perfil predeterminado.", "2eb906706c": "Importar cookies del navegador", "af8c83ed61": "Importa cookies de Chrome, Edge u otros navegadores para que los agents puedan reutilizar tus logins.", @@ -4928,7 +4929,8 @@ "ad8cb0ee22": "Corregir PATH", "0289434ed6": "Activado", "8b3054dac7": "Registrando...", - "8f2675c2f3": "Se importaron {{value0}} cookies desde un archivo." + "8f2675c2f3": "Se importaron {{value0}} cookies desde un archivo.", + "5301857d88": "Desde {{value0}}" }, "BrowserUseSkillStep": { "0871b6998d": "Permite a los agents navegar y verificar páginas en el navegador de Orca.", diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index 19a1528be64..775f4b584b3 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -4867,7 +4867,8 @@ "d420c43729": "{{value0}} Cookie を {{value1}}{{value2}} から {{value3}} にインポートしました。", "b4c167764d": "ファイルから {{value0}} 個の Cookie を {{value1}} にインポートしました。", "a3f8c2d1e0b4": "{{value1}} ({{value2}}) から {{value0}} 件の Cookie を {{value3}} にインポートしました。", - "b4e9d3f2a1c5": "{{value1}} から {{value0}} 件の Cookie を {{value2}} にインポートしました。" + "b4e9d3f2a1c5": "{{value1}} から {{value0}} 件の Cookie を {{value2}} にインポートしました。", + "c5a273a809": "{{value0}} から" }, "BrowserUseComputerUseNotice": { "15b5e680ba": "Open Computer Use", @@ -4891,7 +4892,7 @@ "be6df68384": "ファイルから…", "e44c5d681e": "から", "67d9a53f47": "個別のログインのプロファイルを管理する", - "112f70adc4": "最後にインポートされたのは", + "112f70adc4": "{{value0}} から最後にインポート", "72d4815523": "既存のログインを Orca に取り込み、agents が認証されたページにアクセスできるようにします。デフォルトのプロファイルにインポートします。", "2eb906706c": "ブラウザの Cookie をインポートする", "af8c83ed61": "Chrome、Edge、またはその他のブラウザから Cookie をインポートすると、agents がログインを再利用できるようになります。", @@ -4913,7 +4914,8 @@ "ad8cb0ee22": "パスを修正する", "0289434ed6": "有効", "8b3054dac7": "登録中...", - "8f2675c2f3": "ファイルから {{value0}} 個の Cookie をインポートしました。" + "8f2675c2f3": "ファイルから {{value0}} 個の Cookie をインポートしました。", + "5301857d88": "{{value0}} から" }, "BrowserUseSkillStep": { "0871b6998d": "agents が Orca のブラウザでページに移動して確認できるようにします。", diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index c5cf11e87c6..62132106ac7 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -4867,7 +4867,8 @@ "d420c43729": "{{value1}}{{value2}}에서 {{value0}}개의 쿠키를 {{value3}}(으)로 가져왔습니다.", "b4c167764d": "파일에서 {{value0}}개의 쿠키를 {{value1}}에 가져왔습니다.", "a3f8c2d1e0b4": "{{value1}} ({{value2}})에서 {{value0}}개의 쿠키를 {{value3}}(으)로 가져왔습니다.", - "b4e9d3f2a1c5": "{{value1}}에서 {{value0}}개의 쿠키를 {{value2}}(으)로 가져왔습니다." + "b4e9d3f2a1c5": "{{value1}}에서 {{value0}}개의 쿠키를 {{value2}}(으)로 가져왔습니다.", + "c5a273a809": "{{value0}}에서" }, "BrowserUseComputerUseNotice": { "15b5e680ba": "컴퓨터 사용 열기", @@ -4891,7 +4892,7 @@ "be6df68384": "파일에서…", "e44c5d681e": "에서", "67d9a53f47": "별도의 로그인을 위한 프로필 관리", - "112f70adc4": "마지막으로 가져온 위치", + "112f70adc4": "{{value0}}에서 마지막으로 가져옴", "72d4815523": "agents가 인증된 페이지에 접근할 수 있도록 기존 로그인을 Orca로 가져오세요. 기본 프로필로 가져옵니다.", "2eb906706c": "브라우저 쿠키 가져오기", "af8c83ed61": "agents가 로그인을 재사용할 수 있도록 Chrome, Edge 또는 기타 브라우저에서 쿠키를 가져옵니다.", @@ -4913,7 +4914,8 @@ "ad8cb0ee22": "경로 수정", "0289434ed6": "활성화됨", "8b3054dac7": "등록 중...", - "8f2675c2f3": "파일에서 {{value0}}개의 쿠키를 가져왔습니다." + "8f2675c2f3": "파일에서 {{value0}}개의 쿠키를 가져왔습니다.", + "5301857d88": "{{value0}}에서" }, "BrowserUseSkillStep": { "0871b6998d": "agents가 Orca 브라우저에서 페이지를 탐색하고 확인할 수 있습니다.", diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index 531548d7947..41d3bbeb76c 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -4867,7 +4867,8 @@ "d420c43729": "将 {{value0}} cookie 从 {{value1}}{{value2}} 导入到 {{value3}} 中。", "b4c167764d": "已从文件将 {{value0}} 个 Cookie 导入到 {{value1}}。", "a3f8c2d1e0b4": "已从 {{value1}} ({{value2}}) 导入 {{value0}} 个 Cookie 到 {{value3}}。", - "b4e9d3f2a1c5": "已从 {{value1}} 导入 {{value0}} 个 Cookie 到 {{value2}}。" + "b4e9d3f2a1c5": "已从 {{value1}} 导入 {{value0}} 个 Cookie 到 {{value2}}。", + "c5a273a809": "从 {{value0}}" }, "BrowserUseComputerUseNotice": { "15b5e680ba": "开放计算机使用", @@ -4891,7 +4892,7 @@ "be6df68384": "从文件...", "e44c5d681e": "从", "67d9a53f47": "管理单独登录的配置文件", - "112f70adc4": "上次导入自", + "112f70adc4": "上次导入自 {{value0}}", "72d4815523": "将您现有的登录信息带入 Orca,以便智能体可以访问经过身份验证的页面。导入到默认配置文件中。", "2eb906706c": "导入浏览器 Cookie", "af8c83ed61": "从 Chrome、Edge 或其他浏览器导入 cookie,以便智能体可以重复使用您的登录信息。", @@ -4913,7 +4914,8 @@ "ad8cb0ee22": "修复路径", "0289434ed6": "启用", "8b3054dac7": "注册...", - "8f2675c2f3": "已从文件导入 {{value0}} 个 Cookie。" + "8f2675c2f3": "已从文件导入 {{value0}} 个 Cookie。", + "5301857d88": "从 {{value0}}" }, "BrowserUseSkillStep": { "0871b6998d": "使智能体能够在 Orca 浏览器中导航和验证页面。", From 5ba4e2454d508d31d9563528ba70972fb574847b Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Sat, 4 Jul 2026 14:47:36 -0700 Subject: [PATCH 25/45] Remove "Open" status badge from PR comment actions (#7338) Remove the unused "Open" action state condition and its associated `statusBadgeOpen` class from presentation definitions. --- .../src/components/right-sidebar/checks-panel-content.tsx | 7 ------- .../components/right-sidebar/pr-comment-presentation.ts | 5 ----- 2 files changed, 12 deletions(-) diff --git a/src/renderer/src/components/right-sidebar/checks-panel-content.tsx b/src/renderer/src/components/right-sidebar/checks-panel-content.tsx index 62268e292b6..30a78f5391b 100644 --- a/src/renderer/src/components/right-sidebar/checks-panel-content.tsx +++ b/src/renderer/src/components/right-sidebar/checks-panel-content.tsx @@ -1609,13 +1609,6 @@ function PRCommentActionBadge({ ) } - if (actionState === 'open') { - return ( - - {translate('auto.components.right.sidebar.checks.panel.content.7c1f0a2b11', 'Open')} - - ) - } if (actionState === 'resolved') { return ( diff --git a/src/renderer/src/components/right-sidebar/pr-comment-presentation.ts b/src/renderer/src/components/right-sidebar/pr-comment-presentation.ts index dcd72e755a7..6ce3560f6a1 100644 --- a/src/renderer/src/components/right-sidebar/pr-comment-presentation.ts +++ b/src/renderer/src/components/right-sidebar/pr-comment-presentation.ts @@ -41,7 +41,6 @@ export type PRCommentPresentationClasses = { audienceTab: string audienceTabActive: string sectionTriageLabel: string - statusBadgeOpen: string statusBadgeResolved: string statusBadgeQueued: string commentHeaderPrimary: string @@ -160,8 +159,6 @@ export function getPRCommentPresentationClasses( 'flex h-7 items-center justify-center gap-1 rounded-md px-1.5 text-[11px] font-medium text-muted-foreground transition-colors', audienceTabActive: 'bg-muted text-foreground', sectionTriageLabel: cn('px-3 pt-2', RESOLVED_SECTION_LABEL), - statusBadgeOpen: - 'shrink-0 rounded border border-status-success-border bg-status-success-background px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-status-success', statusBadgeResolved: 'shrink-0 rounded border border-border bg-muted px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground', statusBadgeQueued: @@ -211,8 +208,6 @@ export function getPRCommentPresentationClasses( 'flex h-7 items-center justify-center gap-1 rounded-md px-1.5 text-[11px] font-medium text-muted-foreground transition-colors', audienceTabActive: 'bg-muted text-foreground shadow-xs', sectionTriageLabel: cn('px-3 pt-1', RESOLVED_SECTION_LABEL), - statusBadgeOpen: - 'shrink-0 rounded border border-status-success-border bg-status-success-background px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-status-success', statusBadgeResolved: 'shrink-0 rounded border border-border bg-muted px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground', statusBadgeQueued: From c641e554867e70f5e4828832473d4018940764cb Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Sat, 4 Jul 2026 15:16:50 -0700 Subject: [PATCH 26/45] Normalize GitHub work item type from URL to prevent issue/PR mismatch (#7371) - Resolve true work item identity (issue vs. PR) using the URL path to override stale or incorrect cached payload types. - Prevent invalid PR start point resolution when launching an issue misclassified as a PR. - Validate and reject mismatched URL types in the worktree metadata dialog fields to avoid incorrect associations. --- src/main/github/pr-start-point.test.ts | 19 +- .../components/sidebar/WorktreeMetaDialog.tsx | 13 +- .../sidebar/worktree-meta-updates.test.ts | 66 +++++++ .../sidebar/worktree-meta-updates.ts | 18 +- ...poserState-host-context-boundaries.test.ts | 16 +- src/renderer/src/hooks/useComposerState.ts | 162 +++++++++++++----- src/renderer/src/lib/github-links.test.ts | 31 +++- src/renderer/src/lib/github-links.ts | 19 +- ...github-work-item-background-create.test.ts | 63 ++++++- .../lib/github-work-item-background-create.ts | 6 +- .../github-work-item-background-request.ts | 15 +- .../src/lib/github-work-item-identity.ts | 20 +++ .../src/lib/launch-work-item-direct.test.ts | 47 ++++- .../src/lib/launch-work-item-direct.ts | 31 ++-- .../src/lib/smart-github-submit.test.ts | 22 +++ src/renderer/src/lib/smart-github-submit.ts | 15 +- 16 files changed, 464 insertions(+), 99 deletions(-) create mode 100644 src/renderer/src/components/sidebar/worktree-meta-updates.test.ts create mode 100644 src/renderer/src/lib/github-work-item-identity.ts diff --git a/src/main/github/pr-start-point.test.ts b/src/main/github/pr-start-point.test.ts index 91d8e5ad07e..8783b2a4af4 100644 --- a/src/main/github/pr-start-point.test.ts +++ b/src/main/github/pr-start-point.test.ts @@ -22,12 +22,12 @@ describe('resolveGitHubPrStartPoint', () => { getPullRequestPushTargetMock.mockResolvedValue({ pushTarget: { remoteName: 'pr-contributor-orca', - branchName: 'feat/onboarding-model-choice-782', + branchName: 'fix-issue-6933', remoteUrl: 'git@github.com:contributor/orca.git' } }) const fetchRemoteTrackingRef = vi.fn(async (_remote: string, branch: string) => { - if (branch === 'feat/onboarding-model-choice-782') { + if (branch === 'fix-issue-6933') { throw new Error('fatal: could not find remote ref') } }) @@ -40,28 +40,25 @@ describe('resolveGitHubPrStartPoint', () => { const result = await resolveGitHubPrStartPoint({ repoPath: '/repo-root', - prNumber: 1849, - headRefName: 'feat/onboarding-model-choice-782', + prNumber: 6934, + headRefName: 'fix-issue-6933', baseRefName: 'main', gitExec, fetchRemoteTrackingRef, resolveRemote: async () => 'origin' }) - expect(fetchRemoteTrackingRef).toHaveBeenCalledWith( - 'origin', - 'feat/onboarding-model-choice-782' - ) + expect(fetchRemoteTrackingRef).toHaveBeenCalledWith('origin', 'fix-issue-6933') expect(fetchRemoteTrackingRef).toHaveBeenCalledWith('origin', 'main') - expect(gitExec).toHaveBeenCalledWith(['fetch', 'origin', 'refs/pull/1849/head']) + expect(gitExec).toHaveBeenCalledWith(['fetch', 'origin', 'refs/pull/6934/head']) expect(result).toEqual({ baseBranch: 'def456', compareBaseRef: 'refs/remotes/origin/main', headSha: 'def456', - branchNameOverride: 'feat/onboarding-model-choice-782', + branchNameOverride: 'fix-issue-6933', pushTarget: { remoteName: 'pr-contributor-orca', - branchName: 'feat/onboarding-model-choice-782', + branchName: 'fix-issue-6933', remoteUrl: 'git@github.com:contributor/orca.git' } }) diff --git a/src/renderer/src/components/sidebar/WorktreeMetaDialog.tsx b/src/renderer/src/components/sidebar/WorktreeMetaDialog.tsx index 217d2fd6735..5163a839ebf 100644 --- a/src/renderer/src/components/sidebar/WorktreeMetaDialog.tsx +++ b/src/renderer/src/components/sidebar/WorktreeMetaDialog.tsx @@ -11,8 +11,11 @@ import { import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' -import { parseGitHubIssueOrPRNumber } from '@/lib/github-links' -import { buildWorktreeMetaUpdates, type WorktreeMetaSavedPayload } from './worktree-meta-updates' +import { + buildWorktreeMetaUpdates, + parseGitHubWorkItemNumberForMetaField, + type WorktreeMetaSavedPayload +} from './worktree-meta-updates' import { useWorktreeIssueLink } from './use-worktree-issue-link' import { getScreenSubmitShortcutLabel, isScreenSubmitShortcut } from '@/lib/screen-submit-shortcut' import { ExternalLink, LoaderCircle } from 'lucide-react' @@ -95,8 +98,10 @@ const WorktreeMetaDialog = React.memo(function WorktreeMetaDialog() { } const trimmedIssue = issueInput.trim() const trimmedPR = prInput.trim() - const issueValid = trimmedIssue === '' || parseGitHubIssueOrPRNumber(trimmedIssue) !== null - const prValid = trimmedPR === '' || parseGitHubIssueOrPRNumber(trimmedPR) !== null + const issueValid = + trimmedIssue === '' || parseGitHubWorkItemNumberForMetaField(trimmedIssue, 'issue') !== null + const prValid = + trimmedPR === '' || parseGitHubWorkItemNumberForMetaField(trimmedPR, 'pr') !== null return issueValid && prValid }, [worktreeId, issueInput, prInput]) diff --git a/src/renderer/src/components/sidebar/worktree-meta-updates.test.ts b/src/renderer/src/components/sidebar/worktree-meta-updates.test.ts new file mode 100644 index 00000000000..a53eeae4163 --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-meta-updates.test.ts @@ -0,0 +1,66 @@ +import { describe, expect, it } from 'vitest' +import { buildWorktreeMetaUpdates } from './worktree-meta-updates' + +describe('buildWorktreeMetaUpdates', () => { + it('rejects issue URLs in the PR input', () => { + expect( + buildWorktreeMetaUpdates({ + displayNameInput: 'Workspace', + currentDisplayName: 'Workspace', + issueInput: '', + prInput: 'https://github.com/stablyai/orca/issues/6933', + commentInput: '' + }) + ).toEqual({ + comment: '', + linkedIssue: null + }) + }) + + it('accepts PR URLs in the PR input', () => { + expect( + buildWorktreeMetaUpdates({ + displayNameInput: 'Workspace', + currentDisplayName: 'Workspace', + issueInput: '', + prInput: 'https://github.com/stablyai/orca/pull/6934', + commentInput: '' + }) + ).toEqual({ + comment: '', + linkedIssue: null, + linkedPR: 6934 + }) + }) + + it('accepts issue URLs in the issue input', () => { + expect( + buildWorktreeMetaUpdates({ + displayNameInput: 'Workspace', + currentDisplayName: 'Workspace', + issueInput: 'https://github.com/stablyai/orca/issues/6933', + prInput: '', + commentInput: '' + }) + ).toEqual({ + comment: '', + linkedIssue: 6933, + linkedPR: null + }) + }) + + it('rejects PR URLs in the issue input', () => { + expect( + buildWorktreeMetaUpdates({ + displayNameInput: 'Workspace', + currentDisplayName: 'Workspace', + issueInput: 'https://github.com/stablyai/orca/pull/6934', + prInput: '', + commentInput: '' + }) + ).toEqual({ + comment: '', + linkedPR: null + }) + }) +}) diff --git a/src/renderer/src/components/sidebar/worktree-meta-updates.ts b/src/renderer/src/components/sidebar/worktree-meta-updates.ts index 7f6f5d70935..26769c65061 100644 --- a/src/renderer/src/components/sidebar/worktree-meta-updates.ts +++ b/src/renderer/src/components/sidebar/worktree-meta-updates.ts @@ -16,6 +16,20 @@ export function parseExplicitGitHubIssueUrl(input: string): string | null { return trimmed } +export function parseGitHubWorkItemNumberForMetaField( + input: string, + expectedType: 'issue' | 'pr' +): number | null { + const link = parseGitHubIssueOrPRLink(input) + if (link) { + // Why: issue and PR numbers live in separate GitHub namespaces for refs; + // a URL path mismatch must not silently link the other field. + return link.type === expectedType ? link.number : null + } + + return parseGitHubIssueOrPRNumber(input) +} + /** Pure save-payload builder for the worktree meta dialog: empty inputs clear * the link (null), unparseable inputs leave it untouched (omitted). */ export function buildWorktreeMetaUpdates(args: { @@ -26,11 +40,11 @@ export function buildWorktreeMetaUpdates(args: { commentInput: string }): Partial { const trimmedIssue = args.issueInput.trim() - const linkedIssueNumber = parseGitHubIssueOrPRNumber(trimmedIssue) + const linkedIssueNumber = parseGitHubWorkItemNumberForMetaField(trimmedIssue, 'issue') const finalLinkedIssue = trimmedIssue === '' ? null : linkedIssueNumber !== null ? linkedIssueNumber : undefined const trimmedPR = args.prInput.trim() - const linkedPRNumber = parseGitHubIssueOrPRNumber(trimmedPR) + const linkedPRNumber = parseGitHubWorkItemNumberForMetaField(trimmedPR, 'pr') const finalLinkedPR = trimmedPR === '' ? null : linkedPRNumber !== null ? linkedPRNumber : undefined diff --git a/src/renderer/src/hooks/useComposerState-host-context-boundaries.test.ts b/src/renderer/src/hooks/useComposerState-host-context-boundaries.test.ts index f45f7ee3c17..c43a051c8ab 100644 --- a/src/renderer/src/hooks/useComposerState-host-context-boundaries.test.ts +++ b/src/renderer/src/hooks/useComposerState-host-context-boundaries.test.ts @@ -209,6 +209,8 @@ describe('useComposerState host-context boundaries', () => { 'const applyLinkedWorkItem = useCallback' ) expect(directLookup).toContain('sourceContext: selectedRepoGitHubSourceContext') + expect(directLookup).toContain('lookupGitHubWorkItemByOwnerRepoForSource') + expect(directLookup).toContain('type: normalizedLinkQuery.directLink.type') const submitLookup = sourceBetween( HOOK_SOURCE, @@ -236,7 +238,11 @@ describe('useComposerState host-context boundaries', () => { 'const intent = getSmartGitHubSubmitIntent(name)' ) expect(selectedPrSubmitLookup).toContain('smartGitHubPrStartPointSelectionRef.current') - expect(selectedPrSubmitLookup).toContain("linkedWorkItem.type === 'pr'") + expect(selectedPrSubmitLookup).toContain("linkedWorkItemIdentity?.type === 'pr'") + expect(selectedPrSubmitLookup).toContain("startPointIdentity?.type === 'pr'") + expect(selectedPrSubmitLookup).toContain( + 'startPointIdentity.number === linkedWorkItemIdentity.number' + ) expect(selectedPrSubmitLookup).toContain('resolveGitHubPrStartPointForRepo') expect(selectedPrSubmitLookup.indexOf('resolveGitHubPrStartPointForRepo')).toBeLessThan( selectedPrSubmitLookup.indexOf("return { kind: 'none' }") @@ -421,8 +427,12 @@ describe('useComposerState host-context boundaries', () => { ) expect(projectGroupSmartHandlers).toContain('setLinkedGitLabIssue(null)') expect(projectGroupSmartHandlers).toContain('setLinkedGitLabMR(null)') - expect(projectGroupSmartHandlers).toContain("setLinkedIssue('')") - expect(projectGroupSmartHandlers).toContain('setLinkedPR(null)') + expect(projectGroupSmartHandlers).toContain( + "setLinkedIssue(identity.type === 'issue' ? String(identity.number) : '')" + ) + expect(projectGroupSmartHandlers).toContain( + "setLinkedPR(identity.type === 'pr' ? identity.number : null)" + ) }) it('disables repo-backed folder smart lookup when a folder target has no source repos', () => { diff --git a/src/renderer/src/hooks/useComposerState.ts b/src/renderer/src/hooks/useComposerState.ts index 202563742eb..dc6de85382b 100644 --- a/src/renderer/src/hooks/useComposerState.ts +++ b/src/renderer/src/hooks/useComposerState.ts @@ -95,6 +95,10 @@ import { lookupGitHubWorkItemByOwnerRepoForSource, lookupGitHubWorkItemForSource } from '@/lib/github-work-item-source-lookup' +import { + resolveGitHubWorkItemIdentity, + type GitHubWorkItemIdentity +} from '@/lib/github-work-item-identity' import { resolveGitHubPrStartPointForRepo } from '@/lib/github-pr-start-point' import { isWorkItemLookupText } from '@/lib/work-item-lookup-text' import { @@ -475,6 +479,38 @@ function getLinkedWorkItemSeedName(item: LinkedWorkItemSummary | null | undefine return getLinkedWorkItemWorkspaceName(item)?.seedName ?? getLinkedWorkItemSuggestedName(item) } +function getGitHubLinkedWorkItemIdentity( + item: LinkedWorkItemSummary | null | undefined +): GitHubWorkItemIdentity | null { + if ( + !item || + getLinkedWorkItemProvider(item) !== 'github' || + (item.type !== 'issue' && item.type !== 'pr') + ) { + return null + } + + return resolveGitHubWorkItemIdentity({ + type: item.type, + number: item.number, + url: item.url + }) +} + +function normalizeGitHubLinkedWorkItem( + item: LinkedWorkItemSummary | null | undefined +): LinkedWorkItemSummary | null { + if (!item) { + return null + } + const identity = getGitHubLinkedWorkItemIdentity(item) + if (!identity || (identity.type === item.type && identity.number === item.number)) { + return item + } + + return { ...item, type: identity.type, number: identity.number } +} + export function getInitialAutoManagedWorkspaceName({ draftName, draftLinkedWorkItem, @@ -905,10 +941,16 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS const [attachmentPaths, setAttachmentPaths] = useState( persistDraft ? (newWorkspaceDraft?.attachments ?? []) : [] ) + const initialLinkedWorkItemSeed = normalizeGitHubLinkedWorkItem(initialLinkedWorkItem) + const draftLinkedWorkItemSeed = persistDraft + ? normalizeGitHubLinkedWorkItem(newWorkspaceDraft?.linkedWorkItem) + : null + const linkedWorkItemSeed = persistDraft + ? (draftLinkedWorkItemSeed ?? initialLinkedWorkItemSeed) + : initialLinkedWorkItemSeed + const linkedWorkItemSeedIdentity = getGitHubLinkedWorkItemIdentity(linkedWorkItemSeed) const [linkedWorkItem, setLinkedWorkItem] = useState( - persistDraft - ? (newWorkspaceDraft?.linkedWorkItem ?? initialLinkedWorkItem) - : initialLinkedWorkItem + () => linkedWorkItemSeed ) const taskSourceContext = useMemo(() => { if ( @@ -982,6 +1024,9 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS }) }, [projects, selectedRepo, selectedRepoIsGit, selectedWorkspaceTarget, taskSourceContext]) const [linkedIssue, setLinkedIssue] = useState(() => { + if (linkedWorkItemSeedIdentity?.type === 'issue') { + return String(linkedWorkItemSeedIdentity.number) + } if (persistDraft && newWorkspaceDraft?.linkedIssue) { return newWorkspaceDraft.linkedIssue } @@ -994,6 +1039,12 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS return '' }) const [linkedPR, setLinkedPR] = useState(() => { + if (linkedWorkItemSeedIdentity?.type === 'pr') { + return linkedWorkItemSeedIdentity.number + } + if (linkedWorkItemSeedIdentity?.type === 'issue') { + return null + } if (persistDraft && newWorkspaceDraft?.linkedPR !== undefined) { return newWorkspaceDraft.linkedPR } @@ -1134,9 +1185,9 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS const lastAutoNameRef = useRef( getInitialAutoManagedWorkspaceName({ draftName: persistDraft ? newWorkspaceDraft?.name : null, - draftLinkedWorkItem: persistDraft ? newWorkspaceDraft?.linkedWorkItem : null, + draftLinkedWorkItem: persistDraft ? draftLinkedWorkItemSeed : null, initialName, - initialLinkedWorkItem + initialLinkedWorkItem: initialLinkedWorkItemSeed }) ) const nameRef = useRef(name) @@ -1922,16 +1973,28 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS let cancelled = false setLinkDirectLoading(true) // Why: Superset lets users paste a full GitHub URL or type a raw issue/PR - // number and still get a concrete selectable result. Orca mirrors that by - // resolving direct lookups against the selected repo instead of requiring a - // text match in the recent-items list. + // number and still get a concrete selectable result. Full URLs carry + // issue-vs-PR intent, so preserve the URL route instead of probing by + // number only. const lookupRepoId = selectedRepo.id - void lookupGitHubWorkItemForSource({ - repoPath: selectedRepo.path, - repoId: selectedRepo.id, - sourceContext: selectedRepoGitHubSourceContext, - number: normalizedLinkQuery.directNumber - }) + const lookup = + normalizedLinkQuery.directLink !== undefined + ? lookupGitHubWorkItemByOwnerRepoForSource({ + repoPath: selectedRepo.path, + repoId: selectedRepo.id, + sourceContext: selectedRepoGitHubSourceContext, + owner: normalizedLinkQuery.directLink.slug.owner, + repo: normalizedLinkQuery.directLink.slug.repo, + number: normalizedLinkQuery.directLink.number, + type: normalizedLinkQuery.directLink.type + }) + : lookupGitHubWorkItemForSource({ + repoPath: selectedRepo.path, + repoId: selectedRepo.id, + sourceContext: selectedRepoGitHubSourceContext, + number: normalizedLinkQuery.directNumber + }) + void lookup .then((item) => { if (!cancelled) { setLinkDirectItem( @@ -1954,6 +2017,7 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS cancelled = true } }, [ + normalizedLinkQuery.directLink, linkPopoverOpen, normalizedLinkQuery.directNumber, selectedRepo, @@ -1963,24 +2027,31 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS const applyLinkedWorkItem = useCallback( (item: GitHubWorkItem, options: { preserveBranchNameOverride?: boolean } = {}): void => { - if (item.type === 'issue') { - setLinkedIssue(String(item.number)) + const identity = resolveGitHubWorkItemIdentity(item) + const normalizedItem: GitHubWorkItem = { + ...item, + type: identity.type, + number: identity.number + } + if (identity.type === 'issue') { + setLinkedIssue(String(identity.number)) setLinkedPR(null) } else { setLinkedIssue('') - setLinkedPR(item.number) + setLinkedPR(identity.number) } setLinkedGitLabIssue(null) setLinkedGitLabMR(null) setLinkedWorkItem({ - type: item.type, + type: identity.type, provider: 'github', - number: item.number, + number: identity.number, title: item.title, url: item.url }) const suggestedName = - getLinkedWorkItemWorkspaceName(item)?.seedName ?? getLinkedWorkItemSuggestedName(item) + getLinkedWorkItemWorkspaceName(normalizedItem)?.seedName ?? + getLinkedWorkItemSuggestedName(normalizedItem) // Why: a pasted URL/#123 in the field is the lookup query that found // this item, not a deliberate name — replace it with the title-derived // name or it silently becomes a slugified-URL workspace name. @@ -2002,20 +2073,25 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS useCallback(async (): Promise => { if (linkedWorkItem) { const startPointSelection = smartGitHubPrStartPointSelectionRef.current + const linkedWorkItemIdentity = getGitHubLinkedWorkItemIdentity(linkedWorkItem) + const startPointIdentity = startPointSelection + ? resolveGitHubWorkItemIdentity(startPointSelection.item) + : null if ( !isProjectGroupTarget && - linkedWorkItem.type === 'pr' && + linkedWorkItemIdentity?.type === 'pr' && + startPointIdentity?.type === 'pr' && getLinkedWorkItemProvider(linkedWorkItem) === 'github' && selectedRepo && selectedRepoIsGit && startPointSelection?.repoId === selectedRepo.id && - startPointSelection.item.number === linkedWorkItem.number + startPointIdentity.number === linkedWorkItemIdentity.number ) { const selectedPrStartPoint = startPointSelection.resolved ?? (await resolveGitHubPrStartPointForRepo({ repoId: selectedRepo.id, - prNumber: startPointSelection.item.number, + prNumber: startPointIdentity.number, settings: getSettingsForRepoRuntimeOwner( { repos: [selectedRepo], settings }, selectedRepo.id @@ -2102,11 +2178,12 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS throw new Error('Could not resolve the GitHub item before creating the workspace.') } + const itemIdentity = resolveGitHubWorkItemIdentity(item) const prStartPoint = - !isProjectGroupTarget && item.type === 'pr' && selectedRepo && selectedRepoIsGit + !isProjectGroupTarget && itemIdentity.type === 'pr' && selectedRepo && selectedRepoIsGit ? await resolveGitHubPrStartPointForRepo({ repoId: selectedRepo.id, - prNumber: item.number, + prNumber: itemIdentity.number, settings: getSettingsForRepoRuntimeOwner( { repos: [selectedRepo], settings }, selectedRepo.id @@ -2757,8 +2834,9 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS // worktree's comment should surface (`orca worktree current`, sidebar). // Prefill the note if it's empty or still equal to a prior auto-fill, so // we don't overwrite anything the user has typed. - if (item.type === 'pr') { - const suggestedNote = `PR #${item.number} — ${item.title}` + const identity = resolveGitHubWorkItemIdentity(item) + if (identity.type === 'pr') { + const suggestedNote = `PR #${identity.number} — ${item.title}` const currentNote = noteRef.current if (!currentNote.trim() || currentNote === lastAutoNoteRef.current) { setNote(suggestedNote) @@ -2800,10 +2878,16 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS const handleSmartGitHubItemSelect = useCallback( (item: GitHubWorkItem): void => { + const identity = resolveGitHubWorkItemIdentity(item) + const normalizedItem: GitHubWorkItem = { + ...item, + type: identity.type, + number: identity.number + } if (isProjectGroupTarget) { - const linkedItem = toGitHubLinkedWorkItem(item) - setLinkedIssue(String(item.number)) - setLinkedPR(item.type === 'pr' ? item.number : null) + const linkedItem = toGitHubLinkedWorkItem(normalizedItem) + setLinkedIssue(identity.type === 'issue' ? String(identity.number) : '') + setLinkedPR(identity.type === 'pr' ? identity.number : null) setLinkedGitLabIssue(null) setLinkedGitLabMR(null) setLinkedWorkItem(linkedItem) @@ -2826,8 +2910,8 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS // selected run host. Resolve git refs against the run repo; keep item // metadata/source context separate for provider identity. const runRepo = selectedRepo ?? eligibleRepos.find((repo) => repo.id === item.repoId) - applyLinkedWorkItem(item) - if (item.type !== 'pr' || !runRepo) { + applyLinkedWorkItem(normalizedItem) + if (identity.type !== 'pr' || !runRepo) { setBaseBranch(undefined) setCompareBaseRef(undefined) setPushTarget(undefined) @@ -2838,7 +2922,7 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS setPushTarget(undefined) const startPointSelection: SmartGitHubPrStartPointSelection = { repoId: runRepo.id, - item + item: normalizedItem } smartGitHubPrStartPointSelectionRef.current = startPointSelection const itemRepoSettings = getSettingsForRepoRuntimeOwner( @@ -2847,12 +2931,12 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS ) const resolvePrBase = resolveGitHubPrStartPointForRepo({ repoId: runRepo.id, - prNumber: item.number, + prNumber: identity.number, settings: itemRepoSettings, - ...(item.branchName ? { headRefName: item.branchName } : {}), - ...(item.baseRefName ? { baseRefName: item.baseRefName } : {}), - ...(item.isCrossRepository !== undefined - ? { isCrossRepository: item.isCrossRepository } + ...(normalizedItem.branchName ? { headRefName: normalizedItem.branchName } : {}), + ...(normalizedItem.baseRefName ? { baseRefName: normalizedItem.baseRefName } : {}), + ...(normalizedItem.isCrossRepository !== undefined + ? { isCrossRepository: normalizedItem.isCrossRepository } : {}) }) void resolvePrBase @@ -2863,7 +2947,7 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS startPointSelection.resolved = result handleBaseBranchPrSelect( result.baseBranch, - item, + normalizedItem, result.pushTarget, result.branchNameOverride, result.compareBaseRef diff --git a/src/renderer/src/lib/github-links.test.ts b/src/renderer/src/lib/github-links.test.ts index 31c7f72aadd..64a9bd59df3 100644 --- a/src/renderer/src/lib/github-links.test.ts +++ b/src/renderer/src/lib/github-links.test.ts @@ -133,7 +133,36 @@ describe('normalizeGitHubLinkQuery', () => { it('accepts full GitHub URLs whose slug differs from the selected repo slug', () => { expect(normalizeGitHubLinkQuery('https://github.com/stablyai/orca/issues/923')).toEqual({ query: 'https://github.com/stablyai/orca/issues/923', - directNumber: 923 + directNumber: 923, + directLink: { + slug: { owner: 'stablyai', repo: 'orca' }, + number: 923, + type: 'issue' + } + }) + }) + + it('preserves PR route intent for full GitHub URLs', () => { + expect(normalizeGitHubLinkQuery('https://github.com/stablyai/orca/pull/6934')).toEqual({ + query: 'https://github.com/stablyai/orca/pull/6934', + directNumber: 6934, + directLink: { + slug: { owner: 'stablyai', repo: 'orca' }, + number: 6934, + type: 'pr' + } + }) + }) + + it('preserves route intent for URLs with uppercase schemes', () => { + expect(normalizeGitHubLinkQuery('HTTPS://github.com/stablyai/orca/pull/6934')).toEqual({ + query: 'HTTPS://github.com/stablyai/orca/pull/6934', + directNumber: 6934, + directLink: { + slug: { owner: 'stablyai', repo: 'orca' }, + number: 6934, + type: 'pr' + } }) }) diff --git a/src/renderer/src/lib/github-links.ts b/src/renderer/src/lib/github-links.ts index 0f1fc4f25a6..05ea8c812f5 100644 --- a/src/renderer/src/lib/github-links.ts +++ b/src/renderer/src/lib/github-links.ts @@ -1,15 +1,23 @@ import { isWorkItemLinkQueryTooLarge } from './work-item-link-query-bounds' const GH_ITEM_PATH_RE = /^\/([^/]+)\/([^/]+)\/(issues|pull)\/(\d+)(?:\/.*)?$/i +const HTTP_URL_PREFIX_RE = /^https?:\/\//i export type RepoSlug = { owner: string repo: string } +export type GitHubIssueOrPRLink = { + slug: RepoSlug + number: number + type: 'issue' | 'pr' +} + export type GitHubLinkQuery = { query: string directNumber: number | null + directLink?: GitHubIssueOrPRLink tooLarge?: boolean } @@ -67,11 +75,7 @@ export function parseGitHubIssueOrPRNumber(input: string): number | null { * Parses an owner/repo slug plus issue/PR number from a GitHub URL. Returns * null for anything that isn't a recognizable GitHub-shaped issue or pull URL. */ -export function parseGitHubIssueOrPRLink(input: string): { - slug: RepoSlug - number: number - type: 'issue' | 'pr' -} | null { +export function parseGitHubIssueOrPRLink(input: string): GitHubIssueOrPRLink | null { const trimmed = input.trim() if (!trimmed) { return null @@ -118,7 +122,7 @@ export function normalizeGitHubLinkQuery(raw: string): GitHubLinkQuery { } const direct = parseGitHubIssueOrPRNumber(trimmed) - if (direct !== null && !trimmed.startsWith('http')) { + if (direct !== null && !HTTP_URL_PREFIX_RE.test(trimmed)) { return { query: trimmed, directNumber: direct } } @@ -132,6 +136,7 @@ export function normalizeGitHubLinkQuery(raw: string): GitHubLinkQuery { // slug differs from the origin remote. return { query: trimmed, - directNumber: link.number + directNumber: link.number, + directLink: link } } diff --git a/src/renderer/src/lib/github-work-item-background-create.test.ts b/src/renderer/src/lib/github-work-item-background-create.test.ts index a4e6d8f0adf..ef7adfdb2b9 100644 --- a/src/renderer/src/lib/github-work-item-background-create.test.ts +++ b/src/renderer/src/lib/github-work-item-background-create.test.ts @@ -163,6 +163,46 @@ describe('createGitHubWorkItemWorkspaceInBackground', () => { ) }) + it('does not resolve a PR start point for a stale PR-typed issue URL', async () => { + const deps = makeDeps() + + await createGitHubWorkItemWorkspaceInBackground( + { + item: makeIssue({ + type: 'pr', + number: 6933, + title: 'The board columns are displayed backwards', + url: 'https://github.com/stablyai/orca/issues/6933', + branchName: 'fix-issue-6933', + baseRefName: 'main', + isCrossRepository: true + }), + repoId: 'repo-1', + telemetrySource: 'sidebar', + openModalFallback: vi.fn() + }, + deps + ) + + expect(deps.resolvePrStartPoint).not.toHaveBeenCalled() + const beginCalls = deps.beginBackgroundCreate.mock.calls as unknown as [ + WorktreeCreationRequest + ][] + expect(beginCalls[0]?.[0].linkedIssue).toBe(6933) + expect(beginCalls[0]?.[0].linkedPR).toBeUndefined() + const continueCalls = deps.continueBackgroundCreate.mock.calls as unknown as [ + string, + WorktreeCreationRequest + ][] + const request = continueCalls[0]?.[1] + expect(request?.linkedIssue).toBe(6933) + expect(request?.linkedPR).toBeUndefined() + expect(request?.baseBranch).toBeUndefined() + expect(request?.pushTarget).toBeUndefined() + expect(request?.branchNameOverride).toBeUndefined() + expect(request?.compareBaseRef).toBeUndefined() + }) + it('shows the pending workspace before async preflight resolves', async () => { let resolveSetupDecision: ((value: { kind: 'decided'; decision: 'inherit' }) => void) | null = null @@ -566,17 +606,36 @@ describe('createGitHubWorkItemWorkspaceInBackground', () => { await createGitHubWorkItemWorkspaceInBackground( { - item: makeIssue({ type: 'pr', number: 7, url: 'https://github.com/stablyai/orca/pull/7' }), + item: makeIssue({ + type: 'pr', + number: 6934, + url: 'https://github.com/stablyai/orca/pull/6934', + branchName: 'fix-issue-6933', + baseRefName: 'main', + isCrossRepository: true + }), repoId: 'repo-1', openModalFallback: vi.fn() }, deps ) + expect(deps.resolvePrStartPoint).toHaveBeenCalledWith( + 'repo-1', + 6934, + expect.anything(), + expect.objectContaining({ + type: 'pr', + number: 6934, + branchName: 'fix-issue-6933', + baseRefName: 'main', + isCrossRepository: true + }) + ) expect(deps.continueBackgroundCreate).toHaveBeenCalledWith( 'creation-1', expect.objectContaining({ - linkedPR: 7, + linkedPR: 6934, baseBranch: 'feature/from-pr', pushTarget: { remote: 'origin', branch: 'feature/from-pr' }, branchNameOverride: 'feature/from-pr', diff --git a/src/renderer/src/lib/github-work-item-background-create.ts b/src/renderer/src/lib/github-work-item-background-create.ts index 7fa94fd9d2b..260cd2ad657 100644 --- a/src/renderer/src/lib/github-work-item-background-create.ts +++ b/src/renderer/src/lib/github-work-item-background-create.ts @@ -32,6 +32,7 @@ import { import type { GitHubWorkItem, SetupDecision } from '../../../shared/types' import type { Repo } from '../../../shared/types' import type { TaskSourceContext, WorkspaceRunContext } from '../../../shared/task-source-context' +import { resolveGitHubWorkItemIdentity } from '@/lib/github-work-item-identity' export type BackgroundGitHubWorkItemCreateResult = | { kind: 'background-started' } @@ -182,6 +183,7 @@ export async function createGitHubWorkItemWorkspaceInBackground( const restoreView = deps.getActiveView() const creationId = deps.beginBackgroundCreate(initialRequest) + const itemIdentity = resolveGitHubWorkItemIdentity(args.item) try { const repoOwnerSettings = getSettingsForRepoRuntimeOwner(store, args.repoId) @@ -201,11 +203,11 @@ export async function createGitHubWorkItemWorkspaceInBackground( let pushTarget: WorktreeCreationRequest['pushTarget'] let branchNameOverride: string | undefined let compareBaseRef: string | undefined - if (args.item.type === 'pr' && args.item.number) { + if (itemIdentity.type === 'pr' && itemIdentity.number) { try { const result = await deps.resolvePrStartPoint( args.repoId, - args.item.number, + itemIdentity.number, repoOwnerSettings, args.item ) diff --git a/src/renderer/src/lib/github-work-item-background-request.ts b/src/renderer/src/lib/github-work-item-background-request.ts index f37c71ded0a..5b157525053 100644 --- a/src/renderer/src/lib/github-work-item-background-request.ts +++ b/src/renderer/src/lib/github-work-item-background-request.ts @@ -14,6 +14,7 @@ import { CLIENT_PLATFORM, getWorkspaceIntentName, getWorkspaceSeedName } from '@ import { getLocalRepoProjectExecutionRuntimeContext } from '@/lib/local-preflight-context' import { resolveSourceControlLaunchPlatform } from '@/lib/source-control-launch-platform' import { repoIsRemote } from '../../../shared/agent-launch-remote' +import { resolveGitHubWorkItemIdentity } from '@/lib/github-work-item-identity' import { resolveTuiAgentLaunchArgs, resolveTuiAgentLaunchEnv @@ -232,19 +233,20 @@ export function buildGitHubWorkItemStartupPlan(args: { } function getGitHubWorkItemName(item: GitHubWorkItem): { seedName: string; displayName?: string } { + const identity = resolveGitHubWorkItemIdentity(item) const intent = - item.number !== null + identity.number !== null ? getWorkspaceIntentName({ sourceText: item.title, - workItem: { type: item.type, number: item.number, title: item.title } + workItem: { type: identity.type, number: identity.number, title: item.title } }) : null return { seedName: getWorkspaceSeedName({ explicitName: intent?.seedName ?? '', prompt: '', - linkedIssueNumber: item.type === 'issue' ? item.number : null, - linkedPR: item.type === 'pr' ? item.number : null + linkedIssueNumber: identity.type === 'issue' ? identity.number : null, + linkedPR: identity.type === 'pr' ? identity.number : null }), ...(intent?.displayName ? { displayName: intent.displayName } : {}) } @@ -257,6 +259,7 @@ export function buildInitialGitHubWorkItemRequest( const { seedName, displayName } = getGitHubWorkItemName(args.item) const workspaceRunContext = getWorkspaceRunContextForRepo(repo, args.workspaceRunContext) const ownerHost = parseExecutionHostId(getRepoExecutionHostId(repo)) + const identity = resolveGitHubWorkItemIdentity(args.item) return { repoId: args.repoId, worktreeCreateProgressMode: ownerHost?.kind === 'local' ? 'stepped' : 'indeterminate', @@ -264,8 +267,8 @@ export function buildInitialGitHubWorkItemRequest( ...(workspaceRunContext ? { workspaceRunContext } : {}), name: seedName, ...(displayName ? { displayName } : {}), - ...(args.item.type === 'issue' && args.item.number ? { linkedIssue: args.item.number } : {}), - ...(args.item.type === 'pr' && args.item.number ? { linkedPR: args.item.number } : {}), + ...(identity.type === 'issue' && identity.number ? { linkedIssue: identity.number } : {}), + ...(identity.type === 'pr' && identity.number ? { linkedPR: identity.number } : {}), ...(args.telemetrySource ? { telemetrySource: args.telemetrySource } : {}), setupDecision: 'inherit', agent: null, diff --git a/src/renderer/src/lib/github-work-item-identity.ts b/src/renderer/src/lib/github-work-item-identity.ts new file mode 100644 index 00000000000..c9b24e316ff --- /dev/null +++ b/src/renderer/src/lib/github-work-item-identity.ts @@ -0,0 +1,20 @@ +import { parseGitHubIssueOrPRLink } from '@/lib/github-links' + +export type GitHubWorkItemIdentity = { + type: 'issue' | 'pr' + number: number +} + +export function resolveGitHubWorkItemIdentity(item: { + type: 'issue' | 'pr' + number: number + url?: string | null +}): GitHubWorkItemIdentity { + const link = item.url ? parseGitHubIssueOrPRLink(item.url) : null + if (link) { + // Why: stale cached work-item payloads can disagree with a pasted URL. The + // URL path is the user-visible intent, so it decides issue-vs-PR launches. + return { type: link.type, number: link.number } + } + return { type: item.type, number: item.number } +} diff --git a/src/renderer/src/lib/launch-work-item-direct.test.ts b/src/renderer/src/lib/launch-work-item-direct.test.ts index 98913111af8..33ae88fde57 100644 --- a/src/renderer/src/lib/launch-work-item-direct.test.ts +++ b/src/renderer/src/lib/launch-work-item-direct.test.ts @@ -229,9 +229,9 @@ describe('launchWorkItemDirect', () => { openModalFallback: vi.fn(), item: { type: 'pr', - number: 42, + number: 6934, title: 'Fix the bug', - url: 'https://github.com/acme/repo/pull/42', + url: 'https://github.com/stablyai/orca/pull/6934', branchName: 'feature/fix', baseRefName: 'main', isCrossRepository: true @@ -240,21 +240,21 @@ describe('launchWorkItemDirect', () => { expect(mocks.resolvePrBase).toHaveBeenCalledWith({ repoId: 'repo-1', - prNumber: 42, + prNumber: 6934, headRefName: 'feature/fix', baseRefName: 'main', isCrossRepository: true }) expect(mocks.createWorktree).toHaveBeenCalledWith( 'repo-1', - 'review-pr-42', + 'review-pr-6934', 'abc123', 'inherit', undefined, 'sidebar', - 'Review PR 42', + 'Review PR 6934', undefined, - 42, + 6934, { remoteName: 'origin', branchName: 'feature/fix' }, undefined, undefined, @@ -274,6 +274,41 @@ describe('launchWorkItemDirect', () => { ) }) + it('treats a PR-typed GitHub issue URL as an issue without resolving a PR head', async () => { + const { launchWorkItemDirect } = await import('./launch-work-item-direct') + const openModalFallback = vi.fn() + + await expect( + launchWorkItemDirect({ + repoId: 'repo-1', + launchSource: 'task_page', + telemetrySource: 'sidebar', + openModalFallback, + item: { + type: 'pr', + number: 6933, + title: 'The board columns are displayed backwards', + url: 'https://github.com/stablyai/orca/issues/6933', + branchName: 'fix-issue-6933', + baseRefName: 'main', + isCrossRepository: true + } + }) + ).resolves.toBe(true) + + expect(mocks.resolvePrBase).not.toHaveBeenCalled() + expect(openModalFallback).not.toHaveBeenCalled() + const createArgs = mocks.createWorktree.mock.calls[0] + expect(createArgs?.[1]).toBe('issue-6933') + expect(createArgs?.[2]).toBeUndefined() + expect(createArgs?.[6]).toBe('Issue 6933') + expect(createArgs?.[7]).toBe(6933) + expect(createArgs?.[8]).toBeUndefined() + expect(createArgs?.[9]).toBeUndefined() + expect(createArgs?.[12]).toBeUndefined() + expect(createArgs?.[24]).toBeUndefined() + }) + it('uses the Linear identifier in direct-launch workspace names', async () => { const { launchWorkItemDirect } = await import('./launch-work-item-direct') diff --git a/src/renderer/src/lib/launch-work-item-direct.ts b/src/renderer/src/lib/launch-work-item-direct.ts index def1c5c6c8a..61f49274edb 100644 --- a/src/renderer/src/lib/launch-work-item-direct.ts +++ b/src/renderer/src/lib/launch-work-item-direct.ts @@ -16,6 +16,7 @@ import { ensureHooksConfirmed } from '@/lib/ensure-hooks-confirmed' import { getConnectionId } from '@/lib/connection-context' import type { GitPushTarget, SetupDecision, TuiAgent } from '../../../shared/types' import { getLinearIssueWorkspaceName } from '../../../shared/workspace-name' +import { resolveGitHubWorkItemIdentity } from '@/lib/github-work-item-identity' import { buildDirectWorkItemAgentStartupPlan, buildDirectWorkItemStartupOpts, @@ -70,6 +71,16 @@ export async function launchWorkItemDirect(args: LaunchWorkItemDirectArgs): Prom const repoOwnerSettings = getSettingsForRepoRuntimeOwner(store, repoId) const promptDelivery = args.promptDelivery ?? 'draft' const repoConnectionId = repo.connectionId?.trim() || null + const githubIdentity = + item.number !== null && (item.type === 'issue' || item.type === 'pr') + ? resolveGitHubWorkItemIdentity({ + type: item.type, + number: item.number, + url: item.url + }) + : null + const itemType = githubIdentity?.type ?? item.type + const itemNumber = githubIdentity?.number ?? item.number const repoProjectRuntime = repoConnectionId ? undefined : getLocalRepoProjectExecutionRuntimeContext(store, repoId, CLIENT_PLATFORM) @@ -107,10 +118,10 @@ export async function launchWorkItemDirect(args: LaunchWorkItemDirectArgs): Prom trustDecision === 'skip' ? 'skip' : setupResolution.decision const workspaceIntentName = - item.number !== null + itemNumber !== null ? getWorkspaceIntentName({ sourceText: item.pasteContent, - workItem: { ...item, number: item.number } + workItem: { ...item, type: itemType, number: itemNumber } }) : null const workspaceName = getWorkspaceSeedName({ @@ -118,18 +129,18 @@ export async function launchWorkItemDirect(args: LaunchWorkItemDirectArgs): Prom ? getLinearIssueWorkspaceName({ identifier: item.linearIdentifier, title: item.title }) : (workspaceIntentName?.seedName ?? ''), prompt: '', - linkedIssueNumber: item.type === 'issue' ? (item.number ?? null) : null, - linkedPR: item.type === 'pr' ? (item.number ?? null) : null + linkedIssueNumber: itemType === 'issue' ? (itemNumber ?? null) : null, + linkedPR: itemType === 'pr' ? (itemNumber ?? null) : null }) let resolvedBaseBranch = baseBranch let resolvedPushTarget: GitPushTarget | undefined let resolvedBranchNameOverride: string | undefined let resolvedCompareBaseRef: string | undefined - if (!resolvedBaseBranch && item.type === 'pr' && item.number) { + if (!resolvedBaseBranch && itemType === 'pr' && itemNumber) { try { // Why: direct "Use PR" launches bypass the Start-from picker, so they // must still resolve the PR head before `git worktree add`. - const result = await resolveDirectPrStartPoint(repoId, item.number, repoOwnerSettings, item) + const result = await resolveDirectPrStartPoint(repoId, itemNumber, repoOwnerSettings, item) resolvedBaseBranch = result.baseBranch resolvedPushTarget = result.pushTarget resolvedBranchNameOverride = result.branchNameOverride @@ -157,15 +168,15 @@ export async function launchWorkItemDirect(args: LaunchWorkItemDirectArgs): Prom undefined, telemetrySource, workspaceIntentName?.displayName ?? item.title, - item.type === 'issue' && item.number ? item.number : undefined, - item.type === 'pr' && item.number ? item.number : undefined, + itemType === 'issue' && itemNumber ? itemNumber : undefined, + itemType === 'pr' && itemNumber ? itemNumber : undefined, resolvedPushTarget, undefined, item.linearIdentifier, resolvedBranchNameOverride, undefined, - item.type === 'mr' && item.number ? item.number : undefined, - gitLabIssueNumber(item), + itemType === 'mr' && itemNumber ? itemNumber : undefined, + gitLabIssueNumber({ ...item, type: itemType, number: itemNumber }), undefined, undefined, undefined, diff --git a/src/renderer/src/lib/smart-github-submit.test.ts b/src/renderer/src/lib/smart-github-submit.test.ts index 43ec3c36325..64e58824e0c 100644 --- a/src/renderer/src/lib/smart-github-submit.test.ts +++ b/src/renderer/src/lib/smart-github-submit.test.ts @@ -299,4 +299,26 @@ describe('getSmartGitHubSubmitResolution', () => { expect(resolution.linkedIssueNumber).toBe(2050) expect(resolution.linkedPR).toBeNull() }) + + it('uses the URL path to normalize stale PR-typed issue results', () => { + expect( + getSmartGitHubSubmitResolution({ + type: 'pr', + number: 6933, + title: 'The board columns are displayed backwards', + url: 'https://github.com/stablyai/orca/issues/6933' + }) + ).toEqual({ + workspaceName: 'the-board-columns-are-displayed-backwards', + displayName: 'The board columns are displayed backwards', + linkedWorkItem: { + type: 'issue', + number: 6933, + title: 'The board columns are displayed backwards', + url: 'https://github.com/stablyai/orca/issues/6933' + }, + linkedIssueNumber: 6933, + linkedPR: null + }) + }) }) diff --git a/src/renderer/src/lib/smart-github-submit.ts b/src/renderer/src/lib/smart-github-submit.ts index c6ee5abff47..1b4e818f26a 100644 --- a/src/renderer/src/lib/smart-github-submit.ts +++ b/src/renderer/src/lib/smart-github-submit.ts @@ -4,6 +4,7 @@ import { getTaskSourceCacheScope } from '../../../shared/task-source-context' import { getLinkedWorkItemWorkspaceName } from '../../../shared/workspace-name' import type { LinkedWorkItemSummary } from './new-workspace' import { parseGitHubIssueOrPRLink } from './github-links' +import { resolveGitHubWorkItemIdentity } from '@/lib/github-work-item-identity' export type SmartGitHubSubmitIntent = | { @@ -196,12 +197,14 @@ export function getSmartGitHubSubmitLookupCacheSizeForTests(): number { export function getSmartGitHubSubmitResolution( item: Pick ): SmartGitHubSubmitResolution { - const fallbackName = `${item.type}-${item.number}` - const titleName = getLinkedWorkItemWorkspaceName(item) + const identity = resolveGitHubWorkItemIdentity(item) + const normalizedItem = { ...item, type: identity.type, number: identity.number } + const fallbackName = `${identity.type}-${identity.number}` + const titleName = getLinkedWorkItemWorkspaceName(normalizedItem) const workspaceName = titleName?.seedName || fallbackName const linkedWorkItem: LinkedWorkItemSummary = { - type: item.type, - number: item.number, + type: identity.type, + number: identity.number, title: item.title, url: item.url } @@ -210,7 +213,7 @@ export function getSmartGitHubSubmitResolution( workspaceName, displayName: titleName?.displayName ?? fallbackName, linkedWorkItem, - linkedIssueNumber: item.type === 'issue' ? item.number : null, - linkedPR: item.type === 'pr' ? item.number : null + linkedIssueNumber: identity.type === 'issue' ? identity.number : null, + linkedPR: identity.type === 'pr' ? identity.number : null } } From ad329e2f262a530e70bbc882bcc6fbb16604ffd5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 4 Jul 2026 22:41:11 +0000 Subject: [PATCH 27/45] release: v1.4.122-rc.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 52cae65e873..8c0ff488d53 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "orca", - "version": "1.4.122-rc.3", + "version": "1.4.122-rc.4", "description": "Next-gen IDE for parallel agentic development", "homepage": "https://github.com/stablyai/orca", "author": "stablyai", From 50339fbe82dddf9c2decde476b7ea1a73772f4a8 Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Sat, 4 Jul 2026 16:36:29 -0700 Subject: [PATCH 28/45] Revert "Add default browser action for terminal links (#6129)" (#7378) This reverts commit 3614cbc40cd823c7612d70b236d591e4fd132000. --- .../TerminalContextMenu.test.tsx | 20 --- .../terminal-pane/TerminalContextMenu.tsx | 57 ++++---- .../components/terminal-pane/TerminalPane.tsx | 2 - .../TerminalQuickCommandMenuItem.tsx | 38 ------ .../terminal-url-link-hit-testing.test.ts | 126 ------------------ .../terminal-url-link-hit-testing.ts | 34 +---- .../use-terminal-pane-context-menu.ts | 26 ---- src/renderer/src/i18n/locales/en.json | 1 - src/renderer/src/i18n/locales/es.json | 1 - src/renderer/src/i18n/locales/ja.json | 1 - src/renderer/src/i18n/locales/ko.json | 1 - src/renderer/src/i18n/locales/zh.json | 1 - 12 files changed, 30 insertions(+), 278 deletions(-) delete mode 100644 src/renderer/src/components/terminal-pane/TerminalQuickCommandMenuItem.tsx diff --git a/src/renderer/src/components/terminal-pane/TerminalContextMenu.test.tsx b/src/renderer/src/components/terminal-pane/TerminalContextMenu.test.tsx index a70b378d2bd..0cfa3f1619f 100644 --- a/src/renderer/src/components/terminal-pane/TerminalContextMenu.test.tsx +++ b/src/renderer/src/components/terminal-pane/TerminalContextMenu.test.tsx @@ -57,8 +57,6 @@ function renderMenu(overrides: Record = {}): void { canClosePane: true, canExpandPane: true, menuPaneIsExpanded: false, - linkUrl: null, - onOpenLinkInDefaultBrowser: vi.fn(), onCopy: vi.fn(), onPaste: vi.fn(), onSplitRight: vi.fn(), @@ -116,24 +114,6 @@ describe('TerminalContextMenu', () => { expect(onForkAgentSession).not.toHaveBeenCalled() }) - it('shows "Open in Default Browser" only when a link is under the cursor', () => { - renderMenu({ linkUrl: null }) - expect( - items.list.some((item) => childrenText(item.children) === 'Open in Default Browser') - ).toBe(false) - - items.list = [] - const onOpenLinkInDefaultBrowser = vi.fn() - renderMenu({ linkUrl: 'https://example.com/docs', onOpenLinkInDefaultBrowser }) - - const openLinkItem = items.list.find( - (item) => childrenText(item.children) === 'Open in Default Browser' - ) - expect(openLinkItem).toBeDefined() - openLinkItem?.onSelect?.() - expect(onOpenLinkInDefaultBrowser).toHaveBeenCalledTimes(1) - }) - it('shows one shortcut per terminal menu action on Windows', () => { vi.stubGlobal('navigator', { userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)' diff --git a/src/renderer/src/components/terminal-pane/TerminalContextMenu.tsx b/src/renderer/src/components/terminal-pane/TerminalContextMenu.tsx index d46d66e5865..749b1e98a28 100644 --- a/src/renderer/src/components/terminal-pane/TerminalContextMenu.tsx +++ b/src/renderer/src/components/terminal-pane/TerminalContextMenu.tsx @@ -4,7 +4,6 @@ import { ClipboardCopy, Copy, Eraser, - ExternalLink, GitFork, Maximize2, MessageSquare, @@ -32,11 +31,12 @@ import { } from '@/components/ui/dropdown-menu' import { shouldIgnoreTerminalMenuPointerDownOutside } from './terminal-context-menu-dismiss' import type { TerminalQuickCommand } from '../../../../shared/types' +import { isTerminalAgentQuickCommand } from '../../../../shared/terminal-quick-commands' import { formatPrimaryShortcutLabel } from '@/hooks/useShortcutLabel' +import { AgentIcon } from '@/lib/agent-catalog' import type { KeybindingOverrides } from '../../../../shared/keybindings' import { translate } from '@/i18n/i18n' import { isMacPlatform, nativeChatToggleShortcutLabel } from '../native-chat/native-chat-shortcut' -import { TerminalQuickCommandMenuItem } from './TerminalQuickCommandMenuItem' type TerminalContextMenuProps = { open: boolean @@ -46,8 +46,6 @@ type TerminalContextMenuProps = { canClosePane: boolean canExpandPane: boolean menuPaneIsExpanded: boolean - linkUrl: string | null - onOpenLinkInDefaultBrowser: () => void onCopy: () => void onPaste: () => void onSplitRight: () => void @@ -83,8 +81,6 @@ export default function TerminalContextMenu({ canClosePane, canExpandPane, menuPaneIsExpanded, - linkUrl, - onOpenLinkInDefaultBrowser, onCopy, onPaste, onSplitRight, @@ -132,6 +128,27 @@ export default function TerminalContextMenu({ const showEqualizeShortcut = shortcuts.equalize !== 'Unassigned' const showSetTitleShortcut = shortcuts.setTitle !== 'Unassigned' const showClearPaneTitleShortcut = shortcuts.clearPaneTitle !== 'Unassigned' + const renderQuickCommandItem = (command: TerminalQuickCommand): React.JSX.Element => ( + onQuickCommand(command)}> + {isTerminalAgentQuickCommand(command) ? ( + + + + ) : ( + + )} + {command.label} + {!isTerminalAgentQuickCommand(command) && !command.appendEnter ? ( + + {translate('auto.components.terminal.pane.TerminalContextMenu.c2f0b72b8d', 'Insert')} + + ) : null} + + ) return ( - {linkUrl ? ( - <> - - - {translate( - 'auto.components.terminal.pane.TerminalContextMenu.openLinkDefaultBrowser', - 'Open in Default Browser' - )} - - - - ) : null} {translate('auto.components.terminal.pane.TerminalContextMenu.f3eeb1de13', 'Copy')} @@ -215,13 +220,7 @@ export default function TerminalContextMenu({ {quickCommandRepoLabel} - {repoQuickCommands.map((command) => ( - - ))} + {repoQuickCommands.map(renderQuickCommandItem)} ) : null} {globalQuickCommands.length > 0 ? ( @@ -235,13 +234,7 @@ export default function TerminalContextMenu({ )} ) : null} - {globalQuickCommands.map((command) => ( - - ))} + {globalQuickCommands.map(renderQuickCommandItem)} ) : null} diff --git a/src/renderer/src/components/terminal-pane/TerminalPane.tsx b/src/renderer/src/components/terminal-pane/TerminalPane.tsx index 4db3c5035a5..d3724351da3 100644 --- a/src/renderer/src/components/terminal-pane/TerminalPane.tsx +++ b/src/renderer/src/components/terminal-pane/TerminalPane.tsx @@ -2983,8 +2983,6 @@ export default function TerminalPane({ menuPaneIsExpanded={ contextMenu.menuPaneId !== null && contextMenu.menuPaneId === expandedPaneId } - linkUrl={contextMenu.menuLinkUrl} - onOpenLinkInDefaultBrowser={contextMenu.onOpenLinkInDefaultBrowser} onCopy={() => void contextMenu.onCopy()} onPaste={() => void contextMenu.onPaste()} onSplitRight={contextMenu.onSplitRight} diff --git a/src/renderer/src/components/terminal-pane/TerminalQuickCommandMenuItem.tsx b/src/renderer/src/components/terminal-pane/TerminalQuickCommandMenuItem.tsx deleted file mode 100644 index 4dac903b0b3..00000000000 --- a/src/renderer/src/components/terminal-pane/TerminalQuickCommandMenuItem.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { Play } from 'lucide-react' -import { DropdownMenuItem, DropdownMenuShortcut } from '@/components/ui/dropdown-menu' -import { AgentIcon } from '@/lib/agent-catalog' -import { translate } from '@/i18n/i18n' -import type { TerminalQuickCommand } from '../../../../shared/types' -import { isTerminalAgentQuickCommand } from '../../../../shared/terminal-quick-commands' - -type TerminalQuickCommandMenuItemProps = { - command: TerminalQuickCommand - onSelect: (command: TerminalQuickCommand) => void -} - -export function TerminalQuickCommandMenuItem({ - command, - onSelect -}: TerminalQuickCommandMenuItemProps): React.JSX.Element { - return ( - onSelect(command)}> - {isTerminalAgentQuickCommand(command) ? ( - - - - ) : ( - - )} - {command.label} - {!isTerminalAgentQuickCommand(command) && !command.appendEnter ? ( - - {translate('auto.components.terminal.pane.TerminalContextMenu.c2f0b72b8d', 'Insert')} - - ) : null} - - ) -} diff --git a/src/renderer/src/components/terminal-pane/terminal-url-link-hit-testing.test.ts b/src/renderer/src/components/terminal-pane/terminal-url-link-hit-testing.test.ts index 8af8a006890..87d03c748b7 100644 --- a/src/renderer/src/components/terminal-pane/terminal-url-link-hit-testing.test.ts +++ b/src/renderer/src/components/terminal-pane/terminal-url-link-hit-testing.test.ts @@ -1,8 +1,6 @@ import { afterEach, describe, expect, it, vi } from 'vitest' import { extractTerminalHttpLinks, - getTerminalHttpLinkAtBufferPosition, - getTerminalHttpLinkForMouseEvent, TERMINAL_HTTP_URL_MAX_LENGTH } from './terminal-url-link-hit-testing' @@ -49,127 +47,3 @@ describe('extractTerminalHttpLinks', () => { expect(matchAllSpy).not.toHaveBeenCalled() }) }) - -describe('terminal HTTP link hit testing', () => { - type TestBufferLine = { - isWrapped: boolean - length: number - translateToString: ( - trimRight?: boolean, - startColumn?: number, - endColumn?: number, - outColumns?: number[] - ) => string - } - - function defaultColumnsForText(text: string): number[] { - return Array.from({ length: text.length + 1 }, (_value, index) => index) - } - - function makeBufferLine( - text: string, - options: { isWrapped?: boolean; columns?: number[] } = {} - ): TestBufferLine { - const columns = options.columns ?? defaultColumnsForText(text) - return { - isWrapped: options.isWrapped ?? false, - length: text.length, - translateToString: ( - _trimRight?: boolean, - startColumn = 0, - endColumn = text.length, - outColumns?: number[] - ) => { - if (outColumns) { - outColumns.length = 0 - for (let index = startColumn; index <= endColumn; index++) { - outColumns.push(columns[index] ?? index) - } - } - return text.slice(startColumn, endColumn) - } - } - } - - function makeBuffer( - rows: TestBufferLine[] - ): Parameters[0] { - return { getLine: (y: number) => rows[y] } as Parameters< - typeof getTerminalHttpLinkAtBufferPosition - >[0] - } - - it('returns the URL at a buffer position without opening it', () => { - const line = 'Open https://example.com/docs?q=orca now' - - expect( - getTerminalHttpLinkAtBufferPosition( - makeBuffer([makeBufferLine(line)]), - { x: line.indexOf('example') + 1, y: 1 }, - 80 - ) - ).toBe('https://example.com/docs?q=orca') - }) - - it('returns null when the buffer position is outside the URL', () => { - const line = 'Open https://example.com/docs now' - - expect( - getTerminalHttpLinkAtBufferPosition(makeBuffer([makeBufferLine(line)]), { x: 2, y: 1 }, 80) - ).toBeNull() - }) - - // Why: the context-menu "Open in Default Browser" item hands this URL to - // shell.openExternal, so a dangerous scheme under the cursor must never be - // surfaced as an openable link. - it.each(['file:///etc/passwd', 'javascript:alert(1)', 'smb://host/share', 'vscode://x'])( - 'returns null for the non-http scheme %s', - (dangerousUrl) => { - const line = `Open ${dangerousUrl} now` - - expect( - getTerminalHttpLinkAtBufferPosition( - makeBuffer([makeBufferLine(line)]), - { x: line.indexOf(dangerousUrl) + 1, y: 1 }, - 80 - ) - ).toBeNull() - } - ) - - it('returns wrapped URLs from mouse coordinates', () => { - const rows = [ - makeBufferLine('Visit https://exa'), - makeBufferLine('mple.com/docs', { isWrapped: true }) - ] - const screen = { - getBoundingClientRect: () => ({ - left: 10, - top: 20, - width: 200, - height: 40 - }) - } - const terminal = { - cols: 20, - rows: 2, - element: { querySelector: () => screen }, - buffer: { - active: { - viewportY: 0, - getLine: (y: number) => rows[y] - } - } - } - - expect( - getTerminalHttpLinkForMouseEvent( - terminal as unknown as Parameters[0], - { - clientX: 30, - clientY: 45 - } - ) - ).toBe('https://example.com/docs') - }) -}) diff --git a/src/renderer/src/components/terminal-pane/terminal-url-link-hit-testing.ts b/src/renderer/src/components/terminal-pane/terminal-url-link-hit-testing.ts index 93ff8954be6..67505d5f622 100644 --- a/src/renderer/src/components/terminal-pane/terminal-url-link-hit-testing.ts +++ b/src/renderer/src/components/terminal-pane/terminal-url-link-hit-testing.ts @@ -191,7 +191,7 @@ function getTerminalScreenElement(terminal: Terminal): HTMLElement | null { function getBufferPositionForTerminalMouseEvent( terminal: Terminal, - event: Pick + event: MouseEvent ): { x: number; y: number } | null { const screenElement = getTerminalScreenElement(terminal) if (!screenElement || terminal.cols <= 0 || terminal.rows <= 0) { @@ -260,34 +260,9 @@ export function openHttpLinkAtBufferPosition( terminalColumns: number, deps: UrlLinkHitTestDeps ): boolean { - const url = getTerminalHttpLinkAtBufferPosition(buffer, position, terminalColumns) - if (!url) { - return false - } - - openTerminalHttpLink(url, deps) - return true -} - -export function getTerminalHttpLinkForMouseEvent( - terminal: Terminal, - event: Pick -): string | null { - const position = getBufferPositionForTerminalMouseEvent(terminal, event) - if (!position) { - return null - } - return getTerminalHttpLinkAtBufferPosition(terminal.buffer.active, position, terminal.cols) -} - -export function getTerminalHttpLinkAtBufferPosition( - buffer: { getLine(y: number): IBufferLine | undefined }, - position: { x: number; y: number }, - terminalColumns: number -): string | null { const logicalLines = buildCandidateLogicalLinesForBufferPosition(buffer, position.y) if (logicalLines.length === 0) { - return null + return false } for (const logicalLine of logicalLines) { @@ -296,11 +271,12 @@ export function getTerminalHttpLinkAtBufferPosition( if (!range || !rangeContainsBufferPosition(range, position, terminalColumns)) { continue } - return parsed.url + openTerminalHttpLink(parsed.url, deps) + return true } } - return null + return false } export function openTerminalHttpLink(url: string, deps: UrlLinkHitTestDeps): void { diff --git a/src/renderer/src/components/terminal-pane/use-terminal-pane-context-menu.ts b/src/renderer/src/components/terminal-pane/use-terminal-pane-context-menu.ts index 2dd939f614e..d84f1a080ab 100644 --- a/src/renderer/src/components/terminal-pane/use-terminal-pane-context-menu.ts +++ b/src/renderer/src/components/terminal-pane/use-terminal-pane-context-menu.ts @@ -41,8 +41,6 @@ import { useAppStore } from '@/store' import { translate } from '@/i18n/i18n' import { recordTerminalUserInputForLeaf } from './terminal-input-activity' import { copyTerminalHandleForPane } from './terminal-handle-copy' -import { getTerminalHttpLinkForMouseEvent } from './terminal-url-link-hit-testing' -import { openHttpLink } from '@/lib/http-link-routing' const CLOSE_ALL_CONTEXT_MENUS_EVENT = 'orca-close-all-context-menus' @@ -83,8 +81,6 @@ type TerminalMenuState = { menuOpenedAtRef: React.RefObject paneCount: number menuPaneId: number | null - menuLinkUrl: string | null - onOpenLinkInDefaultBrowser: () => void onContextMenuCapture: (event: React.MouseEvent) => void onPaneTitleContextMenu: (event: React.MouseEvent, paneId: number) => void onCopy: () => Promise @@ -128,7 +124,6 @@ export function useTerminalPaneContextMenu({ const menuOpenedAtRef = useRef(0) const [open, setOpen] = useState(false) const [point, setPoint] = useState({ x: 0, y: 0 }) - const [menuLinkUrl, setMenuLinkUrl] = useState(null) useEffect(() => { const closeMenu = (): void => { @@ -187,17 +182,6 @@ export function useTerminalPaneContextMenu({ pane.terminal.focus() } - const onOpenLinkInDefaultBrowser = (): void => { - if (!menuLinkUrl) { - return - } - // Why: route through the shared funnel (not shell.openUrl directly) so this - // matches shift+click's system-browser path, including the loopback - // worktree-label rewrite for local dev-server links. - openHttpLink(menuLinkUrl, { worktreeId, forceSystemBrowser: true }) - resolveMenuPane()?.terminal.focus() - } - const getShortcutPlatform = (): NodeJS.Platform => { if (navigator.userAgent.includes('Mac')) { return 'darwin' @@ -488,7 +472,6 @@ export function useTerminalPaneContextMenu({ const manager = managerRef.current if (!manager) { contextPaneIdRef.current = null - setMenuLinkUrl(null) return } const clickedPane = @@ -516,11 +499,6 @@ export function useTerminalPaneContextMenu({ return } - // Why: only hit-test the link once the menu is actually opening; the Windows - // copy/paste path above returns without a menu and needs no link lookup. - setMenuLinkUrl( - clickedPane ? getTerminalHttpLinkForMouseEvent(clickedPane.terminal, event.nativeEvent) : null - ) menuOpenedAtRef.current = Date.now() const bounds = boundsElement.getBoundingClientRect() setPoint({ x: event.clientX - bounds.left, y: event.clientY - bounds.top }) @@ -532,14 +510,12 @@ export function useTerminalPaneContextMenu({ if (!manager) { event.preventDefault() contextPaneIdRef.current = null - setMenuLinkUrl(null) return } const target = event.target if (!(target instanceof Node)) { event.preventDefault() contextPaneIdRef.current = null - setMenuLinkUrl(null) return } const clickedPane = manager.getPanes().find((pane) => pane.container.contains(target)) ?? null @@ -568,8 +544,6 @@ export function useTerminalPaneContextMenu({ menuOpenedAtRef, paneCount, menuPaneId, - menuLinkUrl, - onOpenLinkInDefaultBrowser, onContextMenuCapture, onPaneTitleContextMenu, onCopy, diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index ca2bbd195a2..6a2d2f906fc 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -2452,7 +2452,6 @@ "0a917b591a": "Paste", "f3eeb1de13": "Copy", "c2f0b72b8d": "Insert", - "openLinkDefaultBrowser": "Open in Default Browser", "925f49f210": "Expand Pane", "df766809e0": "Collapse Pane", "cff67afad1": "Copy Context" diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json index 9d4cdf90b27..bac90e0d918 100644 --- a/src/renderer/src/i18n/locales/es.json +++ b/src/renderer/src/i18n/locales/es.json @@ -2452,7 +2452,6 @@ "0a917b591a": "Pegar", "f3eeb1de13": "Copiar", "c2f0b72b8d": "Insertar", - "openLinkDefaultBrowser": "Abrir en el navegador predeterminado", "925f49f210": "Expandir panel", "df766809e0": "Contraer panel", "cff67afad1": "Copiar contexto" diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index 775f4b584b3..126c2912414 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -2452,7 +2452,6 @@ "0a917b591a": "ペースト", "f3eeb1de13": "コピー", "c2f0b72b8d": "入れる", - "openLinkDefaultBrowser": "既定のブラウザで開く", "925f49f210": "ペインを展開する", "df766809e0": "ペインを折りたたむ", "cff67afad1": "Copy Context" diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index 62132106ac7..413de5cc0fe 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -2452,7 +2452,6 @@ "0a917b591a": "붙여넣기", "f3eeb1de13": "복사", "c2f0b72b8d": "삽입", - "openLinkDefaultBrowser": "기본 브라우저에서 열기", "925f49f210": "창 확장", "df766809e0": "창 축소", "cff67afad1": "Copy Context" diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index 41d3bbeb76c..94c8cf87079 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -2452,7 +2452,6 @@ "0a917b591a": "粘贴", "f3eeb1de13": "复制", "c2f0b72b8d": "插入", - "openLinkDefaultBrowser": "在默认浏览器中打开", "925f49f210": "展开窗格", "df766809e0": "折叠窗格", "cff67afad1": "复制上下文" From dbd5064235a8843ca2e555386afe213786ad600e Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 4 Jul 2026 16:42:50 -0700 Subject: [PATCH 29/45] fix: refit desktop watcher xterm when mobile-fit override arrives (#6410) (#6596) --- .../components/terminal-pane/TerminalPane.tsx | 56 ++++++++++++++---- .../override-affected-panes.test.ts | 59 +++++++++++++++++++ .../terminal-pane/override-affected-panes.ts | 36 +++++++++++ .../terminal-pane/pty-connection.test.ts | 39 ++++++++++++ .../terminal-pane/pty-connection.ts | 5 ++ 5 files changed, 184 insertions(+), 11 deletions(-) create mode 100644 src/renderer/src/components/terminal-pane/override-affected-panes.test.ts create mode 100644 src/renderer/src/components/terminal-pane/override-affected-panes.ts diff --git a/src/renderer/src/components/terminal-pane/TerminalPane.tsx b/src/renderer/src/components/terminal-pane/TerminalPane.tsx index d3724351da3..afc5f3f70c5 100644 --- a/src/renderer/src/components/terminal-pane/TerminalPane.tsx +++ b/src/renderer/src/components/terminal-pane/TerminalPane.tsx @@ -93,6 +93,7 @@ import type { AgentType } from '../../../../shared/agent-status-types' import { resolvePaneKeyForManager } from '@/lib/pane-manager/pane-key-resolution' import { safeFit } from '@/lib/pane-manager/pane-tree-ops' import { captureTerminalShutdownLayout } from './terminal-shutdown-layout-capture' +import { getOverrideAffectedPanes, getPanesNeedingOverrideFit } from './override-affected-panes' import { inspectRuntimeTerminalProcess, isRemoteRuntimePtyId @@ -350,8 +351,9 @@ export default function TerminalPane({ const daemonActions = useDaemonActions() // Why: override state lives in a plain Map for perf (safeFit reads it on // every resize). This counter forces a re-render when overrides change so - // the mobile-fit banner appears/disappears. When an override is cleared - // (desktop-fit), we also trigger safeFit on affected panes so the terminal + // the mobile-fit banner appears/disappears. On both transitions we also + // trigger safeFit on affected panes: mobile-fit shrinks the watcher's xterm + // to phone dims (matching the live phone-wrapped stream), and desktop-fit // resizes back to desktop dimensions. const [, setOverrideTick] = useState(0) useEffect(() => { @@ -376,17 +378,49 @@ export default function TerminalPane({ const unsubscribe = onOverrideChange((event) => { setOverrideTick((n) => n + 1) - if (event.mode === 'desktop-fit') { - const manager = managerRef.current - if (!manager) { + const manager = managerRef.current + if (!manager) { + return + } + // Why: pane IDs are per-tab, so resolve the affected PTY through this + // tab's live transport bindings instead of global numeric pane IDs. + const getAffectedPanes = (): ReturnType => + getOverrideAffectedPanes( + manager.getPanes(), + (paneId) => paneTransportsRef.current.get(paneId)?.getPtyId(), + event.ptyId + ) + if (event.mode === 'mobile-fit') { + // Why: when mobile starts driving, the agent re-renders its output at + // phone width and that phone-wrapped byte stream flows live into this + // passive watcher's xterm. xterm must shrink to the phone dims now or + // the wide desktop grid renders the narrow stream as overlapping, + // garbled lines. safeFit honors the active override and parks xterm at + // override.cols/rows, matching the incoming stream. rAF lets the DOM + // settle before the resize; no loud fallback is needed because the + // override branch of safeFit is itself the authoritative resize. + // Why: override events fan out to every terminal tab; skip the rAF + // unless this tab has a pane still parked at the wrong grid. + const panesNeedingFit = getPanesNeedingOverrideFit( + getAffectedPanes(), + event.cols, + event.rows + ) + if (panesNeedingFit.length === 0) { return } - // Why: pane IDs are per-tab, so resolve the affected PTY through this - // tab's live transport bindings instead of global numeric pane IDs. - const getAffectedPanes = (): ReturnType => - manager - .getPanes() - .filter((pane) => paneTransportsRef.current.get(pane.id)?.getPtyId() === event.ptyId) + scheduleFitFrame(() => { + for (const pane of getPanesNeedingOverrideFit( + getAffectedPanes(), + event.cols, + event.rows + )) { + safeFit(pane) + } + }) + return + } + if (event.mode === 'desktop-fit') { // Why: fitAddon.fit() measures DOM dimensions, so it must run after // the browser has settled layout. Running synchronously inside the // IPC callback can produce stale measurements. rAF ensures the DOM diff --git a/src/renderer/src/components/terminal-pane/override-affected-panes.test.ts b/src/renderer/src/components/terminal-pane/override-affected-panes.test.ts new file mode 100644 index 00000000000..ba0ca79d821 --- /dev/null +++ b/src/renderer/src/components/terminal-pane/override-affected-panes.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, it } from 'vitest' +import type { ManagedPane } from '@/lib/pane-manager/pane-manager-types' +import { getOverrideAffectedPanes, getPanesNeedingOverrideFit } from './override-affected-panes' + +function makePane(id: number, cols = 120, rows = 40): ManagedPane { + return { id, terminal: { cols, rows } } as ManagedPane +} + +describe('getOverrideAffectedPanes', () => { + it('returns only panes bound to the event PTY in this tab', () => { + const panes = [makePane(1), makePane(2), makePane(3)] + const bindings = new Map([ + [1, 'pty-a'], + [2, 'pty-b'], + [3, 'pty-a'] + ]) + + const affected = getOverrideAffectedPanes(panes, (paneId) => bindings.get(paneId), 'pty-a') + + expect(affected.map((pane) => pane.id)).toEqual([1, 3]) + }) + + it('returns nothing for a watcher whose panes are bound to other PTYs', () => { + const panes = [makePane(10), makePane(11)] + const bindings = new Map([ + [10, 'pty-x'], + [11, 'pty-y'] + ]) + + const affected = getOverrideAffectedPanes(panes, (paneId) => bindings.get(paneId), 'pty-z') + + expect(affected).toEqual([]) + }) + + it('ignores unbound panes (resolver returns undefined)', () => { + const panes = [makePane(1), makePane(2)] + const bindings = new Map([[1, 'pty-a']]) + + const affected = getOverrideAffectedPanes(panes, (paneId) => bindings.get(paneId), 'pty-a') + + expect(affected.map((pane) => pane.id)).toEqual([1]) + }) +}) + +describe('getPanesNeedingOverrideFit', () => { + it('returns only affected panes whose grid does not already match the override', () => { + const panes = [makePane(1, 49, 20), makePane(2, 120, 40), makePane(3, 49, 21)] + + const panesNeedingFit = getPanesNeedingOverrideFit(panes, 49, 20) + + expect(panesNeedingFit.map((pane) => pane.id)).toEqual([2, 3]) + }) + + it('returns nothing when every affected pane already matches the override', () => { + const panes = [makePane(1, 49, 20), makePane(2, 49, 20)] + + expect(getPanesNeedingOverrideFit(panes, 49, 20)).toEqual([]) + }) +}) diff --git a/src/renderer/src/components/terminal-pane/override-affected-panes.ts b/src/renderer/src/components/terminal-pane/override-affected-panes.ts new file mode 100644 index 00000000000..98f20bddcab --- /dev/null +++ b/src/renderer/src/components/terminal-pane/override-affected-panes.ts @@ -0,0 +1,36 @@ +// Why: a fit-override event names a PTY, but pane IDs are per-tab and the +// override stream reaches every subscriber (including passive desktop +// watchers). This resolves which of this tab's panes are bound to the +// event's PTY via the tab's live transport bindings, so both the +// mobile-fit and desktop-fit branches of the override listener refit the +// same set of panes without colliding on global numeric pane IDs. + +import type { ManagedPane } from '@/lib/pane-manager/pane-manager-types' + +// Why: transports return string | null and the binding lookup yields +// undefined for unbound panes, so the resolver tolerates both absent cases. +export type PanePtyResolver = (paneId: number) => string | null | undefined + +export function getOverrideAffectedPanes( + panes: readonly ManagedPane[], + resolvePtyId: PanePtyResolver, + ptyId: string +): ManagedPane[] { + return panes.filter((pane) => resolvePtyId(pane.id) === ptyId) +} + +function paneNeedsOverrideFit( + pane: Pick, + cols: number, + rows: number +): boolean { + return pane.terminal.cols !== cols || pane.terminal.rows !== rows +} + +export function getPanesNeedingOverrideFit( + panes: readonly ManagedPane[], + cols: number, + rows: number +): ManagedPane[] { + return panes.filter((pane) => paneNeedsOverrideFit(pane, cols, rows)) +} diff --git a/src/renderer/src/components/terminal-pane/pty-connection.test.ts b/src/renderer/src/components/terminal-pane/pty-connection.test.ts index dadcf8ac0a8..383d5d141f4 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection.test.ts @@ -3120,6 +3120,45 @@ describe('connectPanePty', () => { expect(pane.container.dataset.ptyId).toBeUndefined() }) + it('refits immediately when binding to a PTY with an active mobile-fit override', async () => { + const { setFitOverride } = await import('@/lib/pane-manager/mobile-fit-overrides') + const { connectPanePty } = await import('./pty-connection') + const transport = createMockTransport() + transportFactoryQueue.push(transport) + const pane = createPane(1) + Object.defineProperty(pane.container, 'getBoundingClientRect', { + configurable: true, + value: () => + ({ + width: 800, + height: 400, + top: 0, + left: 0, + right: 800, + bottom: 400 + }) as DOMRect + }) + ;( + pane.fitAddon as unknown as { proposeDimensions: () => { cols: number; rows: number } } + ).proposeDimensions = vi.fn(() => ({ cols: 120, rows: 40 })) + pane.terminal.resize.mockImplementation((cols: number, rows: number) => { + pane.terminal.cols = cols + pane.terminal.rows = rows + }) + setFitOverride('pty-fit', 'mobile-fit', 49, 20) + + connectPanePty(pane as never, createManager(1) as never, createDeps() as never) + const onPtySpawn = createdTransportOptions[0]?.onPtySpawn as + | ((ptyId: string) => void) + | undefined + expect(onPtySpawn).toBeTypeOf('function') + onPtySpawn?.('pty-fit') + + expect(pane.terminal.resize).toHaveBeenCalledWith(49, 20) + expect(pane.terminal.cols).toBe(49) + expect(pane.terminal.rows).toBe(20) + }) + it('continues post-spawn size reconcile after a transient mobile presence lock', async () => { const frameCallbacks: FrameRequestCallback[] = [] globalThis.requestAnimationFrame = vi.fn((callback: FrameRequestCallback) => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection.ts b/src/renderer/src/components/terminal-pane/pty-connection.ts index d9e65bd34d7..dffa30c47d8 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection.ts @@ -1847,6 +1847,11 @@ export function connectPanePty( const setPanePtyFitBinding = (ptyId: string): void => { bindPanePtyId(pane.id, ptyId, deps.tabId) pane.container.dataset.ptyId = ptyId + // Why: override hydration can arrive before this pane knows its PTY. Once + // data-pty-id is bound, safeFit can park xterm at the held phone grid. + if (getFitOverrideForPty(ptyId)) { + safeFit(pane) + } } let activePanePtyBinding: string | null = null // Why: bind time lets async liveness reconcile ignore a request started From 4fa21f727a92798e1f3e13557cb24e32979d759a Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Sat, 4 Jul 2026 16:54:23 -0700 Subject: [PATCH 30/45] Use official OpenCode SVG icon instead of remote favicon (#7373) * Use official OpenCode SVG icon instead of remote favicon Replaces the remote favicon loading for OpenCode with a local SVG icon. This avoids flaky external network requests and ensures the icon renders properly across light and dark themes using theme-aware currentColor. * Update OpenCodeIcon SVG geometry to use official 512 canvas Update the OpenCode icon geometry and viewBox to use the official 512x512 canvas sourced from the opencode.ai favicon. Using a square viewBox matches sibling glyphs. --- .../components/agent/AgentCombobox.test.tsx | 10 ++++++++ src/renderer/src/lib/agent-catalog.tsx | 5 +++- src/renderer/src/lib/agent-icon-glyphs.tsx | 25 +++++++++++++++++++ 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/renderer/src/components/agent/AgentCombobox.test.tsx b/src/renderer/src/components/agent/AgentCombobox.test.tsx index f1b44096213..6547c3704bd 100644 --- a/src/renderer/src/components/agent/AgentCombobox.test.tsx +++ b/src/renderer/src/components/agent/AgentCombobox.test.tsx @@ -27,4 +27,14 @@ describe('AgentCombobox', () => { expect(markup).not.toContain('https://github.com/Gitlawb.png') expect(markup).not.toContain(' { + const markup = renderToStaticMarkup() + + expect(markup).toContain(' id: 'opencode', label: translate('auto.lib.agent.catalog.e7a4ca5103', 'OpenCode'), cmd: 'opencode', - faviconDomain: 'opencode.ai', homepageUrl: 'https://opencode.ai/docs/cli/' }, { @@ -336,6 +336,9 @@ export function AgentIcon({ if (agent === 'copilot') { return } + if (agent === 'opencode') { + return + } const catalogEntry = getAgentCatalog().find((a) => a.id === agent) if (catalogEntry?.iconUrl) { return ( diff --git a/src/renderer/src/lib/agent-icon-glyphs.tsx b/src/renderer/src/lib/agent-icon-glyphs.tsx index 1bf69207005..d722f7a1d42 100644 --- a/src/renderer/src/lib/agent-icon-glyphs.tsx +++ b/src/renderer/src/lib/agent-icon-glyphs.tsx @@ -112,6 +112,31 @@ export function CopilotIcon({ size = 14 }: { size?: number }): React.JSX.Element ) } +export function OpenCodeIcon({ size = 14 }: { size?: number }): React.JSX.Element { + // SVG geometry sourced from opencode.ai/favicon.svg's official 512 canvas. + // Why: the branded fills are adapted to currentColor so the mark is visible + // in both Orca themes, and keeping the square viewBox matches sibling glyphs. + return ( + + + + + ) +} + export function AgentLetterIcon({ letter, size = 14 From b3f86813b37a410b7557b5135cfb9db7f514f250 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 4 Jul 2026 16:57:09 -0700 Subject: [PATCH 31/45] fix(ci): register PSGallery via PSResourceGet for Windows SignPath install (#7379) Register-PSRepository -Default failed inside the legacy nuget.exe provider with "Missing option value for: '-source'" on the hosted windows-2022 image, leaving PSGallery unregistered so the subsequent Set-PSRepository threw "No repository with the name 'PSGallery'". Prefer Microsoft.PowerShell.PSResourceGet (bundled with PowerShell 7.4+), which has PSGallery registered by default and avoids that code path; fall back to PowerShellGet with an explicit NuGet-provider bootstrap only when PSResourceGet is unavailable. Co-authored-by: Orca --- .github/workflows/release-cut.yml | 36 +++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release-cut.yml b/.github/workflows/release-cut.yml index b792dd9cf47..96af9bd0cd1 100644 --- a/.github/workflows/release-cut.yml +++ b/.github/workflows/release-cut.yml @@ -883,12 +883,32 @@ jobs: if: matrix.platform == 'win' shell: pwsh run: | - # Why: Some hosted Windows images start without PSGallery registered. - if ($null -eq (Get-PSRepository -Name PSGallery -ErrorAction SilentlyContinue)) { - Register-PSRepository -Default -InstallationPolicy Trusted - } + $ErrorActionPreference = 'Stop' + # Why: force TLS 1.2 so gallery downloads work on older hosted images. + [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 - Set-PSRepository -Name PSGallery -InstallationPolicy Trusted + # Why: on some hosted Windows images `Register-PSRepository -Default` + # fails inside the legacy nuget.exe provider with "Missing option value + # for: '-source'", so PSGallery is never registered and the install + # below dies with "No repository with the name 'PSGallery'". PSResourceGet + # (bundled with PowerShell 7.4+) has PSGallery registered by default and + # avoids that code path, so prefer it and fall back to PowerShellGet only + # when it is absent. + $useResourceGet = $null -ne (Get-Command -Name Install-PSResource -ErrorAction SilentlyContinue) + + if ($useResourceGet) { + if ($null -eq (Get-PSResourceRepository -Name PSGallery -ErrorAction SilentlyContinue)) { + Register-PSResourceRepository -PSGallery -Trusted + } else { + Set-PSResourceRepository -Name PSGallery -Trusted + } + } else { + Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force | Out-Null + if ($null -eq (Get-PSRepository -Name PSGallery -ErrorAction SilentlyContinue)) { + Register-PSRepository -Default -InstallationPolicy Trusted + } + Set-PSRepository -Name PSGallery -InstallationPolicy Trusted + } $trimChars = [char[]]@([System.IO.Path]::DirectorySeparatorChar, [System.IO.Path]::AltDirectorySeparatorChar) $documentsRoot = [System.IO.Path]::GetFullPath([Environment]::GetFolderPath('MyDocuments')).TrimEnd($trimChars) @@ -917,7 +937,11 @@ jobs: } try { - Install-Module -Name SignPath -Repository PSGallery -MinimumVersion 4.0.0 -MaximumVersion 4.999.999 -Scope CurrentUser -Force -AllowClobber -ErrorAction Stop + if ($useResourceGet) { + Install-PSResource -Name SignPath -Version '[4.0.0,5.0.0)' -Repository PSGallery -Scope CurrentUser -TrustRepository -Reinstall -ErrorAction Stop + } else { + Install-Module -Name SignPath -Repository PSGallery -MinimumVersion 4.0.0 -MaximumVersion 4.999.999 -Scope CurrentUser -Force -AllowClobber -ErrorAction Stop + } Import-Module SignPath -ErrorAction Stop Get-Command -Name Get-SignedArtifact -Module SignPath -ErrorAction Stop break From 568722a320c74f12063b63a116f96a8effe56e2b Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Sat, 4 Jul 2026 17:06:09 -0700 Subject: [PATCH 32/45] Update mobile Android APK links to 0.0.22 --- README.md | 4 ++-- docs/readme/README.es.md | 4 ++-- docs/readme/README.ja.md | 4 ++-- docs/readme/README.ko.md | 4 ++-- docs/readme/README.zh-CN.md | 4 ++-- src/renderer/src/components/mobile/mobile-platform-copy.ts | 2 +- src/renderer/src/components/settings/MobileSettingsPane.tsx | 2 +- 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index f5cace99b1e..580cf023ee2 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ Monitor and steer your agents from your phone — get notified when an agent finishes and send follow-ups from anywhere. -[iOS App Store](https://apps.apple.com/us/app/orca-ide/id6766130217) · [TestFlight](https://testflight.apple.com/join/YjeGMQBA) · [Android APK 0.0.21](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.21/app-release.apk) · [Docs →](https://www.onorca.dev/docs/mobile) +[iOS App Store](https://apps.apple.com/us/app/orca-ide/id6766130217) · [TestFlight](https://testflight.apple.com/join/YjeGMQBA) · [Android APK 0.0.22](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.22/app-release.apk) · [Docs →](https://www.onorca.dev/docs/mobile) @@ -230,7 +230,7 @@ yay -S stably-orca-bin Pair with your desktop app to monitor and steer your agents from your phone. - **iOS:** [Download on the App Store](https://apps.apple.com/us/app/orca-ide/id6766130217) or [join TestFlight](https://testflight.apple.com/join/YjeGMQBA) -- **Android:** [Download APK 0.0.21](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.21/app-release.apk) +- **Android:** [Download APK 0.0.22](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.22/app-release.apk) --- diff --git a/docs/readme/README.es.md b/docs/readme/README.es.md index 630cf497921..be00fd56369 100644 --- a/docs/readme/README.es.md +++ b/docs/readme/README.es.md @@ -36,7 +36,7 @@ Supervisa y dirige a tus agentes desde el teléfono — recibe una notificación cuando un agente termine y envía instrucciones de seguimiento desde cualquier lugar. -[App Store de iOS](https://apps.apple.com/us/app/orca-ide/id6766130217) · [APK para Android](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.21/app-release.apk) · [Docs →](https://www.onorca.dev/docs/mobile) +[App Store de iOS](https://apps.apple.com/us/app/orca-ide/id6766130217) · [APK para Android](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.22/app-release.apk) · [Docs →](https://www.onorca.dev/docs/mobile) @@ -227,7 +227,7 @@ yay -S stably-orca-bin Vincúlala con tu app de escritorio para supervisar y dirigir a tus agentes desde el teléfono. - **iOS:** [Descargar desde App Store](https://apps.apple.com/us/app/orca-ide/id6766130217) -- **Android:** [Descargar el APK](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.21/app-release.apk) +- **Android:** [Descargar el APK](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.22/app-release.apk) --- diff --git a/docs/readme/README.ja.md b/docs/readme/README.ja.md index df6adecb5da..9325abddfe5 100644 --- a/docs/readme/README.ja.md +++ b/docs/readme/README.ja.md @@ -36,7 +36,7 @@ スマートフォンからエージェントを監視・操作 — エージェントの完了を通知で受け取り、どこからでもフォローアップを送信できます。 -[iOS App Store](https://apps.apple.com/us/app/orca-ide/id6766130217) · [Android APK](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.21/app-release.apk) · [ドキュメント →](https://www.onorca.dev/docs/mobile) +[iOS App Store](https://apps.apple.com/us/app/orca-ide/id6766130217) · [Android APK](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.22/app-release.apk) · [ドキュメント →](https://www.onorca.dev/docs/mobile) @@ -227,7 +227,7 @@ yay -S stably-orca-bin デスクトップアプリとペアリングして、スマートフォンからエージェントを監視・操作できます。 - **iOS:** [App Store からダウンロード](https://apps.apple.com/us/app/orca-ide/id6766130217) -- **Android:** [APK をダウンロード](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.21/app-release.apk) +- **Android:** [APK をダウンロード](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.22/app-release.apk) --- diff --git a/docs/readme/README.ko.md b/docs/readme/README.ko.md index a2e113f090d..91a2641eb76 100644 --- a/docs/readme/README.ko.md +++ b/docs/readme/README.ko.md @@ -36,7 +36,7 @@ 휴대폰에서 에이전트를 모니터링하고 조종하세요 — 에이전트가 완료되면 알림을 받고 어디서든 후속 지시를 보낼 수 있습니다. -[iOS App Store](https://apps.apple.com/us/app/orca-ide/id6766130217) · [Android APK](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.21/app-release.apk) · [문서 →](https://www.onorca.dev/docs/mobile) +[iOS App Store](https://apps.apple.com/us/app/orca-ide/id6766130217) · [Android APK](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.22/app-release.apk) · [문서 →](https://www.onorca.dev/docs/mobile) @@ -227,7 +227,7 @@ yay -S stably-orca-bin 데스크톱 앱과 페어링해 휴대폰에서 에이전트를 모니터링하고 조종하세요. - **iOS:** [App Store에서 다운로드](https://apps.apple.com/us/app/orca-ide/id6766130217) -- **Android:** [APK 다운로드](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.21/app-release.apk) +- **Android:** [APK 다운로드](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.22/app-release.apk) --- diff --git a/docs/readme/README.zh-CN.md b/docs/readme/README.zh-CN.md index 1831f10e4c0..bd8892c24e8 100644 --- a/docs/readme/README.zh-CN.md +++ b/docs/readme/README.zh-CN.md @@ -36,7 +36,7 @@ 用手机监控并指挥你的智能体 — 智能体完成时收到通知,随时随地发送后续指令。 -[iOS App Store](https://apps.apple.com/us/app/orca-ide/id6766130217) · [Android APK](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.21/app-release.apk) · [文档 →](https://www.onorca.dev/docs/mobile) +[iOS App Store](https://apps.apple.com/us/app/orca-ide/id6766130217) · [Android APK](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.22/app-release.apk) · [文档 →](https://www.onorca.dev/docs/mobile) @@ -227,7 +227,7 @@ yay -S stably-orca-bin 与桌面应用配对,用手机监控并指挥你的智能体。 - **iOS:** [从 App Store 下载](https://apps.apple.com/us/app/orca-ide/id6766130217) -- **Android:** [下载 APK](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.21/app-release.apk) +- **Android:** [下载 APK](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.22/app-release.apk) --- diff --git a/src/renderer/src/components/mobile/mobile-platform-copy.ts b/src/renderer/src/components/mobile/mobile-platform-copy.ts index c78190f42e9..057a20f4968 100644 --- a/src/renderer/src/components/mobile/mobile-platform-copy.ts +++ b/src/renderer/src/components/mobile/mobile-platform-copy.ts @@ -20,7 +20,7 @@ const IOS_CHANNEL_COPY: Record = { const ANDROID_COPY: InstallCopy = { ctaLabel: 'Download APK', - url: 'https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.21/app-release.apk' + url: 'https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.22/app-release.apk' } export function getInstallCopy(platform: Platform, iosChannel: IosChannel): InstallCopy { diff --git a/src/renderer/src/components/settings/MobileSettingsPane.tsx b/src/renderer/src/components/settings/MobileSettingsPane.tsx index 2ec4fcd516c..6e7513fc973 100644 --- a/src/renderer/src/components/settings/MobileSettingsPane.tsx +++ b/src/renderer/src/components/settings/MobileSettingsPane.tsx @@ -12,7 +12,7 @@ export { getMobileSettingsPaneSearchEntries } const ORCA_IOS_APP_STORE_URL = 'https://apps.apple.com/app/orca-ide/id6766130217' const ORCA_ANDROID_APK_URL = - 'https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.21/app-release.apk' + 'https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.22/app-release.apk' export function MobileSettingsPane(): React.JSX.Element { const showMobileButton = useAppStore((s) => s.settings?.showMobileButton !== false) From afd9cd5e02c6c1a93c8ce3ddec0ac2fefb99af24 Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Sat, 4 Jul 2026 17:11:21 -0700 Subject: [PATCH 33/45] Improve translations for Spanish, Japanese, Korean, and Chinese (#7381) Translate various remaining English strings in Spanish, Japanese, Korean, and Chinese locale files to provide better localized terminology for common UI actions, settings descriptions, and toast messages. --- src/renderer/src/i18n/locales/es.json | 30 +++++----- src/renderer/src/i18n/locales/ja.json | 84 +++++++++++++-------------- src/renderer/src/i18n/locales/ko.json | 58 +++++++++--------- src/renderer/src/i18n/locales/zh.json | 8 +-- 4 files changed, 90 insertions(+), 90 deletions(-) diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json index bac90e0d918..b1a8371f613 100644 --- a/src/renderer/src/i18n/locales/es.json +++ b/src/renderer/src/i18n/locales/es.json @@ -1106,7 +1106,7 @@ }, "NewWorkspaceComposerCard": { "cbb47ee0dc": "Solo disponible para proyectos Git locales.", - "d861de981b": "Sparse checkout", + "d861de981b": "Checkout sparse", "090cfedeb4": "Escribe una nota", "f8728aa4f9": "Nota", "0ee17638fe": "Nombre del espacio de trabajo", @@ -1443,7 +1443,7 @@ "af9e877f30": "Sin etiquetas", "d6cda23ef1": "Miembros", "cfaadb6b22": "Sin lead", - "34da8ac06c": "Lead", + "34da8ac06c": "Líder", "579f98afcd": "Añade un breve resumen...", "ecbcc83140": "Nombre del proyecto", "b6795e65fd": "Cerrar", @@ -1481,7 +1481,7 @@ "733b8f2421": "Linear / Vistas", "bc06ed0fb0": "Volver a vistas", "3cb855080f": "vistas", - "b4e10f096e": "Owner", + "b4e10f096e": "Propietario", "a04fe7ba73": "Visibilidad", "0aa8525950": "Modelo", "dfc0c79bd8": "Issues", @@ -1599,7 +1599,7 @@ "d45a910c4a": "Cambiar estado Linear de {{value0}}", "d8a517ad89": "Identificador", "50387522d7": "Sin agrupación", - "d747aed72f": "Board", + "d747aed72f": "Tablero", "a6f7e93d7f": "Lista", "e78ec261ed": "Vistas", "727069bee5": "Proyectos", @@ -1723,7 +1723,7 @@ "db234b7519": "Cerrar", "7cb40c04eb": "Siguiente coincidencia", "0f3066256e": "Coincidencia anterior", - "42e466b9f1": "Regex", + "42e466b9f1": "Expresión regular", "90c61387d9": "Distingue mayúsculas y minúsculas", "e07012f26e": "Buscar..." }, @@ -2003,7 +2003,7 @@ "643d7e9496": "upstream", "51d1608920": "Origen", "cdc9bd64fa": "compacto", - "30b2c9df91": "Upstream" + "30b2c9df91": "Ascendente" }, "PRFilterDropdowns": { "979be3cf6b": "Asignado", @@ -2148,7 +2148,7 @@ "c5f79616c3": "Equipos", "25a2196732": "Objetivo", "3fb6473111": "Inicio", - "111bef9aa8": "Lead", + "111bef9aa8": "Líder", "3be47aed6f": "Prioridad", "f5ef24cf46": "Salud", "9ddb58edbd": "Estado", @@ -2250,7 +2250,7 @@ "38ca0b1400": "Esto elimina permanentemente sus archivos locales. No puedes deshacer esto.", "c4f4782c02": "No sugerido", "0a2e3c7cba": "Revisar", - "e97e4580c7": "Dirty", + "e97e4580c7": "Sucio", "9623a5107d": "commits sin push", "e8b3741ff7": "ignorado", "a9957007eb": "Ignorar {{value0}}", @@ -3808,7 +3808,7 @@ "eb9884e55b": "Discord", "5687ab246a": "GitHub", "5f83d86d92": "Changelog", - "cdc87f897e": "Docs", + "cdc87f897e": "Documentación", "e565171a7c": "Atajos de teclado", "4cf5b868d7": "Enviar comentarios", "a428c25998": "Ajustes", @@ -4254,12 +4254,12 @@ "6c1efa2cf8": "En revisión", "6b8285b8dd": "Hecho", "93ac840dcb": "Bloqueado", - "2c19d1db33": "Play", + "2c19d1db33": "Reproducir", "111db162bf": "En pausa", "642da473f2": "Alerta", "6380517b10": "Bandera", "251c817bdd": "Temporizador", - "409528031f": "Review", + "409528031f": "Revisión", "5f9ca31a84": "En espera", "821d156f54": "Discontinuo", "226d1e7773": "Progreso", @@ -4685,7 +4685,7 @@ "385212c7a1": "Agent predeterminado", "f9f127d664": "Override de la ruta o nombre del binario, y edición de argumentos de lanzamiento o entorno predeterminados para este agent.", "f95b5c79b8": "Instalar", - "fe4d630c94": "Docs", + "fe4d630c94": "Documentación", "8dc0192e48": "Desactivado", "df123171d1": "No instalado", "c8794e622e": "Detectado", @@ -5152,7 +5152,7 @@ "1de48ad940": "Árbol de archivos de diff predeterminado", "1b87897af9": "Muestra u oculta el árbol de archivos al abrir vistas de diff combinado.", "12cbc0d0d6": "Lado a lado", - "05b6df93b3": "Inline", + "05b6df93b3": "En línea", "7311f67ee7": "Vista de diff predeterminada", "b492397d34": "Formato de presentación preferido para mostrar diffs de git de forma predeterminada.", "a5db1d3975": "ms", @@ -10015,7 +10015,7 @@ "2cfc6be192": "GitLab", "7a47af0565": "Linear", "0a180280bd": "GitHub", - "b3c60c2b7c": "Smart", + "b3c60c2b7c": "Inteligente", "26824f60dd": "Todo", "6fad211c66": "Cerrado", "2319d87718": "Fusionado", @@ -10487,7 +10487,7 @@ "8ed213397c": "En ejecución", "d340c052fb": "verify", "9a097cae12": "1 pendiente", - "2f37142229": "Squash and merge", + "2f37142229": "Squash y merge", "0aab7ab84a": "Agregar seguimiento de errores de diagnóstico local", "dfe313e0c9": "ABIERTO", "6e3f5223c5": "Explorador", diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index 126c2912414..d9e157b3635 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -57,7 +57,7 @@ "openWorktreePalette": "ワークツリーパレットを開く", "window": "ウィンドウ", "help": "ヘルプ", - "paste": "Paste" + "paste": "貼り付け" }, "tray": { "openOrca": "Orcaを開く", @@ -652,7 +652,7 @@ "devDescription": "Dev-only tools for exercising UI states.", "devBadge": "Dev", "devSearchNotificationPlayground": "Notification playground", - "devSearchNotificationPlaygroundDescription": "Trigger representative toast and notification UI states.", + "devSearchNotificationPlaygroundDescription": "代表的なトーストと通知の UI 状態をトリガーします。", "devSearchKeywordDev": "dev", "devSearchKeywordToast": "toast", "devSearchKeywordSonner": "sonner", @@ -876,7 +876,7 @@ "moved": "moved this", "from": "from", "to": "to", - "activity": "Activity", + "activity": "アクティビティ", "noActivity": "No activity yet." }, "checkActionRequiredHint": "This check needs a manual action on GitHub (for example, approving the workflow run) before merging is unblocked." @@ -2014,7 +2014,7 @@ "13b3ac0a84": "状態", "79c54552f7": "フィルター", "8a2ffbf9b3": "{{value0}} フィルターを削除します", - "19bb6f115f": "reviewed-by" + "19bb6f115f": "レビュー済" }, "PRFilterPickers": { "fdf387297c": "クリア (", @@ -4235,7 +4235,7 @@ "6c1efa2cf8": "レビュー中", "6b8285b8dd": "完了", "93ac840dcb": "ブロック中", - "2c19d1db33": "Play", + "2c19d1db33": "再生", "111db162bf": "一時停止中", "642da473f2": "アラート", "6380517b10": "フラグ", @@ -4393,7 +4393,7 @@ "5e6f7a8b9c": "This removes the saved SSH host and its credentials from this computer. Remote files are not deleted.", "6f7a8b9c0d": "キャンセル", "7a8b9c0d1e": "Open settings", - "8b9c0d1e2f": "Remove host" + "8b9c0d1e2f": "ホストを削除" }, "HostRenameDialog": { "1a2b3c4d5e": "Rename host", @@ -4415,7 +4415,7 @@ "63f36455cc": "Reconnect", "59b553e2aa": "切断", "2d3e4f5a6b": "Check connection", - "3c4d5e6f7a": "Manage host…", + "3c4d5e6f7a": "ホストを管理…", "6e7f8a9b0c": "Remove host…" }, "WorkspaceKanbanDrawer": { @@ -5888,13 +5888,13 @@ "hostSetupStateNotSetUp": "Not set up", "hostSetupStateSettingUp": "Setting up", "hostSetupStateError": "Error", - "hostSetupStateUnsupported": "Unsupported", + "hostSetupStateUnsupported": "未対応", "setupPathPending": "Path pending", "openSetup": "オープン", "removeSetup": "削除", "hostSetupBlockedVersion": "Orca server version is incompatible", "hostSetupMissingCapability": "Update Orca on this host to set up projects", - "setupProjectOnHost": "Set up on another host", + "setupProjectOnHost": "別のホストで設定", "setupProjectOnHostHelp": "Choose a host, then import an existing checkout, clone the repository there, or track a setup that will be provisioned later.", "setupExistingFolder": "既存フォルダをインポート", "setupExistingFolderHelp": "Make this project available on another host by linking a checkout that already exists there.", @@ -5923,7 +5923,7 @@ "cloneFromUrl": "Clone from URL", "cloneFromUrlHelp": "Clone this repository onto the selected host.", "addPlannedHost": "Add host placeholder", - "addPlannedHostHelp": "Remember this host and finish adding the project later.", + "addPlannedHostHelp": "このホストを記憶し、後でプロジェクトの追加を完了します。", "existingFolder": "Existing folder", "addPlannedHostToHost": "Add {{host}}", "addPlannedHostConfirm": "This only records that the project should be available on this host. You can add the folder or clone later.", @@ -6548,7 +6548,7 @@ "ab20575a8a": "オフ", "ab3a1f9068": "wsl.exe", "ask_before_closing_running_terminals_title": "Ask Before Closing Running Terminals", - "ask_before_closing_running_terminals_description": "Show a confirmation before closing a terminal that has a running command or agent.", + "ask_before_closing_running_terminals_description": "実行中のコマンドやエージェントがあるターミナルを閉じる前に確認を表示します。", "scrollSpeed": { "title": "スクロール速度", "description": "通常のターミナルスクロールバック、修飾キーでの高速スクロール、全画面 TUI のホイール速度を調整します。", @@ -8228,7 +8228,7 @@ "0fe0073f0c": "新規ペインとライブ アップデートのデフォルトの terminal フォント サイズ。", "5930244899": "フォントサイズ", "ask_before_closing_running_terminals_title": "Ask Before Closing Running Terminals", - "ask_before_closing_running_terminals_description": "Show a confirmation before closing a terminal that has a running command or agent.", + "ask_before_closing_running_terminals_description": "実行中のコマンドやエージェントがあるターミナルを閉じる前に確認を表示します。", "scrollSpeed": { "title": "スクロール速度", "description": "通常のターミナルスクロールバック、修飾キーでの高速スクロール、全画面 TUI のホイール速度を調整します。" @@ -8359,7 +8359,7 @@ "integration": { "cards": { "d5b3be8ecd": "Re-check", - "8cbc39f862": "Learn more", + "8cbc39f862": "詳しく見る", "707180d09c": "glab auth login", "4be0616873": "The GitLab CLI is installed but not authenticated. Run this command in a terminal:", "54a640af7a": "Install GitLab CLI", @@ -8391,7 +8391,7 @@ "8c20e76308": "Each connected Jira site has one token stored by the active runtime.", "c24e56c532": "Test", "3e7c10d286": "Testing...", - "a2c0015fb8": "Verified", + "a2c0015fb8": "確認済み", "e2ff968276": "Connect Jira", "60996beda6": "Add Jira site", "7ca5ffffdb": "Browse, create, and start work from Jira Cloud issues.", @@ -8416,7 +8416,7 @@ "integration": { "cards": { "793a06e899": "Re-check", - "1a9475dace": "Learn more", + "1a9475dace": "詳しく見る", "19fb419c12": "Gitea credentials are configured but could not authenticate. Check the token, API base URL, and repository permissions, then restart Orca if environment variables changed.", "60708f23da": "only when Orca cannot derive the API URL from the remote.", "709057ad91": "ORCA_GITEA_API_BASE_URL", @@ -8574,12 +8574,12 @@ "07939d084e": "Reset {{value0}} to default", "9b02917027": "Reset to default", "a799f90f82": "Disable {{value0}}", - "25e6e76618": "Disable shortcut", - "035a822ef0": "Add shortcut", + "25e6e76618": "ショートカットを無効にする", + "035a822ef0": "ショートカットを追加", "a0e2ef0e61": "Add another shortcut for {{value0}}", - "245c83af24": "Add another shortcut", + "245c83af24": "別のショートカットを追加", "482a60225d": "Enable {{value0}}", - "6287677c37": "Enable", + "6287677c37": "有効にする", "01481b964c": "Add shortcut for {{value0}}" }, "ShortcutRecorderButton": { @@ -8587,10 +8587,10 @@ "3732775d74": "Add shortcut for {{value0}}", "88764af2c1": "Change shortcut for {{value0}}", "30feb099d6": "Change shortcut {{value0}} of {{value1}} for {{value2}}", - "5d982a2a1f": "Listening for shortcut", - "152e0bcd64": "Add shortcut", - "5bd56445da": "Change shortcut", - "f5ed5dcbf6": "Press keys…" + "5d982a2a1f": "ショートカットを待機中", + "152e0bcd64": "ショートカットを追加", + "5bd56445da": "ショートカットを変更", + "f5ed5dcbf6": "キーを押してください…" }, "ShortcutRemoveButton": { "9e29aff18b": "{{value0}} ショートカット {{value1}} を削除", @@ -8639,21 +8639,21 @@ "forceDeleteClicked": "Force Delete clicked", "devOnlyCallback": "Dev-only callback.", "infoToast": "Info toast", - "infoToastDescription": "Long informational copy with no explicit action.", + "infoToastDescription": "明示的なアクションのない長い情報コピー。", "localCanaryBehind": "Local canary is behind origin/canary", "successToast": "Success toast", - "successToastDescription": "Short confirmation state.", + "successToastDescription": "短い確認状態。", "settingsSaved": "Settings saved", - "shortSuccessCopy": "Short success copy.", + "shortSuccessCopy": "短い成功コピー。", "errorToast": "Error toast", - "errorToastDescription": "Long error copy without recovery actions.", + "errorToastDescription": "復元アクションのない長いエラーコピー。", "failedToSyncWorkspaceMetadata": "Failed to sync workspace metadata", "nativeActionToast": "Native action toast", - "nativeActionToastDescription": "Uses Sonner action and cancel slots.", + "nativeActionToastDescription": "Sonner アクションとキャンセルスロットを使用。", "deleteFailureToast": "Delete failure toast", - "deleteFailureToastDescription": "Custom footer with View and Force Delete.", + "deleteFailureToastDescription": "表示と強制削除を含むカスタムフッター。", "behindBaseRefToast": "Behind base ref toast", - "behindBaseRefToastDescription": "Persistent prompt with an inline Settings link and footer action.", + "behindBaseRefToastDescription": "インラインの設定リンクとフッターアクション付きの永続プロンプト。", "notificationPlayground": "Notification playground", "notificationPlaygroundDescription": "Dev-only triggers for checking toast layout, recovery actions, and long-copy wrapping.", "devOnly": "Dev only" @@ -9306,7 +9306,7 @@ "f5bc5c4cf1": "The hosting provider reports conflicts, but local Git did not reproduce them. Refresh the review or push the branch to recalculate mergeability.", "5bc9bda2af": "Run from this worktree", "e87fb3d929": "Copy mergeability refresh commands", - "1e53e45072": "Copied", + "1e53e45072": "コピーしました", "084c516efb": "Copy commands", "5dc3af25c0": "コメントを選択", "d7a2f9c401": "Send unresolved {{value0}} comments", @@ -9321,10 +9321,10 @@ "e8b4c1a903": "Resolved · {{value0}}", "c3a8e5d710": "Needs review · {{value0}}", "a7f0c7e8d1": "Queue", - "8a621a2c4f": "Grouped", - "b13f85d75c": "Timeline", + "8a621a2c4f": "グループ化", + "b13f85d75c": "タイムライン", "f5cf324efa": "Comment display options", - "5e6e5a13fa": "View", + "5e6e5a13fa": "表示", "actionRequiredHint": "This check needs a manual action on GitHub (for example, approving the workflow run) before merging is unblocked." }, "empty": { @@ -9502,7 +9502,7 @@ "d8a4c0e369": "Authenticate before creating a {{value0}}.", "e9b5d1f470": "Check out a branch before creating a {{value0}}.", "f0c6e2a581": "This branch is not ready for a {{value0}} yet.", - "8f9a0b1c2d": "Nothing to commit. Branch is up to date.", + "8f9a0b1c2d": "コミットするものがありません。ブランチは最新です。", "h3i4j5k607": "Checking whether this branch can create a {{value0}}…" } } @@ -9587,14 +9587,14 @@ "allSessionsLower": "all sessions", "thisScope": "This", "allScope": "All", - "scope": "Scope", + "scope": "スコープ", "currentWorkspace": "Current workspace", "allSessions": "All sessions", "viewOptionsAriaLabel": "Session History view options", - "viewOptions": "View options", + "viewOptions": "表示オプション", "agents": "エージェント", - "sort": "Sort", - "lastUpdated": "Last updated", + "sort": "並び替え", + "lastUpdated": "最終更新", "created": "Created", "group": "Group", "folder": "フォルダ", @@ -11709,7 +11709,7 @@ "4bb7424d6b": "Canceled", "6e776f9ef9": "Download failed", "756bfc25c9": "開く", - "09a9489aa5": "Show", + "09a9489aa5": "表示", "2a4c4b8e1f": "コピー" }, "BrowserToolbarMenu": { @@ -11796,7 +11796,7 @@ "51a470b966": "ssh", "b09b2384fd": "一時停止中", "eaa02014f8": "有効", - "29baf8f4c2": "Source" + "29baf8f4c2": "ソース" }, "AutomationEditorDialog": { "fb1896a5e7": "キャンセル", @@ -12066,7 +12066,7 @@ }, "AutomationSetupDecisionField": { "5a7863909c": "新しいワークスペースごとにセットアップを実行", - "18f000ad4e": "Advanced", + "18f000ad4e": "詳細設定", "874b72195b": "When this automation creates a workspace, prepare it the same way creating a worktree by hand does — run the project's setup and open its terminal tabs." } }, diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index 413de5cc0fe..14aa226e98d 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -57,7 +57,7 @@ "openWorktreePalette": "워크트리 팔레트 열기", "window": "창", "help": "도움말", - "paste": "Paste" + "paste": "붙여넣기" }, "tray": { "openOrca": "Orca 열기", @@ -652,7 +652,7 @@ "devDescription": "Dev-only tools for exercising UI states.", "devBadge": "Dev", "devSearchNotificationPlayground": "Notification playground", - "devSearchNotificationPlaygroundDescription": "Trigger representative toast and notification UI states.", + "devSearchNotificationPlaygroundDescription": "대표적인 토스트 및 알림 UI 상태를 트리거합니다.", "devSearchKeywordDev": "dev", "devSearchKeywordToast": "toast", "devSearchKeywordSonner": "sonner", @@ -876,7 +876,7 @@ "moved": "moved this", "from": "from", "to": "to", - "activity": "Activity", + "activity": "활동", "noActivity": "No activity yet." }, "checkActionRequiredHint": "This check needs a manual action on GitHub (for example, approving the workflow run) before merging is unblocked." @@ -2014,7 +2014,7 @@ "13b3ac0a84": "상태", "79c54552f7": "필터", "8a2ffbf9b3": "{{value0}} 필터 제거", - "19bb6f115f": "reviewed-by" + "19bb6f115f": "리뷰 완료" }, "PRFilterPickers": { "fdf387297c": "지우기 (", @@ -5288,7 +5288,7 @@ "aefa1ecb59": "Git 사용자 이름이 구성되지 않았습니다.", "f35007e6e8": "git-username", "3d172725cc": "없음", - "1f32ba27a6": "Custom", + "1f32ba27a6": "사용자 정의", "a182c5125e": "Git 사용자 이름", "sourceControlGroupOrderTitle": "소스 제어 그룹 순서", "sourceControlGroupOrderDescription": "소스 제어에서 변경 사항, 스테이징된 변경 사항 또는 추적되지 않는 파일 중 무엇을 먼저 표시할지 선택합니다.", @@ -8574,12 +8574,12 @@ "07939d084e": "Reset {{value0}} to default", "9b02917027": "Reset to default", "a799f90f82": "Disable {{value0}}", - "25e6e76618": "Disable shortcut", - "035a822ef0": "Add shortcut", + "25e6e76618": "단축키 비활성화", + "035a822ef0": "단축키 추가", "a0e2ef0e61": "Add another shortcut for {{value0}}", - "245c83af24": "Add another shortcut", + "245c83af24": "다른 단축키 추가", "482a60225d": "Enable {{value0}}", - "6287677c37": "Enable", + "6287677c37": "활성화", "01481b964c": "Add shortcut for {{value0}}" }, "ShortcutRecorderButton": { @@ -8587,10 +8587,10 @@ "3732775d74": "Add shortcut for {{value0}}", "88764af2c1": "Change shortcut for {{value0}}", "30feb099d6": "Change shortcut {{value0}} of {{value1}} for {{value2}}", - "5d982a2a1f": "Listening for shortcut", - "152e0bcd64": "Add shortcut", - "5bd56445da": "Change shortcut", - "f5ed5dcbf6": "Press keys…" + "5d982a2a1f": "단축키 대기 중", + "152e0bcd64": "단축키 추가", + "5bd56445da": "단축키 변경", + "f5ed5dcbf6": "키를 누르세요…" }, "ShortcutRemoveButton": { "9e29aff18b": "{{value0}} 바로가기 {{value1}} 제거", @@ -8639,21 +8639,21 @@ "forceDeleteClicked": "Force Delete clicked", "devOnlyCallback": "Dev-only callback.", "infoToast": "Info toast", - "infoToastDescription": "Long informational copy with no explicit action.", + "infoToastDescription": "명시적 작업이 없는 긴 정보 메시지입니다.", "localCanaryBehind": "Local canary is behind origin/canary", "successToast": "Success toast", - "successToastDescription": "Short confirmation state.", + "successToastDescription": "짧은 확인 상태입니다.", "settingsSaved": "Settings saved", - "shortSuccessCopy": "Short success copy.", + "shortSuccessCopy": "짧은 성공 메시지입니다.", "errorToast": "Error toast", - "errorToastDescription": "Long error copy without recovery actions.", + "errorToastDescription": "복구 작업이 없는 긴 오류 메시지입니다.", "failedToSyncWorkspaceMetadata": "Failed to sync workspace metadata", "nativeActionToast": "Native action toast", - "nativeActionToastDescription": "Uses Sonner action and cancel slots.", + "nativeActionToastDescription": "Sonner 작업 및 취소 슬롯을 사용합니다.", "deleteFailureToast": "Delete failure toast", - "deleteFailureToastDescription": "Custom footer with View and Force Delete.", + "deleteFailureToastDescription": "보기 및 강제 삭제가 포함된 사용자 정의 바닥글입니다.", "behindBaseRefToast": "Behind base ref toast", - "behindBaseRefToastDescription": "Persistent prompt with an inline Settings link and footer action.", + "behindBaseRefToastDescription": "인라인 설정 링크와 바닥글 작업이 있는 영구 프롬프트입니다.", "notificationPlayground": "Notification playground", "notificationPlaygroundDescription": "Dev-only triggers for checking toast layout, recovery actions, and long-copy wrapping.", "devOnly": "Dev only" @@ -9306,7 +9306,7 @@ "f5bc5c4cf1": "The hosting provider reports conflicts, but local Git did not reproduce them. Refresh the review or push the branch to recalculate mergeability.", "5bc9bda2af": "Run from this worktree", "e87fb3d929": "Copy mergeability refresh commands", - "1e53e45072": "Copied", + "1e53e45072": "복사됨", "084c516efb": "Copy commands", "5dc3af25c0": "댓글 선택", "d7a2f9c401": "해결되지 않은 댓글 {{value0}}개 보내기", @@ -9321,10 +9321,10 @@ "e8b4c1a903": "Resolved · {{value0}}", "c3a8e5d710": "Needs review · {{value0}}", "a7f0c7e8d1": "Queue", - "8a621a2c4f": "Grouped", - "b13f85d75c": "Timeline", + "8a621a2c4f": "그룹화", + "b13f85d75c": "타임라인", "f5cf324efa": "Comment display options", - "5e6e5a13fa": "View", + "5e6e5a13fa": "보기", "actionRequiredHint": "This check needs a manual action on GitHub (for example, approving the workflow run) before merging is unblocked." }, "empty": { @@ -9502,7 +9502,7 @@ "d8a4c0e369": "{{value0}}을(를) 만들기 전에 인증하세요.", "e9b5d1f470": "{{value0}}을(를) 만들기 전에 브랜치를 체크아웃하세요.", "f0c6e2a581": "이 브랜치는 아직 {{value0}}을(를) 만들 준비가 되지 않았습니다.", - "8f9a0b1c2d": "Nothing to commit. Branch is up to date.", + "8f9a0b1c2d": "커밋할 항목이 없습니다. 브랜치가 최신 상태입니다.", "h3i4j5k607": "이 브랜치에서 {{value0}}을(를) 만들 수 있는지 확인 중…" } } @@ -10322,7 +10322,7 @@ "4b2fc4b80c": "사고 강도", "be8917699e": "모델", "4d9b6d84df": "지원되지 않습니다. Claude, Codex 또는 Custom을 선택하세요.", - "560d4feb00": "Custom", + "560d4feb00": "사용자 정의", "29d119fe95": "에이전트", "f9382b48a1": "AI author 활성화", "1c0cb4fabb": "AI 작성자", @@ -10530,7 +10530,7 @@ "ca2cfbf188": "Terminal을 아래로 분할", "e370fa8c2b": "Terminal을 오른쪽으로 분할", "b85eab49dd": "src/auth/session.ts", - "99f5224f1e": "Edit", + "99f5224f1e": "편집", "0d93c298a7": "src/auth 던지기", "17cfdc3344": "Grep", "9923847785": "Read", @@ -11709,7 +11709,7 @@ "4bb7424d6b": "Canceled", "6e776f9ef9": "Download failed", "756bfc25c9": "열기", - "09a9489aa5": "Show", + "09a9489aa5": "표시", "2a4c4b8e1f": "복사" }, "BrowserToolbarMenu": { @@ -12066,7 +12066,7 @@ }, "AutomationSetupDecisionField": { "5a7863909c": "새 워크스페이스마다 설정 실행", - "18f000ad4e": "Advanced", + "18f000ad4e": "고급", "874b72195b": "When this automation creates a workspace, prepare it the same way creating a worktree by hand does — run the project's setup and open its terminal tabs." } }, diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index 94c8cf87079..258a5cdd31e 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -2014,7 +2014,7 @@ "13b3ac0a84": "状态", "79c54552f7": "筛选条件", "8a2ffbf9b3": "删除 {{value0}} 筛选条件", - "19bb6f115f": "reviewed-by" + "19bb6f115f": "已审查" }, "PRFilterPickers": { "fdf387297c": "清除 (", @@ -5154,7 +5154,7 @@ "bf16ef0af2": "关闭", "3f6892f307": "开启", "b82f86d7d2": "Rich Markdown Spellcheck", - "5195f0b9ef": "Show browser spelling underlines and suggestions while editing rich Markdown." + "5195f0b9ef": "在编辑富 Markdown 时显示浏览器拼写下划线和建议。" }, "AdvancedNetworkSettingsSection": { "3e431564b5": "本地主机,127.0.0.1,*.internal", @@ -7409,7 +7409,7 @@ "defaultProjectRuntime": "默认项目运行时", "defaultProjectRuntimeDescription": "选择本地 Windows 项目继承的运行时。", "d2d2d929c0": "Rich Markdown Spellcheck", - "4497e2e2bb": "Show browser spelling underlines and suggestions while editing rich Markdown." + "4497e2e2bb": "在编辑富 Markdown 时显示浏览器拼写下划线和建议。" } }, "git": { @@ -8182,7 +8182,7 @@ "keyword_warp": "warp", "keyword_themes": "themes", "keyword_yaml": "yaml", - "keyword_legacy_title": "Import themes from Warp" + "keyword_legacy_title": "从 Warp 导入主题" }, "yaml_import": { "title": "从 YAML 导入", From 4828a09e10b94168d2d3a972df2900276ef6fda9 Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Sat, 4 Jul 2026 17:10:23 -0700 Subject: [PATCH 34/45] Add star history to README --- README.md | 4 ++++ docs/assets/star-history.png | Bin 0 -> 129489 bytes 2 files changed, 4 insertions(+) create mode 100644 docs/assets/star-history.png diff --git a/README.md b/README.md index 580cf023ee2..e30d4a34a65 100644 --- a/README.md +++ b/README.md @@ -256,6 +256,10 @@ Want to contribute or run locally? See our [CONTRIBUTING.md](.github/CONTRIBUTIN Orca contributors +

+ GitHub star history chart for stablyai/orca +

+ ## License Orca is free and open source under the [MIT License](LICENSE). diff --git a/docs/assets/star-history.png b/docs/assets/star-history.png new file mode 100644 index 0000000000000000000000000000000000000000..39d065095903549997c42eda88d45a47b282d4d4 GIT binary patch literal 129489 zcmaG|cRbbIACIo5TUxiQjED-kLT2V|pi&6Q$cS8vH?{9bnnFkHjAS z7x+c9`}@az`;PCsdHsrlHPOh*!FFTq6|q%~HtVCG%-yl%ay)eJ1gO2`-qd`!LVHbv z@fzcsM{`_f4@%Z*QrtOvqL54Gwb$iKXBmsQ6#8AKM8_!PJK&dc!Jqj~Lou;q z!|H}=!z#_0@%^p+(`#$F3Dr;IY}6;jVDRG%DCd1d#2X(_EhJd)?Qh@%9TTU_6c~kl z<%~YKXXrl66e-o2tq1cpNnpM%;iOz;Yd{?Ea?_M+(ugZVPSVwKlv!bwInzjxn6#J!Z9SHVALGM$~1nw^MBWHt#n zaD?dn*`3v{|60bO>8i#EUj2qLDtI@w2W}JPFzy}fa`L$B(W+Q(gJF_g3x^((btGci znORnjh)CzA#+Qc(22LI#E5z;-1Uuba9$2-e5jYj?;qk%s7f0NOVSWCf-a4rnP!Z?A zj8eBR>`XPFK(zA5@?d2gvhj#REakJJp3nEp(|Rs~Zj*yHey2i!!+ZJFz~UqE7}MTO zf>{8Cx9Fe44oEXPZ#o0W>E-sXM($7p%r}ZQ^2(pH@DfY!9?R(mUYyu4KcaquQ*qB80o9g? zC2nj9Pt7yH4J2ppK@Ugxkj!}Qq0lB%gIA*h0ek7#2A5pjd{QsQdG})1R|l~phU@ro zSudhY$z4RLX(N`{;G6Z!Kkm;I`uV0=qToLjHf1JhE8aH=ca~p+fxh-*dTj(%jCu_9-Ijx%5RU)0{ysP`So?llC znAemOuswtI`~fSdy=m3YbEeoL>L$nUQJSdggK!qRv9~mw;LUPcn^at%6k5vmnV@10 z!C8c6D+eeyR#@XnOt}582S^nSHXPt=vP)-b^1y`up zX{~7i6aM?(22;_2HxCHd9oihYzFlfkmUDZlByzsShe*deN4x0Hf3RxS?AnP1AWRZB z-a{QRph&c`#jsnX-ldW-55bwd!1bfoDZ{Bk9CqzrDa}wr1Ge00T`jSxk&ZI}HripK zjb%f6dxQmuiDb^v&@;Q1j|=C z@HmwSk<34p^p<+f*Mq}ZB(Jq_Z^$C1k2rp7!@HytvSegHC6OF{u|u!2vzo}AZ|kE` zizJz5uEb>~_J)5jpEm2SmOXMth+Au`M)4Pgx~zYF1FET`?X;Jcb0;pBu$}` zwVZ@M4c&VP$Mx=_cz4f?%?1+n+fxLCS|g6%tFLxjS|9?S!4hwEZ5-HnP2vCAOQsCi zv%~mp?=8)s&Hiig-v;Nu1e_>|RV>>5PpU7@gx%lQ)g5u!RG#)O|6muNZSJmy3oxsf z4Y}ae2zz3Q>pO&4|Npk40pDDJu0E=G+hK$K=ZJ0poNRNjtP0?2eT_KGwpuu>o=>-T zY3m30_zUENto$Xs?iy|}UJipMcJE-#ctGfj*H2lM{J-VF;J|J;C$)730OL_}keKiv z%L6}S5I~^3j(^$oTQQOFj2C|#_J&%j83x2rUn8Ls+hQ7&DV*xu)|bs^WF0XVPenQ> zNvZpq?BN5H?x6wlJ%bNRqfnOxI8ueKp*XfSyt&!E`u`>%%{hm}=Rim>kq0w8{&jiK zRuaLfc*W(#Y+BbGM$M4D)Z3I5j31C>cCJsmNC=F_mfhHOWdwqX8a>oZA5Q+S&x&IeOj-f`Bh<%&k z)|DA>aizx3oZE1E-wHYm#+kdmCE6h56--CPTX^TfatqS8HyAz1H~(a4P_mQ zj4<6z4lvV}Qqlq3P#6VQQnQgGVtS@8g{v^ojd z4m~?={>E|rq5Q#QEhHQ_DGweWFe1wNSHf+m>m|Smqsa{?HkMbTP87G7a9g{!l?5}p zaCE%d(nl~`wfu$cFB|*|*w$hUPU0K7WHN)@!=;&bP)>UWYodVicfNkXBGY;G-`}`3 zC9qEA(-%DU|ewu{f-X+Q0j(x4gJBHI78=|BqvKdvWj&#eYcDMl=gC&L3VJ zDuRY&CU6fiL6k6eR`nEA+WHp9+m~~scFc&F-Yrja^uwgI2M3`E{ z1lhj~77zq)t~Hz6`#(Kogzp{-*g~$jHpeoqi}0sv+Pyqr1^`_)96E-YIAX$`|J=)7 zhYqEg0u%lZGw=_G4iypT{N$f|q2mQ%z@H8s$Z)`^4(>UW4TtV-Qx381E~Z@H?w-Bu za_AUm3q^MSAMV`h_N&Z=^VKEGU#NX8d}l|2Mhf4H}b-hl0OfU8|cJtgqKtBQzVG zHT;q&p`su4wl2zFJ!mX%ZS_k9{a(hYnI_&R0mS{XRtoh_K*o8A*wnT@xQ?ro)<=Cl zvpH&Qbv|#oW=$yG$Hd40!P^-=v@Vpp~ADNInRrY!?L-Ak=L&Tn-62#dcCAn%PTmx6zW~za! zn43Q5quzT7UKn2Vkr562Yl-~_oG&4x+kK{E+>xL30@D}lKJ%!zinqqCONElgl~?=4 z)_%0kuj`guXEcxRFqNPL&}m!=S0#aZ4|hha8%50PlOGi>v|vlSM7tLI!)2V7+VFMy zF`<5<6PRNklB)#SD!HqLTjr5#%l%@y_(a+Bi+`F z{F|a|Zj^8m{{b85dw}*a%625Nw63jlF3m{!XFz02yspJnp=!}3qck29<{5E%326pD zHa=48-}b3nc?BwRqGM-J5h*}S>vC20Z_Dw3B_@ztcTUI^6F+Zf?&ARtJFm`hY1BV= zE;S(vj=ZmGoH|o;?N}6iHmLp5J6tllvg~Rd#7AmQE3<<@P?o+Dro5e&nMPCUrS)?c z`=snf5+!Yazm}9pS)-<>!B|(f8Kn*$Ng@KbZ3@>iXUTvF9DlneQB&ETxw*b({{a zXHRdh>tp6I9Ro|!$Q;jIsyoOW^6I=yScsz1!@pDdf()Y@?`w##r#8q9@+R2a3F0$W zp*BL%l((o`w&)Bl;ny7JCR~mRvfyldbxBrzuQELQe&DnU4Na_{xYb zD_Q|fqCH>gv)4;nk9J(uPLW{UHaArhN(}f_ozTah7nlpgU6*^A@hh9x?{EEKQuiO2 zi7Un^byoGjM;RG}2?Otwdu8!ce(4|B60(y*y$9+QVNzVG+=ZnO*RrSmWULI}dM}d` zK$pHU*QVQ?0OWzpVO0Lpl`9oTb9%gl4C0|lS{4ir*^Qh6t+er$wpE`A)moI}#e~^D4cMQ4y}h&$q2pjWOU26FY6lP-CQz*BpQjf$AZiji_yQ$t z5%>DZp(No0uytJusvbnW@B#zB&4XJn5o9cMd(>ad8hO*bWJAi4{g_Yw$qPEm8^K0N zHwo?iqW*jx7WGDbk1CEEejvxUd$hc$Ejgtd^4f7WRwGKOd&+tL(HrBo%~?sml4~6* z%e0g$;E`J2THY$Eur6KHNzN={^`0#LQme5#<6<0YL< zEInOLykfxIPn!*FPj#-4V1JF%F2!{f#SE$(DAQFZw|RmpzvGseVZ6v*o@40|^5C+2 z5~$<%X^so+x-ux^ROY9Y1Ne>RuN!Y~or}vH`rKkv7;x8mqToPGwS6DAzRMB58{ve8 zm^w&TU(6hMM>cAdJz3&%sQ0w}L_rGPaY^Tlg8j)P$G`>y$rskgyB^JOJZp&LF{{7B z9|dnA1TW5yBv(fdSeYrRx%zkjK94{?ugjCpVJ_@{LGPFmZ8sU;W2Gc7Fn`uwn_?&~ zy;Cs#H%=|(Zb$+3EaTa1!cc5I3FU)VCg7z!=6w}py&RofPn?)u=B}+S$uFOGmr;K5 z<6N?GKJHTK4jF*i=CmYcqLTIMh!Ea!*irn4if^X`5S=<0sOd!yjV%@`^p{ zRC!)Jur~V33zH6L51d>=?c6Z1WnaeV*A_~RZdNN1lE`Bh`gAJdJuFoQ@3R?8gu;Jl z`RTq7^g(2V*^PZ;MH6sV{7f_M=WZMmTb@W+o+>XLZJ8@L=Ew;-0^?Snnv|+$s-0HN z6~)$)ffz)IQ>vxt?l0qH{VT0&>bH+f^jEJQ0y}YuHRXJ}xlCo0^$RohLp`e}WrVrx z#JIGrLZ3F>gq#J)L?=Z(3wM}n`N*fGIwL~3FBTu=r}DWv&kZqrU53wScNXK=Et_yET#pYTWJd1)3!?Y9yS8zG5k zh~!+o%sqm~v}_szj!>36Nia|^MZY~@cBb$HoMqV5?VK@?zL}J<%;D(!KqdpF?Dun{ z10VsZMEBy5-+)wcj%E(WD~A&J-21s)IMA{b<&0lKIUin4{HW{2B#RAvGds?OzFLAT zmNi^TS?0S$f=vfDjh@$BeZVYWk`21UkgWb%>^1oI|A%1FOHIx0PPHm>rsLOD}L zCiqFNLJQkpBA49gaZUK8^T_Ae_%gpJcD_kB8GA-mxw#{z*4OIIaF;T;R4qnRo29u< zUP8a2Xk$4oX+1aa-aA@}kgO#7u+Vj;L0%?G=^YPxRouzFS#n7Ly(R*z@nG)Km=w_D zrWA#%UB{emOm>vQ$6GtXsZ8JJC$^Xua*ch;Q~#tO=NoQbb$PH zt$RMD^t9cFfWZvNB6{2ZW*RJ_Uw!Nz=nssY=A4%SwZW4U1$wY3Tsgu>@;FTzWuOXU1!KVhcm4@o_8p0o^;RS^}elJBPi%%v8+9CcTr@6fN+eh9t1aVj< zM1Tu-A|p}3sA9?k6|0n9YerREQ3p3k$Xg|l70AE0U3n|6L{Z*!+N>Bc=rC@l;f~LL zQqJzr{+e3qeeQJV_-!d<;`LUeo6=QsjlM)AU1 zt-G@VV{WU8qAx#U29gs6r^t$0fvjydG@3iL6_!Ze?Y(ZOq7G4v=FG{y+x*SR3x~^wCUCEz5GtSW7-4>RJot$ek z4ew4*1MG+%7eJ%(mO93z5yiPsV1ZxioR?b;*6Y7i=2`M#Ck2!bKcp9gk^-K9Ux30+ zDS|VJ7@>9jJGTR2rlIWiXRGWUy20e|6(dzaUJ%iwbUmjz`crX+t1#muRiwhmi)4|N znfex^0xg>1ggi(08eXIpy62q#gEx#fgaP~3H>sMctu#G~lDnBTnx>iwh#)@``%{8e zQs7&MWm}ZD!-#gvoA-!)5{oF+ibtNk;!MKEiu})hEIjW&Dm3zna16L2)3S8Oq|$rIy7n?NefoieSx~s#~cQ( z4Zf#e(R1%L)V4H77G(E_*`c*P`*GOHuWoij=?m-+>?d(8Ml~`{g!;h%ZLd!Waw6XN zPHCZ0Uq;`f$?#^?gJrQt1a=ZWAnim66||<3+IQ+$5tYP=kMwy?MP46SYGXW1=%&mI zs9YFM=+p$<%2#tZLvOrJio;XQeN`g=ZRp`inRX4aVNT-Yh3GzCR(ZJ!qviYqqOY_h zdYkiBXJ>#m;1qx9BQ^&7J4^R{BS8CqM~C(GA|8RWF>d*im2Jei1g~NYF|d%S==O-Z zt$n@&xNIvy7i!btUENEOmBg{j&jV(y@`(<7pt`2sG$abt*GEcx12!VDamjtQJ9mP^ z4>y@%)|1rox7mZdzYX69I!xMOQJ2y0C0k&SRz6x5TI8kR#wN>j3PkN6{5iQ=4+tq;ncM^DQX{V zhJquGAuIYW?bC%6Y$aK4X4hY;An7Y6p(wsv?S;c+E?ic-PY;&PRg~fQsZ*;XgWvC) zr*biu3-_}biAbZ#(~uiRb%dyTlcQ}_HN!ej=hm**K`>zmPFZ4@;DcX{364+QYZ&~H zbRT2OFJ>80>^q`h%Gu2HNX5L8_dxc!Lcf3&3hu7%Fe1}|n8J4+7lTx6&|-HU`7?fT z<7CTyed?iUl-v(LJoYhjEayux8q;hu_fw{`1^Ov84+kJsOqM)GFq{Gbs}#ZnXH*~H zd1{*$O$;$c4hG?y_2{`J$3eDZ#HxP4)=7t}D7kk2tah}v>ldyj zCF0qyMMtGH2zCBRrcWgX+Vo0vmycd}>wh&D{)8#SBd&HN4_?985PPhrho2n3hSkx~ zR_xB|oQ*evb9rz%vL${nn_(mF94QPDLtHie*6U~P_1C-x@;!C=r;>S553A=lD@KH; zzB75?_Ma}DXVs?A!LnZ{yj1m=^7eT=lruH( z4lv;n+9c2Mi2?c)+;fZ-tbZXYRn)cI&dI^}D&vnu38SkZ17|eY1czz!JuqVLsQFO0 zFxgWN+?>|mL%-aa6g)Uthzl~f{USVd9@0a`^P#i@oGf{YF~cqYYepz=p;(6$FL02U z)I!0a|54x-lNG3h^sBlWATBjli;93~Rvsx&!`*I@IT&s5lB%l}1n;4Y7G-VU=MnBW zegs4>3y=nhhLS7dD|X{K{J8YvJz1_c9X};sX4YIK#dSrbQ<*P!4w+(gEbePK%+7?( zXEWm}8bG+ji^JBO;zl>zj|-#RUj}~yr2u32a|Z9Q78V8EIaOc=d$)K5(JfvR8s5JI zIZ3Rm>>-Z}53>5rdA^!jp02LuhzSQip>iIhQTes*g$~Cd`S3FIm(IsUM_9dL`718N zli}l9**M6 zT)MjZeE5OLhcx3tjvkZ2Kb_WAmfBC|^=gS}_YH%!-;1Nf_hSR&YS#=Nb}ZD?nPNe6qz^$3uKXaH`obIaTaGdlY8>LK;jG###)tYMuVXAz#f=KRU2~9l|G5O}T z!QW|7Xr>m1xCDsU&9&&p1hi0enMX~@2~d|ugP=QUu!nxl+I{>oNC$?QkKJH>7H@3= z!@Mn%noag#Uv^{QI<~0tu`}h?j7Z4zA%ty7MOF4Fl~aW;b}CyMraig5ER2`j_pMff zCJH1y%2~(C%V?I}0-X^L`tphv(!YY(KIZ38Z0S@&-1Ce{C)(pu3r#N)xnlYJ3_SM< zUoEvBpRIPAR_dFr5*oiS_)TOxe+xd8T-6Lq zwEn1Txf}k54Mn-$jT06r41=?-pAa0&C~O$z$o0w~%F*emI>;HsjHcs7#RE=CJuSgz zEu^WtKH%a;`bl{P8nVTu$j#;CTU)~J1{G^L#^7)Bq@%Qa}*n;DrKf9 zQQ()c5z$Ib0|L*&142)&e9T*oGj(7ScLGWV_8f7svDs}pAcrEkvs<9BN+S7y_Fto@ z$l5nj7X!g@g_HGQd#^Qv+-y(;TdV0)kK^pe5UM#bcXI};vVCs#;$HDQ`$}b+Xi^b@ zRkpWL&X9Z@5PnRDK6&4C4S${(b{KD7amkqX<|b1rWel;vS~@zjVnYwqOJ|=HsX-E z-i|Zx87bmzx)xWw3SwttcjLGB6mN35@R%~qIe}7&Jk|B%bp1GX0%cyQ zmU42>f`cT!#{oFGPO*s2{@z8Qj|$Xls&0woD?NCd*zhyh0o{LE>GnOY-;#7DZ8HPzv8W_p|7uN@5zugbZKtgy!hrc)K% zhKV9z;=kVbd(Y(pnSz@$6|;&i{a_CNaxsRw7g2RxqbDN+H_yRjbit2T&`U>0yX3*7 zioGhA-vdvb%O1Um+v2EuNRJ*Je>FaF7RplKm{F82QmlA5_VvF{^j6N;zw%s*wO*;y z!dqnSd#;-eD7VZVbY;5Xfxi|tr6oX-_rBZc2bGlT_q)u1`i}i}qpA1ZPr2JDuZR}r zLD8wQ9$hk(M{^Gi7T14$f^v11o8`TejlhHKyj20maxQtkPVq?8^M?-ID0vOVE1wacqLmaNr<`3VY0Z`05wfAR#Ot#XM) zw6i*bLH0Z-x&?MdCjtU*ltC?`P#{*~a=9e9mOG!rVYy<0c()AlqzMPR2|1B}dmsvN z>2!vl`2^eoP4CiP(R3If>vid1JnPnb$rNPdjrx>h=OrgjJXBh_oeK8?K}}JT9@`Lk zQ3#6%`w%WSkA2KjM<^keFcf4!WAyckZMkZvTXTN-A1cB0aF*I>1`TX};(Ao&Q6>Cz+I>c@ZxiiD=fTFvs(Zhl@7{MTbWoO_I%AaVd`Xb}skR45|5$0Pg-sdG?U+LNOhi7_VdX#291-da z$6A||bwdeK4pmGaUdqNeeyR~AS4F<8Ih*^Gva1f2{+q}E_F%TF0yW>H;qCkz%U$&y zU3({phZU+ww9Jza5AzCoi%Q-gmvW#;kVGkb$|jR7N&QH()J^g5t^h^(kT+9o)#+1P zxd36$uknsHKdnICfi*W&`3rsZ%cwirXq80a5>Tv=iURsfwD*cmqBQLb9BReQcJycW z<;ga;7DwEimV_ScNrGL+uv`TvyShgYC#jD>ReJYS>RpPwNQtM^o>_9A9^uJXikU@U zGlpR2^PspkAoUb}E=0ggizl$9(XQ0IP&k%zUIuG8_V6iX{Os&!O40+9c8G^bUx{B3Ga6QKM-@CxEnmZIs z8Utry(&JF~$fay!Q8(WzKta5Wsf8fW&y#d5o;izdakrof0~&>^_!z)WfoVd8dOjYU z4qvnXtWozQVreL$lpDmP2{B51X{#zLHps?P_#;>bmO?+mu#1Q{7Zg4>wrO3ihJS); zk_O7jF7W#%z%vx}*H*6S#^&CNs^c{BSS=MG@W`^fJpS2H;t>@xXQr<4)N7mBfJ!SI z`TY+BPVU0ofG)pgkCvyNg_+jJ_Rlh#zNcFX;G#;_^e`5OQ{@$DDLaO!7GDBd9hj>} z4pjG@PGeNs>1U_eiRXe!T=b=zbG5;n z`B@J-FEk*KF8S^-J*5<7scA%R`(?}O3627Wa&C_d1<0J0~PlqqY`%7-lN59 z_KJ*STtj71=`z+WB;xX=W#rW6!L%8z1B2 zP*De4!M*_P?vzYiMeBkUEjM*hlI_E>E7?8pll@^wZSK!_Np!0?PW4hvWSJX5UYAodtm( z0ZMA+IgihjuGo^YyXi8{=rbmayg(l2J1J5CH)n*?4uoOiCHL{@l=+Hki+~uf9!jB* zCFfD6BBT*YY9P+#_1Cs(hre`>lykl8!I5k2KBd*$W|GA&?ktat74!PVtYOQaM_H1pN|fDVEFuLO;XOUS>>xvB(?m1fJqvL{7* z(%<^y91?vePuq>Wro1=LRWCqLWe+evUhS!)+)t~x=AoR1D7i-(L|Z)mp*Cv3Fj3eE zKdDwVpfTAB&^@qEnf^&<1$g|}pRkyM7A5-`u%QHd6ycHia_ z8WZ{&4dxKk=_lHo+JA53sx^PZQf@~a08l28g}Ni#m754|62`-B5ogdZ#D6M&Quu2` z&sBrVV+sl(Yys%uz~K%?-ZjH%=AYf^jU3=Q)Mj@%_|vY0v}b!BUiBeXT;&)9m^$vfi#I zgHis`ps>$PO3lRL5%?G(fSc`}ixFF46&GVvz$%Bx$4K9~uWqB0cx!gSOK9w4;1Oyf z_4m$~h@-668fhda7{X2xJwzs|+TTV_wjv|GC|qDAsDHr^2n;n;Yhg_pBn14M6BI5C z>TuX8s~9EXrrsIR=eg9&h>76de84YOS=9-3PI{$)va4>hyOZerx9TX!lD3#8DxF?& zYHRCMM1pe7@NBb2c=t-l-`4o_PFq949xS(74w-Su?KH@3 zSB_B5Ru}dP#6YonnM*vpfHC_*lUolwPlAopy}x**AzNm0`KOkImf4)XZ?CGW30EJ8 zMS-8xT3*d05PPhzo1f~8%ChvdEmU8yozKFDhNG7TWU|{J`ERM{B#f&`M8S!zem;Eh z{ECIW7&mblm1IL@y2yMxcPOapu{w$+J}_EtbAQE+ehA90xvT_=WE=knPkt=>8PQAC zl;j02!~u66vw8qPPsql3Vr3VS>!pRF((_hETSk5WO<8%!%c=D53oYZU&$_f)WW9Ps ziqr2*+i9O_TQD(%`5?ladf7B?0uAQi_-#%pd;uafa(!_Q>j!Tt+ITQyPMG(`${*tlCK^3S_1AAS3#8L+kR z1H)|W$rYDckQL|xec1g;hx`B%m2Cd2F?vEJl*#k%&G7)zgH*t#G5KClrvbbTD$`nM z&5BH{a^UP0r4&jc9ajf4;W|~=-=07tt5=*gnpp-!(Lz7Ue$hQkP{wd&L8&D+#hVD8 z5+j^Fh4uq%uMQIncAr3%YRkJZP-72fvm_0Cd@yr7McIx$=Aw4`m+ZQ z>IsWElk7hNY-sZa;mGMJ`?V+z2Uc-Sy0z2V%3ayn8Q$mg_}d>Bk>w?`0MS-D4stM;z-^cx&N@I+`;%`(~RFpGPc}>H0r<1^G zyhEb-pPQ+78qvpdmGeI9gEIy&ykb7AVMSx^WtK(FGv5CF_)^s|MQPaI85D`C1dGtsOB?2ox z@B|=W8ey#aQ*9t4k*NV z{&cz!?gXLV$na^0li?QAhKb(JJZOgJ8m?Lpr0IP|Ca&(=sM2b3tESk2$mVnPxzH-{~=R-5WUle(v@b_$QZ&7GDO?qh1 zPK0G&MGjL-o#!92tC0N?ev?|NL*hoDYVYV_ON~zoN%-HWyxstPYAKOYy9H2Ze%-ye znCz!w@M%OSVt1+w^ZG>1MGP90715Wj7?xxzKi7VC zKTto65epUJ_rrZT7L!D33{**Gx2i7`3doNv844083mQRT$e%r?MWbW6Rm+3jPs{CR zfSbR6<9IXhA&f3J1%N7m)&p1NPaNPuAX~vn;AS{!z zvM&^nv!W$H@>_(-PS!_^i{_FakLYv-w>&Jw^D~aB+cw)j;5F4###eZBuyNh$(l~w0 zo%F|9pvGEv<^}DW+1R_dh05%~JEot%k*P15e(ozgF8!`L(a4ypKQB2uu-akFyk+fS z5%fC9y(B9ys|w&^$_p7S;%@yey}TP>oKFQ}hZmmlyHb*)lrLLk(%YX%@eLyjGa=r0 z@Oy2vCa+;&Hl8yt4Os<>RW*fs;5I}R4WOS>BBV=*6^QeX)9B4eqG|a?mtX#lddN+GTtzyouRH*Z{mgS=g z=koM{QcxWZ)tY+iaN9uA1`|W7@e37_id)Hc>vv7yfpZBD;FTQ+@=iBT=KB8Ni4u= zAR7<(f=I5;IoYd6T{Xwqe3uz+JqLmj{|}a+A$qGZ73EEuJ?=0r<*#n5PjR-mB6YAn zCPuh{nl%V6qV<@>6 zoPD13qh+T}e+s~uJ-VU_yOqeJn=E=HTA28=(gi-WR*78rWX19-Tu8g=7n8T}8?D=u z%K^3bzxXuM{csTG@H6&2l{ML^YNP1-1X1PiYd`&W^fAQ?F2|0k+DK;&6_2x^%>g(D z0V`bs=143=E7~www>ZlVxLd299RQJi_iOfS2{vO->koGl?EwA;>#CJx zNGS!8eFf6ftmGTi$xU}A;^c}89Tf`DrPaBw=U<2|pTSv8 zX;G*PNg@i`OGW1rM`+v=!vXG)qUw4;ZB@UX$3AfN!S(N_*5)ANZuFy_!@i5O3Q zo$`k2wc?QVBaLN~2<6gej+I8qC!*-##jb=>Z-v3Q^Va^^BxuB76V zk{`hT&^a!2vHJbSo>D^~kM>7$1)D+~>zBI>@IL|Uy##=W5E7?K0+^noG5YhMm+m|U z&Z6D)+c>?$FVz~~#DK0wa}R;FES@{Y36{?TUEQ$p@Rx4R{M(|UJ`A8WX`B^ns7 z6E9nQ5E2i7mKp^5Q-;g#(z9X+1uwGj%Vn_JgF|}Kq2%L9e4fWRX-WE=7Ck=R9@0Q0 zuk!$GvL3{9K9D+#R;uESFFlbone*eGqVl=mhmCjlH=iOFxhM;uRm)4>PUD^|Rl?I1 zDFADX26zKt0K>Fce_lH#!n0(h{v{78wl17ENUY$(+|J(xmVXag8)$VNdjkwM3ce1Q za?XDY1M~#eCU`7+y2=8eouiaipN84=-Aq&%hP3`gkh4POXM^lr9rhG=9Y8_`tz(ji z9#wpGZ}Eo^shKo*^%q^@u@Yf1m59(L)HdmnUiT|QX zW}^hPOX>id{!o5gQUX9vKUr_KFqXRb?a`gkRT7(GQvl;$3+c}C zfg(Q@-O#6>)v&`cAy00(2Q~nt`#symat-yIzCGPXl_V1V?#@pvLPU1DvGPA2Qn>Io z2;@9d`*|sWG;^jAU$Z9=lOkVr0=x3z2;WbJb$Gf}-jbRvsKzI;qm>-HNHuP7d1T5> zjyt_9HrdADgH5@@`c6e!@=8U#C$aF_9Ylk(*=n|5|JfpXyH5%gp-M8d1!3H*6zmNC zqfZJnnH{t~y35c*hrZsE_s_<6z?a-ay&LLrdo5tc*qf-!pP6PLg|(@^)<0<8h6(TK z`tk&&AWsEf#DF@{|Lc9n#8ZG~v)SQ#8)(A+*ZU5d!$Gr|?A+n4H~(kvJJhd4RJ47K z4Yc}y_P$4)1TO!m1KWRHgt1k2IeZOtvm6#qKf%Bm^#3~FG3p?#Jy)p8C$YZ1|I_(S zcNSca65$`Yb1B1rbiNB%1I7ERvV9}%xJB-NbiUW%Ao*~#dEfR_W*0lHv+v$UQwepj zm#LC4C=0*S3AHuxb~`UP`g@>zChGAfKl$Ftx85GK@nN&&xdWs^M{=#AAw`%~W$|Mo{aO?0kn2=M6Xm7pWi}*yu(X+n<|BX+#IDCK#3@pt) zLQviB|53ogV-%YXJPP2FF^}#H0K&Da>n)0`I0@yLzS!mbS z_3FiI3aTaNCm$p~Zg)p+PPWmgvukE^z+mQQM&Q18^8@~zW!Ke25p6(HI_64d{Gp`_ z{-1k{zcz6yYA>L?ABHZ+1B@z@S{bmQPQ*%7q*5%vBg*liy%DgjQ#yo+?u z1dv`bx(T|hPWar~VK4BHnoq=py*=3-zzH6SpKoN%X*~Ad%LBZqc^zzpsqph%FzWEV ze1|>rLY%a9H^odE;D&U3-XuM?y0Lxq=a-odlmtH)H4*X+3z2fP*-TK^!)=*gPRZte{V-2w;&WUnFb$4JtQ|G3v_!!BW?05c$GMIhaub5`@qe{Lm( z0YnHyH_12Gk76br*l8c!SoOLI_-Dl#kXo9^c-tG@lyD}7nuj2xiU|5KHu2cPe%JE9UuckY7NP!d!^fDQFp>~=32di6Rvwloc3n$6~Su+l+d!hheF0Gfq& zn%`mY%{1JW9f7E~wd<|sccb%=7a3J&6%3rIWL4iSr#4domA+dog0)~wjtAJ3rSKS* z@Ln7F+!h4zzrjCPwp$z5Rnwmr9Hrc3DEtw~Oc%Jinp#;WLa)xX;*ee`=X((O;utc5 zHuqt5`o29*4%qy*;sl&wdq;B)J8bY=csSZn4(6Av+b%0V_GkjowjlT$bp6o#bH6VC z*I5ZFFcAMZI;zraC5394P59crKjZfssWK7Nd&ov%xK2zc{hLH-K7y6pQ89i*Jr=1* z;wv(fl=bu@$omd9#Simuide)v3>&a|4{9xwCyRO8z8~UAroXlLeNJI`{ltEqBa+;6bTvZ6 z+%kbpLN0VJuPvVdI0WtKn-SmgWnFw$uO?Zl8FmiOsq5c&dLKGBXEYHfPET!ITc_Fi zv|-%UnA5=|bxgCg(ID$CN2|$iv4A8!rZhUMb}l-Lnk$1L=2bVL7Q8g^^`LwGr4fcO ziydzfReG>Oc<{TZLy*AJQc;~l&%v#Jv_ZjHnXN{-d_h`|C|Hk)FrPD zoDBQ@v?sT!O)yUqwD_1)5AlW6_KwFjI=C9Uw7fIDD+cg(8GOF%tp=&pl|$k*IODG? z-^Y!c935=bojxvQ=RoD;QIN&g2Nfe3s1}-J3?)rLOD0Y>*phx1i@7L7(vDM=GFSqaH%x&OqQfI)HS#Mu>k%lg0ZX`Z@Ja&teA`x_nK- z1n74CFmB&3bglKB#)=4Ae7br0wIs!P1VOyxCx5HEgNLn4i~Ic1UTeZ*T3eUmN{)>kpybxOiaTCdSN1arlRscX0&pwIV-PdL4gx%4fY< z5z4PXQ7YHCyi=qyTl4ditw(rAhR1UUWO8|jog78xR#NXd84Ga4=Ef1G(GGM~+y~4} zVRnF!`;G4=_@|-A&vVebR1i|S5}L_NBgs6M zeplmtcwBZ7LC`Eb&Ix0iyjnG^`q}Az<=6_S0)?#5GlHm#a6V}oK-w?IWsPMHcG`_s zR;_GiEeLRyEw!+|F3dNGvH9jdTJl4(9`}8s3fy}zP1D_BLUR)%23i~kndld96mc@< z%1~axLH#lgcuqnsaz-yocr4-AU4oD|A+743_3$@%7`vLB8-|k1$zk&eH>YO zj+o#f6;WYNuzBT^NJ)h|okF0|SKx4rO~jc#j$o&kdv7V2gB)Md8-W7k8Gxt)m?q?G}z6265;pnOm zWsQ++aiYe(uE54Mq8N1wy|iq@fQ8jZEGecZ*Y7ISMm`2bwMWT}9vurHxfB4ZA@~nU zt*J_50sAmdKwnrsF{__r9DT172C7U3IU6luS9CS$UL>C083-meSo?UI^@Ng){viYc{Pfx69uE-`}(F#YmNXiVF6^ZnH< zmEFgk=DtO2tqC%NO(YFl7QWn@bNM-w4)vdS3b1ldB?&&6CVYe7sa@pWVY0Zncd|pV7SO99&geJbyu_f zL!+D%J_gzJv>m$k^(h-~=?zW7%%JwnBkyx4O<$PA3>Of&-!Ib=%=S*z5#BZO+KI;}cYOu7NJ>DS8MT)V*nzxLG8;|jhp*=_@dsH{ zsXaG>r3iYcXa93}?8R+|W;fmr#J0v5Wq!5#iP!52=v!eIaA{tcek1|614B~xf%IYq z1JqUNGFTrmR5HIj2jDv?prbklYJz3&pjlA{@T7XsUzGdBiM`{)o*)Js&#uVf2n=1R zM=RZTkJ%(i%~2080YY~gE?#lXFC=_;`{@*LLBNMuQ12kI$e)kefc{q= z0i#*Z+x)->D*aeY;waNCWYQG~AmjYT5!7a5Bv0FXP|a+gUcr8~pE)kPb?3sCuHA5D zfvf<>wWhLTyfv4UC2Ov6Z~;YZ)!Cv-AMXHEb@D#W2NWuLkv~1ZQ*g6PKM%L*3taw+ zUjfNgl^oDM`?*v{;O8TsJ9SVgCBekE+p&w^e!nq7>xp`G3rt^HN<0|=5~V*y+Bfc>02dz) zY=_d2V{CgfqGg`BNVB_vPFt6T2t+yXh@(k28xN+4L+ssX>8EZxIAoS}tlRF5O>o>_ z<;l9$Ri-z=k!#Lo$R9lovdA-_4UjigFl=BRlq`bNfE5#{NQ0~6flk*gUA6E+nU$FK z&)kyPmPA{H{IAG8)_KTK|wg0c+p5P3rl1&YF2w!Dm+v#~ z%N|W@>!ZzCz?ZQlEO!^cmmP*a#e9xS6QoI^GRPgem+-k$%8 zkiZEN-6EeG&)*611*P^ELpIEGwBV+cK?sM|3NBo!=LhILsFOB&0UiA+$){7!e@^@# zq2T&O!lx;z#PpwdlVaN^fVZa`+k25>p<2U4hFeD9lK4L5ybj=gB~Senn_fz;lh-{R zzM8w#FtcEG;Fw`Cf(nPs=FjdrG6*nKiWIj?4(GuyK#0oc#%thmB^gQ$d$U%6T{MUv z2i=FvDk9k&t)4)YZ!<@zx>R&j2nqVA@rDBX zxmv5*lt8ocR1u=B^i3SMZq-QoicO#Xa^5PaKGcmnm~o+*po+Iyfblwn4V38o>G+q- z*H-3ZG?u$52ea@iCUYk5y(@=6+o>Yd5h)E_y;6Sw;K|7frkTYSf2?0RlHu2EnC5MZ z)6tu@os4UOuq})F^()X-PVeyGrWym4QW8jc>uuFXcS1*SuUUXi-RnE_JdST)A7Ks_ z&5*pvv+5K%{tUTKv zHSim{K#gl;nXLj*;n&j9a%}kC;h~r>o9(IIUq?X*t@;qO>1geS22!W1sJ$75dD-+& zjRPw(qrY574X*0^`82I1bCq|G1xA_-_4T}nOo(9Ex;NNoXUql0`3EY~z-_>%Zc9wf zbjy-?5L+LRnZ)`Qj4(85+3O$ODRz0hEw)`03{&H#SDDYSldNCz24cphQrF#W8aCyd z`sP3A`;Xv_7jvDO{|cL^7X2Fzd?C)-P&pr!|2A)E@>b@@`P`%{SCCbKu0ykOZ`ApZ zsH8gKq@ceoKhBu;e`1fxcQ!N}-ZsF$!Xg3_70T%$=DOUUdD{mP@@<0|+e zmGObBmaPP$#CmaTJeIC3wX;KT-P>b16^XoHpnC8(x0D|LKxxu`^u|XMVeZjcr6WDh z&xcD+SJ+$=&i`t1^;f z#5iVJJlus^VCxKec!{}yEYv-RDrl(-GkvGdirztO5E6U~_ubfe{IqfkZHu<0(Yn(2 z9U+>Ry+&dpAcnZ!=ThD~21Vuj`7iGtrn38; zIy||d245zJeN2QG_fE}J`CSVfk2==mJ~v!Edf?S;@Dm2%by3PL7s^|F+B5PQ9SUFP zaPnL420pTyP>X~R)Mt3n7U&cromyOKpq*5;d`rB3up7+<{qj5EZrUl{aNyq|=8KCt zDs_KBW-26dGTqMxS7evq@`Q4?5xvnh{iai4k+V%*)nKswF4{)6Q`+0)j@O#IxCSajK<_nUe-$}EN$`09}up7da(B&-w=HLr*NsrsgE4hu#5kK_Q5O|8at z4JaeN?%4s#O{dr}S#9&QqE_JGa0O}e1{K@#++=#E!NQQPDR)T9z$Cp5=p?(WF3_cXD-XtNAH)WC;^U+RHJd9SyJWz_;A8PP|m*L7Qu z3Q%iC9c|rPUhZdf9n>>?9ks+-wzftB3_8#Ca5(dAUzgi$)yabGf%KrRnUP@~v0|KpU;0BfGHcQekyW(mpGm3p(+Y zKiEumbQX@MaF32$6>r9T7Oh$Ppi z@1)Dqd;~=E>7)mdzY$s6Z&YL|X!zh-$2-fW?PR?O)6**{$SrJgjQ(a}h9-+sxLCu8 zS}H0GQ-H#A@BIG&ze~$0bu^XO`zzt(~~Mrnrn+)b#* zVeU|)6o6#vk1eWhQjO||9IT&S+K$;LJHF1}|0hus8Blsy&_}ILe(m#%49Sd@EzcIn$owR}fC$7s@MORzwMmMEa zRMRTECnQA9Q8ThEg}N%*I4xnOy8+RxSMF|Bz}8>jKk39o{n0MSH}Pvw0vPhQ6nb{_ zDNO|0fL3}RUXyo)>FYHh@nT6{#-Hz=<|pNgDdqf{rJA#=SH?Ln{xpGEr^e-SF7AJ zK7ww`E&y)5*OH^<(Kz_FHHC-IN=G&G(;1I2-y2o3=|%k&Z9ZQpOZxgcTIqa8)E6aX zo1a<^u7a?ZZTHfM6-EnOy|8t6IyDpk5?cHCwkR7${A?by#yr_iBTOq1cPf7TfC;AP z-^fBO%R&i=rE#fiI`SghPUAwX$c+L{ZtJs)aM9!FW&R0m z-7X*1$)K)f{Alh?mK0HXrwDkSgOsL|)wABM*GObw?Tcr5-=T)CR>tzgX6WSVSp?e2ks3<_UHx%L zgO!xFeL>CdZOj3-EIPODv@T4fw+l3mIMJqXrrgN9?lpO`3a~#k*_PbLxVeKzO(8=n zUwGw;(pj$po{7TvYdpxrXLW`?j_H;HlyG5hy{&CgCra@>5}C2TxTo9a$2e+OiZcI} zeDS-M)CrM;GH+%O(b<+1UW0?_$K3hWKhY{ATPTNIvxo@O;cw^7?9Qe7R89dCuB&22 zu&tJAO+79Gk!F2s z7r$q$0S*U>c&-JbG)?a?3c$v2Tix-X66hW;@Z5Pp%S51?q9u>GHq*m(qvoPpqt#^F z*d#bPL0TpuIhn_PwgDhdKUaNj!kFABseAwc_FB0SyKgD7Fcfb~VTs~N+pZ+O=i3)M zpEt?ZK7DPH9K1}gA{*HzdGg8kv=-OCL)J|h;+9!JuPVP7Cvv>?Vxi_Uv7mdrK^2n%OuJBYE>jA;Q=zc*oDNAwx@?D<$w{HRap~Id9 z@{5Vx>&`ob0I~am?UezZq^CYkKSgHOyT_+)Z@p38hj3wB@1rQ+p}vJ2@kW$);xgeg z;kYeTXebDTIoO`6Q@blLFN5DPw2I{beOI!?F=(VVnVs4`fOzk;40n{}7LCN7)o+55 zMSkGj@abduyM)QuDd!+T7s;15)s`b}1zk5>LFX%|z2x5^jujW%wCgotErA2=$~vB3pzKtT@$5*v@bqZ+w}MIvYniD{kD+{Hbijh=W;SG_jh~JnCJPm=H0q6Iekhv!D`y3v7wh+P|s4SpNN&bi3 zVm;Akr03NgY)hboy{9HK$r3{yzYuX@^JQE|Yv2Lvg|^r4-iZ>*Bsd`ese&|NA zdwm_Ej1l<~tA3E>qzdCx-KU&89^-x4-Z=B#_>S5`w9b+n(7u#4+bQ2-a@&64WH%G8 zBm6^ueg?-Dz{>I$CS@eh0hsv zwvUwYN48cwAQZWJqRc(!r*`gsqkxpZIB{&md$@)DN5vPT-9H`rjl3u6XWv=Unipq` zy0zuD0;lct<%(esV~w^M6=PT5w}7`(^bHn437021^W8S(^cBz> zLs@yzpf|J;;e>_;`sq~Ltp%x{3C*4 z!gIZSA2*fd^g}y9$yS|~{K2bu{DwU3~a=Z~vgF4`QE!PbWj zHNOm_Il8ho4V5ixZrcQl-9l7w#Sgim#HkgGewZy$7`o=(ra^`@sRXkL{?6KO;5G^DGS>3 z1=;#Si&CSE|6!mRdjZOylNvZ2ba^I2S|vae)8Wi+xj_u-v>r+8k&IM%m_Xd$Fj57zR11 z$8_%uQg_Vz?bm$_C+Yw=@fc-~UR5rWJJzXHvh^O*k-;H5TQd>bdlb53r7zn~T;ln_ z+}c#mm-f5Mt#wu($``rj@2e=~+QS||%YZ>`om@w>?Jq_8n?DgTvCA-iVtBy@n2hO! z_p~>LCCR*DDeEel+|SPF+bYk)6(*(&txh(IMK)I;L#d1?3%zV?ifj(L@1@%km$575 z9Q*k#jdRzQ0|5MU3>pBlY9aomLJ)^L?97nVy3;|`$PFQAV%Hm;nu71t3g4ak#u~O? z*VumgfqG&m@S9uitV3>z)^~=kn=TWGdX?g~p#5(MW=_+}7Wxz+jy{jj`)@3vTB)4G zV}46hsJL@*27{JD*6bPHNv4Zm*d)AFMn83Mp<1MA6k-ACw zgiqJW0{YqS$G{F2C75`#*}Zt;So;1GC0A7Z#oL+}Me=N1J|FJ7^wWG;JEfgc+`De? zN8X674lH{B>pmEEI|-;%G`&K?1lt8}lr3PoDCLZb`hk-uf}&)dy& z!A%O2HVq==JFDMVq^7w@kc_X5W|7muQ!_3NyLaNI6cvL7E?i=&GnXz{15$=36QE}R z#%}kxX?I>f)zj{Lu;i_OaI%c4<@1 zA9LKFPbBg5qQ244q0U=*^|rUmgW2)uoaIYGcscCYsOK`g{&`u^->eC^Hc*{@k3OvX@S1=iP)5qz248k=Ild~8G9~vEHi=R*1+{S zB8N34I$f;10sw!z{ON;|Wkd1(Fw45JP-@~_L)Lw7P#> z>hzrS=+%p|y$9ZH*sY)}IexwYyozcwzJUe<1Bua4Alo5U43|C-5npC^3#QHl)PQ|U zjpDLe#er7>qY*c|;RCDK=pZ4B=)rm|kL%mAG8^o%a#jgC*Ou?ifWtbsJrK zez^AevGm2*9|#r&#Hb65N9);FcJI#KAG?SdgeBbp^C|PB54(<3MC`*`9*7D@Dn)LO z0PtKNYtI0BoY_Dw;_?Ge76=fZ4USOvDYiW6`u>rOGjSr>&vPok=MlYWp1?ri69Uxk z3?PEX-+H4+?Nu?9%ZWb%mD?=O&?P?#yOVrocHxqHWb!7X{yTyIbp6TO!ww5@RV&C? zS@Y6&mjU7AN3aEx9H%Dt!<4hS#v0=9vaWPmt*Asou$G8w_x(CAFa5XwmNHPYppy7~ z!g}y05FW)|UtuGr^z*x~+tEgKE8qZ0^eV;(HNRA368;Xo3cw{RPp$6chwZ=TKmDA3 zJ8Dg4EFtn%25*TeI)Nw02%D6j=Fjrl=)=6;@ZMOwG7dh3_WAg}hL(I8q9A zdTZB;+Yb>xs85{wDXjv_mxReXdi$mgOgx3P`8sy-I*XKR`2wKcoyn$^~hNLG7Myg`)Z)gR$X-w zsGeRQb^`pP&BlI+WC(*3D)6Xg{Q|lp-;z4xI6;QWC0R!2B1p?P)EHpYwrlGSl{+`$ zqbN`{@Z2Jl|8qlKQDZ53M;Wr>jvX0@`~@mk^@u|L+a^13;Cc$yZG8<*>1$L(2fe@% z!b#I$D)J-`TUYnm){CwOX&1y!mOP{dew*#)OepxV_a+_s)uKypm;0^%NS?D9fB+eK@LMXoHGiu*j3ZI@v{MvJxd)8T2KhZh-v2VvU zXk$`{x2~FWkIUyM?JMj3!u@FBM7dd%PI{92-HM1OIz{8+e1q*7s%hOi^P=H-9r@W? z-G8jR49tFK@}~;R(h(byvD=d}xeE)r0rL#Pw*p7#Bk9DcVj_o$=9Ez&Y>#fmU z2v?tcBp2P`pPoOQKC2x)(q>s_mVVA9$qzPhma{J~{#?&&%jjdFz+;`0qK)~TB8g|Z z!tIqBAe$1VwlcCYwP@?4IV?9 zVC*)(`yRsF8U&^K&~av#`c!WtPvdl){`jjyO;(%-=0m#b4nE4msmEQoczql=STVVC zTJimNg_I`0QXR5Sh!@F8nQFPjGwKd=CkT)eXWvYc-YMK3>I~u}YJpmSjp)|Bq0W=4 z_c#7ZMG+V!J0KmkhbHBDC~u!@!{ryz@o5bQbD>~vBfif1(2SV+7e-EZcQljbO8P>C zV3VyFb?P0}V@=1d{+^etdpkA23n4kzf*&0My*N~#;V{ncLS(wUsg=hiyDk*0><9W# zxgE2fFk6n7sywSFlpc89LP4afzD~S8OZy$!QK+j%=uekwqz$;xdOD2K)67%&+j?Z^ zTp!4XY~;Gnl`Mm!r+OExuMOo^PfiU;&i($-14uLlB)|<0HHs7g?BiRXbv06~HGWGI zYRmQsSjdiEv1>cf_~`OWJu!M1k((>TYAy2v5q^6`v*yEmjpu-`F6EtHZwAkwt=;yX zQk|(J@tc5^xSr+sPTH59jo5idkEs-!yB6Z%t+(8eNa9bBJNz-XHdPDN%BoQmX?b;; zdeBlc;mR!vstRgOYQ`;P``-ZcBAU>)xiHmEV6ksNXdD1tgzE#Vg4ibZS?V*T`)fj? zdcNd4$A9xr%F`!*Q0O=#F{rR7ceU9i7hb8npYw3OES#3jHHQJpm+hB(1 zn&k+HleI>&gllkojo`R?oPTT$s}%f8MlG`V)fj}JTFUYL{tQoRA zF9tOYe&yVR!XfLJki%_Y0O~%tydvUuu68f6@(Vz_()7#CIS+at6U}Ra1T^2h7x$dC;mjb~V=hk4cnE->dXPS3q_e z0-1697xG5D;9K>Hq4~;x?>!JWjvfay&9sCQE+4to!Ji@Q8+j?DE(Xhb1)vyY{L&1B z)nJt=8CTo(mYzk}JOMcZWONK1U@N&AK?d0D(Sr^v40OcdFr;s_)GFm_RT8VPj(+V3 zS%Ubds$xk&%nmQ%B82h}9|18rl3JJb2ry3e8)vuBg1TSbDzr+{k++Olp8Iw0TGw$o zQE^IiD0ZWH*s|K#*|pzT=`xdKqY>U$oG6s}^E8K2a}u-{e1`G1j3}Q7a-$A{%+LojHiR9}ISHk2l!X&Hlxl7=Ymk;`N9fi86i|y4#mAqHV)u@>j{&}3N- z82wSLYPpc`*#}990YeP*jdE*ADsBUW+M!!KGJjbaV$&!qj-)&BPp${r5v*@K z$C*a>V4bFsuLESG8N6%2+0>GRdRt>$F|86BBp^p?txny=GS_b^B@c8yZe1+XeoLm5 zkl=`5_-AC0(QIv%Yd z(Dig0-|{VHu~*OrlvA?BbR=Rkmnxrt>_y4Ma2J%9Yg3(9ux-v=HGgJKP%+A`lmF`H@>5w*Tl#CfL;!p@i|0=F#}lo zvyu?i57Z!l_Y*|8qKv3&5%i2O9UxTe5ljU%Ir#abuJI$aWVbfJRT1Ffz~lJvGhiAJ zCXNG5{~QZS&)F|Ac^7+zV$=T`wGK9j&|jFFnq#s1-vlmDf)#PDNl`%I4TXdj2LAGC zH-N-_o}R6~POFmVFi~BF#)+VLjrr#Vy1F4%7cx3oxacL!GX&rk)HM|YnUsM-Z~Fx_ zq7SjjB;1L#_nKR34*p+?sB#y<$DLEdE@6!;`eZGE5L^9koqCq3*dcX*p8am8d zk~0GN>S~nskDRE#J{Gk*o(sTVWv{wu4Ldmm&nB&!HL)WLXgv6J?=*~wl9l^l7RBAj{`CE8h zxUCxrrk^mv`E^wxV{55Z}2GDz{n?y z&&*G9fJ!!j>l*NRri_}9!8qROvN6CQn_LANG<+k)Smf%hBD@>X^UFa!wh1{m`*06I z+xG9T+YmUtEzu=|@q^Z-hcal^3Cw~UK#1##@tlE~A3}+ADLh6lb&!K*=TBblmFlGk zC-qX7ckTVz?Fe}85n=37cWj@Y2HccnjfvL9g>=}dO{FkqI7_=lPJHJh_7VUiby)Gk zSVfn!ksh}K33GHoN40c8K44{&*tzgltAB#O?&ig5w!E#n2aOK#XOoiowsrI4`=4DH zQ__fpQG}uIIzjZIebRgyfn>)}o0V%z{|{JzQ$;j_%t;uaL1P{ZrjS`WjbO{yLI()h zJyyr>dpe$)sCN^EZb{A@qCAhSzGwGA+;G4FC$;NW(-OgfNIY}u@vpKsolnZwmBQrU zBmAWkwC7AV%+KAok?VYHB--)u{`S$D(0NqHMi>$VFslvR4V|=?d%woU<|&Ij3O_`v zLPU%&OWRTyyrleoQfjtb6?c8&k?5J6(yAQUA_>$!BF1QRoZ|u_t#9nQZUR{Q-J(qf zz>yfM*<~L&s|zzqzh%*Cc5*=Oukdv#8iw@5edH5>B&1-DZ8b3O9e7@T2uTS$xx?S$ znQd$;t~isj?4WBuB!#eK#*cHmQFoMx-XtFFlKL&o8j$m2RS{L;Y;_fb3ZDj8FE;rX%NuHM6AyFTE5o? zcvV?(DB*0*76KoV39K+5SYfB!L>OLS*OXyfPl=L4G0<1VFY!PM)SN!*IcR@l;$3M6 ze^P=uu8Bd#+Y*2>Y&NcI9qVftChKvWFxa98kX_8(>UAVJ#BOfuPsYIYC}LQPXI{MMLcC; zygf`m+hCg2j>mD0U~8U0Gr<+9+h$!rQETOi5$|%g8 z|2dK#SQLFCFRkxjeB)Qb4)FT-N9=r2dy9x~e!nq)++iuifd<$$ZPnWTOMuFehwQe^ zhq6K&m0GDN=MRX-QWQ+Bc@-EK0{>TUCSj7eS)}I*(fIOpUoN$9RiK{vO%M`(my6_i zNX!MiP>`O4EJ=2MGcVW-5lJ+%$^0{iLa@JwfgO~K6S?>!kVaG*t|<8)$~!V-3K)8@pO^3{ijnT~rS@_zrj|p>uxbX ztj5P`SF(r8 zGkhe?#?S$!DOgRFm(zNo)Id71)?N{^ftxr5An&lQ_Uuy@a4PI_PDQQ$awQf^40M*i z3Nu-tk~O#>?bA(}m1s+5Jeeyjxh5q5K0fIHZgQv>Ln?te4V72WE+Vz=)~feqyvdV9 zO`vBI3xUW`hhgSLn@Y7ai9UhXGsVTTDBLR@++( z1O0#p)SO`joWx~qGQd**x#tM)$%?56a(V*gRmVe#zFW3~Z}*+Lij_nFv}sgUDcjK2 z$+4p{a`aAbTJJ8ZqSy*bgnCmu*M4}OUBVT_kiR+ic#vY-HCzV^^ay&(J&8Y=VO z>W_+P7EmZ0+yUzu%C50T>tR&E5*pD~Pi8r~$EH(5+k1NcyLXcY1eSz9pRl?VLY_wU zG~{KEzX@Fl#^DzMjzBUD5{^NC8w%25KD^DbPz_DUXTvMR_8B}Q`}3#QQN3Id!Ajgx z%|ip}6bO?KLv-EY>?bP?l~;%V!J>QbcK77 zdylXy1euAT7jPyyGIR`v4&+56vjI`U$CWPw9|*DbO*|p;1};1==~q(s30!;hiT9go ztQ&-2&9j5r-I)hhH;(WdAW7)(>mL!_K}5>OpVPs|XcT>}=Z1doN>UJOO4Yps zsel#Hbhuf7ZHIQD@lFa)_ z{&_Pe{tL-2&~W0gJOfNRQBS8PF0nrvdAyOcuO+GGGcDp#I`Tr;b*&+Y-X_3ETsZW# z0{Q8@^XhBg^OIAv|tK>5}RDOOHhY-2Y!@ehRK@p-zGu^oeO^e4q_Nv_H}p2-}|n_L6Q zW0WJ0$tjPlNG;)e-3A3}L@_4A%u|nvxd{b14Gg(1@@8v`f)1%~c{v;?Utsn>1(_39 z(CRe1*x$S{Lsuq5U|}+O!=@_~H{WH*?DH}zVYqcaIk@E6%K}dCnbAXX_t-Ie@%xfxZ_tdqVGP1!wo`~;1DR{x&l{yTe;W9#mo+5WHTLDM zefBG*Cr8E?4t`l&JlZOuW-7;H;B3)+x_UUF=T+l7-?VN1c+0pBdLEMPxV?>rSU(~h zj?i9#xsM@LZw>X+Q*3iA(s`_!Ii>5%I_jP*iS~-8z?tVsx#DNJa#VIt)jH%@49V7H zcVo5%${1W2rY-UF3|X5!Cc_jm&=3HIbmt9wUaR)WcAC{?9kOyeaUYpC}Mp$c9LQ?#buOop-I z^Z(bcjA0=r;ds=OM^|^3Hf$cfoaISwTytjgT4A2w@tagZOw(vK56817YSG2@1^At= zNDhHFtN2<(`AO}(?ZgyQPz!KdE$6d>-3B7<3~ywJcK%Ngp;!ER(5OPlyF zB&|arx1@D+dSR55e?~6LTUvPy!!;e8FyXEHaejwCzCRJ)rNbxSG}siB|A87kitPm^ z=Fg~)&H#HWWJmX>s(8tsfck$FUmL?(d80%GR?ZqXM-@z(DTBT4zK@M+*OTB-9{NNx zv*}rZVXpQKyEngb3ACRpFV%1sTfV9zsMvov*S+WI@Z7Lh`tXlm`r^MaEo*m?oDwZ& z`SRA1vkZm4-SO$O`##Q;6nYb|5V*OG!GbHxTKb^kPDTr&f`8!IuGd#!rXTM~-eG>< zfhcX8ay*GF&?_q`WGJz}6(`YhHMgqq*Wsh~Hc4=#-nh-Hl+2#k7;=R>YveYRt(=jOw(?1np-d za-ltj+g28{{IXTkX##mRaj~*`yCwwzUxQu$eRhXI51S;Y;`_%wDht zHW=RrP6Odbb8T+AzKSQ`$JuC}nZnxdwSzlZQaWY1tQES)aD{4kc>@VwuT;U_NZ#WY zwvMZ^s!Gs5ag=ZMAr#wb){seR^S{X&QjTfY)2Mqfo-%glH8s4C>FuM9=gA~E!%8o& zWl=T@^0ZwMgoBteh8Ct1MLuy~k6}dy#k-Vw$?cCCK=AFbZ;*?<)z~V`&PcN z^Z_^K_@IZF3uvRh=Z{Wggkwd|Y*%iDDLSRrHs!Pu=o z;x$9=L<=Rnx~#mx>Sn=X7@f&%NPJqh?U@;+k%Lp zY%I3!z01{n$&jKqZbDoct_Rtq>s7htVQrNxWKk>De)+?)Mx^3+?h$oB}`_^XD8NdDpPtz=Sb z7Ft=0kvEO3svS;O{JoCxC3(j`7v0u6x->88RfzE|4V(W4oNP8n%^d4cD4lLDEhr=3 zpgb8Gnm-a>Y|GadZGhr7&E~-~#o3}8vnuX9#9O9NFz~^7YjYT0TC>k^*$I;GiH-Mc z4tTrETyi`7d`N))@`7>iw*NNy?>Bb(zNs2ca8x#NsIn8_-B`-P^Dh^Pjj3cJiz4|W zoVo|@8|#qv;u$B(L-dT@RfC_7ZS9xFEYkSn=Qf;NyzWy8 zdGYq*baS)zHQNS}2vs+c6Oc0xd!+5{)LE}905)x>wjr~ z|D6Qk87wVV_vfItaGpG3o>zqR91v6;L?RW$hNxr%-4*jdnpephx`Bou)143)rFt>^>@8(w<8 zA^D@(BWzT2z>|O%>Ai1@tWJQ57S$cozuUjE_sbuaB^5oaK^u;&Zy-Y9)0u3J#+k@d zH8&sQEsw;Kfz_|HCzB)^f1(sG>pK?)|M2ozDifQ9ZGrVV+l7}g2s8YKbDUjbpio&TBCd(yx2e?OO7*NK~3im}UP^C_|Y%HyvVakofK+&|ji*gr< zOn73I$Q4$d4wy6bwNdi$rSgm`Iel!ra_-Pn z^UwnOd|MJ0ZVu9P-v!Gn_4_CxIJrvEw4|U`Zq$IHn3XrXS|F;TzoNeF=C{ z_=jd8p1A+++$M!vzK+U{N)g8d0)7Xj(Si3`X3Rhu&c;!it$7&E3v|g2s@wFGo6K({ zQw%fy`{%_8_Uu^^yi3Y#0}-#Ca^ji;>U9#IRh+&7{3?lB$F)&^m(-dQ`D}MeJy9RIy0CiUWB6D5_I^VEkLBtZboxjNL5H!NHGQjrdM zncGhZ6V%V@%FIGNaYWI{+zxsU45Cw6NaIF%l}^W=28%Xy@QI!>jo5-Ui7fa9;uXND zZ-mFyzj9)FTu=&I!0r8#U~q7bJ7|b($V4@VtU}sbI1#JdxezM-8$zmO7lc;CLWYiJ zErTFUNiw$USC3YNzICU`Gf4(82bqr|R{lNu;Ll5zLgL-y?;^F^+^)x=)c2D!i`MJ` zh?PA{qZ!@Pwc2bo?tt{iAzT)^yxwVFGw+?cA;eI2>0hA6zU@gEyGC3)NIaY#!B@G- z=I^My<-JAjzgRs`5m|?Z#mImRSh&zftpjWPCc$rSHAXRWGGgk&v6+j~yr;=PJ z6_}#1W`Wou3Z{VwU7In-osqdc#wYPTH6ZLuT4mWtp=V;RQyg7Ye)z-4AJcYX%jW2- z&ZxQwYolMseC!k^=Fcp_ia(C`C9fX*lt3|><(pwYGG^Tnlt=k!i{*0y{I4QH_bVR;{>m|^)@t@Xlj_H}bWbFvu=79K$M zae@>8IkMMq&B2zWV~@0mw9*E9Jfuht!1E+0ljI@^yN!EHF}D&GGSc8&vpMei>|}W0 z#OlBQ_rL2w5;tFI+N(lGX1jbiE5|~NI3e08aE(guJ0s>nnR*_MBsyn3QjWMA)RZJB z7Q%rNBC%gec!I0ZX?>Xb@CEQM`Qq@zvvy$+xW4w}vG_#H_>v2scp|O)@Q1WG5XS6; z9MZNVGi9=~B>tI-84i5tUeUaVbvz>jpRA1!ZMg%E^gl1%R?-*C`isQDB44kAZ>dUf zRN698>b$se2mLw7>{m8uSt$|~cK1_kZj_$eRE9PAdJsmtf|t2V+1+1f3UvQ+;BQW2 z#vPUExnz^*PQgZrTX1b7zAWx2WiaZuB5nkHFvAh7==K+HfB4?9jMA8rp&m%d=rN0}H-JO`U01M)cxsBY0+cJSw)8>8 z9T#!ZZ1R$F;Mh(DIKX#bCrM|kj<6X%AbnQJ^8&czCU*JwQiYsM@Naqm_nPvnM9a5H zj+Er%omHnnwjU~y4Wc>M!-(=e4FF(MFfDc$s=8-Md6EJzUg%VD@Hckjei;^9{+lllF*8EO~`$(FYVj~70Jh@0N| zHln5a4uFm}XdLMmElu?v0oaj#N6T1GjS)16`XYk22*7@*4N3uWL=Z+Fzr5x+*pwMJ zRztiN$RyNKl#^&L+)*mH9>r?HUFL9x=Ds-2t(6i%e0od(u`xXgGweicSPt7=Qk++f zoH?(1ZMs9m3dUtQMPGv_s+Go8e*3lqO`y^R0{OG^`2fI(%X;<4O=-}hS4dI4 zmJb+W23olEy{zA~A6)FNd^!x=8$!F>f4>t4z(H9>bSqHGq>bq;j3$Qoju3MJAGa$9 zdZY1;>ZM$VGbdZz6927KC@va=jd^s9ihR9%0ii?ZFD7|_50X%hiwr70Q(%vh9>krr z1w>12YDJ`!R11ua4{X?S^mhTHq^m=0_`-8H)boEX$-xTfkD^=0x)6iE)WZi?o8i^l z0RVCETTSGJ4|il5E`&6w=Bm|&AGX&PNwr0{5^sue{j<&h2IazM3dlizaUqXsRRYsP zp*sK)BZ%OPa+i7->&`~Ws`XkVWSY2-1Vq06stN)$bz+7~54f7R=3}HTZ!l)E0T_`; zYqG+pPk@N!YMDq&kG5XP+Tf0DbpGQ`j49z;COO>1yjs?A|FGf)5lY}W=Q#rtJ!!w4 zN&x=FR^Psa?1XLgFePp~>cKlWeqPfe{%y5#50l`o=MN|`z$x}f#5FGs&Pg?5Om*`#f>PKu7I({QIPILq0?tgO1 z?bEXbLWD;)GIHYTL${isL5X{^n(q@#z05kNd>eKmrtwKYkA7UjUt>+6_ok^NraEE@ z{HxlN4-2O3t@H&IDaI*d2E_7F7aOh8<2QD|6?uSAd1z>6=meT@4n3|<&y?_mm^-B54^rF9CUHy)dV*&ay531cqJ?GOlzP4+5Hflnw(tB1 zu%O1@YFw8yfo!sFEcP+}D5wV`z&F0?iCd~~jqn;yklX;IXh83JZ6|m=%nDr}FE^)I z?DJQH8~8dwVSDFWn78_Rg0r@_UAHXp^<#Vv@GG~25b`@&^xB*{bxO5Zn31TB3)rZg z5#Pq@5gby(S50z+&FsBN+wF_pJu3(4N4~9o9xx31eqxS;NN69LAs!4P9~B>i6o30# z_NdUC;qe2fd0-LMu^w3)%B!SJlgX)*8msCUWOs3b~Ch&~3bI`no0m;{QyL z_+Q2$uk%SO^1#yc*l|1{!6q(OXT;M1=E8>t#D6=Q3ldxey}CPgPZexv3(~Q^57Z5L zoeQ5hB6VXCPwFGPgYLi+=a*zu9CNWlt~LH>|CdbwH25o_ArQ#8bBEQ|ckuiC0b1o(gMistE4Y2!ok2Y32 z3-KptCS8}y0xgO^>Kf_I}KRS)>kgQh{9^qPNC;9XxzsG;8}cAon~2|>fbO_jRBq=~qGb5akOZD-IP!W&6~4&w{^sU082|mr`gWa1{3rTrC!s$)MbEwM197 zith%1UTg#8*gYuy1SX&%xJpvnN>X-%rYm*{joJ-Oh`sV zVT;IvJ|B78xYIYMXi>sFU4Y4+9dev6<-dhKzTP8m&^uhfe(He_@D(1d)pr{RHHH3* z@TsP*V+@EZ4%)nTzsyXx>j2)J#LF|#;9;PuK@yT9V?Dsj=DI8y*dYp*p$0c$r?HMt4;(QQ1n6!u&4uBEep-Rv4uY5!k>3{??O}*2Pfm0&;2Xx z@gccM)P3r=T^0ZT$O}Ua1rQhD{M6s^)-Ujf*dXI2*K^>G-j7Cfe9UnfZ3BIFAMtuD zv`a0pBUWETR?!ZPwD+}AOk_+=O?z90`tC4W-u}=YOrkAtVn5y%fTnnIoy~-oEVKYB)fgo= zz;*gM<+QUl4;ESWdT%1)&Nvf1OIFoB!K$3Ww=7v-ht^14>fkwf(9isPrbrw+X2e>e|IsN7EG zv34cP$q?Mcjw?A9vk4v&GJMkYN^!jEOXrM#=;^j%RhB-0MHJe7!3nAd&k5B78MsY6 zrE*?G7a5MrghGLs)?Hed>t2*1gdC8hi|%gBO`gipI(e0eCb>V_uhwLN%&2jn@7FZ! zRgzBggEyFc{q}U&KEt&Y{MGao8Rq7m%4SqR*it0tY#Ik)p$@O5J9>-mSkG(!eFv!L zVOepm#n}mBu3<-%%?|h*fLWhQcoIVN?_D|Z%cyAL?y~t`#SJ__!& z>@^5?MpM*HNHR0nTJrKWd3{A1$QwbeUa>&r0hcf%6*;+fO?mB+EqhS9 zK7pH`G?=DLec}0X)%ZGC+u3q9yA%oY3m)AZ)?6Uk%jga8XuIGNA zaCSbG|b&Sp~%>3aX-3LG6s zSs^y>b2d?A(9fs{ko_b&X_UWYuzQ90xs4;@5k%$DYum;X*ud3?5x%#S+CWgu9EGW{4gM;Q#{HLL4mq6QtBh_X`PCiUHe2z4Au96bcUWE|Hi`V?AMRT+H-yk8@xHcRV6?Pu zsnM;e>1uNsu8*^vQ;ng1XzZH2+OvkaS8YV20r7HF0F{rH+3Do&eBWc8fm>B0EL-S&+<1?xDPpqI4$dX?lTWHog^Av815cY|u&?y+VetAkSNFB@>xJ{{9W#dvCxofWqY| z2sU}0qxNk41yPOA?!HSwt0w|AXwKEegU=h~`RkHIf~#9`j@{U5t^g0Hmn{eLA2Y~f53N4ok27~Y@Wwg)>{(#mkYtwT#}qRZnrYcJA2xK3&Zr&J5q zGDG-A%fMd${csdK@pm#;R0(L7wMBr+BFqCvtM)<$S3h#9Kz#q%oFji4)l7N;A}296 z4II|cfpNiXOE?GZ8*RH&|OWj(zB3F!vo~Lt|CKnRhaHb+{0r8m*2&(SY*aq*1>J}5W;(oN8IiN3pJ{E5z3uRrA@TCtcwG`Z5YMvIW z^XWgMn*+d$50Yia`Qyj@&>bkGqxl0Dvx;%S>B42`w5wMLg?|V~1&nMq|1FhQrcgeH ze7E%}LUm5MV2j-1dJ>r^x#xMAL}syUrkL?O$q-NiTAVTRhCj&lT?1JE>+4g38ns-J znhNAK*#Z9T(Bf^`eJX6Y;|Dk+fwfRGo_#em|xO#7~)WaBdPv2&8myCFB*1QR<&CeJ7DH%zxk ztNmb!wgQt= z$&lIT9R>eT!5d9ip4h79jM9RTp%7_9EL_pNJNR;bzaLZ0JhyFB-eBfjbcEwsn1q`9 zC~L*p2N>ST)f-E&H=%pbWv^f?V&vT>V0n!kLLYY+re{!<3;QkC8Zh_0_?Y@={3kf6 zyy_Rar;FbOmGw-*%gI4>K`PhX*`6FE24m_jph*|We%{4yMZd`+;b^I-d6&iRH0mr( z0$u}+2GmX{m#M3n=kAiJIpYC*81nhkvqnXvzX}4idRr(jokhUq>?c9(dfCQRr|}8c z!EJ$Ph(yKYK7OE66aQVXu06ywB9?p_SWh$04fghW!8lBQG^$ii=U~pn+eAgevgcH> z|7mMk!ce7Hb4wYej1U=>5egAP;eDPnjJoe1?|pS|$NBxf;~Agl z^L!pJ)^eCqyTuvqf16x1uoU-)v2+oZ-k$R@LP}T2b@{V$Bpt2P0*t$?^t~HP5UoVt|xI8 zy>awL(ctOF=QOlS0RWh6usk=9QoFEy!`6nXDh`~s^brOmhzaupLi-cTbB-ULoF2%n zFUVLn`uOLI$HmZD{#^^SV`01jFUa(Iq{C%N_MIdf6-CY*eyWLP5J8A6?M8yaf;Cu>o1BYe z%>9~7^8aNtnFzNzcB%fjpk?8#p)3K3NvYq7K~SYQ67N}OIRJxa9)4PQp5bxynEHS9 zI16Qvar~5}rmy@AKjg6v%PBUZVy^+_dJCts4f;g;MtmdFwx`Wft)j%U#E8uHj#)_V z{*rz=cG-j%_3s}Vt1E^{)E;-w0;pLsZBSShg!ow!|zk2ZP_wAT3^gw(O2?R?KIKTmLRE-E(3Pw(cy9C^Bz93Ql4a z#>QBtQoow?j)mnv^WsHuNJ4NjPRqMG{)mNrml>LNDCYq4u$V)$c%^f@LS)=V1h=S` zw1GozT9}o-0Erh#jh3-fLUw#Rx&$b7tnCVEUVzBgoz4Hz+@Fz}Hv7#5?-4{8upOF* zLqucFtQImb=`+1_;U1)Mz)QGQUE2|LN8O?1D(StEm@@)ttLH!VAjXVR3$cDrenk+;j_4Mfc)Zuy-g>|m z0*R!w>ckoE^E=X)cojn6UeWiY&3*i_+_-t53}e5z2a4s-jzPb&bK^?0+}zxcsW+}1 zc=JqcuECyz*PLGqRhs&1#bQecC&lkPcPPxy>=%GI;ULJ;N0nfU^)XOfS*^+MX^K1m zG$R`!%4yrZ`p2$RKWNE53o09`Eg-G;3}9Sda2Nhb{h-27ZyP1=W+gXQj$$?@NL*m# z;8~J~OvAu?0}>GZ!D}c;KZ8>k<1bNHq~R% zmygea(Dp{{>wJCHo_bFp%$*5wX`v!LZMbu*sOPAmYKy0tqBPM*GVC3(1CaLRe+&5R zcWr|iFHP>RYu}jB&MJk8{p-Rxi$;BN=HSY{vJ*1tcNW+PK4dLl=Lw<)AhI+N#0y^T z)<~C*=2HWZo5l)7HOCz_AFUfq99>;qykXyuF%q%Fn}m;Z>?>uGH65b#$>2UGLIsD| z6R{ivqZH=15WcnH0H`MTAz8@wHIxH30!ctKecbASmISH&g-4i_jmJH(uhhXTms5O@Y zP28;VcUGm~W6aqad)sSXl>rCrR+9;A?i9g!DB$eBt3SYChYM@Aohklo_dU1TOIR~_ z<#N;=hzI$qBi+fDp0kR^v%`o~lkTqi!Gk#Zs#;n6h3Dg0*PCo!RLn4W_^&ICDZmKQ zwReDt(+7w>>%^SL$%SsiwHEa+2cTjVgjG!t%36sWkX10E;s{Rn9vTCAb$+PW9M!+P zaJIJqXC}*3HC=PWTnY%qu^y{>p<93$CkY$a5iYEYmE7*Aa$rXYUwbWhPdcg z(JDqn8^%ZRIOSYWwjtM|e?>2mCEX4WIFH@2HH9F`RML6bRluo)tnvm*qtQXllQ@p) z2?+7eYcpbgXmkl!;s}hHEImpFdPCSEvD(-S#zNku=$MY9k>_IqPM>EF`23QZkl`Lo)4SkHFZcQNXHFa}G z^Y*>TCui~zJ{A^+!ZOXxHJSmqVg32cm7^AQ1>G7;nDtbg2|4;kBBe0rP#q<+P19{~ zUIz#(3*5zZhbhsMF)#ulA4JiDP>mx>Jyi}^VVI9iBWmEQ%~^P7aUjny)48|_|Il-Z zR03FBoKSHDw7;H%oxQiP91>=#o536+qj0<@?gwOU!Sk1FOia1Wviv_;ZnQgEX~mQ$ zYW>;SgP<)~&sx5Wl#86GQ5aIggO29bhJ1~XRVz^FtPKUDp86wU6R^s;ivc|o)TCs; z5GDDZfy?#%r*#B<38%G;MCx$j#}RZp!0qA&X%(L@;3)5cO7Ziv96~zvf2e<@#3(tN z<8-9EFqt|<(}s4;ixSzU+cg=h1MI_}+rjz&Opl7~v_=$SP$3`2HJ*2>NqL3}g}cD7 zYzD#TWK>8wi!>Ctq!xWWGp&lGBh1@p#bn@cn-?ba8y@xDCIqZFkP$qsYqY`YZ={=4 z*WL{MrCvEFt{LOL>6PzHC8&JmdN;)_U_`54^LV~HWoHIlQCqi7(nHn=&;3RVhHY(&JSz6VSxx4`W65|D0N*8MJA?c5MZ z9|rGv5gnCl2kgA$e$!|PZ2m~R%}ktOe$!TsW-ud}d!QTTJN$2<3}L=d*b8C@@Y-yv z#$cl@#FZuT!N_Ii_<@#D9V4WGCaqajL|SAbh-Nq6`Krk0H30Xo!(W4H=XQVmkewQo zJpveu`4ynI{gBu*TQFo$$RTC6?b`nU__h_?N&=MU1T}4=4FZ3$_n~j}^Z5)Ai}AN6 z7`3tUH@*N=E2aw-|1kIX1;D-dQ#S9tf<#yYu>k%?JS5F4%~nXSndZ6 zu$3r7u9u*yG&yWPrG)9*C=400wG4%Szqv2>bH;v;FQ0)qBY;zr;@!%{qdsYb3h;wu znRjvq^_u_MYKgWM`&zqBBUd(~`dwd{nbvxCV6W@J-ovctlFEO!`4aCS+Xoad?Rp-A z#N%@~0Jb_B1~GDLaKycqOZRv9?z9#CkipH)LWcB6vU9`2FlDS$cS?s);xoNC_9v4> zxFS(*6Q(2woJG0DyFhP^3J+D=s#R8gnvF+8N8?q(Cgti^Wp5q~-a}u6^NS>v(`Nfl za6kDB;cs|J3O7qqI~l9`X#kUhmtSBf7{OXzd&5(FWa9-G%q0|}(ivk+_=Zy{oIZ8nFm|o) zG7uGw0uPQah+F#nIWAERpxae^lYn9fsVHpr$px=P2YD!~Sk{lfO5%@H*+nvuN+QWX zspLf{ul%(XCouY<$RG((AMb*1!tt6(377UIc$(uZ(YrtxEV;S!`hhDuX~BZ`ayi$z zTSUde4_+Y^@J9x7UaIiTy9m4k14hDjF~nJ)X))ghoGceKjoiRfc7+D+n{%iW9iTXH z_`YwJLs0X#!$U6vNdT;rxlH~XI|AySB{+{;OS}9{=Un$AwJ>I;;|AzW7<(VG-#i$% z1Nn+12M3H^(8TF(68#(CNjYP0U)`;pn*c|O+2nO->otzK40!haS%DvD;Uy&(J8_`k zm)7K!{aIq&N|r^B&}h~g119G9^0IQ~P(c}5?H?`Q3z0I_#&vac`*EbL!u86}%|iy= zxbXYG{$stMJNglnaHOG>B(;T*V`wB}m%kC|r^h~RYF&ZQ&C4rlo6t8t`Z@Tb4Lp9Z zAuxWEg&ahIpoeDL-~72Ab^S;`SkM0Id&CNh0=LH;HIzy4?2+it@^ypyDRIFL!Ef}S z^Ls)6Gn_uPjOwD za%x}2;$Ng@DrZMrWaGrV=7`w%o7x=vO(=SzVkT<`AThlqRY7INzK&N>#GEwsMiRME zxB(d6IB+O^*WI-OAZIxGT)A*(w~0*BsZaXK&88XS6|sTc(;+=Qs-p*Ua1hgqJ~nIA z`mN`mu=<|DM`T$PL)yy~iF>>%;OBr6JDxnrVR(@OV}sK#Wb&j$4B~k~?bhyAbVh}8 z>t=NrTKpUcq+AfD%G{5dIu7t#=un7Tv+D!q&Q}SSds*Z7DUK{>f0H)EAY_?U;!c7B zxCf5R({}#r*t!J{^4ZHl21g;9xJ%D+-$a z1N`uHDR#zU|I$&z$@%3=DT`Q!RJW=ax%-2gK47PGz_d5E>hi%X?TQJIy}OgrV%ssC z8Q`b?ZS~Y>NnGwPNtoc*tUyN6N6E+byquPShNq>eLY)jqxrTnVdAdKM<#+-kJ9tki`FC%$QWF|lx=J*Jit!S|}pgpnrMtv}M zQ0X6>Pag`2*+11AAlTJWS$+t#Qin{wTLGGmJl)KksSK}zv=qA` zmpDV7@Cf|YWzIotA5m*4XpFqHf-WjqdL?kh+!_yMD>rnumu1scSig1U~SZAr0 zU46%7#8KUpb#0J&m~(YPO1%=Y_{@)K_7ey6eLXt>YRE9PFaYbhs}CL{?Tc13z!y)Z zSl;j)oufl0<72m}^Gv&iv+4Lq= z$X456KQp1P1APsS0bb4);P-*e{wyeHW{SK5aasuo=(FEC-AHf*7Ij9Bg{?PKBP418 ziGOHpP*w&!I1Q?rE$>o%OIZ&D35?o)<=NaX@LpEvm!L;4bdb4+3XwL(U4*A{BO%L6G z5;*u4jLI`WSzE_f#~`K6y%oA;%o?q1wX}Q8A3q(P^<>!){m*_*Qep+OTSQ{I-3{f! zf#8cFD-+f`8FyPpqDLC;)fq!#H&*0szj^l$H2J0!|D{rgGnCXvV0=`X+BiS7&LrFG zEI_q%?3*!dJhOz~O7*B0jBup&9L$MfPfEX^pgs*ih0s445WM5903k=u=s`*KkaRhY z1n&Gd^LU5k-Of1Yy6oE-S$LarlgOxSZku}JiBi1{*X_3|IZ(D=pC3djB)f+f)nC|t28cl1;&t2y$ZW76qH8%(ye%orK<0$JCzci+)CUf@&C=$n{q z(rRVxef{kU(wr!ctY_!S7K8=rpy-+cV8EJp&sHdLKPqvgwT<`N=6(&;Fe0I&!5XQ^M@%HB1*X&4P2FT4Jc- z%g07m^7sOHGYjPvm?Xo*ucZnztTz^ z=ztc0b)+6T2t_m?5C(rvyx;@wzCPazc3P&EN5+O%^vW^E<6m~K4gwc0qXAGV3d-6* z(f_VTceU}=o-I$$EKbWXFL(0VtJ*S*lE29=m2AIO*{196prk^x7qv=IxRMFxBPyW6 zMMHA;L@~rCwT8CPqmfJ|>zGb{3YgrNH2!n!TN^^GpusFj9d9ZL{RD`Q4DY^+;?i%@ zB*nGI>K+y<$$+wU&t4!hE))~|PdYEqi_ylrD|~iWhGppihGN~rvgU-0jEp!Hi1Kta z>ee4MiB;I6^KBt-vN`F%`Pu%7@0*OT?>_POg~;BWQ0 zrOeT?`S<^N4e0cNV*%{0K0udT42&PX_se?CRda_6`u$vS+@8EgS7qGSB`WP2i#*9YbuLP#GtVzugwrriXi_0EjOZ!&AZ-fcx z4OY(|iF+TmW}7+dMq%Ml$nsFM`XhJ(6Pzm^x*8_14S*Y!!S+p^sq7jkyai0!$C--C zZBkWT+j~n-^Iz=OjM`_shM({9QHKpe+G9W#QC-|UH$O^ejbR82JFpX1^{TRT*=aij z!rT7A;v;DH4MOF+ol4O7ad7i^*L6gQHOCK;hbxA9%dl-HS9}s4$D*fMcvDRUc(GMqWetU!-8qn*b_AsFZaJl8th>?UGn`S@GZOXUmoA&A zyjzf83kDH7{|kIq;X&n|va)#DJKYZpNEuv_;!NIf7o!S;Hy(I%Q>*L|jLDFqme;T% zfz_RlF0Q`T5SF!`h3j?gYtY0iuflts@506VfNSEx_Z^mn9nn~c@1QhcX|~eb1k%#0 z6JPcz6)aI7eg6o0KAjusMOn&GPIgIJFv##TU5_y!OiqZ1xbb~TSflIjKuy*md&H+v z(7yU&ipdRwV4y2$j-PS#TGjn72+RI!h2=7HI^+6sm!-IX^9^}@yi+mi7{cvsC84st zBDdEUnj%3e9}+vi)S=f?)$Z7%l>ARxTAFbmwC_Zwu>pMHi~YZ~`iOkZ0?xp??>`Z^ zX2!>4pCjoQYye)&QG=8;{f(!z6YrqKPf3XAhY`V7m*3vlxLN)dde%?QT@_hMILG^j<;rC(^&>szv_C<=r>l zIX7lIQJaJMgj;t6ahuUeJJT*m@CBihj)0PN6aR8^8ywPNeR;6rEikLPf)FA=`i4YV z*|w!>>tCt=_%aGn>k6DU{u3(f=P*%HD$6Af4TJ`4#xb9!Qte6y-`;ry!}~1G^}@93 z;Hk;}DVc`!I|#U8H&e!762-afZ$mKhB?Mdprmh4=GeUmuS@|~IXX`OJ z_ONlvxjpVRHrR1wtmBeJoa&}ck{w6I)8R&T2-&;y4UP`9xo`5~`lT;-x-Atn9x1Ro!_PyYtSASH(280;*IO*b3Rr z#ln-L*vbD2U;!!%5>WYM^?UnXhk%hb?nvH(<+1KelsT_F$kg`xcB49F1*g`zk|CEe zpSI-PwkCvoEq+jjY_~w5Gcr)GRYOm^6d(-65n#5xg;qQ&Fl}ns3lN~@!yXGe?>$LE z_78zHsZ|mJxKaNFtY1VGcJo~T$@%m*%W1{~V8)oWbIqNg(MF1}V#=H6re?ZjWv2St zFNZ$E%-vmtSUn>^@dI4q2BDAbz;mD4G`r;p56rH|{wBb)z)G<9yq-nk#KYF~A z5~9K*YS)$%$3?^L-%yNA#L?Fu6HhKVpRMZ#S#>CfkjD1LvA2uzp?kOlrL*KCGsQ-Fo(P_2DuL4qIxAO4kuCCJWw_^SHnJT8Ai}u4bVb7ae>s*;fZO|HF5#9P? zr{UcFqNK!mxT1CLw(b>5ZOb!{?MNbaVp$p69wG4Sy$got8DzX4MvFz&IWX%)yBC5O zv;WtVtGAC|=|I2$RRz{gr1%d%uUE%vC}+vZJVJ{>A|=rJzn%uV(RKIgom;c}xzcf6 z9Xr*=`)e*XodqN>}_Ue@63BYwY0_UpUZvg9qm+bw$S0V7wsr*<4YpBNn>gpNs znrR25MMS$20)Nm~k8*`=v;2=uX}$5`5oii-Ke)F9h_8R5lYkQO5bG2MmP`G*wp|Q4 z*;_MDV6EIPLd)EC_i3W6SPThPyy{{`$6;ZS7o7gA(4PX9eDzqVriA2r@N%PFnp;** zja{8u0fa{@D(Ma?Xv)YrxDRl(I=~ql^H(AiO1Ad zVDTM5RN1}%ZQO!s9GJ3~jdw0(zR@&Mv^4>A={AY74J_TxPOIYWIqw0$ zPcXk%lRVZ>OmF+*)X@PPT)brC`#_zmc-uiy!7NphZ0!K;)AEp=A2h6<)xWSPCXi>w zP?kO@3KvRQAO9XiRR46#qMl({U@+}*wGa^&PrItESzunz7v22-yA2ZU31Ce0wAQffH)<59}Y4usizi(y5p@&RP2fo^S)?MKa_i5|A(LEgX)Dw5VH)JK%#^h2_j{?p@t2B9PHvfp!Ii%K z^|fnq3={QTvttImB84)0%y}5tl{!~1*-&+F$kf0X?!V6;RFyXue2Bo~(1%)U_WWiyIJ^%+t z4Xsr%vhQIvl-sb{fHnKO#|Azw?1bpFMJFeL&G8pc%m~iVK~kUx(C24C*_($A{mRhU z0_L5@&FnItNV$CIJa+HH3?mNkYZnl3#Zv7kqyStmqNUR|D?&;*is)0S{*ZC@knpq+_y{0{3{g&D8-M><_g5;SNH(|2 zd<*n~j~IZ~=XWX<_}9&+{W=mi@#V}`(X0v3O@tMFuEc-&u~|(!kaUq5n2pe(e-`o* zxjx9oVWQY1J4OdU2A9PG<)M0s`~4989|4@W7>ElzsG|7bDwS45i-26D1?S9#NY?;C z$UN?sY46f}K9QRZKpal)5n;tKU)ZPGLwI$m{KTIS6kdR!{TcA(cmts7pC~blO z)Z`fdK%&jm!0IWEZD$sx`r9yiGwFHIY*cFp`>yLb``$s;!h)j^ko{i$;qmkS)*0oW3E(v{(%WF}&N(=c4y4z%h>F-O{g{blh*06;1z zi*nI`0ckF|8U1Xk5h{&=bPp5oGh>k@SWNo4h{WB6RsmMQ(mKW&m; zQJ+}B!@I@*__sflrlUNPH7m<3K#$VGHnd(vg*^1AWoaKPN{c3%v6iR&18JCju((M| z@uMScj%l|aoQKG)7?=V-)FuJgZt0g_#K@27F?pj6i4KC2wle#K#{+z)woa67d?^?_ zD=C1n_vq+cLDJw0>4(KY4&k4U$H4PHy9xuTj&=R>_kpv>2SVrf^YzqE!{dP2hFlL( zz2!kIun#4a7p~@{xD-{*C^UP4Z+deAcKnxLhUu}?e8lpL4mn6kJQN(IUHCI zXYwtKN&mRm_1LzGkn#^t|1LpZxsFcCDFDe*J_EENEi463_7#3oTdQX8dXO4yR;Cc1 z%t}S5$u8uNPg1D;-`RJ8M@aHz1%qo@phx9d=aS39XO$cT`3QTbp-Ae&jj?xiBhGO= z{S)bR^$(UI(?oXG@*&%Dx1t~(SsDH*tBfDBGgcCX`P2-tDA9=!9-)7IJv*|z#kQXq ztZ?;r*N+j`wyddqbN=7|2ncAT)a*;ped&Aq;eOUd4@he`BVD~;SRYu$0}cxLFoL3l z6owcoBh#Ysd&tJvZ^HK~4fGlhr#o+3s|;=Kr|~yfyWBU^FXfC>S_OOtxI2RE^w50T zyguXUay|uFU7(vFA)-TPcpBLOP|%#~2c!nv5dSW1 zL&Y;pRIwN}`t9n}mv@m$I<&9DozQ2On?rVTlfFZK9=WTxuk2in8~^n(E(A;B@CbIG ziKA9vtskU|44DQ%QtmkL@!3GBQ0SYcJU~D{woXm7QkBmTp+#eS4D+1N>Gur8t8(B0 zi$lR~#_gNt@Y7yem(V$lC{`W-VDyWXTxOp$6eo~S>)W~*aAV*!LN))@t5=23>w4YA z?|MUAS^~wHy55NghnCHj{GV+jN$=P^q~m!o#I{Y-HW=?Dznd~)B7ufWtf7$rR0IaF z=(hxlb68hvLnIVHcA07~QBC0dL&Vt2^=bj%!O9z-HUr#7DP%eJJJ=3Y?e?wUKrVsn z`(XO_)%A+&MEN*#z*lWSd)EYQcOL;L)`^EfwHHHtC&pd}2;g*BB-IUkeY&&+IjfT9 z>0HbfEDp zr!k~J5+J^>)P+_GQ|r^>RQn_lveBU#RQf0Q^c8Bi7eOZ;wZcwF~5G;h__{4+e0S5%%M{5k7JCu$Ku{S7pdJhw>*iyFqS z2#@eQ*Cc3UMERFB4TymMe)vXL8DTtRr7lx5!}QFT!V|Y2wjGS#na7dwtKK_#L&v9! zRM=-?2O!R^ZOOjC+-Pf^>}_>~(rkz;2*RlO8zh-=I0fYdF(8=)6{8TmG6=V5wZ?oo_XY)KFeZV{K^Y`DM3<|18?45f#eT8(b9!v( zUbxdJF*1I$qmGtA=ivdpSP~ygyTaBns9;K9=2Sgs15j3SYwbigAd=mK5X}`pnV!A^ zrO|;6bMHE(kQE+L##>yap{x6YXvLyUqI|52->CJksl0+utUgf8RbY1;L@SC>Ev(#~ zjYlRua`^&#hiB)PpWMQ#$S^wW`D|3P0GEY#S<^a~9MBIvG(3(@sYN zG0G!PSVfD4FWGQ*PrgvIpUkT@w|)lQjboQ2CuhzI+?&}r;r`W-pADWBuj;xSk=)*X zP7z>2ze&rpkY(|(Qi!!*W2R1gk3s;^qDWTNW``qD$jNO^Hv)y-R%-8O&-7fuvKU99T9^1+Fr zJgCx_K=988MTFhrl-Q%TDbAqwhBBU7udhI!freFc97V-|GQeJ%8#p$T{?hIVjSzL4 zU90u7N0PH78HCIMlm&zRMWzxvdX*}=H`aUPe}**QUdKTlps$M=kzx7pUOIWQ(CZO- zC*7Mq0uWRgVi?AZ{({h8p0wMR2QlBf8PIWK7`Gz1oh&1?-U)(4YR@t@Ehx37pwQyG zJq3+2ui#>5+rA{w&FY|}L+9+p+JBk^t0OGJ$N$+%$6&mLEIAsi?Xa9_@dOVxI`RKd z1)4_n#Y{GCsk71#-d>yH)R?vh_yZ+JmX~E|z(S=X)6?0US9sxkHmL+ZjrHSoWR=3f zyBVNlB&mGAB9jCG?PJm*&PW*Cz8iHRmk(g!3%G6o8@dAS?o|vdx@FX?2cp~ah;d?` zY5Wce#((g87#)X{`~qn3O|#*tiRv$8UI^Nc>Zk0sy#zXKA1+sC0NS=;om&LN5NwOy zM&xK4r4Y&Y%b`bKqL%N_qcy-uScz#3HC-DB@hUF8D6|_+^Z-(~lOI4OM-;0)m?i1v z$w@B#rHPZWmFp{Q!Wclj+r8_N#<^JKOWeWIHgH@joTd6F27_IX?_T^FYS@y)khLZn z_i6tHNNg`?X`h!6{H4Q-p%qVBLafdx!yz%Xy>TzMilx9opxCjti)^KqxuMzDoV5?E zh5Kn2{>}Q#Rtl7bMDx7vuTP&bX z%z?ybs7&5SDX#_KwVU_t(Bkr?fXB!m`B+T79>CKLMR~dwVA+>w@VS_FXUboDOCLT}KhhC%&Vwm|j z$ekBaM{6M+rX1Gof^(yELruS4G*`q^2)N*MJG<}r0I0Dk#YR|+Px=YYzSU|?&WEvn zAAx-y1paZ#`AxGL;q)^0UuFu>A%!p(N)%T_LG0}|8{g~6aIw6(OLv?Y#55f_p;({K zu>Oo0d!b|w8eoVVd5}pD7*6In?3ux-?0q?huGY|2`#)sW0P$1bvCBX!I^e}75nsh! za8$vk6?p5}gZXJU#~2@OmWIw|%u;3ib`L@o$cEYX`GpmCfnsmg*bgIhwO$9WP!s_- z`@pQvx%BTMC`Wyh-+7OY>WvG&=fIeaVg1@o96C;s12`i>+$%mkX1doG99T#^z(67n zgogqN__#jN$)W>#yo_odxCskOV2u@c9d+do-?$6n*sfJCt7moXlJ$x_roX^y?HxDA zELmCc{+P&^FF{xK0RiU(DFrHw7}8f+oe_XSM^bZGhtIelr0CU#9pk|0ScT-g>v^_b zCMkO7pf4!1`e6g}|EXm`q!UWg(TKjT-vOni?e<4?qD!fDam~WZH$OYJOktnO8I|5!SM*pZN70$Qk2jveA!*W zv*zHHK;W`7){XiD6%HWnCEkj-h&fb+mR_`%{`f2M-spzy!0F#kQCcw2oaQ$S;>b_q z)K`yJ{(Yb)H4B)Tz^61zxYLB}xqnYEUj)H$k7KEa-|W;Z7^)BOX4g8`x%p+yDsY;h z#i#fyd|6JYJ%j-yV^qc8A$k@q|3rxd0GWlP8-h*Z7o z#pC4j)Ok-v99S-ewix$4;i+=qkcaM-L*ob3J~Of(_qw>I0mT9Fhg)gb9|x_*D;F+m z)CZY{l?6O=93g`*?*byWBP(`}KWOJL8TUBud_tl>QfjJEJk}I@y1HVKo{NdPJ~04b zqXRv)QP38nf}jDGd8l^D!Y|FZ5O`w}9j036DuzCnKL`}EyH}vTw|)`R?zsnt>d*h; zCYI+3n>e}w`tf6PauioMtZT}UAT_mvwjeZ__oN`9v-=4mq?t~V7TB_bIyH_RNqfiq zp}Pl*I2{MNy&#xQwhzc!1H;iS2A_+e&Q^fI2J2>Bba;UG!W1`0H|;G)bqm+)rN_tU zxnwM2i2*l2Ek3(pmHgxq)te`B0Y0ZnDq!@UJ)DgPv-bFFw^qOJ!4U5~z7hIa`78mZ zmu)|u$w+hGa8xqN`+(CdC2&S~?-B+m*0_$|69R+Cc z9;V)|x=Q6cfFf{7>W#WX^#GDRhR~4#5;4 zYVSpV1Q=F*u=@BFOCtKqOFd5D{B?9x^~8LzksDD@xi-_^)9LR$xdB+Hy0#-|tzXa} zR73wl@$kD~q@RN-frUz=Qg8Vx<-U%o)p`|05-t*gv-RT8642YFZDAWl03ZMCSqge( zC8f^XQCA{jq~u3IkB$<}N><$(d(Gh7bJPX`>tcN0EqOb&4vZAyfQkBmV62%@!CK7H zN>Eh*+dL%wUYz#Gc5d{uq=9u5?uEQ`b&P2v1YAZ9YWjtcxm|ypRDZbMIqY=Rsd_7K z=}27!>E)%zPbln|LOYPX4K=T|Yt?O$r3wG?$jvA0Ko_VWT{QkVb2XM>QGb5`;KF;^)(ZEPbE|h| zh|7<4T@Q^uYL)>-x!~NqS)mTem+kB{BVSOH(7n;(T-hq=m{dr(5_KHHxFXFIyZ;(M z1}hMpuAbv_&6h|1SqTJbx+OW(-h6EOqIQa-Y&EsB8RDDd;&uS!Tb@>V&OkCW0~ROi zu$}7tDmPOiV{8<5>p&KBCFDQ}*)Vws{&G+-V7wzcMhmG041$5Fztlc>nRxC@x!EW}3 z-DDZsARl{F7a|{E*O@)v3D?n`AhKO>o!ErX^ud82NFSJqjIggXlw{8dky4yxqqMku z>{ZVVCXk3(gNT{gXIGoq@xt`6hqIl+CZ*!2Gt|x(f$i#46P@zgCHvA}>gpX2fZ z^{MSv=D^CeIlcX953y;Mv!ren*fRf_(S@#=M)?S`cHCYrJLt8J?301KGcOEPO)Wx4 zaXTbj=|}7FJj1a(s=uV#$J^x@s>dtCx;&&jw|MR)<9HmP3n;cZV1sL`t6)P|7leJsn^v0h_v453N;X*JMjIl>z zNrBTdnW)POClbeYfk+&p*ua}Ra9=YgWd)S?(QdSh)zcL7DThGj5cRvLXYlu2LoAUr z##QU{ZEZwFl|KVj7o+0_85c{S>wF*5Q~~` zx)>Rg?d*16PZeH1I51@dXW6~>nSrRb2}w2j(gQ{3$rBndP%S# zc1ODP0!jZzaR0Ih*p89CH^^fh;IS=^>P5f%Nk#dfgaNXZJ1P{ZE1me;=*MRqU51Vd z{2g(H@(rLxb%eCtn8OHeOpya>nx4t^n#KyV?*+b{Ia}E_iSc1lt6Li{@kewNlU2(- zL5@uN>OCGbrw)WCPAIEBuS9yzQ7(-L%%T52_>4VgxOV1o$ZO@nKyRlC4OA|K7d5~6 zpeDv~b~O)87tQ7<$X2EolgD^1Hi;-cZWn>YrPu(KzSCz+@5(&ThCKVbdJic;=!>gk z7JQM4d_(KlG1?9cZHJ>EsxwZv4m&v6*=E57L^s*W`eL%m8p}z~3(kF0;w*`Uft#=k z3KUzP2JJpTKezCaL1Bjn(|@|n-34C-3p4e=r|>zNP0HGZLt8{x%P09({z7!fHZSU; zZte)7LPzg`+PZnTR;8!FZiz(yyYBx9ZwVTL?q}!lvD}cuz4B2Z_#5O{KrQrSU|$sc zVZS>QfC6-jM}IlBWj9+?8X`0mAYU!>7{=>O*#JVX2Ww9AY@ofh(RLt>9 zHsX3c!^NO@w)9+?-92xr38MEQDeRy=sazLAJ@Y`UL z@XRm6M8#1FfOna@lvMhDGiS;k9v3v zxz013yUk1ls)AypiJ-7hjZE1xKM7gzOt;_{juhHK{c@n#@SSvn1?e>&Oi~{>L-&Vd z5iqjW3t4{6OL*Z%kg*s0LNQrKD+Me2THvILK(i+01Z@s^HU-WrpHvn*%}P?n34PP| z1b#?_P%B1nlgQPWiG-Vz(PYn@yiY6dCqv{`eW=VM(mBV~GpDH&I!8Je@&?bN97NfA zXSJ6z^=HyfVljgiQJPHB0cb`$g+F3KTPvE$*ceG?LO_#r>3&F86{Vgk?yj09VX+^@ zo&Qjl0k`GnuafRzN0@5R9RfEIg2+Z`O9Q3p34n-YvgS|mlLYt9 zmu2tM^*{o0??PVMH%zbBq9l@vcn&C2-)%oDuGD(z%ZttBtYwiGAQv2xT$6JviJN*{ zjbKo_e;KYiU4JOk>|mD7CL7$nluxp zqw!fv^6fj|j(>T5s4RP1)+$$tcfyu+02@bFqejm7D>z^d^Kiye5F)E%8|z#0bK_$oFMJ7(E1Y8gO4Zxy2rtIinC0gI}+n*o@(tBq%^S+dW#BG8ewHc6TI-pXTq+kBz- z(lchwj$2F=ps}onB{^_KYV(Bc1!}3$UFn%XORCVmLz;C$;wn&)z?QR}{Q=#+Q60Xi zt~r~5uIVOUy`&uZ=BV_BKxnd>u&8TpcgnGM?^?{AOc?$c-cRroA%!M8kCz~Zzs8*o%S>#95-mVJKbyt;jq{(*S2TV7%Y-Xuz>_ z9of2r0J&s9^LCtb#$9)Xt&g%~9DW=8vXyB3r$TRxUh5@2i`pX@YUky8O!Fj97S}Et+?nxg^K+#w&If6l zS&0wf02eHOacl|)C=P%V5EnT1o`TJ-j?h1qe2P5oPtJV^Vxt~>TRl%?<;JUm&=q6) zT_X}#K!ziTD!`FKjr$${%Mt~r7Q-YPb810C}%>w)V9+8%t4)k)g5 z6-R(iRnlG-(FQ^rnNI!Ad!W|r=l8~)wtuh(D|qDS8lRxt)3KdAcC#)rQtHx=xU=au z%nbDE@-pdL#%n{lQfPZ3>xMD&=lZIuom^Ebo!?BFp!`jTKQI%aF(=*fp1I1(krQk( zXMq#DURc{3sM)41sk9en*a|hyN)*#Z*h?#AJGoocQKurLbg!uJi#(B<@4|m<-X%*f z@mZ}kx*T<&Omb1&4yag$L!s?vY$tTZ!2G!K;&WVW(B>#@q5Bwmu~QBGZkhJI2HCNO z8(Tt`zQ1QTOG=)4(hxhVB_L$g10>e%DQUU(0p;Qxu^GU?x-jUfqXSf@)n>10$@4)7 zR`$BvTB)a@!|UYlwvJL-yjbFw7TpDgIq)qM0hTTX*^9Q9u@E}z$XEg_a8JYveCl=x z^}K{%Jg)Hh`-p;wgDTY|jlh9&88>w3;1Ac0mgfTKq0g@ z>F?>e^#j;&p8Yr|(=waQ@V63{#dwb`gmlA>a1Ub_lEF3-F8oDJ0Te*tHO4038xRaM3f2aWd}5 zqzHraPnT}lT!%)^*XuMWdenV+zDoW0F|X!qh;@IJ|D*i74EPUj#%^UCCf8PIYir(L z(s|y;%bfWoHN^4}f!9;K@#o0+%YEux*dI@|6gcSsiEZj7-q@UviUc4$5sd!IPoU%^ z{rbWz$FAwYTd*aL9*g4C40Y!hPckpJ)(|{V2QoLtMx~ zfF`Ba2}hh~1dTYkDS-L%SpHqcrDr@a74YL?E(a4kaC<^uDs5Gi%?v z@bGY~i(wL;M1K8ph?c>_Jha!!Fxcu-ZqS_Ukq#2J>m?qKceam+fL>VxoudZv+(&gg zzpSP!cTot4g!U1E$np--wXn0ov|Zc0n9-qAKtUnG0k^g*EN&l^!ay zD+6$lY8Ms(fYd<}^ZkPwbvTfzfe&N%U~~^PLqh@Mv|_jel*u*eE6>xDDKUtS{*=Dq zK)>T?NNA=8b|30UkrCnDEcGpY(NbWdH6N)KdOFm-v8Nl&hp{Z)FmvwZyYtJB6@pN|_k~ySzb6+%o1Q>gAzSh&+WyK> z{pc4oL<(NY8xi+!J+aXLN?a%{xN+v8ft6wnX&6@0hDLbn-P*xb9MuoW-@6lNJmv$| zQcP8N9!Q&DzaF&R3ZfHO_q#GxO`{J$R23G*JRhgPdkl}976)+E+XpeCWLLeB5$_4= zqml##!~TL*TO=EjI&06vWSQn(HHtgn-Z6?kgXEqV}Lf4a;dDHzN527;ej~4SHL#sAm&BCN|dhW?1B_@ zkRt;E4lGg(-QsRhTuSMhd3bvVsPg+mV6gp7OQCTav(bcn08 zJ&;rUSd-!-0ob-5+Pn=)qAAp{*%N*XiiOZ!nP8YQ(>Dg-yB?N5=F8`S+Z|&`{BT)R zK6CrBzQVnYt;vz86Ix`rO^ zJ#>kz`do=kK8`VXO%SJhT3Ri3UxQMN`7@G&)Qgjk$37*A>>6=VR?TiKGd>1ju|LCu zPcYngEO?lgU#eqh;~`702CkSsL+qs|Ns62iaQZ@A^)+*J^g92lt++Uil}x}VzhowU znmO~?c13*jeHLLsCgjlYXx-2_1aW?bgGsR$!^68r>yQg>&EWo#6p4*{7qONpuF?fyyu^Og%vzn zt(T~`d2b1hDcz}FXlAU?#&*yL{t7QIlPl~TO)jR^8-}_`iwc8^6IpvBABSi!cS zS=!R?cv=#9>(mhz`avwkf$4{$%eQ|MbfALyv(7ZhnF#(-bZ@{73$S2R-d{s@bNKCZ zl{e|HM;Srsq^tM2akSx}ltbV2YXed2E@}8s*%g5{tI0xW`{$c_%Hbox{E}*~8nODJ z&utaM_&t5h*^I@`l1AQ8RZSB4eDQbT2s*f#E|q8?Xbp$G!D{Kf=>j2aix}Y}_4@g< zCKaO=+cSh}kHve$r}gWO#OP|$*QY2E^>9@7pSPPK)zBSQ%u{z}Q3e(e$g|u!5pv6H zcLn}WRtNd37|VGcq5;bbp39gS7k&1tZclXnkT2%9+YOjAFG9-P9HEscMJjYF!z9gL z3yG58OFLS8Fo*sHy%tf$K#|gcmXHcBk(!-Q5bho8*LXu@FtTYN<3H2SZM2H%BHh%* zF53k~r3h{4hsy;U0}%IG{to0`v=7nN$VE`Fc+Tjt$t}KKgu^?`rUHKYT9S8iF5@w+ zu1vBC%jOK&OG%Bg>O5i15*IBlld5Hv(=Kz3TRYnNM7z0+(}9nK0R3(H+uWZrdmRZd z`u7ixW0T(%51(=WJ1^7yQ{jtmFtz}0@V7Y4e)s6sJ=y}(-&SN67Yo*L%DkryCXq;wzFjBI}d-_+@5}y>c zXC;!wUp%_SMHEgZ{fQqUC5jN%pC@f&W-;0jc{NCA_LqKUku%1>;YqwkDKpmW+iMW* zBQVTCqJ9qk5w}gSfvLRn(H}#c_?sC2n);Ij$NQ$YET)_>rbfo8t)tq{UzWXXGFCmo zc(garowY}Qm(wyzJDc2}zkWKNluXg)iCibwy}j*Tvgq_X032sj_h-}w+B1mR2P)1y z4g3C;lmpBnzKd2?uiu?=9X>;fBJN)RPqyEQi4ZC6_hZFo#)4Mr5H2jIKl3fFKz z%iRmkId1WP)1U7dt5Mnt__7U8ZLftgzMlHuNb$^Fd<2&43xTU+;uViZR;*AgZ`}84 zf9!yw(e!IjdRW*K(?fPQTYoq)=hOHr!j4lOT`;0(Gq0;%?V`I_@60`s62mP2Hjx#& zxjNye7%P;W0oz$u@cr%zZ%oj}|I}ocQ)K^TnWI@pDcZ}U_+xCiVYS3}80!(c7SP{v zy%8+&{DB+~f68A^&vaxK?ZoG!h;Sw5%{)`ZTBF_~^1Ta}{nz5wn{Er-fPd);fhfkt zxAQuM0Eo2f``VoK(@VFG;K4;*I*J7AqoSm}I=8f4biJRGA4PtK!+GH3^a}_Y2~&tl z+X5A~Do6Y?`!wVCSOnIw?_bWu6Z7VnEveeY%v_8kMrvL0k?AF5vJae>oVitf31Nuo zx6{=lR=Hu%wVc@S#%QF4xQ!r$6X*&moyT$36?a$yGE{$ALL%-$XFJ&b3; z$J?*Qq|xL^SSa>t#inxTJlxZ(xG!?O&lb5IvBC5-Tsx%1K~Z zZxgtL`nh!w(e80PBs_n5@o&kJg6JQvv>`+hMU<-5g+dj!2x>bROVA%OT(*dX{sYyGQu8RrsCZv)Xvj1=oK8o6ec6{B7! zQWnw5dA*B?$b$J-V9%VdstA={8r-HR!m1a&R>OAsizo;1-kw?OB3Ri&xUhQ8N|O>Z z2r_c1L9bez9LoMJ6FB+zuEzrrG4kRh;{n1O`dMKj-hw;k3rp^T??=B+b<+BF)455J($5n>6Dr7P( zYEn*)DX~yuQ&yxvgy3UT#H!SHxd%lxU1L_%SF;?dQPlty*%b46klJkFfRfDLr0`I>oN{%tog)ZhzFG2#O?j8$w2` zJO?M)ZLqB4P@phi-W;LP52@SXG-58L`%Y*ycWh$$d#r21frob1X6j7|#&)?)H(Ii9 z$k18QtjwGpf`vfE-~)4^Yz=>_lVaqfcfbC0v38T4AX*o=0E7W%`W+K*J z`+`!TaLAUxg-}KY;)7hYhuq<1CCB-87uB0!X1T>9?kHha(0Qug@WYu;V)hbdr^D&X zBUZDYo&E(XHKeJWyzIqMWuujae*@|!mve$#i_^Hm)8T)?D%*`nkIJ9)X&Ex$Vjz_% zL=bVs`x^=L$1ZE>x>G@82i z`pS8Ceu{l>eV@tkeUF?<$nyFqD{Vf{>>n;h+-I+4{!F)|)N>Xz-88?#>#Fp5)Gf4%!cTocdXI7=NPCNB71&4PaSu!!t!^B zuR%cce#KYIjGA?!k>kphXtubLCQn~V)m=6 z(WodwBMNf5Pm1;4mp4CZC#U&}EJb4kry8?AC|wLYNF`Uu2qSwq*D%i`B;elFi;8%2 z@Ki2nkwGUt-e4Y=6$Jz@e};OqM*Oz>U4O!Y2iiW6bE zT{=it%KG1wqX7|E5(7)OA)4s+eHjT}G$v{_mD;9U6az)xhaOg>NK*tGD5KoXn&;?| z41+@dx{XZk|9)?EJifc1O~bcNW*~!{j5pkANO=ard}kfz)uC8-LxxiHh8c9p5E`O4X~f3p4L}rE4x;izOIAbnmU1ayWHeWw09tjYxYNl z6hB1vQP=e5Z|Z&EvP)*=7KJ$DWNjX%JsV`bH(5b@5o1h$@a*gPWTU98ydPPxjh zYq^m%iF`waY+z=wuz)q|ehFHAXPMdbSop?m0%B~!*zYTvrycWAX8+lBDQuwM3dWB7icW0$h5f?5f(|u7`Al3-6xFg*ib@xtv zXP_W*1|a~&+2QR+~f=mC_`>cXsbY*|O zoZ|9W^XvCL*%;PMd-GH5>6l%9)yWnnXLDz7yn0eY-qwX-IF$dJJXu)aPUcggSV{&F-S%#=W@-K98cD}ErH5FQ8- zXgG_@Rr^kM?$|B6eN8VukdMoH|5u=gwMMky)wtW-8^Ky%%{U7qhSvTC9mF4_N~qyk zCUAc@8Zv{W+l{63h79c~b%zdZp)NsW2{pw@r({|w%^j6<(lcFDY8Ig~xq=BXv?6;z zz&3eH39v4+9y^V8Mm!j2Rm1``$_!Y*zIV2rdhHzJ$gpc{1Y2{DKOBuGMams3J{V>D zyI+YYK}In{zKg~tyEm(MDB8DT3K*OQ2Jv-^+yfml<3WmuHqBmHf?mq-IFVm(X&YqxGZ;MzKt zhd3J++60=fjDTyb2BvHzC#pbWK_TVeq$!Ii_I$NHqUTc`FzPzf55aC;s&c6GjL6qr z7K{vWEI48ylq}+wV`1_(3`-XSU=kQpT-BerC+SvDs)G_Y*gYWLu-kPBMk*1NIbP?V zVG+0Yw;$$ZSUaL{yxCu;cOXzmn@}Uk7>{jDAqkS@{`w<>E5PsV;b|KKm{pdAoT_`C z>Qz11SMo%BT(c?CM_mV=P#1t&!{waFWJXmg2!*w8!1u3w(!PeoC2g7n2J0KI0fNRW z?~NzcLU0#%UqZ(q*ZhavBBC{2FjyM=Mvrc3eiN(6Q)9vds`fC}AwxiaNBwX{8%O*y zf`Wvz5WzU`fIl2{^<^|! zzMN~Co6jK7pv|F;#SX8olLB!Ux+&ga&>^qKcfMN%6+*E>2;H1z-~m~_l>(GNim+)# z4=CKkVu(62Lm6#ihf`>V&DfT}OEG!>4Q}CGQc(C!Q81#&OU-`l4Z`oXBvt)or3*Ha zt-*A_8M(Q2$W$KzBK|QA&J}lQv#2(K#SGNRWoz7JwAsuo zg5nTo?>e~VFNXnkmR3$kkV&s{K%6z(X7mn4Sd2r9o4F)@1yle=O>@I)j=5(;JW`K- z%b@d{zoLQ+aw9uQhDxXiL#`PvtcWSf2SKnYD&Z|uDadiKYf$;A04A5FEs5vhv><0K z)Ihh?WiWYyu9gvS5OJvBZ<*_yDgvffutV~FlobL-rfHKCPw6_9kx>x`T;&qhIxQ6i3s1~F|Fv6Bvh1iI>^s76 zra5mwS4SOxPl$!@K-{VO$gJl^xN+oEH{2}vB>2x#A(Ul*ghH?}7#Wg2oO?%YsaSQY z`7lqxi&}>Dz`4%j5a&-%wo9&llx6v{0N^K{sC)Av2kI1DNds2(JKV-L2{1x6dw|pe zuM+TCvZ^rfNZr6ILSN|_TqgeJM{(bJnD|xm9V%su;J(J%Fy*hT$YiF=a)07gf?#G| zH`E`F^PTF4VSx^6P&n@Xv-gFP!&}Y;m=Ju|Gf{}e!kqAIIi_aK4UPiAFh$vp9cPWMi@cgq=ijh_X=ecvZa>ZPrUn_-YJQ`uCjuJv902M1Lf%=RM% z%<+Y`V@s`6$G4HinV-Tm;&ImEIFd)FDO!}rz<4OKqwrI*-zPU6Q1Fsol)ptpq=czQ z3p!&VFBbPS;Yn{l+;-uC+|_Kx-w8Flr;%yzmk7zhKjAvwLaChRa?((3^!Vj(WX8*2 z)af&4ieR8iJxq16nEX=&L;mN=`d5xeJdW^gI{~`~V#hf5YhuLOlPu{Ba3~Whl;=iM zJDWlEkd1N5PPj3m2x+|+aCfgT0dq$Dpn3k9V;wRK6}g~H!qO0bw&2lfWr4qhH;ss`sB(Bw&y!XaQj5ZF zSyAcWZX7{EiDq{b!Z-|7YvR9)ie^75soe>k@;^U9mHertCpD0#yH?{r`$K3DrVsRp zT}$Tc}V+>fx_0v@{?i(|J$FnH9;O z|7jPq&dEU5Johz>Lll7mzV4S^=$*pndzrhT==znM9N^C9nO85{HZdC!R_!n$NB4-G zEa`$oomoAdqJd7*%|D7KDUp62M{_Xs%(?*hm^=7-y1WW%UxgQ7)WaF58jlI(-?M%E z%}~>W2h+t)W~HlOhHoK^oPQ6uK)RSYl>1NTLbYRuQkQem(~z|^4Qsr;s)TZcLwTF! zvh&UfIEI>m?0msX-@LO4b9E>a3JT#;BDCKYYTeoD2X}IOg#LDe zvqxZ->Z#*e>RLtbi_X}_C^nLiyU$()IzmBxU4xGLGU&w2@^u$>1DCi6r1f^YdNrh> z6eorKn01QWGAn(gh_%1{w)};geAtxcPsp8SP4IpVAafCN!^`*IaB=R&L1cXDZsEpH zpcdfdhdX;V&R&N+%!qg6uGfqZ9F~k>k-Sim%%SO$+3$oav=GU~1R3&+F&G(ETE4Td z$2lONObDjC3EYD&7u8pbR?Ky9q(1w>@ZP631@f=1Z$M5`W>@2mbk%CQs-RGihqbtq zWKn0*6f5m3VW!5P?lHC1Gj>iMW1@-pRT&~HP(Ai52+@uu-c#$JCMCUE4>th>z*T53 zS6yDcE?0Qz=kpYmNFQH zC7?t|%5F`488n;Z1?^b|u0)EMw2*_dnzfh!DaO(vT~j$r z64qgr$M%?&*wa~-+5a~7CcIOZ#dUB5=1(Pt!Z1*K));HQICq$sAmA9dFw^*P_8fOI zax0TthfP41t=?9;ydk!1M7nCgl-s~gjuq!GkI`k*EA1pLRv$b0?E}vRi51t2;%7i- zUGDk1)a(mTs@e_LAjj^xhRg=^`}tzcsmRlhPc_}|y7Dci@^^}R$w2f!6>`v+@IVNv za|_S^yPPMa+D|6pH6-A4!bQJlgB++lM$r_Ri4gEP$&Bg z<~XY+Nx{JHrtdJE@tt^mfxKN6QyE+=H3rQHZ_~V5H26AU5Se?+BMpIp?;-}JOsAH8 zL-5^7A!#k`n3`Nha>C6wMMHkw;Uf}SQoJOZ%Az4!Hy8H>{XV}R%%*(xWAg}h*;IUb zh!<3{IKV;ur5K+ujQ zaNsm!ykwkW<0aDlyCju$uy{ebr=op@=`uo@cqr0vjOclaFU{=jhc2B5o^>#f(iyq+ z;R$p;2@JH{+p6k+ zkS%%l@BLloZW}=JPD5IBuLp@M5fCUwLbJ1-eXazODqmo4CI{~Nsju1?nM2<{1)Iqz$=<9gZU%i(FhUS{cR*q37a$$4(VP;Yx%&NYw z0b{)q8sRR(PdlmZ0yc}qnSx@7#G%HHH9VzY;!W{lyW<+ZtXipiQD%mqCtME}!&aJu zTx#&41(_T8LSB^Tz;l>-{2lsBHbe)Mf4wE;2KVOqL36`L7*p<=^Lzq#ehKC?D;V>BqCScFmOi|LXd zCT+uZ8Z3?2T!f+G8EG%z=7;M;(4_RG2nsm6VVaSvYQW6e`aH+gYx(VvTf1H!UYwui zm~{ACZVsW|yo)y8-zXH@#>$96B8&m_VQN3w(URx5&*72w%os%ZAns2mxR93>z-2YU z8lLV8-K*4YxCmFN9~3vMNc4e8cnMA}70||12*W;O-XC#)_t4}ok3EZ!u+Lm?%>p35 zNOBaZPXWuL8KT0PAItP=#e_Ob0-g1J%O=j5Hr+y|ZGLYzvn>E?U32jF&%*(5VcQrC zW6V4KJ{A4#%b%s9a=0*7y)|#U+WfF6xN;re;Q+k*a-NvfQ|gX{u2cP z@mLq7txl&$U{S1R5rjYdI58I$art_z)3F`ASBWb@SbhdmpC&m4%xB@$*r}f@;?J5- zVzPHnt^zo|3xJ6yfrBBJdWN4#i`SB}(dlW+hmLYmA$RA#DpFtP(Bl;+n8HvfHZqTZ zIt1RHL?>NH;XW$)YIOJh-=`Y`n6NbS?F`n}I)n;_k2p^;qjwC22=O$;L*>A>GiHkR zxd^tBVs+tzgN-EQWU(CVyB+9EguDxjc*J=#flY`=Vlk-Y%q|ZrBy&$jd;7+W$e+yT zO7szQqZ{(|C~Bm)jGCHp1DGboi>G@cY+QEyKt5xN(C>@LZEnyhO3s{NtQ`;uXJM{J z5=W@WtD$LJtBAn*H)K8^1}u&k%dU`SDZ6{$sRnT=$HHXcSceY|mp!(|tcVrDcO=lx z)w_5!v$6_gc{JECd)b*q=<<+&BH{cC9ZK>rNG@x!TL0v}4udGYPw3&l^5uXbFgZ@^ zp9V-ya#ODxoj^>)Shiv^co$&JEb)h1LTW#Yt``-TSPOM5A_N9tLI$zHSI-gJ?0-1munobzG&xlp!fe5OBLZf$2@->3j>W( z^%P`!EROD$Q@M!7OuOl%4DW%sA2BGjAH-B1a^G@Qk+Db=&ZLWFXeICj*0Q0N+-f1w z@T4upvL}TS4%_32Iv6vXP#Jdy0YYR{464N)Cw`nx5&imC(n7l!tR(nY_@*cyb;!JH zPF}3N^5@;|=SSFgi!Uz*Sc3RKC#I~5k0WeFRLt$HqpB$zy z6%lj0QN6ah=V&%1xx?wotU@ody9yX0@5ZmnG^GiZS6eAaw*!xXL2m0Q-BNOLw1=;{ z&V8A@M;Hzv>hUcG1po6JWz_*9*#>P$#GY&RA~^fCQw8HB}T`z1&3CZF?wH4fDrMv7$Oa&v` z5)#f4SxxWq*;h17odZNxF;mk1Zgj%Z2imx^m~VU`tFr1CHvzg0Y`u+}n< zL|}TX_$JTIu6u9z7kq+-)*M|=Avyy=lS*zpo42F>^lLOmQI|A?ixz#m4?-a+<}QW> z036C9KAPTGfN*mF)lXwTRC^+4xxOSiH!|}QDNu8(4YBcRl$d@VQz2rCq|C#Jb z8fQ3cZARp)*(q@(1V0&AjWK^&a5c!BUy`OasTwU@fv6I4oyk!syo81E1BE*TC;jNs zE17DRB#c$UkPVEpoLa$~kQY0{jYW}JK1BkVyRZzwG0_04t`q#h;k?MP>J7M=zE0AG zWM#yWQZObkREjSbx*_b3wihkV&f{!{sM^#?S@{9XIaryAg&9bFN2 z==t}VM3=F~cr!RNlb`qdQxfb3XPC1oHjz^osda$5GpSRl$*bGX{1&rMg{1h(X5n5= z_y@KTDD@#RmSe@3ac}HZMh&n+BA`TF=bS#J5dN!QCxgC6bWEEz!HMgC%ab^7lq1Ft z%>U)KNif;QtwEPI9cE0nAq+yA0mUg#X%;$IZTPTMZ`GLGImsMOOy&qumhVZEbusJMZfdUgN ztf2hnyRlaeLn~dH=8IKwj|Vuue2H)H*2*86@T6%NkjzOYz=E`EXxU}xLFk%Qy&H7e`hJPK98Tyavc9fl9tW|*uQgPOFg;U?f6F_t)SKSS&NL+e$(RH6o z;$JX7JPMnIc6oo0xvs0Yl~H}&!@p|K#ggJoDyH;_q{=+>CJm*&AT*KSWD%5^Q|#bOx!$7a;0eg z84uZ>x}Hd+;#*8sa@n8pi54bsa5Y2gtkm4GB$kbzYg1Qdq#q<8^ckH%3awJ=nxR8-z-A zT)m0Wydv1n)}jY5mf6LskcllMJEYrts_Up8#^1zP>hic%%uoHt&0r!bBmT*lHS|NR zJXey}V0SA<=F3P^q;$c*yT$hfk+>qIO(?E1#VR3Mm!_p5Py?j8>JbS3N@ySM#;hg+ zad4X5)2`@TAJ?;_*;lp|QJti%Yt3ozx$%t2n$86=BtP}l+EI`CP-=Rmc{ejSH;3i4 zzmb!L!;LFGg`NJ8d?{k!eiJC$l?8-jnXggIw*lB5Xbf8J=^Ht8Jt&ia3H4(NsvtASyA6?W__)a_oCHai;X%BwhAHlBd-^h6x~v zuj7g_9vJaXpUNFR%j|v2ZAXp#K*T{dUhaMHkmkW4HB&?vHpB|75#hP$Q){iYAZuHz zl!yT~mza|pBA*k>1PqQu`O>hpm2|kNvHaah~Wm+=&QKaehxYC2q zG@yH$$LHp_S=sz~T^=SsTN5#h3|C)W!@%WeA1%*2Tl`3%q~ zX+c6;#eK}TjZG0gE_7H4jvh&*il53ZaYS8)bJozkeBv*T=2u&w)=5%Am8WX}o64gjC)0uET21N|lQLCuU^L2Y~R%|51QNU;(5bWG37=LlcXCp@rci z7~4p6uhp5p9Ldeh7^BioXFKl=TS9J^X!1(a5iy!8ZAMxLEy5GNwzXI%Rp742Owt6d z(@DEPMu5u~;dZ48R!Xy`>jt7zNM;j_C)5dssW zZ40B#P&<@%73-7$wj>o`5Tr`e{NW8%%8DGK9k}f1*}fG?8$?* z{#v@0_qT@K+c?zs74($4jb>;akMzAVgkO=z6gKDTWV}hVmyp71R$TPpG57!D7$Z(N zgwW%!pi$9zJ+nUrZh_V*wAf_@r^rT~$6hgB&<0Ty;`;K&N(p}REmR0S!M_o4=(g?6 zjk^vL0a_Y*9wR{%I12n*QPX_QQ1mVqg)8^>kuY=8ice1Drp)N=QIB4{y|s!2F!udj0?D2cxi-rnSz0O6Iz|VPR<*?iTPB;LC`DTk z7^3dt5EJf|6}4LQ9+Pf~3L66|QM<|R2$h5RFj=AQL+O_gdZs>+sXK$!0sO2GIW>Os zA6{8OHMwZaVjn}z2bHRCrK;2c08N4K(^@d7PgJm}LJM0T`&5yz4e7-cVAe6qv9Lt% zZ0Bu?i9Fkd*CW~l{K0~UV^Pe(N0o-MOX(Ml(4IlmW1gXI6!{+`5}?`ccUfgh zBMrAvzFT*0BVZfQ!%$<>rXivtg(#wPo&!;zf)oqcRZ9CW-HGS=Mw0TD7h}^lJ$z4T zEEw55#=vrl2vK}m zdc%{}7n;t6{04W3amFcZ>jq7^O}Qz-ix$2=L#RPGqX1`!a85Eg@@Q&rqS6_}4u$?C zFPf=(xOD^eR^ykBjal(L>B;8dxQxS8kCyDkY_UvW*bvbaG*Kt+T*9;My&U0G0A0n5 z^KB*_3t(=HM$El}eXhm5*;V23Twt!drNR%KdE9g~A;2-|w0t4iDLWd=qUyb4` zK%)|b+$0YzRv+780Ce#1gIW~R(u*|6O1e|9 zs7ybYPCj@!rplN0FM7F=u2mBZ7TdUY$7;kw6w@R8p9Eq2~z*95;@NurKMG7%5d6w+Ku*8wZI1_4X0u#VhD zc?Qq7HKIM=-^#_yjB=wz;XxkPlEJ}=Y8MB^up&rUN-G?N&o_Z?alb4Zn5fK#>=AmK zq9`7`J&YQEC$z9^y)hz;-;KZ7dAGeNMPqplwAIvY>V+Wp2BI;FKe5fzp?D3BV!LFH zL#5bcrz4UQ{OQI%mocWsy@s`F`U^|nwPo5fV7d%0$a;b`Ka%P4f8Yr?!u=eP%7-Aa zf61`C5Cx-}3d)(XC?2vxIYBt&i3TU0sEHBAEp%SGWAUgRgBHEumz<+X0TRg{Q28sZ zCdj6t{}yUd%e|+{xKLJW0IfDXG<;1+xpznc=3!c$~A07r=Vq^GUA8J{N{H+LKc*D{iu36;WbK=p@-DRAs# zRDM$D86dSl;ZpqRj+Rk%V@DM@zg-GvKO$M;c-(TZBz)k|a;fCXaun_`scMjGkr=mg zT@tOVTOkQKfpMn6e#u~7qr!CZwnhC0`X4dvQljMTN?62KJZriTO|ZmEV9h_X?xRcy zKoo>zFG$bTMMsNFu5jZrk5t6%T9=-@?C&X*Gp+hd(_R7RN$=M>r}M1bJ0S-I>g@nC zS~Tj@ckS6v+OrMXV^UEa^AmYiyYYG8iAJ`0W#Q^w?=Djs&+a-~NZ-lJg+H+%Z!bRr&NY_kJyi7V7q1@7*3D#ez|M4i0 zrq+<%alp8U3$_A)Ss}|4KtYfkhMhKjm>!?QJ*G7c&-@V;vj+sC?Pml&1=vd zL5gw%mwCBa1B3|+v>UHeLj$W{A0ur2``EF* zR)>9U`EiJ}Kx?tZZHxuH@V&Q~wO3Pwbo$)`Q9N$O_^OtiR5?tP)Nooq>Mr|%(jm?H z7g_^Rzy02>KsNblOs`fjQY;xRM32PS85Yc-(v34_`2JyM6+1#Wp-_qSiB698%o3={(P5+wwmD^m{izWZAX|fO|tUV2AW9{SmSz0gK)i?R}-gwY`u%-S*Vbaib^uu`({jVINLrx7EVW<}toN*2R`hjVaM zOPi4UluMEFnT0_WS}<>2kB}?kPJz)NB1R)_>=Ukzn_yfAE6fv;gsB()cLz-;LzGtRkGk%;>dw)0zXa8kcWoJQ$1RP%6dRbNX!AFUfjDGN(Tb zn#b(GUrU~Gx%=Ck#EzA0|EjXw14KEOooMkDbCOm8TOUbirL-Vn9Z~z5OtCE+6y{$X zL9h4KtKHSaqi4M627$z8lhnvOdy@lX0TK>WO0uIx6f*7OO=dGU^|(`>P5H5ok4()3 z{Z*s!Rkjxu%DzKVf;`-05wd!nubza()>+StV9=sY+LGxZSLhvD4D;6dH9cmWg?WS!As#d&}0&<;8T-uKejMLV}Fqn zIh)|Q=zUgo@8W_=r$ovm1IS-c!af5NqT35CL{Jao(=+%QT4Ff_o%VkZn`72M^O$r9 zy*G8lu`84&d;K{OCB>AUJftZJFDzupG68L|AuPk}baDg0h>Bd7H1rbrV3NaGGg%k> z{?2e1U6Xr1$@pnAhEKt`%0P$taEgc;bm8O~UVsBkdEcVFO12y(SWj}cY4T21?BlX;Vt)aw4y~8MUpwNlFe!T?@rNU(RLoP;(s+RG&cfn^eC!Givyv}QD!ucHU6xj z4w3R6ZewG-ouQ7rfXR=YLS|kyCR!3hS6$CzArM3hN98+Y=mv4-Ub7}sv;plP3T~@X zWZ23wsHBX_66b8%Q$|SY1~ZpTPSTYvP9s0kWtWwdc$&uD)ROg43S4#Y4t4T`qfDRZ zu+h0kH^T$0XX&Pyq3(p3y78>KraO~S~Ef|^ljAZk>vJ#x% zz%dq>-P2OJ5uL`+8BFtKY2~L3oq^;zG`F&k4)HJsEoklaLes3q-J}V_=g?-VuBldH zmsHr8ol=#=q3kkxDXa_p3QXfIpY+Ze-9JD8q9*&<)WOiA-K(C)H_%R~>G|l{uti;>2 zWPv+fBgKU(*;%?OKS)j*k^^O1YyMUS@@pCX{T4xnD>rCyrIis9I?cWp{t%L$yyS7f z@&0|!c-Q(blH%#fw*VaI4DHHz5=Rm?Htsb5`o%nYaad>^dZfK;3alT#yU~&guh&qu z>NTuMs-ccIVN_+rSk~rsR8ieXg~UiFULV8C&sw!Ml%I@1UTiuhY4Et$N}_gJrWAS& zW|AcQvz+&j#5}Mn3)+9R%OD|Wv+RRLO?5E$1@Z0)GHGd54@s!&vKloheYhXRg)J)I zMG4qBRN~)7QHVE2>zRgeQjycsgD(2gy=&fcD@#QxcsHe3Y_MeB&zC6WH#$Ffru|rG zy}^#8emWL_M)jDEX&Fta1nsZOlu^@Y>IizMlv~hOWZv{Vp?$b8zTo)qe0FwW#`wI;x^#-F^>&tCQX04upto8H$ z{L^D7Q)OpCkS6` zh&Cg?+mI@8)>K?E>EUK0`I+)h2P-?ySsVG!+_sfZS*WbYl+ajp?V&*$Tt;PZi)gvmfVbS~`>kZCOY;s9{uC zv9ny-#UOJrd0+HsB#!3~@lc_wSmU1V^SPmAU5=gEy5{aIxRP@t>Jm9|Zwj;gbV^uU z%xvlw4XvleIu?(m;1P-3c#S2cU)kz>h>x3!Ubcr<&zoTPEZlv~jD zr&b~z;|&vn{q#o4Y8w&Dr+ZvG``HH9h$PKjT9tC>J=+0=b3_UEz(xh5u7M4~ds0?4 zJ+F$Ctyp+*>5G;3Wxp4iMZN8+35)MvL$FbB&Nl>~kK7GPn z*Hfm@_RYtVLgS8s(Smlx&3ADE4@PaD&DONUX?bY6C;NUBC0>!cxF5u zkI7Dcv_~q+ML>0DsVy!*rO0^IJ)=9uxXQK%z=G0&;QsBN-3CD|}X_jGzicP0q z$Mz_4guZH*%5R+BZnrMGBY4(*&V zxf6MS$hNDXbLmcnVCuSY|94hhfA0ERP#F;PE#wof)EgQI_}8n(I483IU47B?f??D1 z%~}-|=|>9|jCBG=?u_QtyJjjGrGGsrZMIk}HeD7QiA#}=vWUNxZi^E#FP;zj9L1BT zpMqEMy}FAz?P=_!Zo1LY1eKb_&u#VzIG)+E<8{R*W_@Xnt%~fJbBp*bJ^NbRCESZk zug>$SZY_5zpGjI2&(8@e*<%`zfKwbAPtuEI?=0y{H18b;pUnSB;%>{iyEJtVFB&2^8jby=5bQj^PLu5^Ph-sWw&Ii1<*qdX&- z`?E`1lIvfsIg5d-IAABPT)z*5!#=B(GW0x|*M$Z(iZFBlW+H9ZReX_>zZT6==5Y}0VH#13{ zqdZ=c(@O)H55_I#woDe(KC6S81yc`g4ijEw|5u7*X}hLswQc>ExTKUcR)w4VurW!P zynIS_9OYb6*s|W4*KUu5zYS?7sn5&~uVa64_%eEsG?N)MQGT|>zSOjWuhOl?Cv3l3 zhpDgNU9ah#7N2ia43s+MpYc#L3#=R6+51Do$Fg#3e+54OSi$F}o``ti^f`3hv-+7Y zno&{jO&iyIBy&XC8-CxnP7wb}*7B`}kdcu4=slkv&1C;Oc{UwV?YTCKw`?A_1w|XF z`8SG<4NYjLOsrXSBzFk9Tek-7fs<;z-;K*zlW0}hQ6POr^Rd{}zAvi2eNuy!S4&3v zU2SVVDGEMopXNx>oXXtVA+6x?=e+9t`;d;Gp&4;G4>hC{WF2BgD`f^EhssQ)JuQ-r zpoXhoQ@&vy`lAMS@|~Y5f|wO`YN^oRSWk(^s1u5}E2Dy=e{S*i;9}6=$Zgje_jb+f z(gNv@*vLore|k8^<8=$32R$sDEdM#M-!#oSZ143drEWL-61;Z^!Df3iKhf$>pqt*A zVoBPNnAiFu9izAFjM7u`Y8E$?8QbXH>Q?@?0lX&z8RE&M&BxP@7R(>o4;jyR)YYZU zMs@#w;12nd7QsahsgzgMBM>;MPiGRWvini?9;0i7tt$+-$c7KiN4t;Cr)6D=VrSBf zfJG{99eU{fs8-F^*O6cU$Hk)1LQlG~Ciz6(u zll$iIit}2VVHum5mwK{7Gc6h&GwqR%DG~0p6V-phGq1%wq{flFoNuR#+*1Z*ur-`8 z+z%A^9f}7(WsI2>at3;u1(XZ_5(x3jdGyUwSxq<8rB?mrv7P1~edo8~ERcYHf(c<@e z(QSG$*Q#TxTjWa@sw7Cu#ri(Wv{$H#ii%$CpL2@w9slCH&S*B_tIhPR<4U-veHOrL zc$1&55}MyWt*amU{Y*iQ^~lFe!^$pua`^;HhUYGh0z;c7FSv_&!pC2Ahz8n1s||7V(5Z)0P$-5T$0gLRh*!>RjNBB`t~Ug?W*j zHnl50VR+S=d8}l(PTr69Bt7>cL+()l0}os&Lw@D^u^IkFl<7OP;*-%`)Al zJv$B^_tlE~JEIk`~N|KOG7@9cfrN$~)qk z6qcEjVq-bPUAotEU-qB-rN$%5CbsrjPWXg3scs!nnNnspwPR)O&&JC9e>`{UEDaj! zE`9d)alFphU|Dup`cYL*`VVRlQgYxFy*3s%fE*tzRVRU??@Ux zo;#LlBWhH5aL%#QO|bKem~{e^Bk70Jq3Q_(%d(Q*hu5aQt+vgD;hHQu3r*7IwG)_g z9zL1g-sr6+*?mjF=O+eC%p|&l6Tcd#9*j;H*%#)ua6D_0J=CP5c6NbmpJ6nbJjxwm zrS_@#Q;?6Po@M)!zImh3tR;S_lrLTqZ9Cg<>PohCC=17&keIE`v8?wrY28(M=S~0g zf_dHXwj$+}lM6N>8j2%tjZVJ_$|`>CmAJ*)_SUy^iivHS&yIG^_s@5`fig*VG*3DB zW=en7a5nG0PM>7znINHiMmhbqMX?t#eu74OI^LRS4M|0HueoRGCOIqE`OdqeK{4C8 zB-8e``;cjhjI-L}&qQn{Rdl93hprIl& z%TLyG^yFD=*;RO^;E7p7u~$!o)xev4b2p*j?{DrKxv2gT92nFsNezmL@|x4AH!(|( zbp7R9FJzR|G+7hAYga;AT;+q{Em!Z%yK79A`c+hXJN}Q9L6Q}C^aFLCHtlE_qvUe< z{kMOomDY7tI1E5P->K1$-3!8-zK_-r((~M6%KpUc9h?0rHM1Z)>ZITDhe>1BcO7Q& z`s3K-(&T;)?s@BQ?9{n*-i6Ym4=aAvnAq0ta&@o?OgEGCO`IMv%d~ymw(zwhC*@x7 zJ&PgNePPN4)~;F|L5bB?nH9y)OiQy&{x}ycp`w;#BfW#F#S<<*PW*Ra$XM=T<-L=W zOVhZy1^n2km3wAXo6RSq>}Sy@&2T+d!p?7cZY~Y*zMbB0taIT9hFgoC z6?z3{c&aTbS#*+M?gVy><@cX=k)#~!JI*D7fQDy2)ZVPP$sin`C+X_PEa>6LT>bxz z(2;nZ^Dr=T&vOYMTG`$pFL@TiCt4^^`wd2~X(BCGkr1`!Uflm~rS%$1u0S#LNl@?> z_(9s$j1m>YcB1yiZTn9yx}UFC{BJ}8y9jpbLsPtzBl}aRewLB``ELI>(uTe_0NO*5 z9%dQo7tIVthy3P9tN-PZ(j* zg^95`skQY&@;w+=;a4wH^zrMP+qa$7mQ0TB8{po2@M5t$3{LPHFQ2Whd_^7jME-$- zf8GxjkgpEsLeADZvOK1bB3O2&kfKT^+KK}}tdIA@` zy?2A#Gz*bCmps!mkiQ#~-zOaS2_BG)u!>M-_*@JU;fo17@pey>CjM`r|y@ znK#7VmkfUheoGu`9&4K8*y(hon5z={P7C3>qWI`l=NvsUcYQBDU?THOhK>c$rf0^h zWA79wkEA8JK@V|(OyS3uD!wBQFz3UDGq$Zxj@>xu_rEpf*K-|eNlym2sUpX@hFJW( zx-m|E=Vj<5-YT&0{q?b`=^HRqf--C9^%3lsN9|q*OG=Y4hPx_%Pgq^Iw_WNy^}uoJ zE|cTxHQ?zHuqV;^M=}qF3n{t9y2E9E0$DS$?;2(j>9Mp5BV%k1DsjK?}1%0_lPq8@2r-7%kxq7r%8s(zqQN>e#wB zP&nw<38P9yBWTD7f)4}&e|*TYtUidW8Gsk=Uof49YrS43i*vMtPxXyxpZ3vfnQ19M zJSrFciZ;7FxHwU3SHFRZ3<>TsC|+~*{^gX}i~fsca22Nho%v>$RpXU^DgxpB`9aS( zZv_)IxxnchjDM-<~NeI#shFyo2eg5-h$cLTB?z_!@_zuG2 zh}^7sUQhVDi@?>VBARiVJ&Ipcz^$rXA1u!ZNtW$x>pZd1x z!?w#+h&-u`o8fh`H`_c|c|?R9dX#?VVt4v#|T;W(aQAI=B^EZRAqm^?F%0yaA(@AmiuC7F%R-M$212WV8&FX79;kXOT7>}<~bd{4% z^VXD_jGWx+x%7L^Z?bW3!u_NRFf^zeF3>$C@6?dyJzlZ}(Zbk$sCJc|AdO1_uY(rn zOU>&TqOCKY3ZDhOOE^fg9d;*?u!evd&}n* z2X+OYwvZc+wGVst;_{j`@|$m8=t~p0(^nmuC;Yp2O(9&r`4KL#bB57ISCL5=sb{n` zI?iduw%pYbN7WLgt55<;+0V7AXiJZ{XE`a>^6XyUoxgR+zf*pdKY|UD zRC4_wz0h(e$l>}HFYV7YmynxEXL`niI!gV@$-+Etf6$vL;CTEZQgILg)irY zcH0q7U%MG>RsW@A(W-tL%9}A0rRT=CUQBlyh%#Fr-R3=nR1}M`@7}=@Z-mY94Qjf5 zPd$Ct0~e*#ir?vu&5KG_O9@|or&fg$k&>vCt!%m=bp7!Ek?jMZ)|uT>~!h3#%BRrV=twF zGKiLVS~C5?G9mj<)|tS1zzOo6-h}IHMH+HS2IEt`U!vqFI_W0Q&sDeMzvXN=o^-iA zp)=q|NzEhqiAD1Tz@z-*zZGaS{V4tUl0!c1bZ5zEcA0FFba&XnBd{N{JfTO_)(T<< z$FQO&%wbsH-7c5&&(W}u&zW=M@4W*5)v8Y_+W`}Y(MKXuzSj6_g%5UsVr7 zpM?D2E3z^-f48xtks(}qzP}G2zP5Hx)>2ZIR7ZeDR(S6WHJ6_bbo}=2#m*78kJL+NZ z*5->wAA&`*BrOEBzq~W4i0!YO3k!Q3I~ied-4iJS(BW&|Y_hky>EMENY|nWZ`moye zQOossm=JR4sYB1|&2GIJXv;v6%#J5^I~PU&A6Hi$7S;N71w|AP3=om>UN1_cAfVL1 zVWr0BD>?S6FH zEB%XUzEs-?l@p#<{w_D+7XZZDH+oG12WMZJOw0x0i?AMY>s-z!a_b6g76|Iv>Vi2~{^ z#mpnVCw?P%D6x;mT?;aJ>sykc+M9up4E`w$w!9_t_ao99#crOu3GF`u$6)0p!B6tbT*=RHq0rHg%8Cxx4gf$v#-& z&Tw#=Oy;MkBRu{G-#44}5zlH`F51V^CX6MI?RH7@E|p!v!_!VO)bSz@nt7j#QRa-b zd-Lx8R0Rh!11aPh^|c^Qx=s zFx$z!YG0kz1H}FJ{{#{fph-Vy`lkJ|X#S)rnFU7bv+PFd8yLp+EZt8}J`>k-oVpZb z$j(&{tgfCLMK=ZRuynT#Y5Oj(F;Ia`GSu0x0V5hoIyJJjMQ%y5PLA`z3Z|M69noRB zJ^hI=J>=2)^ZEKsN0H`tX>ssX8ARK2Ch7e^c`h+Z5FQRE$=dW?N|5o$1>FI>^!2%` zH9wxpA_zOzu;RTiYF+1e1fTjBzZ(ISOo|^WwKQPju047Vo)`0`LfEP{UB9JiYdCMwk4wuQhC-JP5c-=s zyv(XGdf4%D`=`u51x3c{&|SIFc~g5NMyGuD92V~v`fMJU>#h3FVNa<7Q_1(V&JB1g zm50TVN_Oos2Gc2V5%wIi!^2%?>vKSfD%CF=K3g(g^GP~RG{wFaj0MJ23R!~LK1ZHT z!R%u)HSrxv)-c^$&SneBA)NpL>x*>w3x$ z+#laRf^_H`U8)k`YPR!t?8Rqr#8M=F^$1HATh;)>TzRNq(Sfmoo&+qB1 z&v%~rup~7NWY~5hwJ~2vxi;~U}|6QX?{4h90G1c?1Dq9px zyh=OAr`zMJAn;WJAl{3Jk6=r$?{`{C3H`;rrT+B$)8^!n$_7j2VCGW|akz&@1oIH?o>}{L4!-vmNxQ_m>+MC^OITLO8S-!?d43n z(;hBmoj67k%RZ1;4+`@32cc+JXS1!mvlZbp{UaHd!)|`DY%Z{peJQ)#1xAS^N*q3% zN?1mH-nAFFu!238x+D2hB^D|86tvq<61Ss@{P-+jHM8{W9uAdEsDZB7`1;!4_g zp|}=pcy9i`Bk1kPF5psS@%1P@bJuA2#)0#--Fs?Y6Df}(98by`Xqq4w(h@|@k~0ga zKMLI5-0!z1(x{bxS1{C9AgGvF9-5$m#kcspWZ!z1deObN*Zki-P#>s9D|C-Pbv$+P zMjI&uZiWXl&w5EF%RQ(`2qucA8wrS_2JQ+~JG6{AtI|(Bn?2?Dmxd=&IF%z5GEMt} zD`U+y;l3ms@+1x0J*4vKZWR$U(J#F%N&E2e-l*d&Jc%w#Q3mEG_sIKUpdLRz?CHQ4 zl(21`=<*WCr}c^bf{H#ZF%3f!u5)cOrv8V|P+4?ygXQ+<;+GBcE1o3j_k~@KY>Gn>@=nwBuT+bO;P|Z; zMUX>bT>KX#hYORLJFG|1x_w2_6$NdJnAjbxt{ z0m%K*)B|V0a)NP?1F7al4Yem|ZHAk(Xv!>qx`g#VK)6~uoV(Ka6m;)_;&f#p6M6<_ zmglZ&lsO1tY`lj+DzvBr`JFQJNnEvfKRU~%aRBDLGL-YZa0*_t@B8SH%!hS9#QQd6 zl81yAY2OQsKb7P?OmA5}0RRV%D8g{j8ffEh6zrwz1wdubeU)^$`ut8eu&SYDS+Xn6 zno$c>uI0Zf*C#&Tuc>EjENPH&RZFF%CDltoz`?jTb0-LqmFW5gG0Qo9t@~hen$=!S zULLKl}cI{W%$$;we}vrI*C_ z&_fYk7kA4ovxK8PW&LBu@`Tbb+WwQmJx@y~PP6lqe=o^4A`v>po2oFvd-5zVwYWcB z6$6sE<7wOhm}0#1{sISE^!g`l0pRo2P~p()&=D!{$eg1-YoJR_zEishE~rEJ&VtPD zW)P@h%FmlA%AIW>;lPYN#$5pce4;?=0{SWb)Mb@rfFw3b5AhrMYwltUbjBz!(SM+E+P)rk)X;Z<5&`qU3xw&S}a$0@$Tt*=S+) z!bKDB`KTaQ*wgIiuGZ3?*eUrFez~VIfWedPxlE4{qpMv)v&oKWawh{9&Zbs^V*Mtd z;o2;E-{9D0XS%n1>(?l};~%iON!L!1AhB5{9=TCc6zxRyxE)8$(rUuWYU}_sx*9j= z^$rXq7{YFP1I#Y(?z;(l>*D6Wz$m#u`MuF*j6^Mku(}SA)dn)S5N-Utl6Jb+2F25_ z_4a7UuCwwCuYTBr&b8x8n)NY*4r`*n0|tRPwN-=u1d3D;&%e+kgp|d7BbkDR=>-#kWX1=q+4Dydo~8 zhmV7Pf%p8XcQnNz5*j1r^zB;U`kI|<_(dXG>sIG6wZOyu8h9f-Sued?w)1zkXg;dP z0P=o+?orBWXhZfnvUm@0)p7(DrbZ(Xar*2~rf*eISMj6fFJdDT042y28(31dpemaq zKC1GPavQX-mzwE@L%$ZvGnpogRIWkZ#j*faflWYTB`yUUyZ`4-6MzTCr9hc*cUp!) ze!jLITy#QYv@o|`Juq0lm5!jk=b;EH*+hzUXaQ`;n(MQ2!;4M280XyMqz#I?wodP6d zziHripW|rAhz7Wa8j$N-`Bw@y~4|Y1CGheI0}w`*Eqj% zHdQI;%j}pqL0r?_^L~ZT&t<9`MT%VT*&Rb6_)dsK&!$)Cz8?qI=kZ?@Ir1<{)@S(} z-<{^?-&}-I$I7}U^NmU!nomg!8^YP7rv2Oo6Ex?e&WoC6-;bT()BsaXa4rg9u4}I@ zhn=u`&%|}JPdi7~5Z$}PdD{s_P%04;v&x%C?1ZrK)xQr*o)we)^8{E@b0wqo6rBV3%o2H`jvxe~K zeo5)kherqL%jvnE1}wi-^pWqf`K{h$A>a{(0zQx6u`v@HT z$T6=iBjrYmp<`TjUR*@H)+nt3 z?HcE0*zaD<)nhJ`37cXhEdC4~xs$9o0mvG@O9-_VwbgR;{G@(Sf=~Sq`IK|7TcaT# z)uZ*0#8U9=Ca-qE%QB|$ii}KU=38IOc2;#X<=NzfGKO$(3y`4m9f!=^jeplfa@j18 zKkhX%5B~|dmY!Tv))5f{fC^R4lrFXJ!VR2x-jO8^^909=F@|BCoXF)xQ=H32AxJE=gdD6gXIF(E5-kA}^$^Ue%KVgB|?Ptnl%1&J_D@ zv#H6Q<>P7vDm}%x@SPt5>KtFZZcjsa*dYnxo^+;gu$v%*p@bv-2V3hQ)rcF8X zJevtTkH`-W6I$MVv+o!2N^^xZrLgZD#4Z4{pEUJ4D`Jbob;s52!srgnlMnar;UD)1 zWQAFkL0-&dz(jsvr&Z~eBP2>5(hA5tll{Iia9R58{)Ah&8K}SC=eMQu<{wM0 zer1tckogX}bQYF%6qS;hYHRqj-vNQPXZr5S;?0|PrtS$5ZC>x_3KDZ=?~Ci!94b?_ z@)dtyvE{od;#R1bVKS1q@3(~l8(6c*@FmEpO-n1)n5N@7dm0qe32N!dGtU6`5wysM z1&C$l4(OGwxZ|Sdh{HwxY&;mqvD@z!$A7GZ4pq0=^+&LHJ7?2nwg{fo{GnhRL>pwD z?i+CPSM5wvTCP<3>8b_1v~9|dNGo)w4V6}~W)HcEVr#JqkTv%jxc=p!hwa&^donzH z3wF@N5((mf6iLUR8h+SuLw|e>m_k)^PvLI8e&%>M(f4s4i@y_4*rKM`dv=RBSYRTJ zjQQJ$T_7u}a7L_}G)aiFh3KXZ=-mWBkbTXunp)<+*tQ>h ziq41l_Gudhnkn2jYBCu4sPbu0>Z_s>jdW?V-jH&xS+e6fhQ7lkuE3fT;D+as^4)Wg zUCl3bsNWsw^zFttqb6C4yG&jO{rlEWME(jm%hO=f3S6oV6PH=A%6;V%#0r8SvA5?% z7-T=j0b>NgzpQ?XO#`@S9|VG9X8iTlWE()!mB<6^4kD$-0Vv9TOhi+DR~PCQg80l) z`}Nq{nwEc?T>Sh5ikC8J3r*@4-Og@d&ZdS;%YRgbK1DmNkHQF`3OEwI+I|T>X7Y2K z0spW?3%H*ODiK93{cO=Xey}wglsYevmG~I4WY^O-$~bswTzTd4|E}&m7HafuIGDT$ z3SI5h+k3CLX`h!pNpKI|mZ2|i5KY3AaXr0YVlLLI$ zx`KUQ7x%$n{n}R({U7f8=Z%(rB<8QpqQ9)9<=KC(LH+H@0dA82yv}cbB{=>+pH1ji z`u|zJhd&6SslSE*b^ZSY_x^m7V0-7#UgbZ(T^9;KE5B`9^#69g4?ovm`%XVU;E}*+ zK+TVpR3--k>B7J67=L|!=psN~2|v00OB>*7%=!oaW8!)v=m_E;+W+T|*gOhkHX%^Y zU$Ke$D4_vvD=0orP-)=r*nz_*X6W|-ssCKTS49C|B2qm4?A#O*EPBPBO^wHtwN{qy zK=Ayl_l<8rK#50K;XlvX-yc*l^q9@xz6XM54Iitoeba=OsZZArX(YJCCsFqR&o+_Z z*c3s>R^AE4l_&}Ue&69mZPx{h%ZiYT{XgIH&o3M7F9HL#2(i?-4@IJW!(m87NYeF1 zrrM35)wLo=G-jBW^ex$Z(_O~B`ZxRu=>MM1Rs>g??pfj?*L9kc=HDFkf8K68`>XB~ z>>k1;_EV^q4dQ{nuXBH`6tZ28_@w>n-qO|Zanu62GujqOlD zv73VEmj8XX5x=Awh5MM#k@#?qF0GFT{cl|!94KVzMz%STH}#3n+FPUVdX)5x@Y&#+ zzwsXLY{=EP<>_Nn$rfJhHU;TBlA&cY7 zFmsa2W=Pv~Jx;dPcH{nD#~1#Bb>iPxfWbY9Up(?-8}g>knmmTSN$Il5cg%k@yi+J> zCU_djClQPVqweyq->JkK)g&ShFaP$*GTi`i-edo}m=S1o#<1kc>q!8bNx9D5S)E3q z3d%=}rJl|xVm#1h>kz34VSrNBJ&7+_0EEByncH);iLkeB&x<6vG!vESja38Q|D`!# z(|WqK5x!CKVd`dkj}Pu1aV!s3im=Aoqr7p_d+xq2y&*&Soe7Im4)iF$;>qprBAH{c zd09apF#Cjxg35uKW%bN~*FWFy&rGkg9^h5 zNHPP;k~L(Ne#kwikk_ms3+-6iKS7SpGj87sk~Y+4%L51WM@ox5NzHN`hb0;^7Cv#K z0K`-WcgMn-&8cf1#82sy3Whs@@-oy137Z?(wM_#S7OOvcOvHPmKT%U0-w7f&1{c>t zZe?Mp3DT41CI`8@8v__mlilFG*n_R9;#AYZo;BkM6Rg`UCq4_pV%TuZt=YKecl#qFM*k@Leh_zt4}Dy4_y|ITBTx|AZxFS1Ja3jAn5&4t{UtTQ&_A zh_Cvset+wS_ueKccpdFb8V~bbznQSNb))lqHp#s`%acrx#;j>tW4`b)7WiiI?CtWH zV7}Y)I3?_j-k3BMoOJ5TkJ?AIBxaz_>}HgJPlkbJgckQSB?HLPeEncHyb{o`SxWN! zeq&V9A}rGvk_C(c2$L~I@f4`jsXFI&qYD2civs4b#=obk(e0S3qE^pcxmUb87}(}A z+cE~!C0H;n7?^u!Ky47bs&fZ49AkKgaC5$ra;*2zPjdtfs9aps8^CZA~K zxh-=QlQ}6}PRGsYU@p^VyL{TRL&ga0o-pzo6&GY)@4N35VQ!F>uz_6xLZ2d;QzNNS zAhSu>!2=M!z^(o+YpRbhoy@lqgMW{cpGV21JVp1Mp(L-Vv6n2oJbpwXV6WaHK#RKv zu|X9RuAL#ca@PEgWbcKzkZt2No0H`b<@jju=y9eK|JvXOs+{=L9I;m|no?SrRAZQ> zdvz6Eo|07BbIY@F<+9`K#?b=n#pH@h-0%GCnsk%`wgX~7_KCTCN2dRcVajwJ)jF%^ z)-X>z^<6?c;(#Y|QBrpP;B^eW0VmZ>HLq!@w_fvB6aP|v_I zJQobe@f@iAOtl`#H+sZt;{7r?dH2HatnVLeZUTO{6iHr%?*VC3(UsoMen5TIMa7_i zvgP(ph~Np}frM#pa!3Jy2vgPE`E_Ia140W%iaod9rDdvKUPelUFkJx~G!J=YM5 zPrtDlJSp=9BX<1r$mxR?>3iw}5P`b~PM;@3uQ&Lw;UNgIfxPaF0sZV*VG~~ft6D364NF7JpOqMlD9ZCN>fSSR zgeM%}UrRYnx>Q)F)aN_LG&Fq(jF0*j z`GS(^sodwFhF4h?YUJ1*x<&Xhc{yC}F^V{eUHkK#M)5RhK^hV|nd~_G42NDpeC~_d zT^a5KdX^2(u}r#Te}XL$e0+dq-k0b;Hsbothc|8i@^Z5z-g@iDHz@?cT^8tp){C2& z8K0;5^hT~fHh!=GK;%*YF<-u^ij655wx2K2D3E*hUp=kjS-J~4C3GYf?HdE&?zZ_L z4Z^X$KquTte}W}IgPz|}ao$7WyPb_w6c7&Sp;F=^`)o@ehPdU2Oj-NN1{2#1lxni z*O5salN=td$*3{P((SMk^Xu>NUghG%Z{Lx%=;7GIdQ&U-mZ`OuGKRO1U@{Y@yV4rU zY0dE5R%MQ!*Y9Ec_pGreV>M|3akTM}LL#U!46a(*xAW~9StK)H9Ac?>d*TcNRM0&} zzjV^j`RvR~kDG07^P_q-&w}u4(!QFPO$Eaycwrs-!u%J4({`PZGFyu^;m? z12X(uC;cl5D2QR~C@51Lbz=Ruoa?q!p#KfY@q*awc6k0kkI47Qk2K_E*G=Uuc@YjJ z81EVu;_dIz*pW4=e1^xR++@Lm34WvB>+i7+YWHn!(IEO}SZeIig%qbAQZJ}&Ar=zW zJzb+-vV==^;FFX?Juq9G!s#(2l^n#<@a4;$1@JyzN6t-~0ew@cejmLqVGp@81C(S5 zXPykZAg+CMzApsN`fB;tY?1n!PJg4UWPRM%VDM~V&d$xLgSAr$+fA(YCKJ(9CWPLr z>I$DZ7pH94QTz119^dmu1h{J^>J}F&Ot8G;?6t`2**iVf&RItjSVnoL>qlD}u8%l5 z)b^hj#@{fLumnvVA3`H5)m#M0-{%h4pal{!-wQisBnGqWVAWBCx*R6`j!mI@z=&lo z@-zU>J+pBH4x|-NJ+tAx^Lo}@vDd+-Ea}(S`kXr2r_k3!tL)=63oXJz#r{6>!%8BM zZd{Ai%eEIv^@Qk0TGx1vn1Up`NW6NTpdxED?P#TmQ}s7k;(LPtomnlA13wLv*5h;SAE0hdnp~gqzvmt6%Bd~)5-z3( zLn1|j3aiMrcrTBd@s_pkG4fnUlvr=yyXyE_q=IHrsw_xfwoP;i8JJW&^19-FFST^I zwvgx8FN-3{&YESgN8uGPJ4V^_##y@7Ce9g@ky>@<)jL#F^yLBVXa@aid-s*0c1Vjm zsC@wV^0K4MHygP-Mx#r*K8@eYVD0F(`E|5EM1l@Qx)i@1{~{+YNhw1Z#>zG5#>qC+ z?s_x*4$ZCdEYukZW?kD>mP@u*5_y6)fdt;VtPoXyRU`3&-y=)G?;%myujU3rfLHwtC#GPvJC;cb$>R<#jO+K zIr5Iw`x`WOm@k0!g9==aZ10V?Lqbw?;zcM5q4*Bf>44@zy+J?9gu%}KxTWFshzDB8 zjfv{_10$2`Lp93Qa}a{?mhsx(=`1#PY3jFjWs_HfPNKrLDfEWp;m3uc;K>>MJDG)u z2JS-3h6xG~(pnKbw?Rwo0VOV(xfBoM!gj3udJ=CnH4r~KC#@bmi#R588HjRoK|)L+}F#N=faH2hYvlAC87&LdyO zd4=p))yym#+mOF7pb5yr&8n3Cf&6%vg_!C(ij+>ViiyEOMLa%JF&u-|CyxSM;3467 zid$xAh$V`s)>LNNB!x2P^QJS!C8annm=+R@AEQ~q9IL*+Y|0-_A~)BIh(^9u#=z$f z9+Q;aN_#8n+FewvcSJ|RY1dpy;&+4Wcj<~WPw4OykL2I{05bm1z}ID_pB36Tc&<#S zf775jKnz{#>P&T=3%1&_JN`*%1e+ZFe0U^knK2efn_pp0S((|LG!Xf`#y|uZ1%r=$ zV&OfE1DsXQTj&bNcX)>8koXH69MTPFfiP-g2L2OEmD8oxnn-DD%Ew@yj+>c^DezPl zdJ;>7Cs6SXx(TwSlBV_>05@9sem5i?61NJwaqQmab*e%*mLl3dZQBe8A1Pn&0MFV4 z8(28Fj<}X;6xmTbhdeUZahNW6ILVJ4lQFuA$z9an3)QPAUZNdE(q1T_Z)>9XsE| zQ52xCJHKXUHF3R_%K~i7Ge;NH0Zms>hAP|8Nx@G@8_$e}Bbli3f!gmdhSSWv3KO7v zFS>@97lNM*uMMG?>m2C-?Kp~)O-{#Ltrw%CwE%d^ey7+Q%9_Z|YTE8!htB3e^2$TO zb4=e(hO|6vkV-5yC^FCFu-~ZLpGLwCN)mb0Ql;2l4U1%`@ptYikOjFtNP0yKBYPWW{$Fi2z?6%?)ky|>l1 zquQwHoXCdfGhwmI$#@GAkTF=fh8+*`cp1$x5>eTq8q`*k1&ttUop4GCb8tISGmX!0 zSGE$!NRI^$7HiAkS~#6QA-BW9=GWarD*PV+vm1C^4$}IZOPTtUx^&95>ciD^={d@G zua6P;^^8OqNgDo>Cn?B#?$>Rn}*)Ni(>@$WsA%DoM(hv z1X{S(Y|%p7+`^Du40 zs^cfg8pV3D`x+M7TLPzjER;x5Y@xbH1wJal8UyYqQrjH&DEWvl_6rCw%0 zgjKz*XO`9I@;JjWfos{RE8vH%J38CS=fj($mYuce3?wCrkW@8Q447b&vS7Gjxy6`M((oCQ+37JU}xfy0(=2X$=4=z41lte>^s*1 z+pq48>zZh&s4;*R>PnOb$a*t|N_Xi>*b)uiDcp>ud?aSBW|TIq-2t`(O!K;e2A9XAX;$`M}Zvvs~{{Zhlew#rqrTVj-OC1`>C!D^$ z8?{rHD27;8?>H_{LmK$T4tF5*@k6h~l24LeTmH3k?@>Ul`9Xwn)OZKtTY*Keri(+^RQpuqEso?UvlE_m=-meh$QO;v` zbD|v70D2x#$ROu$ANA4myAS)gQ{Kk;uEeg4G%3)WkPBBaf=T)nx`LMSG9HzI^2ale zrmPxO>PnmReQq=-xmQd^GT%!kj@(iz-hO2eUTm^3rQJVJr5syC8znGN|7!hW4*0>k!O~rC8}H}j8_AcYMSYM7keg{ymX@L;x0 zy&uagzoWG}WNZ8EUPC+$Y0^DaKL1@$Ra{a((hEo!a6A1ORQ?OqH!4Y4{=sQf1>0%^ z17#FUbb1Ao;gvef2JCu6PtU~v#D))#j>un5BaU0{Z%pDU(xgYA?M0O&01${zpmCg% z$^&dm%2z}_&+ko+AXP#;cDf%qrVEV2heb@!`AmIGzMaE&*DS80-^kzp$i2XLhrshmi&y0<`=-H;1(|F%IGv#$Eqt7=#*~hyV!wd6P?e(*s-v$J17!cF2m$^yMlPk(TY0Kp!B-=Og{@FRt5#rHw$t) z6bCxK(|FWW5<;76+zw2OsvqVhs4aJZGpn~Ye>oY*c0(HEaxJX}$3l=K-)!F~m*N** zz^r1k+jX97)XqP3j-0%(JE0mA?gZ|P`$QlrJSR8(je9Uy20Gp+3vE@> zsG+vq#!M_e9LTY&Y^%*|;M=YFL+AQ*P+S$a~O~O_>P=v_xRA+;*vh?3Ll36{i9$_;P)8` zr-_?7#e0!D8rMZ82|)OKyW6nB$e~|s29HD&qWe*;=`?qThXpSgkLWc$Hrodq9c3`m zca|mz7UJ%zHQ3(u?_=3-ruv#%f_{Pc(PAtDR;t&!E1T+g|Uc2!r~8jWU1>o$C2koV;bG&nU^xcK2>s$JSr0Q`^FPX4Is~xQKLGH$tB)z7wJ0N zj8!`TtcC%YO23q;ZL@nrLXV)(GWIeZ_pVo5w31$5C$&~9@I37FS+h)Sr5j$4=+2ud zp!Jc1_@*oenzW^RrG7MbQ3a;xF8xB3MR{3%VbP@&t7CRXV?d*QYu0(fVUb|Dy|o{O z&X;pE+cxv-w3(JP8BCh3*d7tSgSe%|3RA$yem?a=eyqF z=9u0ah>j>6TT#G)ROxS`Cl=X7IaA=e?o+%5g{a|q0?-WxJFa*MO|-u^s(SjU zMdAMm_XBw-0$P4WB&grWiAip; zvfUUt?MZgH--Cp@cIvhJyk$8h4RLj#7f(=lIzazG@G_)(J*)yz3|(e&=pJ99Ty5dH zjLFbeTtUCbDNG?+dYza@X&f=KHS?Vb42X55JR>is?=6Gk@g1s#II$eI4 z?8ePOrh2fi&u?E2dMPwXaS&{=y?dZZBagZSKdrKx)~QWU|F}{$0Jalz@5iQjkWly? z>Fl*H9UeUgQZ;1_e(YuuLs+uzAXVD?M&kn7%^Nn8n_9rD<>B#UV_&sN(ksbslGCi&td~W#4>GQkT*ox&U+FY?uT!iRfm}rezopbJ zqV3E5R*&H#(EZBpB`hV>i0yav4NJS1j*O@4cny{m$?g)ul4_@J);_IgT$fEDh6y~Z&Ntz6W)*f< zI*0hB8{VKDYAc{G5?oXl@togBDq~x8IW9!Bs34k*mNL-hq+F*O1qm9=2#u?h!TXJz z`;A=evdjg1*0VJm{Hx+T3d5}K*3?x{rrROuEnZ2M8wC+VfJM}kx*ug%T(<)BrUIdtU8NildHw$^U`Hn8BRsLI3MGW#NX&#;AC1UZlrJ`yvLRLCzVu7j3pn(UV#Z zQX6UbG^t=p`q)i+Vs%YYb6{no+wXgHjQL zsg6A(N4Ts-lEhjs{GfEba@@Z{sq)l4rM|mstd~IdidQ890`mr$oXMwOo`_0rUaF)5 zm}itkkjYexpz?i&P?8fRE3pv`tXTo+j6Ht@35v$1&k|}yWiD$ub|v1@Nw`)mhOVYX zo=+0WL+h7LlI(92GcEcX(>fy4&QsQIa(hW&BhhOeY9Fq|ML!+3VCuO!%aj0wpv-h? z`?ry9^s-aoyO(>jb)WXjIzZG3KHP5e=riN@UsKNHDNahFO)a1;%+Wui;k(nkf3(6k z>ic8(CU5Ul9gjipy;tS=Y%1ZelQUncz3aOVX3QRNKJ8}Q?Pk)Gy$cC21={lV&k@Xq zv9l>ulC|vC*KU*>3=@ZRoB!FKl=k|ED=1o*=;=4m_?W>uw9a`S%08A zLD^Z~O3Ff=i&5~m&G&K8OQaHUkGpx%Uz2(%)})me@NW_c$vMGE37758NYGogq#3>a zQD2La7EMOFm{v}Ed|qI)e>|L=_qa&FcYVm{L;sHE%U_)RW;ufw=i=U^5GNLYfZb*7 zxj@@y#+Az)@mwxn&+L9KpxP^B3m@%)?s)M(OlmzH=N8L{^9)pv-^O<^H8RUArE)nk zaVRV(z_wm%I-IGJJl{ZiM_*@ofZKku1{Pa~ZDzgeeleW=CX0WUB9U=qiS9Ub`-~MFegg#h>r4TB=KA`_H{s{gq|2b}8-c58#<$n{OcRw`Rfn zcA=CyJ}iw_$Qe(Xey~maJ$XI>n+|!|j3b_08t0nR4|GXxkHyw9oq4NotKSdx}Aw+QlN#TKRErQ3#C37B(`Hkhe->IB#d2h4_2~!kSr&fx|TO(ApD>E zwqEoaSOs5?E_R*oXyb*6R?kI<_&*yRVQh5`ZdU{& z?|I3^buonLW)Whz+@)KHTJV{S?)5{g$yC{6p10>_`uS6d)4bkSEW@1bpS8+Po$0o2 zFE8{0l4+;wLPwLB`Zs_Y{5#(J_(Mq-Kojeyhb}Ui8~2KvmON3jFxq|hUDMoEBFjWf z2vY~l>E0_Jx3!IJ(mVbJ0OoD_wY`?ZT?<=@G4zx3Zp;NS&$_)A6Wufv9NpU1EvrU+ zBtI;$*MaKilpIp`TX|g97hqyy@kGsLepQ&z}8b47dMab(Yb{H*Jl|IRE$xbx-=X7V#^7) zk0Nw(em{BoQlAScLNsZ};{c%EDzT?yXPqsrc^;=Q-`; z+IW9#+idmwciDQsRy#aN`xIzQR&E8t3#pcVyPEJ&coEejf4Z)LN1^8pB5Q+lbwz3R zb~v2XTA@ZR8s?*dFy!gvF*(0T*-Na#;)#!&7fxN*rFsqawX}8CE?AmNgiKhkD7f&c6 zi)}Y;9^mGP_) zQZ!rb^9(4_^7_@hdpsB7oNXl?$IA-F-|e$@&4{sc`7C|J-;jG!6=$$_b3Rh%@u%J* zZx9A}DN7?sh7`{Q)&Z@a1jMA|B`Eipj>ya{&RbDO=5I~G0HcKW z*25KrLp=@cd8JR13W#mXOx-v^F+8Q9@KpqM%(x)>EhoK^l#023n-og!;+X1~0e&(o z9!*Nbbd~y*Is-x0Iqxe^%1NI-XuJU;M}rTS6&Cg2J%Db>9yYgAFV|ij%aOgPrH`fB z`Qj_JL-&a|Mby#kl*2-IddZ2V;4-*50!1KKo#HAW-@6=svnRa=$LKs&bRB8hp8j#M zJE3x(vE}A2aLKu`t(pdQbyX-f@06CnYn<~IOV3b`k*bJ*nUdD5VQ-m~ajg+xATPU) z{1ja0V9)cO1lU8DgR>xhU7QHE`whZ zX2FWdmGiykTnR6wZc|D-XGA~SETqLMT$8_g^+}sV?Kb5*yZN(6iJ)S?H|TAXxP)7) zf5FHemUGJcB$)|uUd&w1@2SyoehycwBk{kO<6eq?ZCBq%t(sx&Ov%6Ye(CD0Tc}$( z8^dwBz_hx=C2o-EM&3hy7^Bq%J;x&a+4NgQn3r-UVvaaB&ymTA200s1``+GmjzmeZ zycUT7XbFZZg)o%$1CZC@a&4kny*Fn=F05Mbu?gwlpzUM2BayF;V#+8*x%%n_#1rpP z>rN?G$Y$mJLM~16RSjr&sC^x5TfETQa{(Y9yxxr#FBgoI%>^>X zk#4@YdV}uXMW)xd>WkY%Wq;>(>`AvdJvBZ}JNU4K<3k4P<$`d^lH@2eMnsO)bm^>Ujyy-RJ@ z{=10*Jyt3&-O$7=ZMm_CRab2Ph~PHkK3ItwJ8%&S=jl=ZMGxMR$2}jjN_u}qtTGbz zTozt=D@t?np9V5nQR*uLzV+;Evn;w=@X7}q zD)VF}_A@$>uR4h_!M{w+m*I9WwD(FGFImpsevUOYG7G!{SAZSpW= zZ!*ot%*Qey!62;tsdO-tq(Ol(8wWvuwOk>~q%a1_>AhgTXS*tse&5<84^_|$EO62b z#@j8;s@FxRMg39thxq^7P=hL@PCE{Qi=cMJJxI~qZg=!ntIF|Ex2&$v$;_U&@>%O= zz=@uxfE{+35N-X|A+K;DZ_jY}LeDrZ{~!j>-XeW|inWsasjr_s)%n|inP$jD&!CtB zo0q(=!34f5Y4@R?25&7u^HJz^<~diy+qxAc zgGQO!B_{Hs%U7%Q^CZ9g$oER+Gjv8PI9NN-9tg z`*f^!!dc#NoU#iJ96$C%gh)g6W^@%^Pr5aCrO1wgi%!eEK&A9Rt(H1FBJNG zTd8`O!uYs7k#8%Wqv*A=70=l$?A}x?%yGIU?llUtiK*HSH)sh@#y`l%l!;On@xhOq z`&wE`>ewxh$I14|<(=ifI|C3Yn~68S_MYw9I>jj!VmQqL%qvH$Tr4b%%#--A=&@KE zpVM4Yk67gdQ-!PCqEx`!^*k~9zf%$D_!<;**fjF)ziqbW<-v}v%e#D;IQQd#@bpXJ z)vpN)bpoC#CBFBH?(_1vPS!=PK6EU0$r{yD%VN*Jwc@(Sm?_2|^}y^{g^(6qXo9qJ7;vl<*>=sXoN!f#90j(3 zjlv&OUasroKCY_H;Kwe%80Y9{($f4^w%htws$` z-AZcS^E7*F+{K;9nKD8Aur*Tc{MPp9;AN~m{w=%z8EH9hWs*}f)&ke3g`kwRj73Y> zNmNv-brN+_#c~w@iSh$j3~&`n_D0kJ6~__cRHYw{-4R<{|dYMu$b~a-i#SLY1wH~acz3J^ePS3^;(WZLYcjKQIxrC zq{Jdq#N&*(D#UJy?PWXbvL#WJ@{}~|i7vTz(8DO@xi@8`*WGk4J+Axx&CGAkIn&hr z%X#^p@B7Q=cQWV4bKr6{r|Kp0u!esnhx*ob@GrHh=hY!Nu9{nRb;he7&JM16e{^nC z`{Egr-*+e4-tx?N;ZPFw=-l52CG8W_>oeVtp&$9Szz@jp!dFsT0dGW4t8bgNd2Z{C zBUgGHKiZb|RIdM}+L5YetfWx^z&0SRQwXYxKXd=WT@Sz8dGFSl+DV&j&G$b2rhINs z=-%E3Nt=FY;J=$tnpyfePtd?eA3}w{XQi~&pQ(G3pEubpxF|V&wb$ONEg?@gbkum6 z3nc@7zHaHr#Bz~1iDjxl=)8e)w|v^s*P79ov3B8>o%=WE{aTwHA1X4-cR#+g)cTIg z?*S?El6K!-gcKFaIGi@s$2 z*^hpqo9`4lh_lE@-dztn>dTtk5ZKNX^C6@g`JE@QZqmp6(J@Jy8eS8>V0K#8?!X6s zIBR+RLrr>%cr5bL7mzCzPeQFM7%UL4HnRfQF%EZAd5VQl5|DG<)7-A)0_lP;>+Vzg zIhI(3a*i-fJUv??C~51REzH62Q!;uS5$L^^}8-Rg1Rn)v>t zV65K?ONq5HzMZE?a@WxtMRWs%$+F!XHi2Z=2IwH0%e`;&X3kqWbXtWBl0R*XzZwOAW@x4BY;PZ%MQ zc|%T{1~swrPTpshFqLKWcy~K$T4iEgM9ysNQJs!-5@=MK#N%Z&E6o}~>3iI0!)~oB zOrR_oh5Vwp=BgE=^yfBu8x6^d@j^+a>JlS?OJ;E+Rt}*Rr&x}0b~1l?g{2==!wY27 znsZn59D2-e@EyW|k@%CL!wg8aY(?5tt|VgUY}KR%ZF3`Z$89wi-Enkf0g6j31t2L9 zNTN>ic^L;@yk9}LD&-Dt$5duHfh@-j)DuqItB?Y|VFu*$x9JwP+Vt6EC6uK7a6gq- z=}#mMs~(Ju)dUe5kyn_Es$UkYcxy~MHcE+E!#-71C8sxnU>axi_I*}*nyh*y_q8m!AUI%sgQdWH%>4er< z%(_E4q$}XKDrn_RiOUBu3*NrP#3Unzs2qV|wq5LB>9z=L=G#lNS$0UWfM40aLs+!cxs8{|a#Z7R7U|>+@TSp?7LxiCSTdMj9MtG4%^FraxDoCIW3zJlBRjC9 zAJ7Avt+thGtiJ08sJJ-i86Llm5sJyiOra7Wc75SR1S4eS9@4RlQzS!rj_2q>EQr8*GQkVR6a-$pbT_Jy*i%&o?ON+Qqhlf9>z%zkjT?1|#Q@~!- zAHnTh*0ZAsiY!2M2vv6_V=z6>tfpm3ufg@P#qe(LU^7b-rz zDx~{@euT@nLu-;P@rW7)Hm$INeYjW-(sxc$F%CQ(Z&F{V3xs^TNNL5UB{aa_`D+N> z#^S;h+I{j|^WHE++9YncNFpoo&SvI3dZyBcm_-{Vv=iD>{nPCvmE;HJRj`G|X3$sd zq58hqlle2sDI(r9YV0tEP#+;41KEuZ_`c$yX~JxLqSG18i@~)3m+Nwi;~0v6@Q002 zSXxMbgHD{=J*tV>gb)|zq=j=mh!ezRlLJG;ikHVM5GUv?BA*R2Eet`NP`!v_X@vG$ zBOs2$ZT3Jrs4>*gfVdyu6n4iHSQ#lFdht$Fp^;~G-4on0B98073>mniD&n6SIm6?) z%5d;=m*@Edv_8ghTw~x*F z7(_JF&e2e4fupuD#Z`dZszKkk=nfUaD>G;f`l+l$?*i6Mh&`;!a)MgJEoU#8G~oL7 zf6pDc*g^U|M{hi5lMx4fxtM@GKvF|lx0LnVM&^PB zY5~2`Xm5FQp?vE|uN8QLGT`@ov`;qbFi!k!ni c2px Date: Sat, 4 Jul 2026 17:13:47 -0700 Subject: [PATCH 35/45] Add Portuguese README --- README.md | 2 +- docs/readme/README.es.md | 2 +- docs/readme/README.ja.md | 2 +- docs/readme/README.ko.md | 2 +- docs/readme/README.pt.md | 261 ++++++++++++++++++++++++++++++++++++ docs/readme/README.zh-CN.md | 2 +- 6 files changed, 266 insertions(+), 5 deletions(-) create mode 100644 docs/readme/README.pt.md diff --git a/README.md b/README.md index e30d4a34a65..1043bc9d670 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@

- Español · 中文 · 日本語 · 한국어 + Español · Português · 中文 · 日本語 · 한국어

diff --git a/docs/readme/README.es.md b/docs/readme/README.es.md index be00fd56369..e1403aed2df 100644 --- a/docs/readme/README.es.md +++ b/docs/readme/README.es.md @@ -12,7 +12,7 @@

- English · 中文 · 日本語 · 한국어 + English · Português · 中文 · 日本語 · 한국어

diff --git a/docs/readme/README.ja.md b/docs/readme/README.ja.md index 9325abddfe5..22e461eb611 100644 --- a/docs/readme/README.ja.md +++ b/docs/readme/README.ja.md @@ -12,7 +12,7 @@

- English · Español · 中文 · 한국어 + English · Español · Português · 中文 · 한국어

diff --git a/docs/readme/README.ko.md b/docs/readme/README.ko.md index 91a2641eb76..b2fc4857112 100644 --- a/docs/readme/README.ko.md +++ b/docs/readme/README.ko.md @@ -12,7 +12,7 @@

- English · Español · 中文 · 日本語 + English · Español · Português · 中文 · 日本語

diff --git a/docs/readme/README.pt.md b/docs/readme/README.pt.md new file mode 100644 index 00000000000..6c3fc52354c --- /dev/null +++ b/docs/readme/README.pt.md @@ -0,0 +1,261 @@ +

+ Orca Orca +

+ +

+ Estrelas no GitHub + Total de downloads em todas as versões + Licença + Entre no Discord do Orca + Siga o Orca no X + Plataformas compatíveis: macOS, Windows e Linux +

+ +

+ English · Español · 中文 · 日本語 · 한국어 +

+ +

+ O orquestrador de IA para builders 100x.
+ Rode Codex, ClaudeCode, OpenCode ou Pi lado a lado — cada um em seu próprio worktree, acompanhado em um só lugar. +

+ +

Baixar o Orca

+ +

+ App desktop do Orca executando agentes em worktrees paralelos, com o app companion móvel do Orca no canto +

+ +## Recursos + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +### Companion móvel + +Monitore e conduza seus agentes pelo celular — receba uma notificação quando um agente terminar e envie instruções de acompanhamento de qualquer lugar. + +[App Store para iOS](https://apps.apple.com/us/app/orca-ide/id6766130217) · [TestFlight](https://testflight.apple.com/join/YjeGMQBA) · [APK Android 0.0.22](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.22/app-release.apk) · [Docs →](https://www.onorca.dev/docs/mobile) + + + Orca desktop com o app companion móvel +
+ +### Worktrees paralelos + +Envie um mesmo prompt para cinco agentes, cada um em seu próprio worktree git isolado — compare os resultados e faça merge do vencedor. + +[Docs →](https://www.onorca.dev/docs/model/worktrees) + + + Orquestração de worktrees paralelos +
+ +### Terminais divididos + +Terminais no nível do Ghostty com renderização WebGL, divisões infinitas e scrollback que sobrevive a reinicializações. + +[Docs →](https://www.onorca.dev/docs/terminal) + + + Terminais divididos +
+ +### Modo Design + +Clique em qualquer elemento de UI em uma janela real do Chromium para enviar HTML, CSS e uma captura recortada direto para o prompt do seu agente. + +[Docs →](https://www.onorca.dev/docs/browser/design-mode) + + + Navegador integrado e Modo Design +
+ +### GitHub e Linear nativos + +Navegue por PRs, issues e quadros de projeto dentro do app — abra um worktree a partir de qualquer tarefa e revise sem trocar de contexto. + +[Docs →](https://www.onorca.dev/docs/review/linear) + + + Fluxos de trabalho de tarefas do GitHub e Linear no Orca +
+ +### Worktrees por SSH + +Execute agentes em uma máquina remota potente com edição completa de arquivos, git e terminais — com reconexão automática e encaminhamento de portas incluídos. + +[Docs →](https://www.onorca.dev/docs/ssh) + + + Worktrees remotos por SSH +
+ +### Anotar diffs de IA + +Deixe comentários em qualquer linha de diff e envie-os de volta ao agente — revise, edite e faça commit sem sair do Orca. + +[Docs →](https://www.onorca.dev/docs/review/annotate-ai-diff) + + + Anotar diffs gerados por IA +
+ +### Arraste arquivos para agentes + +O editor do VS Code com salvamento automático em todos os lugares — arraste arquivos ou imagens direto para o prompt de um agente. + +[Docs →](https://www.onorca.dev/docs/editing/file-explorer) + + + Arraste arquivos e imagens para o prompt de um agente +
+ +### Orca CLI + +Agentes também controlam o Orca — automatize qualquer fluxo de trabalho com `orca worktree create`, `snapshot`, `click` e `fill`. + +[Docs →](https://www.onorca.dev/docs/cli/overview) + + + Automatize o Orca pela CLI +
+ +**Também incluído:** + +- **[Abertura rápida](https://www.onorca.dev/docs/model/quick-open)** — Pesquise entre worktrees, arquivos, agentes, comandos e contexto do repositório sem sair do seu fluxo. +- **[Troca de contas e acompanhamento de uso](https://www.onorca.dev/docs/agents/usage-tracking)** — Veja o uso de Claude e Codex, os reinícios de limites e troque contas instantaneamente sem fazer login de novo. +- **[Prévias ricas do repositório](https://www.onorca.dev/docs/editing/markdown)** — Pré-visualize Markdown, imagens, PDFs e documentos do repositório no workspace. +- **[Computer Use](https://www.onorca.dev/docs/cli/computer-use)** — Deixe agentes operarem apps de desktop e UI visível quando um fluxo de trabalho precisa de interação real. +- **[Notificações e estado de não lido](https://www.onorca.dev/docs/notifications)** — Saiba quando um agente termina ou precisa de atenção, depois marque conversas como não lidas para voltar depois. +- **E muito, muito mais** — lançamos novidades todos os dias, então esta lista vive atrasada. O [changelog](https://github.com/stablyai/orca/releases) é a lista real de recursos. + +--- + +## Agentes compatíveis + +Funciona com **qualquer agente CLI** — se roda em um terminal, roda no Orca. + +

+ Logotipo do Claude Code Claude Code   + Logotipo do Codex Codex   + Logotipo do Grok Grok   + Logotipo do Cursor Cursor   + Logotipo do GitHub Copilot GitHub Copilot   + Logotipo do OpenCode OpenCode   + Logotipo do MiMo Code MiMo Code   + Logotipo do Amp Amp   + Logotipo do OpenClaude OpenClaude   + Logotipo do Antigravity Antigravity   + Logotipo do Pi Pi   + Logotipo do oh-my-pi oh-my-pi   + Logotipo do Hermes Agent Hermes Agent   + Logotipo do Devin Devin   + Logotipo do Goose Goose   + Logotipo do Auggie Auggie   + Logotipo do Autohand Code Autohand Code   + Logotipo do Charm Charm   + Logotipo do Cline Cline   + Logotipo do Codebuff Codebuff   + Logotipo do Command Code Command Code   + Logotipo do Continue Continue   + Logotipo do Droid Droid   + Logotipo do Kilocode Kilocode   + Logotipo do Kimi Kimi   + Logotipo do Kiro Kiro   + Logotipo do Mistral Vibe Mistral Vibe   + Logotipo do Qwen Code Qwen Code   + Logotipo do Rovo Dev Rovo Dev   + + qualquer agente CLI +

+ +--- + +## Instalação + +### Desktop — macOS, Windows, Linux + +- **[Baixe em onOrca.dev](https://onorca.dev/download)** +- Ou baixe um build diretamente: [macOS Apple Silicon](https://github.com/stablyai/orca/releases/latest/download/orca-macos-arm64.dmg) · [macOS Intel](https://github.com/stablyai/orca/releases/latest/download/orca-macos-x64.dmg) · [Windows (.exe)](https://github.com/stablyai/orca/releases/latest/download/orca-windows-setup.exe) · [Linux AppImage](https://github.com/stablyai/orca/releases/latest/download/orca-linux.AppImage) · [Todos os builds](https://github.com/stablyai/orca/releases/latest) +- Rodando `orca serve` em um servidor Linux headless? Veja o [guia de servidor Linux headless](../reference/headless-linux-server.md). + +_Ou por um gerenciador de pacotes:_ + +```bash +# macOS (Homebrew) +brew install --cask stablyai/orca/orca + +# Arch Linux (AUR) — ou stably-orca-git para compilar a partir do código-fonte +yay -S stably-orca-bin +``` + +### Companion móvel — iOS, Android + +Conecte ao app desktop para monitorar e conduzir seus agentes pelo celular. + +- **iOS:** [Baixar na App Store](https://apps.apple.com/us/app/orca-ide/id6766130217) ou [entrar no TestFlight](https://testflight.apple.com/join/YjeGMQBA) +- **Android:** [Baixar APK 0.0.22](https://github.com/stablyai/orca/releases/download/mobile-android-v0.0.22/app-release.apk) + +--- + +## Comunidade e suporte + +- **Discord:** Entre na comunidade no **[Discord](https://discord.gg/fzjDKHxv8Q)**. +- **Twitter / X:** Siga **[@orca_build](https://x.com/orca_build)** para atualizações e anúncios. +- **WeChat:** Escaneie o QR code para entrar na comunidade. + + QR code do WeChat para a comunidade Orca + +- **Feedback e ideias:** Lançamos rápido. Sentiu falta de algo? [Peça um novo recurso](https://github.com/stablyai/orca/issues). +- **Privacidade:** Veja a [documentação de privacidade e telemetria](https://www.onorca.dev/docs/telemetry) para saber quais dados anônimos de uso o Orca coleta e como desativar. +- **Mostre apoio:** Dê uma [estrela](https://github.com/stablyai/orca) neste repositório para acompanhar nossos lançamentos diários. + +--- + +## Desenvolvimento + +Quer contribuir ou rodar localmente? Veja nosso guia [CONTRIBUTING.md](../../.github/CONTRIBUTING.md). + + + Contribuidores do Orca + + +## Licença + +Orca é livre e de código aberto sob a [Licença MIT](../../LICENSE). diff --git a/docs/readme/README.zh-CN.md b/docs/readme/README.zh-CN.md index bd8892c24e8..4c538b08f22 100644 --- a/docs/readme/README.zh-CN.md +++ b/docs/readme/README.zh-CN.md @@ -12,7 +12,7 @@

- English · Español · 日本語 · 한국어 + English · Español · Português · 日本語 · 한국어

From 85adb2ddad21a7334f4ec0dc964056f4c5c09fe1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 5 Jul 2026 00:22:51 +0000 Subject: [PATCH 36/45] release: v1.4.123-rc.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8c0ff488d53..083696f8850 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "orca", - "version": "1.4.122-rc.4", + "version": "1.4.123-rc.0", "description": "Next-gen IDE for parallel agentic development", "homepage": "https://github.com/stablyai/orca", "author": "stablyai", From 62f5ad60b296e1517378d8313583e91d17d410f9 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 4 Jul 2026 18:01:00 -0700 Subject: [PATCH 37/45] perf(emulator): stop decoding frames for hidden emulator panes (#7382) Co-authored-by: Orca --- .../components/emulator-pane/EmulatorPane.tsx | 1 + .../emulator-device-frame-visibility.test.tsx | 103 ++++++++++++++++++ .../emulator-device-frame.input.test.tsx | 1 + .../emulator-pane/emulator-device-frame.tsx | 12 +- 4 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 src/renderer/src/components/emulator-pane/emulator-device-frame-visibility.test.tsx diff --git a/src/renderer/src/components/emulator-pane/EmulatorPane.tsx b/src/renderer/src/components/emulator-pane/EmulatorPane.tsx index d0609eb0ad5..742ff08df29 100644 --- a/src/renderer/src/components/emulator-pane/EmulatorPane.tsx +++ b/src/renderer/src/components/emulator-pane/EmulatorPane.tsx @@ -80,6 +80,7 @@ export default function EmulatorPane({ tab, worktreeId, isActive = true }: Emula deviceName={displayName} loading={loading} isLive={isLive} + isActive={isActive} onTap={(x, y) => void sendTap(x, y)} onGesture={(points) => void sendGesture(points)} /> diff --git a/src/renderer/src/components/emulator-pane/emulator-device-frame-visibility.test.tsx b/src/renderer/src/components/emulator-pane/emulator-device-frame-visibility.test.tsx new file mode 100644 index 00000000000..12409234ff4 --- /dev/null +++ b/src/renderer/src/components/emulator-pane/emulator-device-frame-visibility.test.tsx @@ -0,0 +1,103 @@ +// @vitest-environment happy-dom + +import { act } from 'react' +import { createRoot, type Root } from 'react-dom/client' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { EmulatorDeviceFrame } from './emulator-device-frame' + +// Why: a backgrounded but still-attached emulator must stop streaming frames. +// The perf contract is that no frame stream is started (no per-frame IPC / MJPEG +// blob churn / H.264 decode) while the pane is inactive, and that a running +// stream is torn down when the pane is hidden — so this asserts the IPC calls, +// not just the rendered DOM. + +type FrameListener = (data: { streamId: string; bytes: ArrayBuffer }) => void +type ErrorListener = (data: { streamId: string; message: string }) => void + +let container: HTMLDivElement +let root: Root +let startFrameStream: ReturnType +let stopFrameStream: ReturnType +let streamCounter: number + +beforeEach(() => { + ;( + globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean } + ).IS_REACT_ACT_ENVIRONMENT = true + streamCounter = 0 + startFrameStream = vi.fn(async () => ({ streamId: `stream-${++streamCounter}` })) + stopFrameStream = vi.fn(async () => {}) + Object.defineProperty(URL, 'createObjectURL', { + configurable: true, + value: vi.fn(() => 'blob:emulator-frame') + }) + Object.defineProperty(URL, 'revokeObjectURL', { configurable: true, value: vi.fn() }) + Object.defineProperty(window, 'api', { + configurable: true, + value: { + emulator: { + startFrameStream, + stopFrameStream, + onFrameStreamFrame: (_listener: FrameListener) => () => {}, + onFrameStreamError: (_listener: ErrorListener) => () => {} + } + } + }) + container = document.createElement('div') + document.body.appendChild(container) + root = createRoot(container) +}) + +afterEach(() => { + act(() => { + root.unmount() + }) + container.remove() + delete (URL as Partial).createObjectURL + delete (URL as Partial).revokeObjectURL + delete (window as { api?: unknown }).api + vi.restoreAllMocks() +}) + +async function renderFrame(isActive: boolean): Promise { + await act(async () => { + root.render( + + ) + }) +} + +describe('EmulatorDeviceFrame visibility gating', () => { + it('streams frames while the pane is active', async () => { + await renderFrame(true) + expect(startFrameStream).toHaveBeenCalledWith( + expect.objectContaining({ streamUrl: 'http://127.0.0.1:3100/stream.mjpeg' }) + ) + }) + + it('does not start a frame stream while the pane is inactive', async () => { + await renderFrame(false) + expect(startFrameStream).not.toHaveBeenCalled() + }) + + it('tears the stream down when the pane becomes inactive and resumes when active again', async () => { + await renderFrame(true) + expect(startFrameStream).toHaveBeenCalledTimes(1) + + // Parking the pane must stop the stream at the source (no more IPC frames). + await renderFrame(false) + expect(stopFrameStream).toHaveBeenCalledWith({ streamId: 'stream-1' }) + + // Re-showing re-fires the stream; the session was never detached. + await renderFrame(true) + expect(startFrameStream).toHaveBeenCalledTimes(2) + }) +}) diff --git a/src/renderer/src/components/emulator-pane/emulator-device-frame.input.test.tsx b/src/renderer/src/components/emulator-pane/emulator-device-frame.input.test.tsx index fb7fa74135b..b32824bb9fc 100644 --- a/src/renderer/src/components/emulator-pane/emulator-device-frame.input.test.tsx +++ b/src/renderer/src/components/emulator-pane/emulator-device-frame.input.test.tsx @@ -99,6 +99,7 @@ function renderFrame(props?: { wsUrl="ws://127.0.0.1:3100/ws" loading={false} isLive={true} + isActive={true} onTap={props?.onTap ?? vi.fn()} onGesture={props?.onGesture ?? vi.fn()} /> diff --git a/src/renderer/src/components/emulator-pane/emulator-device-frame.tsx b/src/renderer/src/components/emulator-pane/emulator-device-frame.tsx index 5d03be16158..7f9e2cda8ca 100644 --- a/src/renderer/src/components/emulator-pane/emulator-device-frame.tsx +++ b/src/renderer/src/components/emulator-pane/emulator-device-frame.tsx @@ -39,6 +39,9 @@ type EmulatorDeviceFrameProps = { deviceName?: string loading: boolean isLive: boolean + /** False when the pane is backgrounded (hidden tab/worktree). Gates the frame + * stream so a parked emulator stops decoding, matching the pane's visibility. */ + isActive: boolean onTap: (x: number, y: number) => void onGesture: (points: EmulatorGesturePoint[]) => void } @@ -65,6 +68,7 @@ export function EmulatorDeviceFrame({ deviceName, loading, isLive, + isActive, onTap, onGesture }: EmulatorDeviceFrameProps) { @@ -327,7 +331,13 @@ export function EmulatorDeviceFrame({ setStreamError(true) }, []) - const showStream = isLive && Boolean(previewUrl) + // Why: only decode frames while the pane is visible. A backgrounded but still + // attached emulator otherwise keeps running the WebCodecs H.264 decode / MJPEG + // blob churn against a hidden canvas, with frames still flowing over IPC (and + // the network for paired SSH/remote clients). The session stays attached, so + // the stream re-fires within a frame on re-show. Mirrors the browser pane's + // park-when-hidden behavior. + const showStream = isActive && isLive && Boolean(previewUrl) const screenAspectRatio = streamSize ? streamSize.width / streamSize.height : 9 / 19 const screenAspectRatioStyle = streamSize ? `${streamSize.width} / ${streamSize.height}` From 91323cf9252dec0474f37e1481c7716990b928e3 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 4 Jul 2026 18:10:45 -0700 Subject: [PATCH 38/45] perf(windows): dedupe per-pane process-table scans in agent inspection (#7384) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * perf(windows): dedupe per-pane process-table scans in agent inspection Windows agent foreground-process inspection forks a whole-process-table PowerShell/CIM scan per pane on the same 750ms/2000ms cadence the POSIX path uses. The POSIX side routes through getProcessTableSnapshot (500ms TTL + single in-flight, #6288/#6667), collapsing N concurrent panes to ~2 scans/sec. The Windows path (queryWindowsProcessDescendants) had no such dedup: K concurrent agent panes forked K powershell.exe cold-starts, each enumerating the ENTIRE process table then filtering per-pid in JS — ~10-40x heavier than `ps` (a powershell cold start is ~150-400ms CPU + tens of MB RSS). The degraded/local PTY provider path calls it with no per-pane throttle at all. This is the Windows analogue of the idle-CPU churn #6288 fixed for POSIX. Generalize the existing createProcessTableSnapshotReader factory to be generic over its scan result (default T = string, so the POSIX path and its test are byte-identical) and add a Windows singleton reader that caches parsed WindowsProcessRow[]. queryWindowsProcessDescendants now reads the shared snapshot and runs its own descendant walk; runWindowsProcessRows throws on total enumeration failure so the miss is not cached and the prior null-fallback contract (callers fall through to node-pty's name) is preserved. Windows scan-volume regression test (mirrors the POSIX #6288 guard) drives PANE_COUNT concurrent panes over the cadence window and asserts powershell.exe spawns are bounded by ticks, not pane count, while every pane still resolves its descendant. Reverting the dedup fails both cases. POSIX snapshot + volume tests unchanged and green; node/web/cli typecheck clean. Co-authored-by: Orca * test: reset windows process-rows snapshot between agent-foreground cases The new module-level Windows rows reader caches for 500ms with real Date.now(), so one case's mocked process table was served to the next case's assertions (7 CI failures in agent-foreground-process.test.ts). Mirror the suite's existing POSIX resetProcessTableSnapshotForTests() with the Windows reset in beforeEach. Co-authored-by: Orca --------- Co-authored-by: Orca --- .../agent-foreground-process.test.ts | 4 + ...ent-foreground-process-scan-volume.test.ts | 127 ++++++++++++++++++ .../windows-foreground-process-rows.ts | 43 +++++- src/shared/process-table-snapshot.ts | 34 ++--- 4 files changed, 190 insertions(+), 18 deletions(-) create mode 100644 src/main/providers/windows-agent-foreground-process-scan-volume.test.ts diff --git a/src/main/providers/agent-foreground-process.test.ts b/src/main/providers/agent-foreground-process.test.ts index a54ee116ca5..c21d501b49a 100644 --- a/src/main/providers/agent-foreground-process.test.ts +++ b/src/main/providers/agent-foreground-process.test.ts @@ -10,6 +10,7 @@ vi.mock('child_process', () => ({ import { resetProcessTableSnapshotForTests } from '../../shared/process-table-snapshot' import { resolveAgentForegroundProcess } from './agent-foreground-process' +import { resetWindowsProcessRowsSnapshotForTests } from './windows-foreground-process-rows' // Why: the module wraps execFile with promisify, so the mock must honor the // Node callback contract — invoke the last arg with (err, { stdout, stderr }). @@ -73,6 +74,9 @@ describe('resolveAgentForegroundProcess', () => { beforeEach(() => { execFileMock.mockReset() resetProcessTableSnapshotForTests() + // Why: the Windows rows reader caches across calls (500ms TTL), so each + // case's execFile mock must not be answered by the previous case's rows. + resetWindowsProcessRowsSnapshotForTests() platform = Object.getOwnPropertyDescriptor(process, 'platform') Object.defineProperty(process, 'platform', { value: 'darwin' }) }) diff --git a/src/main/providers/windows-agent-foreground-process-scan-volume.test.ts b/src/main/providers/windows-agent-foreground-process-scan-volume.test.ts new file mode 100644 index 00000000000..28a97aa5437 --- /dev/null +++ b/src/main/providers/windows-agent-foreground-process-scan-volume.test.ts @@ -0,0 +1,127 @@ +// Regression guard: bound the volume of full-process-table PowerShell/CIM scans +// driven by Windows agent foreground-process inspection — the Windows analogue of +// issue #6288 (POSIX `ps`). +// +// Drives queryWindowsProcessDescendants across several concurrently-inspecting +// agent panes on the agent-completion cadence (ACTIVE_POLL_INTERVAL_MS = 750ms) +// and counts how many powershell.exe process-table scans actually spawn. Pre-fix +// the call site forked one powershell.exe per pane per tick; with the shared +// snapshot cache the scans collapse to ~one per tick regardless of pane count, +// while each pane still resolves the same descendant set. +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +const { execFileMock, powershellScanCount } = vi.hoisted(() => ({ + execFileMock: vi.fn(), + powershellScanCount: { value: 0 } +})) + +vi.mock('child_process', () => ({ execFile: execFileMock })) + +import { + queryWindowsProcessDescendants, + resetWindowsProcessRowsSnapshotForTests +} from './windows-foreground-process-rows' + +const ACTIVE_POLL_INTERVAL_MS = 750 +const PANE_COUNT = 6 +const WINDOW_SECONDS = 30 +const TICKS = Math.floor((WINDOW_SECONDS * 1000) / ACTIVE_POLL_INTERVAL_MS) + +const shellPid = (pane: number): number => 100 + pane * 1000 + +// A real CIM query returns the whole system, so one shared snapshot must contain +// every pane's shell + foreground node/codex child. Each pane resolves its own +// descendant from the single scan. +const PROCESS_TABLE_JSON = JSON.stringify( + Array.from({ length: PANE_COUNT }, (_, pane) => { + const shell = shellPid(pane) + return [ + { + ProcessId: shell, + ParentProcessId: 99, + Name: 'cmd.exe', + CommandLine: 'cmd.exe', + ExecutablePath: 'C:/Windows/System32/cmd.exe' + }, + { + ProcessId: shell + 1, + ParentProcessId: shell, + Name: 'node.exe', + CommandLine: 'node C:/Users/dev/AppData/codex/bin/codex.js', + ExecutablePath: 'C:/Program Files/nodejs/node.exe' + } + ] + }).flat() +) + +function installCountingPowerShellMock(): void { + execFileMock.mockImplementation((cmd: string, _args: unknown, _opts: unknown, cb: unknown) => { + const callback = cb as (err: unknown, result: { stdout: string; stderr: string }) => void + if (cmd === 'powershell.exe') { + powershellScanCount.value += 1 + } + callback(null, { stdout: PROCESS_TABLE_JSON, stderr: '' }) + }) +} + +describe('windows agent foreground inspection powershell-scan volume', () => { + let platform: PropertyDescriptor | undefined + + beforeEach(() => { + execFileMock.mockReset() + resetWindowsProcessRowsSnapshotForTests() + powershellScanCount.value = 0 + platform = Object.getOwnPropertyDescriptor(process, 'platform') + Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' }) + vi.useFakeTimers({ toFake: ['Date'] }) + vi.setSystemTime(0) + }) + + afterEach(() => { + vi.useRealTimers() + if (platform) { + Object.defineProperty(process, 'platform', platform) + } + }) + + it('bounds powershell scans by poll ticks, not by pane count, while resolving every pane', async () => { + installCountingPowerShellMock() + + for (let tick = 0; tick < TICKS; tick++) { + vi.setSystemTime(tick * ACTIVE_POLL_INTERVAL_MS) + // All panes inspect concurrently within the tick (worst case). + const resolved = await Promise.all( + Array.from({ length: PANE_COUNT }, (_, pane) => + queryWindowsProcessDescendants(shellPid(pane)) + ) + ) + // Caching must not change the answer: every pane still finds its foreground + // node child as the sole descendant of its shell. + for (let pane = 0; pane < PANE_COUNT; pane++) { + const candidates = resolved[pane] + expect(candidates).not.toBeNull() + expect(candidates).toHaveLength(1) + expect(candidates?.[0]?.pid).toBe(shellPid(pane) + 1) + } + } + + const totalInspections = PANE_COUNT * TICKS + // Pre-fix this equals totalInspections (one powershell.exe per inspection). + // With the shared cache, concurrent panes within a tick share one scan and + // the 500ms TTL forces a fresh scan each new 750ms tick -> ~one per tick. + expect(powershellScanCount.value).toBeLessThanOrEqual(TICKS + 1) + expect(powershellScanCount.value).toBeLessThan(totalInspections / 2) + }) + + it('collapses a burst of concurrent panes into a single scan', async () => { + installCountingPowerShellMock() + + await Promise.all( + Array.from({ length: PANE_COUNT }, (_, pane) => + queryWindowsProcessDescendants(shellPid(pane)) + ) + ) + + expect(powershellScanCount.value).toBe(1) + }) +}) diff --git a/src/main/providers/windows-foreground-process-rows.ts b/src/main/providers/windows-foreground-process-rows.ts index d19d6bf3ebd..d26067122a5 100644 --- a/src/main/providers/windows-foreground-process-rows.ts +++ b/src/main/providers/windows-foreground-process-rows.ts @@ -1,5 +1,6 @@ import { execFile } from 'node:child_process' import { promisify } from 'node:util' +import { createProcessTableSnapshotReader } from '../../shared/process-table-snapshot' const execFileAsync = promisify(execFile) const WINDOWS_PROCESS_QUERY_TIMEOUT_MS = 3_000 @@ -22,12 +23,48 @@ export type WindowsProcessRow = { export type WindowsProcessCandidate = WindowsProcessRow & { depth: number } +// Why: agent foreground inspection forks a whole-process-table PowerShell/CIM +// scan per pane on the same 750ms/2000ms cadence as the POSIX `ps` path. Without +// dedup, K concurrent agent panes fork K powershell.exe cold-starts, each ~10-40x +// heavier than `ps` — the Windows analogue of the idle-CPU churn #6288/#6667 fixed +// for POSIX. Reuse the same TTL + single-in-flight reader, caching parsed rows so +// a burst of panes collapses to ~2 scans/sec; every caller runs its own descendant +// walk over the shared snapshot. +async function runWindowsProcessRows(): Promise { + const rows = + (await queryWindowsProcessesWithPowerShell()) ?? (await queryWindowsProcessesWithWmic()) + if (!rows) { + // Reject so the reader does not cache the miss; callers fall through to + // node-pty's process name (the prior null-return contract is preserved by + // queryWindowsProcessDescendants catching this). + throw new Error('windows process enumeration unavailable') + } + return rows +} + +const windowsProcessRowsReader = createProcessTableSnapshotReader({ + runPs: runWindowsProcessRows, + now: () => Date.now() +}) + export async function queryWindowsProcessDescendants( rootPid: number ): Promise { - const rows = - (await queryWindowsProcessesWithPowerShell()) ?? (await queryWindowsProcessesWithWmic()) - return rows ? collectDescendants(rows, rootPid).sort((a, b) => b.depth - a.depth) : null + let rows: WindowsProcessRow[] + try { + rows = await windowsProcessRowsReader.getSnapshot() + } catch { + return null + } + return collectDescendants(rows, rootPid).sort((a, b) => b.depth - a.depth) +} + +/** + * Test-only: clear the shared Windows process-table snapshot so suites that mock + * execFile between cases don't get one case's rows served to the next within TTL. + */ +export function resetWindowsProcessRowsSnapshotForTests(): void { + windowsProcessRowsReader.reset() } function parseWindowsProcessValueRows(stdout: string): WindowsProcessRow[] { diff --git a/src/shared/process-table-snapshot.ts b/src/shared/process-table-snapshot.ts index 7234cd84333..358cb39a650 100644 --- a/src/shared/process-table-snapshot.ts +++ b/src/shared/process-table-snapshot.ts @@ -21,31 +21,35 @@ const PS_TIMEOUT_MS = 3000 // identically to a fresh fork. const DEFAULT_SNAPSHOT_TTL_MS = 500 -type Snapshot = { stdout: string; capturedAtMs: number } +type Snapshot = { value: T; capturedAtMs: number } -type ProcessTableSnapshotReaderDeps = { - runPs: () => Promise +type ProcessTableSnapshotReaderDeps = { + runPs: () => Promise now: () => number ttlMs?: number } /** * Build a process-table snapshot reader that deduplicates concurrent and - * near-simultaneous `ps` scans behind a single in-flight promise + short TTL. + * near-simultaneous scans behind a single in-flight promise + short TTL. * Exposed as a factory so tests can inject the scan and clock; production code - * uses the shared `getProcessTableSnapshot` instance below. + * uses the shared `getProcessTableSnapshot` instance below. Generic over the + * scan result so the Windows path can cache parsed rows while POSIX caches the + * raw `ps` stdout string (the default). */ -export function createProcessTableSnapshotReader(deps: ProcessTableSnapshotReaderDeps): { - getSnapshot: () => Promise +export function createProcessTableSnapshotReader( + deps: ProcessTableSnapshotReaderDeps +): { + getSnapshot: () => Promise reset: () => void } { const ttlMs = deps.ttlMs ?? DEFAULT_SNAPSHOT_TTL_MS - let cached: Snapshot | null = null - let inFlight: Promise | null = null + let cached: Snapshot | null = null + let inFlight: Promise | null = null - async function getSnapshot(): Promise { + async function getSnapshot(): Promise { if (cached && deps.now() - cached.capturedAtMs < ttlMs) { - return cached.stdout + return cached.value } if (inFlight) { return inFlight @@ -53,11 +57,11 @@ export function createProcessTableSnapshotReader(deps: ProcessTableSnapshotReade const promise = deps.runPs() inFlight = promise try { - const stdout = await promise - // Why: stamp capture time AFTER the scan returns so a slow `ps` can't + const value = await promise + // Why: stamp capture time AFTER the scan returns so a slow scan can't // hand back a snapshot that is already older than its TTL. - cached = { stdout, capturedAtMs: deps.now() } - return stdout + cached = { value, capturedAtMs: deps.now() } + return value } finally { // Clear in-flight on success and failure so a transient `ps` error // (timeout, nonzero exit) retries on the next call instead of being From eb2b894fc0958379596c94a47a5858f3463db474 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 4 Jul 2026 18:15:19 -0700 Subject: [PATCH 39/45] fix(store): purge pane-scoped agent/unread state on external worktree removal (#7383) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit buildWorktreePurgeState is the bulk worktree-removal reducer reached by the authoritative-scan reconcile (CLI `git worktree remove`, another Orca window, SSH), remove-project, and the hydration stale-purge. Unlike the single in-app removeWorktree path — which runs shutdownWorktreeTerminals / dropAgentStatusByWorktree / clearPaneForegroundAgentByWorktree — it never ran terminal teardown, so it left every pane-scoped map untouched: agentStatusByPaneKey, agentLaunchConfigByPaneKey, acknowledgedAgentsByPaneKey, paneForegroundAgentByPaneKey, sleepingAgentSessionsByPaneKey, unreadTerminalPanes, unreadAgentCompletionPanes, lastTerminalInputAtByPaneKey (all keyed by `${tabId}:${leafId}`) and unreadTerminalTabs (keyed by tabId). Result: one orphaned entry per agent pane of every externally-removed worktree, retained for the whole renderer session (monotonic growth), plus a phantom +1 on the unread dock badge. This is a direct parallel to leaks the maintainers already fixed in this same function (browserAnnotationsByPageId, editorCursorLine — each carrying a 'the bulk reconcile path missed them' comment). Fix: evict these maps by tab-id / `${tabId}:` prefix in the reducer, matching the in-app teardown. retainedAgentsByPaneKey and runtimeAgentOrchestrationByPaneKey are intentionally left out — both self-heal (pruneRetainedAgents on a worktreesByRepo change; the runtime map is replaced wholesale each sync). Leak-regression test drives purgeWorktreeTerminalState and asserts every map is emptied for the removed worktree while a sibling worktree's state is preserved. Reverting the fix fails both cases. Co-authored-by: Orca --- .../agent-status-worktree-purge-leak.test.ts | 139 ++++++++++++++++++ src/renderer/src/store/slices/worktrees.ts | 38 +++++ 2 files changed, 177 insertions(+) create mode 100644 src/renderer/src/store/slices/agent-status-worktree-purge-leak.test.ts diff --git a/src/renderer/src/store/slices/agent-status-worktree-purge-leak.test.ts b/src/renderer/src/store/slices/agent-status-worktree-purge-leak.test.ts new file mode 100644 index 00000000000..681c26f81c4 --- /dev/null +++ b/src/renderer/src/store/slices/agent-status-worktree-purge-leak.test.ts @@ -0,0 +1,139 @@ +/** + * Memory-leak regression: pane-scoped agent-status, unread, and terminal-input + * maps must be purged when a worktree is removed via the BULK path. + * + * These maps are keyed by `${tabId}:${leafId}` (or by tabId for + * `unreadTerminalTabs`). The single `removeWorktree` path clears them via + * `shutdownWorktreeTerminals` / `dropAgentStatusByWorktree` / + * `clearPaneForegroundAgentByWorktree`, but the bulk `purgeWorktreeTerminalState` + * reducer (used by the authoritative-scan reconcile, remove-project, and the + * hydration stale-purge) never ran terminal teardown — so an externally-removed + * worktree (CLI `git worktree remove`, another Orca window, SSH, or a removed + * project) orphaned one entry per agent pane for the lifetime of the renderer + * session, plus a phantom unread dock badge. + */ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import type { AgentStatusEntry } from '../../../../shared/agent-status-types' +import type { AppState } from '../types' +import type * as AgentStatusModule from '@/lib/agent-status' + +vi.mock('sonner', () => ({ + toast: { info: vi.fn(), success: vi.fn(), error: vi.fn(), warning: vi.fn() } +})) + +vi.mock('@/components/terminal-pane/pty-dispatcher', () => ({ + restorePtyDataHandlersAfterFailedShutdown: vi.fn(), + unregisterPtyDataHandlers: vi.fn() +})) + +vi.mock('@/lib/agent-status', async (importOriginal) => { + const actual = await importOriginal() + return { ...actual, detectAgentStatusFromTitle: vi.fn().mockReturnValue(null) } +}) + +const mockApi = { + worktrees: { + list: vi.fn().mockResolvedValue([]), + remove: vi.fn().mockResolvedValue(undefined), + forceDeletePreservedBranch: vi.fn().mockResolvedValue({ deleted: true }), + updateMeta: vi.fn().mockResolvedValue({}) + }, + pty: { kill: vi.fn().mockResolvedValue(undefined) }, + runtimeEnvironments: { call: vi.fn().mockResolvedValue({ ok: true, result: {} }) } +} + +// @ts-expect-error -- minimal window.api stub for the store under test +globalThis.window = { api: mockApi } + +import { createTestStore, seedStore, makeWorktree, makeTab } from './store-test-helpers' + +const WT = 'repo1::/path/wt1' +const TAB = 'tab-1' +const PANE = `${TAB}:leaf-a` + +function liveEntry(paneKey: string): AgentStatusEntry { + return { + state: 'working', + prompt: 'p', + updatedAt: 0, + stateStartedAt: 0, + paneKey, + stateHistory: [] + } +} + +function seedPaneState(store: ReturnType): void { + seedStore(store, { + worktreesByRepo: { + repo1: [makeWorktree({ id: WT, repoId: 'repo1', path: '/path/wt1' })] + }, + tabsByWorktree: { [WT]: [makeTab({ id: TAB, worktreeId: WT })] }, + agentStatusByPaneKey: { [PANE]: liveEntry(PANE) }, + agentLaunchConfigByPaneKey: { + [PANE]: { launchConfig: {}, registeredAt: 0, identity: {} } + } as unknown as AppState['agentLaunchConfigByPaneKey'], + acknowledgedAgentsByPaneKey: { [PANE]: 1 }, + paneForegroundAgentByPaneKey: { [PANE]: { agent: null, shellForeground: true } }, + sleepingAgentSessionsByPaneKey: { + [PANE]: {} + } as unknown as AppState['sleepingAgentSessionsByPaneKey'], + unreadTerminalTabs: { [TAB]: true }, + unreadTerminalPanes: { [PANE]: true }, + unreadAgentCompletionPanes: { [PANE]: true }, + lastTerminalInputAtByPaneKey: { [PANE]: 123 } + }) +} + +describe('bulk worktree purge evicts pane-scoped agent/unread/input maps (leak regression)', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('purgeWorktreeTerminalState drops every pane-scoped map for the removed worktree', () => { + const store = createTestStore() + seedPaneState(store) + + store.getState().purgeWorktreeTerminalState([WT]) + + const s = store.getState() + expect(s.agentStatusByPaneKey[PANE]).toBeUndefined() + expect(s.agentLaunchConfigByPaneKey[PANE]).toBeUndefined() + expect(s.acknowledgedAgentsByPaneKey[PANE]).toBeUndefined() + expect(s.paneForegroundAgentByPaneKey[PANE]).toBeUndefined() + expect(s.sleepingAgentSessionsByPaneKey[PANE]).toBeUndefined() + expect(s.unreadTerminalTabs[TAB]).toBeUndefined() + expect(s.unreadTerminalPanes[PANE]).toBeUndefined() + expect(s.unreadAgentCompletionPanes[PANE]).toBeUndefined() + expect(s.lastTerminalInputAtByPaneKey[PANE]).toBeUndefined() + }) + + it('keeps pane-scoped state for worktrees that are NOT removed', () => { + const store = createTestStore() + const OTHER = 'repo1::/path/wt2' + const OTHER_TAB = 'tab-2' + const OTHER_PANE = `${OTHER_TAB}:leaf-b` + seedStore(store, { + worktreesByRepo: { + repo1: [ + makeWorktree({ id: WT, repoId: 'repo1', path: '/path/wt1' }), + makeWorktree({ id: OTHER, repoId: 'repo1', path: '/path/wt2' }) + ] + }, + tabsByWorktree: { + [WT]: [makeTab({ id: TAB, worktreeId: WT })], + [OTHER]: [makeTab({ id: OTHER_TAB, worktreeId: OTHER })] + }, + agentStatusByPaneKey: { [PANE]: liveEntry(PANE), [OTHER_PANE]: liveEntry(OTHER_PANE) }, + unreadTerminalTabs: { [TAB]: true, [OTHER_TAB]: true }, + lastTerminalInputAtByPaneKey: { [PANE]: 1, [OTHER_PANE]: 2 } + }) + + store.getState().purgeWorktreeTerminalState([WT]) + + const s = store.getState() + expect(s.agentStatusByPaneKey[PANE]).toBeUndefined() + expect(s.agentStatusByPaneKey[OTHER_PANE]).toBeDefined() + expect(s.unreadTerminalTabs[OTHER_TAB]).toBe(true) + expect(s.lastTerminalInputAtByPaneKey[OTHER_PANE]).toBe(2) + }) +}) diff --git a/src/renderer/src/store/slices/worktrees.ts b/src/renderer/src/store/slices/worktrees.ts index 15d44711ac5..6d5de1c57bb 100644 --- a/src/renderer/src/store/slices/worktrees.ts +++ b/src/renderer/src/store/slices/worktrees.ts @@ -1903,6 +1903,25 @@ function buildWorktreePurgeState(s: AppState, worktreeIds: string[]): Partial(obj: Record): Record => { + // Null-tolerant like omitByTabId: some worktree-isolation callers omit these + // slices entirely, and the production store always initializes them to {}. + if (!obj) { + return obj + } + let changed = false + const out = { ...obj } + for (const paneKey of Object.keys(obj)) { + const sep = paneKey.indexOf(':') + if (sep > 0 && doomedTabIds.has(paneKey.slice(0, sep))) { + delete out[paneKey] + changed = true + } + } + return changed ? out : obj + } const omitByBrowserWorkspaceId = (obj: Record): Record => { let changed = false const out = { ...obj } @@ -1972,6 +1991,25 @@ function buildWorktreePurgeState(s: AppState, worktreeIds: string[]): Partial Date: Sat, 4 Jul 2026 18:31:44 -0700 Subject: [PATCH 40/45] Warn about and manage repository overrides for AI action recipes (#7386) * Warn about and manage repository overrides for AI action recipes This improves the user experience around overriding global AI action recipe defaults with repository-specific settings: - In global settings, display a note if repositories override a recipe, with a shortcut to review those repository settings. - In the agent launch dialog, show a warning if the repo overrides the global default and default the save target to the repo. - In repository settings, support saving and discarding recipe overrides on a per-action basis. * Memoize repo filtering and override summaries in recipe defaults Avoid re-filtering all repositories and re-computing override summaries on every keystroke in sibling textareas. Also use an exhaustive switch check for override field labels, and add a test verifying the overflow note indicator when overrides exceed the visible limit. --- .../SourceControlAgentActionDialog.tsx | 2 + ...ourceControlAgentActionDialogForm.test.tsx | 25 +++ .../SourceControlAgentActionDialogForm.tsx | 29 +++- ...urce-control-agent-action-dialog-result.ts | 6 + .../useSourceControlAgentActionDialog.ts | 37 ++++- .../RepositorySourceControlAiActionRows.tsx | 73 ++++++++- .../RepositorySourceControlAiSection.test.ts | 52 ++++++ .../RepositorySourceControlAiSection.tsx | 109 ++++++++----- .../settings/SourceControlActionRecipeRow.tsx | 85 +++++----- ...urceControlActionRepoOverrideNote.test.tsx | 106 +++++++++++++ .../SourceControlActionRepoOverrideNote.tsx | 135 ++++++++++++++++ .../SourceControlAiActionRecipeDefaults.tsx | 45 ++++++ .../settings/repository-settings-targets.ts | 9 ++ ...repository-source-control-ai-draft.test.ts | 56 +++++++ .../repository-source-control-ai-draft.ts | 74 ++++++++- src/renderer/src/i18n/locales/en.json | 15 +- src/renderer/src/i18n/locales/es.json | 15 +- src/renderer/src/i18n/locales/ja.json | 15 +- src/renderer/src/i18n/locales/ko.json | 15 +- src/renderer/src/i18n/locales/zh.json | 15 +- ...rce-control-launch-agent-selection.test.ts | 148 ++++++++++++++++++ .../source-control-launch-agent-selection.ts | 130 ++++++++++++++- 22 files changed, 1101 insertions(+), 95 deletions(-) create mode 100644 src/renderer/src/components/settings/SourceControlActionRepoOverrideNote.test.tsx create mode 100644 src/renderer/src/components/settings/SourceControlActionRepoOverrideNote.tsx create mode 100644 src/renderer/src/components/settings/repository-source-control-ai-draft.test.ts create mode 100644 src/renderer/src/lib/source-control-launch-agent-selection.test.ts diff --git a/src/renderer/src/components/right-sidebar/SourceControlAgentActionDialog.tsx b/src/renderer/src/components/right-sidebar/SourceControlAgentActionDialog.tsx index 49d01dd3459..a9eb07aa247 100644 --- a/src/renderer/src/components/right-sidebar/SourceControlAgentActionDialog.tsx +++ b/src/renderer/src/components/right-sidebar/SourceControlAgentActionDialog.tsx @@ -65,6 +65,7 @@ export function SourceControlAgentActionDialog( const { handleOpenChange, shouldRenderDialog, + agentScopeNote, agentOptions, selectedAgent, hasEnabledAgents, @@ -101,6 +102,7 @@ export function SourceControlAgentActionDialog( { expect(repoMarkup).not.toContain('Launch recipe already saved') expect(repoMarkup).toContain('Save & start agent') }) + + it('surfaces a diverging repo override alongside the save controls', () => { + const markup = renderForm({ + agentScopeNote: { effectiveAgentLabel: 'Codex', globalAgentLabel: 'Claude' } + }) + + expect(markup).toContain('overrides your global default (Claude)') + expect(markup).toContain('currently runs Codex') + }) + + it('omits the scope note when there is no diverging repo override', () => { + const markup = renderForm({ agentScopeNote: null }) + + expect(markup).not.toContain('overrides your global default') + }) + + it('omits the scope note when the save controls are hidden', () => { + const markup = renderForm({ + agentScopeNote: { effectiveAgentLabel: 'Codex', globalAgentLabel: 'Claude' }, + canSaveAgentDefault: false + }) + + expect(markup).not.toContain('overrides your global default') + }) }) diff --git a/src/renderer/src/components/right-sidebar/SourceControlAgentActionDialogForm.tsx b/src/renderer/src/components/right-sidebar/SourceControlAgentActionDialogForm.tsx index 779a200087c..4f2caf0133b 100644 --- a/src/renderer/src/components/right-sidebar/SourceControlAgentActionDialogForm.tsx +++ b/src/renderer/src/components/right-sidebar/SourceControlAgentActionDialogForm.tsx @@ -1,5 +1,13 @@ import React from 'react' -import { CheckCircle2, RefreshCw, RotateCcw, Settings, Sparkles, TriangleAlert } from 'lucide-react' +import { + CheckCircle2, + Info, + RefreshCw, + RotateCcw, + Settings, + Sparkles, + TriangleAlert +} from 'lucide-react' import AgentCombobox from '@/components/agent/AgentCombobox' import { Button } from '@/components/ui/button' import { DialogFooter } from '@/components/ui/dialog' @@ -19,6 +27,7 @@ import type { SourceControlAiWriteTarget } from '../../../../shared/source-contr import type { GlobalSettings, Repo, TuiAgent } from '../../../../shared/types' import { SourceControlActionVariableChips } from '../source-control/SourceControlActionVariableChips' import { sourceControlActionRecipeMatchesTarget } from './source-control-action-recipe-match' +import type { SourceControlAgentScopeNote } from './source-control-agent-action-dialog-result' import { translate } from '@/i18n/i18n' export type SourceControlAgentActionDeliveryPlanState = @@ -29,6 +38,7 @@ export type SourceControlAgentActionDeliveryPlanState = type SourceControlAgentActionDialogFormProps = { actionId: SourceControlLaunchActionId baseCommandInput: string + agentScopeNote: SourceControlAgentScopeNote | null agentOptions: AgentCatalogEntry[] selectedAgent: TuiAgent | null hasEnabledAgents: boolean @@ -73,6 +83,7 @@ function sourceControlLaunchSaveTargetFromValue( export function SourceControlAgentActionDialogForm({ actionId, baseCommandInput, + agentScopeNote, agentOptions, selectedAgent, hasEnabledAgents, @@ -263,6 +274,22 @@ export function SourceControlAgentActionDialogForm({ ) : null}

+ {showSaveLaunchRecipe && agentScopeNote ? ( +
+ + + {translate( + 'auto.components.right.sidebar.SourceControlAgentActionDialogForm.repoAgentOverrideNote', + 'This repository overrides your global default ({{global}}) and currently runs {{effective}}. Save to this repository to change what runs here.', + { + effective: agentScopeNote.effectiveAgentLabel, + global: agentScopeNote.globalAgentLabel + } + )} + +
+ ) : null} + {showSaveLaunchRecipe ? (
void shouldRenderDialog: boolean + agentScopeNote: SourceControlAgentScopeNote | null agentOptions: ReturnType selectedAgent: TuiAgent | null hasEnabledAgents: boolean diff --git a/src/renderer/src/components/right-sidebar/useSourceControlAgentActionDialog.ts b/src/renderer/src/components/right-sidebar/useSourceControlAgentActionDialog.ts index 7dca868c749..74e7403cd9d 100644 --- a/src/renderer/src/components/right-sidebar/useSourceControlAgentActionDialog.ts +++ b/src/renderer/src/components/right-sidebar/useSourceControlAgentActionDialog.ts @@ -1,6 +1,9 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { getAgentCatalog } from '@/lib/agent-catalog' -import { pickSourceControlLaunchAgent } from '@/lib/source-control-launch-agent-selection' +import { + pickSourceControlLaunchAgent, + resolveSourceControlLaunchAgentScope +} from '@/lib/source-control-launch-agent-selection' import { useAppStore } from '@/store' import { useRepoById } from '@/store/selectors' import { renderSourceControlActionCommandTemplate } from '../../../../shared/source-control-ai-actions' @@ -39,6 +42,15 @@ export function useSourceControlAgentActionDialog({ }: SourceControlAgentActionDialogProps): UseSourceControlAgentActionDialogResult { const settings = useAppStore((state) => state.settings) const repo = useRepoById(repoId ?? null) + const launchAgentScope = useMemo( + () => resolveSourceControlLaunchAgentScope({ settings, repo, actionId }), + [actionId, repo, settings] + ) + // Why: when this repo already overrides the global default, default the save + // scope to the repo so saving the corrected agent updates that override in + // place instead of writing a global default the override would still shadow. + const defaultSaveTargetValue = + launchAgentScope.overridesGlobalAgent && repoId ? 'repo' : DEFAULT_SAVE_TARGET_VALUE const ensureDetectedAgents = useAppStore((state) => state.ensureDetectedAgents) const ensureRemoteDetectedAgents = useAppStore((state) => state.ensureRemoteDetectedAgents) const [commandTemplate, setCommandTemplate] = useState( @@ -54,7 +66,7 @@ export function useSourceControlAgentActionDialog({ const [detectedOpenCycle, setDetectedOpenCycle] = useState(null) const saveTargets = useMemo(() => buildSourceControlAgentSaveTargets(repoId), [repoId]) const [saveLaunchRecipe, setSaveLaunchRecipe] = useState(true) - const [saveTargetValue, setSaveTargetValue] = useState(DEFAULT_SAVE_TARGET_VALUE) + const [saveTargetValue, setSaveTargetValue] = useState(defaultSaveTargetValue) const disabledAgents = settings?.disabledTuiAgents const connectionUnavailable = Boolean(worktreeId && connectionId === undefined) @@ -94,7 +106,7 @@ export function useSourceControlAgentActionDialog({ setAgentArgs(savedAgentArgs ?? '') setSelectedAgent(savedAgentId ?? null) setSaveLaunchRecipe(true) - setSaveTargetValue(DEFAULT_SAVE_TARGET_VALUE) + setSaveTargetValue(defaultSaveTargetValue) let stale = false void refreshDetectedAgents().then((nextAgents) => { if (stale || openCycleRef.current !== cycle) { @@ -116,6 +128,7 @@ export function useSourceControlAgentActionDialog({ stale = true } }, [ + defaultSaveTargetValue, disabledAgents, open, refreshDetectedAgents, @@ -191,11 +204,11 @@ export function useSourceControlAgentActionDialog({ if (!nextOpen) { resetDeliveryPlan() setSaveLaunchRecipe(true) - setSaveTargetValue(DEFAULT_SAVE_TARGET_VALUE) + setSaveTargetValue(defaultSaveTargetValue) } onOpenChange(nextOpen) }, - [onOpenChange, resetDeliveryPlan] + [defaultSaveTargetValue, onOpenChange, resetDeliveryPlan] ) const { autoLaunchPending } = useSavedSourceControlAgentActionAutoStart({ @@ -263,9 +276,23 @@ export function useSourceControlAgentActionDialog({ [resetDeliveryPlan] ) + const agentScopeNote = useMemo(() => { + if (!launchAgentScope.overridesGlobalAgent) { + return null + } + const catalog = getAgentCatalog() + const labelFor = (agentId: TuiAgent | null): string => + catalog.find((entry) => entry.id === agentId)?.label ?? agentId ?? '' + return { + effectiveAgentLabel: labelFor(launchAgentScope.effectiveAgentId), + globalAgentLabel: labelFor(launchAgentScope.globalAgentId) + } + }, [launchAgentScope]) + return { handleOpenChange, shouldRenderDialog: !autoLaunchPending, + agentScopeNote, agentOptions, selectedAgent, hasEnabledAgents, diff --git a/src/renderer/src/components/settings/RepositorySourceControlAiActionRows.tsx b/src/renderer/src/components/settings/RepositorySourceControlAiActionRows.tsx index a44759ff38f..58a735e04dc 100644 --- a/src/renderer/src/components/settings/RepositorySourceControlAiActionRows.tsx +++ b/src/renderer/src/components/settings/RepositorySourceControlAiActionRows.tsx @@ -13,6 +13,7 @@ import { } from '../../../../shared/source-control-ai-actions' import { Input } from '../ui/input' import { Label } from '../ui/label' +import { Button } from '../ui/button' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select' import { AgentIcon } from '@/lib/agent-catalog' import { SourceControlActionVariableChips } from '../source-control/SourceControlActionVariableChips' @@ -37,9 +38,11 @@ import { resolveAgentArgsPlaceholderAgent } from './repository-source-control-ai-labels' import { hasOwnActionOverride } from './repository-source-control-ai-draft' +import { getRepositorySourceControlAiActionRecipeSectionId } from './repository-settings-targets' import { translate } from '@/i18n/i18n' type RepositorySourceControlAiActionRowsProps = { + repoId: string repoAi: RepoSourceControlAiOverrides source: SourceControlAiSettings defaultTuiAgent: TuiAgent | 'blank' | null | undefined @@ -48,9 +51,14 @@ type RepositorySourceControlAiActionRowsProps = { onActionTemplateChange: (actionId: SourceControlActionId, value: string) => void onActionAgentArgsChange: (actionId: SourceControlActionId, value: string) => void onAppendVariable: (actionId: SourceControlActionId, variable: string) => void + isSaving: boolean + actionDirtyById: Record + onActionDiscard: (actionId: SourceControlActionId) => void + onActionSave: (actionId: SourceControlActionId) => void } export function RepositorySourceControlAiActionRows({ + repoId, repoAi, source, defaultTuiAgent, @@ -58,7 +66,11 @@ export function RepositorySourceControlAiActionRows({ onActionAgentChange, onActionTemplateChange, onActionAgentArgsChange, - onAppendVariable + onAppendVariable, + isSaving, + actionDirtyById, + onActionDiscard, + onActionSave }: RepositorySourceControlAiActionRowsProps): React.JSX.Element { return (
@@ -90,8 +102,17 @@ export function RepositorySourceControlAiActionRows({ const agentOptions = getAgentCatalogForAction(actionId, effectiveAgent) const agentWarningText = getSourceControlActionAgentWarningText(actionId, effectiveAgent) const agentSupportText = getSourceControlActionAgentSupportText(actionId) + const actionDirty = actionDirtyById[actionId] return ( -
+

@@ -227,6 +248,54 @@ export function RepositorySourceControlAiActionRows({ />

+ {hasOverride ? ( +
+

+ {actionDirty + ? translate( + 'auto.components.settings.RepositorySourceControlAiSection.e57dde9d93', + 'Unsaved changes' + ) + : translate( + 'auto.components.settings.RepositorySourceControlAiSection.ccb07dd027', + 'Saved' + )} +

+
+ {actionDirty ? ( + + ) : null} + +
+
+ ) : null}
) })} diff --git a/src/renderer/src/components/settings/RepositorySourceControlAiSection.test.ts b/src/renderer/src/components/settings/RepositorySourceControlAiSection.test.ts index 9715002a997..1153604715e 100644 --- a/src/renderer/src/components/settings/RepositorySourceControlAiSection.test.ts +++ b/src/renderer/src/components/settings/RepositorySourceControlAiSection.test.ts @@ -6,11 +6,16 @@ import type { RepoSourceControlAiOverrides, SourceControlAiSettings } from '../../../../shared/source-control-ai-types' +import { + SOURCE_CONTROL_ACTION_IDS, + type SourceControlActionId +} from '../../../../shared/source-control-ai-actions' import { createRepoAiDraftState, dropRepoLegacyInstructionForAction, resolveRepoAiDraftState } from './RepositorySourceControlAiSection' +import { RepositorySourceControlAiActionRows } from './RepositorySourceControlAiActionRows' import { RepositorySourceControlAiEnablement } from './RepositorySourceControlAiEnablement' describe('RepositorySourceControlAiSection draft state', () => { @@ -98,6 +103,53 @@ describe('RepositorySourceControlAiEnablement', () => { }) }) +describe('RepositorySourceControlAiActionRows', () => { + it('renders save controls on dirty repository action recipes', () => { + const actionDirtyById = Object.fromEntries( + SOURCE_CONTROL_ACTION_IDS.map((actionId) => [actionId, actionId === 'fixCommitFailure']) + ) as Record + const source = { + enabled: true, + agentId: null, + selectedModelByAgent: {}, + selectedThinkingByModel: {}, + customAgentCommand: '', + instructionsByOperation: {} + } as SourceControlAiSettings + + const markup = renderToStaticMarkup( + React.createElement(RepositorySourceControlAiActionRows, { + repoId: 'repo-1', + repoAi: { + actionOverrides: { + fixCommitFailure: { + agentId: null, + commandInputTemplate: '{basePrompt}\n\nrepo' + } + } + }, + source, + defaultTuiAgent: 'codex', + isSaving: false, + actionDirtyById, + onActionModeChange: () => {}, + onActionAgentChange: () => {}, + onActionTemplateChange: () => {}, + onActionAgentArgsChange: () => {}, + onAppendVariable: () => {}, + onActionDiscard: () => {}, + onActionSave: () => {} + }) + ) + + expect(markup).toContain('Commit failure fixes') + expect(markup).toContain('Unsaved changes') + expect(markup).toContain('Discard') + expect(markup).toContain('Save') + expect(markup).toContain('repo-repo-1-source-control-ai-fixCommitFailure') + }) +}) + describe('dropRepoLegacyInstructionForAction', () => { it('prevents legacy text instructions from remigrating after an action override is cleared', () => { const next = dropRepoLegacyInstructionForAction( diff --git a/src/renderer/src/components/settings/RepositorySourceControlAiSection.tsx b/src/renderer/src/components/settings/RepositorySourceControlAiSection.tsx index 6f5f771379c..5568912edd2 100644 --- a/src/renderer/src/components/settings/RepositorySourceControlAiSection.tsx +++ b/src/renderer/src/components/settings/RepositorySourceControlAiSection.tsx @@ -1,16 +1,18 @@ import { useMemo, useState } from 'react' import type React from 'react' -import type { GlobalSettings, Repo, TuiAgent } from '../../../../shared/types' +import type { Repo, TuiAgent } from '../../../../shared/types' import { CUSTOM_AGENT_ID } from '../../../../shared/commit-message-agent-spec' import type { RepoSourceControlAiOverrides } from '../../../../shared/source-control-ai-types' import { normalizeRepoSourceControlAiOverrides, - normalizeSourceControlAiSettings, - resolveSourceControlActionRecipe + normalizeSourceControlAiSettings } from '../../../../shared/source-control-ai' import { toSourceControlAiRepoUpdate } from '../../../../shared/source-control-ai-recipe-save' import type { SourceControlAiRepoUpdate } from '../../../../shared/source-control-ai-recipe-save' -import type { SourceControlActionId } from '../../../../shared/source-control-ai-actions' +import { + SOURCE_CONTROL_ACTION_IDS, + type SourceControlActionId +} from '../../../../shared/source-control-ai-actions' import { Button } from '../ui/button' import { useAppStore } from '../../store' import { useMountedRef } from '@/hooks/useMountedRef' @@ -20,18 +22,21 @@ import { RepositorySourceControlAiCustomCommand } from './RepositorySourceContro import { RepositorySourceControlAiEnablement } from './RepositorySourceControlAiEnablement' import { RepositorySourceControlAiHostedReviewDefaults } from './RepositorySourceControlAiHostedReviewDefaults' import { + buildActionScopedRepoAiSave, createRepoAiDraftState, dropRepoLegacyInstructionForAction, hasOwnActionOverride, normalizeRepoAiDraft, + readCompleteRecipeForDraft, resolveRepoAiDraftState, + serializeActionOverride, serializeRepoAiDraft, + setActionOverride, type RepoAiDraftState } from './repository-source-control-ai-draft' import { ACTION_MODE_INHERIT, DEFAULT_AGENT_VALUE, - completeRepoActionRecipe, readInheritedCommandTemplate } from './repository-source-control-ai-labels' import { getSettingOwnershipSummary } from './setting-ownership' @@ -50,40 +55,6 @@ type RepositorySourceControlAiSectionProps = { type HostedReviewDefaultKey = keyof NonNullable -function readCompleteRecipeForDraft( - current: RepoSourceControlAiOverrides, - settings: GlobalSettings | null, - actionId: SourceControlActionId -): NonNullable< - NonNullable[SourceControlActionId] -> { - const recipe = resolveSourceControlActionRecipe({ - settings, - repo: { sourceControlAi: current }, - actionId - }) - return completeRepoActionRecipe(recipe, actionId) -} - -function setActionOverride( - current: RepoSourceControlAiOverrides, - actionId: SourceControlActionId, - recipe: NonNullable< - NonNullable[SourceControlActionId] - > -): RepoSourceControlAiOverrides { - return dropRepoLegacyInstructionForAction( - { - ...current, - actionOverrides: { - ...current.actionOverrides, - [actionId]: recipe - } - }, - actionId - ) -} - export function RepositorySourceControlAiSection({ repo, updateRepo @@ -130,6 +101,17 @@ export function RepositorySourceControlAiSection({ const draftSerialized = useMemo(() => serializeRepoAiDraft(repoAi), [repoAi]) const isDirty = resolvedDraftState.repoId !== repo.id || draftSerialized !== resolvedDraftState.baseSerialized + const actionDirtyById = useMemo( + () => + Object.fromEntries( + SOURCE_CONTROL_ACTION_IDS.map((actionId) => [ + actionId, + serializeActionOverride(repoAi, actionId) !== + serializeActionOverride(persistedRepoAi, actionId) + ]) + ) as Record, + [persistedRepoAi, repoAi] + ) const updateDraftRepoAi = ( update: (current: RepoSourceControlAiOverrides) => RepoSourceControlAiOverrides @@ -198,6 +180,50 @@ export function RepositorySourceControlAiSection({ setSaveError(null) } + const discardActionDraft = (actionId: SourceControlActionId): void => { + updateDraftRepoAi((current) => { + const nextActionOverrides = { ...current.actionOverrides } + if (hasOwnActionOverride(persistedRepoAi.actionOverrides, actionId)) { + nextActionOverrides[actionId] = persistedRepoAi.actionOverrides?.[actionId] + } else { + delete nextActionOverrides[actionId] + } + return dropRepoLegacyInstructionForAction( + { ...current, actionOverrides: nextActionOverrides }, + actionId + ) + }) + } + + const saveActionDraft = async (actionId: SourceControlActionId): Promise => { + if (isSaving || !actionDirtyById[actionId]) { + return + } + // Why: a per-action Save persists only that action's override (its Discard is + // action-scoped, so its Save must be too) — never other rows' pending edits. + const next = buildActionScopedRepoAiSave(persistedRepoAi, repoAi, actionId) + const repoUpdate = toSourceControlAiRepoUpdate(next) + setIsSaving(true) + setSaveError(null) + try { + const result = await updateRepo(repo.id, repoUpdate) + if (!mountedRef.current) { + return + } + if (result === false) { + setSaveError('Failed to save Source Control AI settings.') + } + } catch { + if (mountedRef.current) { + setSaveError('Failed to save Source Control AI settings.') + } + } finally { + if (mountedRef.current) { + setIsSaving(false) + } + } + } + const updateEnablement = (value: boolean | undefined): void => { updateDraftRepoAi((current) => ({ ...current, enabled: value })) } @@ -364,14 +390,19 @@ export function RepositorySourceControlAiSection({ onChange={updateCustomCommand} /> void saveActionDraft(actionId)} /> void onTemplateChange: (actionId: SourceControlActionId, value: string) => void onAgentArgsChange: (actionId: SourceControlActionId, value: string) => void @@ -57,6 +58,7 @@ export function SourceControlActionRecipeRow({ baseValue, defaultTuiAgent, isSavingTemplate, + repoOverrideNote, onAgentChange, onTemplateChange, onAgentArgsChange, @@ -163,50 +165,53 @@ export function SourceControlActionRecipeRow({ />
-
-

- {templateDirty - ? translate( - 'auto.components.settings.SourceControlAiActionRecipeDefaults.817128d94e', - 'Unsaved changes' - ) - : translate( - 'auto.components.settings.SourceControlAiActionRecipeDefaults.9d3cc627f8', - 'Saved' - )} -

-
- {templateDirty ? ( - - ) : null} - +

+
+ {templateDirty ? ( + + ) : null} + +
diff --git a/src/renderer/src/components/settings/SourceControlActionRepoOverrideNote.test.tsx b/src/renderer/src/components/settings/SourceControlActionRepoOverrideNote.test.tsx new file mode 100644 index 00000000000..095cfc4b550 --- /dev/null +++ b/src/renderer/src/components/settings/SourceControlActionRepoOverrideNote.test.tsx @@ -0,0 +1,106 @@ +// @vitest-environment happy-dom + +import { act } from 'react' +import { createRoot, type Root } from 'react-dom/client' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { TooltipProvider } from '../ui/tooltip' +import { SourceControlActionRepoOverrideNote } from './SourceControlActionRepoOverrideNote' +import type { SourceControlActionRecipeOverrideSummary } from '@/lib/source-control-launch-agent-selection' + +let container: HTMLDivElement +let root: Root + +beforeEach(() => { + container = document.createElement('div') + document.body.appendChild(container) + root = createRoot(container) +}) + +afterEach(() => { + act(() => root.unmount()) + container.remove() +}) + +function renderNote( + summary: SourceControlActionRecipeOverrideSummary, + onReviewRepo = vi.fn() +): void { + act(() => { + root.render( + + + + ) + }) +} + +describe('SourceControlActionRepoOverrideNote', () => { + it('renders nothing when no repo overrides the action', () => { + renderNote({ count: 0, overrides: [] }) + expect(container.textContent).toBe('') + }) + + it('shows the singular count when one repo overrides the action', () => { + renderNote({ + count: 1, + overrides: [{ repoId: 'r1', repoName: 'App', fields: ['commandTemplate'] }] + }) + expect(container.textContent).toContain("Global saves won't change 1 repository") + expect(container.textContent).toContain('Review') + }) + + it('shows the plural count when several repos override the action', () => { + renderNote({ + count: 2, + overrides: [ + { repoId: 'r1', repoName: 'App', fields: ['agent'] }, + { repoId: 'r2', repoName: 'Web', fields: ['agentArgs'] } + ] + }) + expect(container.textContent).toContain("Global saves won't change 2 repositories") + expect(container.textContent).toContain('Review first') + }) + + it('shows a "+N more" note when overrides exceed the visible limit', async () => { + renderNote({ + count: 6, + overrides: Array.from({ length: 6 }, (_, index) => ({ + repoId: `r${index}`, + repoName: `Repo ${index}`, + fields: ['commandTemplate'] + })) + }) + + // The overflow note lives in the tooltip content, only mounted once Radix + // opens the tooltip — drive its pointer-move open path and flush the timer. + const trigger = container.querySelector('[data-slot="tooltip-trigger"]') + await act(async () => { + trigger?.dispatchEvent( + new PointerEvent('pointermove', { bubbles: true, pointerType: 'mouse' }) + ) + await new Promise((resolve) => setTimeout(resolve, 0)) + }) + + expect(document.body.textContent).toContain('+1 more') + }) + + it('opens the first repo override from the review action', () => { + const onReviewRepo = vi.fn() + renderNote( + { + count: 2, + overrides: [ + { repoId: 'r1', repoName: 'App', fields: ['agent'] }, + { repoId: 'r2', repoName: 'Web', fields: ['agentArgs'] } + ] + }, + onReviewRepo + ) + + act(() => { + container.querySelector('button')?.click() + }) + + expect(onReviewRepo).toHaveBeenCalledWith('r1') + }) +}) diff --git a/src/renderer/src/components/settings/SourceControlActionRepoOverrideNote.tsx b/src/renderer/src/components/settings/SourceControlActionRepoOverrideNote.tsx new file mode 100644 index 00000000000..ec389927e42 --- /dev/null +++ b/src/renderer/src/components/settings/SourceControlActionRepoOverrideNote.tsx @@ -0,0 +1,135 @@ +import type React from 'react' +import { Info } from 'lucide-react' +import { Button } from '../ui/button' +import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip' +import type { + SourceControlActionRecipeOverrideField, + SourceControlActionRecipeOverrideSummary +} from '@/lib/source-control-launch-agent-selection' +import { translate } from '@/i18n/i18n' + +type SourceControlActionRepoOverrideNoteProps = { + summary: SourceControlActionRecipeOverrideSummary + onReviewRepo: (repoId: string) => void +} + +const MAX_VISIBLE_REPOS = 5 + +function getRecipeOverrideFieldLabel(field: SourceControlActionRecipeOverrideField): string { + switch (field) { + case 'agent': + return translate( + 'auto.components.settings.SourceControlActionRepoOverrideNote.agent', + 'Agent' + ) + case 'agentArgs': + return translate( + 'auto.components.settings.SourceControlActionRepoOverrideNote.agentArgs', + 'CLI arguments' + ) + case 'commandTemplate': + return translate( + 'auto.components.settings.SourceControlActionRepoOverrideNote.commandTemplate', + 'Command template' + ) + default: { + // Fail at compile time if the override-field union grows a new variant. + const _exhaustive: never = field + return _exhaustive + } + } +} + +function getRecipeOverrideFieldSummary(fields: SourceControlActionRecipeOverrideField[]): string { + if (fields.length === 0) { + return translate( + 'auto.components.settings.SourceControlActionRepoOverrideNote.recipe', + 'Recipe' + ) + } + return fields.map(getRecipeOverrideFieldLabel).join(', ') +} + +export function SourceControlActionRepoOverrideNote({ + summary, + onReviewRepo +}: SourceControlActionRepoOverrideNoteProps): React.JSX.Element | null { + if (summary.count === 0) { + return null + } + const firstOverride = summary.overrides[0] + if (!firstOverride) { + return null + } + const visibleOverrides = summary.overrides.slice(0, MAX_VISIBLE_REPOS) + const hiddenOverrideCount = Math.max(0, summary.overrides.length - visibleOverrides.length) + const noteText = + summary.count === 1 + ? translate( + 'auto.components.settings.SourceControlActionRepoOverrideNote.singular', + "Global saves won't change 1 repository with its own recipe." + ) + : translate( + 'auto.components.settings.SourceControlActionRepoOverrideNote.plural', + "Global saves won't change {{count}} repositories with their own recipes.", + { count: summary.count } + ) + return ( +
+ + + + + {noteText} + + + +
+

+ {translate( + 'auto.components.settings.SourceControlActionRepoOverrideNote.tooltipTitle', + 'Repository overrides' + )} +

+
    + {visibleOverrides.map((override) => ( +
  • +
    {override.repoName}
    +
    + {getRecipeOverrideFieldSummary(override.fields)} +
    +
  • + ))} +
+ {hiddenOverrideCount > 0 ? ( +

+ {translate( + 'auto.components.settings.SourceControlActionRepoOverrideNote.more', + '+{{count}} more', + { count: hiddenOverrideCount } + )} +

+ ) : null} +
+
+
+ +
+ ) +} diff --git a/src/renderer/src/components/settings/SourceControlAiActionRecipeDefaults.tsx b/src/renderer/src/components/settings/SourceControlAiActionRecipeDefaults.tsx index c54390e2a8d..56906be96d3 100644 --- a/src/renderer/src/components/settings/SourceControlAiActionRecipeDefaults.tsx +++ b/src/renderer/src/components/settings/SourceControlAiActionRecipeDefaults.tsx @@ -1,6 +1,8 @@ +import { useMemo } from 'react' import type React from 'react' import { toast } from 'sonner' import type { GlobalSettings, TuiAgent } from '../../../../shared/types' +import { isFolderRepo } from '../../../../shared/repo-kind' import type { SourceControlAiSettings, SourceControlAiSettingsPatch @@ -13,9 +15,17 @@ import { type SourceControlActionId } from '../../../../shared/source-control-ai-actions' import { Label } from '../ui/label' +import { useAppStore } from '@/store' +import { useRepos } from '@/store/selectors' +import { + summarizeReposOverridingActionRecipe, + type SourceControlActionRecipeOverrideSummary +} from '@/lib/source-control-launch-agent-selection' import { SearchableSetting } from './SearchableSetting' +import { getRepositorySourceControlAiActionRecipeSectionId } from './repository-settings-targets' import { matchesSettingsSearch } from './settings-search' import { SourceControlActionRecipeRow } from './SourceControlActionRecipeRow' +import { SourceControlActionRepoOverrideNote } from './SourceControlActionRepoOverrideNote' import { useSourceControlActionRecipeDraftState } from './source-control-action-recipe-draft-state' import { translate } from '@/i18n/i18n' @@ -84,6 +94,22 @@ export function SourceControlAiActionRecipeDefaults({ searchQuery, writeConfig }: SourceControlAiActionRecipeDefaultsProps): React.JSX.Element | null { + const allRepos = useRepos() + // Recomputes on every keystroke in the sibling recipe textareas otherwise, so + // memoize the filter and the O(actions × repos) override scan on `repos`. + const repos = useMemo(() => allRepos.filter((repo) => !isFolderRepo(repo)), [allRepos]) + const overrideSummaries = useMemo( + () => + Object.fromEntries( + SOURCE_CONTROL_ACTION_IDS.map((actionId) => [ + actionId, + summarizeReposOverridingActionRecipe({ repos, actionId }) + ]) + ) as Record, + [repos] + ) + const openSettingsPage = useAppStore((state) => state.openSettingsPage) + const openSettingsTarget = useAppStore((state) => state.openSettingsTarget) const { actionRecipeDraftState, savingActionTemplateIds, @@ -134,6 +160,18 @@ export function SourceControlAiActionRecipeDefaults({ } } + const openRepoSourceControlAiSettings = ( + repoId: string, + actionId: SourceControlActionId + ): void => { + openSettingsTarget({ + pane: 'repo', + repoId, + sectionId: getRepositorySourceControlAiActionRecipeSectionId(repoId, actionId) + }) + openSettingsPage() + } + if (!config.enabled || !matchesSettingsSearch(searchQuery, ACTION_RECIPES_SEARCH_ENTRY)) { return null } @@ -158,6 +196,7 @@ export function SourceControlAiActionRecipeDefaults({ {SOURCE_CONTROL_ACTION_IDS.map((actionId) => { const recipe = config.actions?.[actionId] const selectedAgent = recipe?.agentId ?? null + const overrideSummary = overrideSummaries[actionId] return ( openRepoSourceControlAiSettings(repoId, actionId)} + /> + } onAgentChange={(id, value) => void onActionAgentChange(id, value)} onTemplateChange={onActionTemplateChange} onAgentArgsChange={onActionAgentArgsChange} diff --git a/src/renderer/src/components/settings/repository-settings-targets.ts b/src/renderer/src/components/settings/repository-settings-targets.ts index 2ab4fd8d5d5..32bcd1f4510 100644 --- a/src/renderer/src/components/settings/repository-settings-targets.ts +++ b/src/renderer/src/components/settings/repository-settings-targets.ts @@ -1,3 +1,5 @@ +import type { SourceControlActionId } from '../../../../shared/source-control-ai-actions' + export function getRepositoryLocalCommandsSectionId(repoId: string): string { return `repo-${repoId}-local-commands` } @@ -9,3 +11,10 @@ export function getRepositoryIconSectionId(repoId: string): string { export function getRepositorySourceControlAiSectionId(repoId: string): string { return `repo-${repoId}-source-control-ai` } + +export function getRepositorySourceControlAiActionRecipeSectionId( + repoId: string, + actionId: SourceControlActionId +): string { + return `repo-${repoId}-source-control-ai-${actionId}` +} diff --git a/src/renderer/src/components/settings/repository-source-control-ai-draft.test.ts b/src/renderer/src/components/settings/repository-source-control-ai-draft.test.ts new file mode 100644 index 00000000000..1972afde88b --- /dev/null +++ b/src/renderer/src/components/settings/repository-source-control-ai-draft.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, it } from 'vitest' +import type { RepoSourceControlAiOverrides } from '../../../../shared/source-control-ai-types' +import { buildActionScopedRepoAiSave } from './repository-source-control-ai-draft' + +describe('buildActionScopedRepoAiSave', () => { + it('persists only the target action, keeping siblings at their saved values', () => { + const persisted: RepoSourceControlAiOverrides = { + actionOverrides: { + resolveConflicts: { + agentId: 'claude', + commandInputTemplate: '{basePrompt}', + agentArgs: '' + }, + fixCommitFailure: { agentId: 'codex', commandInputTemplate: '{basePrompt}', agentArgs: '' } + } + } + const draft: RepoSourceControlAiOverrides = { + actionOverrides: { + resolveConflicts: { + agentId: 'codex', + commandInputTemplate: '{basePrompt}\n\nedited', + agentArgs: '' + }, + // A half-finished edit on a different row that must NOT be flushed. + fixCommitFailure: { + agentId: 'gemini', + commandInputTemplate: '{basePrompt}\n\nunsaved', + agentArgs: '' + } + } + } + + const next = buildActionScopedRepoAiSave(persisted, draft, 'resolveConflicts') + + expect(next.actionOverrides?.resolveConflicts?.agentId).toBe('codex') + expect(next.actionOverrides?.resolveConflicts?.commandInputTemplate).toBe( + '{basePrompt}\n\nedited' + ) + // Sibling keeps the persisted value, not the draft's pending edit. + expect(next.actionOverrides?.fixCommitFailure?.agentId).toBe('codex') + expect(next.actionOverrides?.fixCommitFailure?.commandInputTemplate).toBe('{basePrompt}') + }) + + it('removes the action override when the draft inherits it', () => { + const persisted: RepoSourceControlAiOverrides = { + actionOverrides: { + resolveConflicts: { agentId: 'claude', commandInputTemplate: '{basePrompt}', agentArgs: '' } + } + } + const draft: RepoSourceControlAiOverrides = { actionOverrides: {} } + + const next = buildActionScopedRepoAiSave(persisted, draft, 'resolveConflicts') + + expect(next.actionOverrides?.resolveConflicts).toBeUndefined() + }) +}) diff --git a/src/renderer/src/components/settings/repository-source-control-ai-draft.ts b/src/renderer/src/components/settings/repository-source-control-ai-draft.ts index 8eb2875a477..a085821f23f 100644 --- a/src/renderer/src/components/settings/repository-source-control-ai-draft.ts +++ b/src/renderer/src/components/settings/repository-source-control-ai-draft.ts @@ -1,8 +1,17 @@ -import { normalizeRepoSourceControlAiOverrides } from '../../../../shared/source-control-ai' +import { + normalizeRepoSourceControlAiOverrides, + resolveSourceControlActionRecipe +} from '../../../../shared/source-control-ai' import type { SourceControlActionId } from '../../../../shared/source-control-ai-actions' import type { RepoSourceControlAiOverrides } from '../../../../shared/source-control-ai-types' +import type { GlobalSettings } from '../../../../shared/types' +import { completeRepoActionRecipe } from './repository-source-control-ai-labels' import { SOURCE_CONTROL_TEXT_ACTION_ID_SET } from './source-control-action-recipe-options' +type RepoActionRecipe = NonNullable< + NonNullable[SourceControlActionId] +> + export type RepoAiDraftState = { repoId: string value: RepoSourceControlAiOverrides @@ -93,3 +102,66 @@ export function dropRepoLegacyInstructionForAction( Object.keys(instructionsByOperation).length > 0 ? instructionsByOperation : undefined } } + +export function readCompleteRecipeForDraft( + current: RepoSourceControlAiOverrides, + settings: GlobalSettings | null, + actionId: SourceControlActionId +): RepoActionRecipe { + const recipe = resolveSourceControlActionRecipe({ + settings, + repo: { sourceControlAi: current }, + actionId + }) + return completeRepoActionRecipe(recipe, actionId) +} + +export function setActionOverride( + current: RepoSourceControlAiOverrides, + actionId: SourceControlActionId, + recipe: RepoActionRecipe +): RepoSourceControlAiOverrides { + return dropRepoLegacyInstructionForAction( + { + ...current, + actionOverrides: { + ...current.actionOverrides, + [actionId]: recipe + } + }, + actionId + ) +} + +export function serializeActionOverride( + value: RepoSourceControlAiOverrides, + actionId: SourceControlActionId +): string { + return JSON.stringify({ + hasOverride: hasOwnActionOverride(value.actionOverrides, actionId), + recipe: value.actionOverrides?.[actionId] ?? null + }) +} + +/** + * Layer only one action's draft override onto the last-saved repo settings, so a + * per-action save persists that recipe without flushing other rows' edits. + */ +export function buildActionScopedRepoAiSave( + persisted: RepoSourceControlAiOverrides, + draft: RepoSourceControlAiOverrides, + actionId: SourceControlActionId +): RepoSourceControlAiOverrides { + const nextActionOverrides = { ...persisted.actionOverrides } + if (hasOwnActionOverride(draft.actionOverrides, actionId)) { + nextActionOverrides[actionId] = draft.actionOverrides?.[actionId] + } else { + delete nextActionOverrides[actionId] + } + return normalizeRepoAiDraft( + dropRepoLegacyInstructionForAction( + { ...persisted, actionOverrides: nextActionOverrides }, + actionId + ) + ) +} diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index 6a2d2f906fc..ea81c2c094a 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -8710,6 +8710,18 @@ "ephemeralVmsExperimentalSetting": { "description": "Shows setup controls and workspace run targets for repo-owned, on-demand environments.", "toggleLabel": "Toggle per-workspace environments" + }, + "SourceControlActionRepoOverrideNote": { + "agent": "Agent", + "agentArgs": "CLI arguments", + "commandTemplate": "Command template", + "more": "+{{count}} more", + "plural": "Global saves won't change {{count}} repositories with their own recipes.", + "recipe": "Recipe", + "review": "Review", + "reviewFirst": "Review first", + "singular": "Global saves won't change 1 repository with its own recipe.", + "tooltipTitle": "Repository overrides" } }, "right": { @@ -9207,7 +9219,8 @@ "c7ff8cef11": "Detecting agents...", "b0da3a4d3e": "Launch recipe already saved", "bff4795a6d": "Change the agent, arguments, or prompt template to update the saved recipe.", - "5c75b24735": "Customize what the agent receives before Orca starts it." + "5c75b24735": "Customize what the agent receives before Orca starts it.", + "repoAgentOverrideNote": "This repository overrides your global default ({{global}}) and currently runs {{effective}}. Save to this repository to change what runs here." }, "SourceControlTextGenerationDialog": { "c5b7fa7cb6": "Save as global default", diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json index b1a8371f613..97cbdff6f63 100644 --- a/src/renderer/src/i18n/locales/es.json +++ b/src/renderer/src/i18n/locales/es.json @@ -8710,6 +8710,18 @@ "ephemeralVmsExperimentalSetting": { "description": "Muestra controles de configuración y destinos de ejecución de espacios de trabajo para entornos bajo demanda del repositorio.", "toggleLabel": "Activar o desactivar entornos por espacio de trabajo" + }, + "SourceControlActionRepoOverrideNote": { + "agent": "Agente", + "agentArgs": "Argumentos de la CLI", + "commandTemplate": "Plantilla del comando", + "more": "+{{count}} más", + "plural": "Los guardados globales no cambiarán {{count}} repositorios con sus propias recetas.", + "recipe": "Receta", + "review": "Revisar", + "reviewFirst": "Revisar primero", + "singular": "Los guardados globales no cambiarán 1 repositorio con su propia receta.", + "tooltipTitle": "Anulaciones del repositorio" } }, "right": { @@ -9207,7 +9219,8 @@ "c7ff8cef11": "Detectando agentes...", "b0da3a4d3e": "La receta de lanzamiento ya está guardada", "bff4795a6d": "Cambia el agente, los argumentos o la plantilla del prompt para actualizar la receta guardada.", - "5c75b24735": "Personaliza lo que recibe el agente antes de que Orca lo inicie." + "5c75b24735": "Personaliza lo que recibe el agente antes de que Orca lo inicie.", + "repoAgentOverrideNote": "Este repositorio anula tu valor predeterminado global ({{global}}) y actualmente ejecuta {{effective}}. Guarda en este repositorio para cambiar lo que se ejecuta aquí." }, "SourceControlTextGenerationDialog": { "c5b7fa7cb6": "Guardar como valor predeterminado global", diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index d9e157b3635..590f6d505cf 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -8710,6 +8710,18 @@ "ephemeralVmsExperimentalSetting": { "description": "repo が所有するオンデマンド環境のセットアップ コントロールとワークスペース実行ターゲットを示します。", "toggleLabel": "ワークスペースごとの環境を切り替える" + }, + "SourceControlActionRepoOverrideNote": { + "agent": "Agent", + "agentArgs": "CLI arguments", + "commandTemplate": "Command template", + "more": "+{{count}} more", + "plural": "Global saves won't change {{count}} repositories with their own recipes.", + "recipe": "Recipe", + "review": "Review", + "reviewFirst": "Review first", + "singular": "Global saves won't change 1 repository with its own recipe.", + "tooltipTitle": "Repository overrides" } }, "right": { @@ -9207,7 +9219,8 @@ "ea4788705e": "キャンセル", "b0da3a4d3e": "起動レシピはすでに保存されています", "bff4795a6d": "保存済みのレシピを更新するには、agent、引数、またはプロンプトテンプレートを変更してください。", - "5c75b24735": "Orca が起動する前に agent が受け取る内容をカスタマイズします。" + "5c75b24735": "Orca が起動する前に agent が受け取る内容をカスタマイズします。", + "repoAgentOverrideNote": "This repository overrides your global default ({{global}}) and currently runs {{effective}}. Save to this repository to change what runs here." }, "SourceControlTextGenerationDialog": { "c5b7fa7cb6": "グローバルデフォルトとして保存", diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index 14aa226e98d..8ae5f66134f 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -8710,6 +8710,18 @@ "ephemeralVmsExperimentalSetting": { "description": "repo 소유 온디맨드 환경에 대한 설정 제어 및 워크스페이스 실행 대상을 표시합니다.", "toggleLabel": "워크스페이스별 환경 전환" + }, + "SourceControlActionRepoOverrideNote": { + "agent": "Agent", + "agentArgs": "CLI arguments", + "commandTemplate": "Command template", + "more": "+{{count}} more", + "plural": "Global saves won't change {{count}} repositories with their own recipes.", + "recipe": "Recipe", + "review": "Review", + "reviewFirst": "Review first", + "singular": "Global saves won't change 1 repository with its own recipe.", + "tooltipTitle": "Repository overrides" } }, "right": { @@ -9207,7 +9219,8 @@ "ea4788705e": "취소", "b0da3a4d3e": "실행 레시피가 이미 저장되었습니다", "bff4795a6d": "저장된 레시피를 업데이트하려면 agent, 인수 또는 프롬프트 템플릿을 변경하세요.", - "5c75b24735": "Orca가 시작하기 전에 agent가 받을 내용을 사용자 지정합니다." + "5c75b24735": "Orca가 시작하기 전에 agent가 받을 내용을 사용자 지정합니다.", + "repoAgentOverrideNote": "This repository overrides your global default ({{global}}) and currently runs {{effective}}. Save to this repository to change what runs here." }, "SourceControlTextGenerationDialog": { "c5b7fa7cb6": "전역 기본값으로 저장", diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index 258a5cdd31e..4166c860372 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -8710,6 +8710,18 @@ "ephemeralVmsExperimentalSetting": { "description": "显示仓库自带按需环境的设置控件和工作区运行目标。", "toggleLabel": "切换工作区专属环境" + }, + "SourceControlActionRepoOverrideNote": { + "agent": "Agent", + "agentArgs": "CLI arguments", + "commandTemplate": "Command template", + "more": "+{{count}} more", + "plural": "Global saves won't change {{count}} repositories with their own recipes.", + "recipe": "Recipe", + "review": "Review", + "reviewFirst": "Review first", + "singular": "Global saves won't change 1 repository with its own recipe.", + "tooltipTitle": "Repository overrides" } }, "right": { @@ -9207,7 +9219,8 @@ "ea4788705e": "取消", "b0da3a4d3e": "启动预设已保存", "bff4795a6d": "更改智能体、参数或提示词模板以更新已保存的预设。", - "5c75b24735": "自定义 Orca 启动智能体前发送给它的内容。" + "5c75b24735": "自定义 Orca 启动智能体前发送给它的内容。", + "repoAgentOverrideNote": "此仓库会覆盖你的全局默认值({{global}}),当前运行 {{effective}}。保存到此仓库以更改这里运行的内容。" }, "SourceControlTextGenerationDialog": { "c5b7fa7cb6": "保存为全局默认值", diff --git a/src/renderer/src/lib/source-control-launch-agent-selection.test.ts b/src/renderer/src/lib/source-control-launch-agent-selection.test.ts new file mode 100644 index 00000000000..63a053f0186 --- /dev/null +++ b/src/renderer/src/lib/source-control-launch-agent-selection.test.ts @@ -0,0 +1,148 @@ +import { describe, expect, it } from 'vitest' +import { getDefaultSettings } from '../../../shared/constants' +import type { GlobalSettings, Repo, TuiAgent } from '../../../shared/types' +import { + resolveSourceControlLaunchAgentScope, + summarizeReposOverridingActionRecipe +} from './source-control-launch-agent-selection' + +function settingsWithGlobalResolveAgent(agentId: TuiAgent): GlobalSettings { + const base = getDefaultSettings('/tmp') + return { + ...base, + defaultTuiAgent: agentId, + sourceControlAi: { + ...base.sourceControlAi!, + enabled: true, + actions: { + ...base.sourceControlAi!.actions, + resolveConflicts: { agentId, commandInputTemplate: '{basePrompt}', agentArgs: '' } + } + } + } +} + +function repoWithResolveAgent(agentId: TuiAgent): Pick { + return { + sourceControlAi: { + actionOverrides: { + resolveConflicts: { agentId, commandInputTemplate: '{basePrompt}', agentArgs: '' } + } + } + } +} + +describe('resolveSourceControlLaunchAgentScope', () => { + it('flags a repo override that pins a different agent than the global default', () => { + const scope = resolveSourceControlLaunchAgentScope({ + settings: settingsWithGlobalResolveAgent('claude'), + repo: repoWithResolveAgent('codex'), + actionId: 'resolveConflicts' + }) + expect(scope).toEqual({ + effectiveAgentId: 'codex', + globalAgentId: 'claude', + overridesGlobalAgent: true + }) + }) + + it('does not flag a repo override that matches the global agent', () => { + const scope = resolveSourceControlLaunchAgentScope({ + settings: settingsWithGlobalResolveAgent('claude'), + repo: repoWithResolveAgent('claude'), + actionId: 'resolveConflicts' + }) + expect(scope.overridesGlobalAgent).toBe(false) + }) + + it('does not flag when there is no repo override', () => { + const scope = resolveSourceControlLaunchAgentScope({ + settings: settingsWithGlobalResolveAgent('claude'), + repo: null, + actionId: 'resolveConflicts' + }) + expect(scope).toEqual({ + effectiveAgentId: 'claude', + globalAgentId: 'claude', + overridesGlobalAgent: false + }) + }) + + it('falls back to the default agent when no global recipe agent is set', () => { + const base = getDefaultSettings('/tmp') + const settings: GlobalSettings = { + ...base, + defaultTuiAgent: 'claude', + sourceControlAi: { ...base.sourceControlAi!, enabled: true } + } + const scope = resolveSourceControlLaunchAgentScope({ + settings, + repo: repoWithResolveAgent('codex'), + actionId: 'resolveConflicts' + }) + expect(scope.globalAgentId).toBe('claude') + expect(scope.overridesGlobalAgent).toBe(true) + }) +}) + +function repoOverriding( + id: string, + displayName: string, + agentId: TuiAgent +): Pick { + return { id, displayName, ...repoWithResolveAgent(agentId) } +} + +describe('summarizeReposOverridingActionRecipe', () => { + it('lists repos with any action recipe override', () => { + const summary = summarizeReposOverridingActionRecipe({ + repos: [ + repoOverriding('repo-1', 'App', 'claude'), + { + id: 'repo-2', + displayName: 'Web', + sourceControlAi: { + actionOverrides: { + resolveConflicts: { + commandInputTemplate: '{basePrompt}\n\nUse project conflict rules.', + agentArgs: '--model gpt-5.4-mini' + } + } + } + }, + { id: 'repo-3', displayName: 'Docs' } + ], + actionId: 'resolveConflicts' + }) + + expect(summary).toEqual({ + count: 2, + overrides: [ + { repoId: 'repo-1', repoName: 'App', fields: ['agent', 'commandTemplate', 'agentArgs'] }, + { repoId: 'repo-2', repoName: 'Web', fields: ['commandTemplate', 'agentArgs'] } + ] + }) + }) + + it('includes legacy repo instructions as command template overrides', () => { + const summary = summarizeReposOverridingActionRecipe({ + repos: [ + { + id: 'repo-1', + displayName: 'App', + sourceControlAi: { + instructionsByOperation: { + commitMessage: 'Use project style.' + } + } + } + ], + actionId: 'commitMessage' + }) + + expect(summary).toEqual({ + count: 1, + overrides: [{ repoId: 'repo-1', repoName: 'App', fields: ['commandTemplate'] }] + }) + }) +}) diff --git a/src/renderer/src/lib/source-control-launch-agent-selection.ts b/src/renderer/src/lib/source-control-launch-agent-selection.ts index 38660a74ce5..e8c00812590 100644 --- a/src/renderer/src/lib/source-control-launch-agent-selection.ts +++ b/src/renderer/src/lib/source-control-launch-agent-selection.ts @@ -1,8 +1,15 @@ import { getAgentCatalog } from '@/lib/agent-catalog' import { isCustomAgentId } from '../../../shared/commit-message-agent-spec' -import type { SourceControlActionRecipe } from '../../../shared/source-control-ai-actions' +import { + normalizeRepoSourceControlAiOverrides, + resolveSourceControlActionRecipe +} from '../../../shared/source-control-ai' +import type { + SourceControlActionId, + SourceControlActionRecipe +} from '../../../shared/source-control-ai-actions' import { filterEnabledTuiAgents } from '../../../shared/tui-agent-selection' -import type { TuiAgent } from '../../../shared/types' +import type { GlobalSettings, Repo, TuiAgent } from '../../../shared/types' export function readSourceControlLaunchRecipeAgentId( recipe: Pick | null | undefined @@ -30,3 +37,122 @@ export function pickSourceControlLaunchAgent(args: { } return getAgentCatalog().find((entry) => enabledAgents.includes(entry.id))?.id ?? null } + +export type SourceControlLaunchAgentScope = { + /** Agent the button would launch, after applying any repo override. */ + effectiveAgentId: TuiAgent | null + /** Agent the button would launch without a repo override (global recipe or default). */ + globalAgentId: TuiAgent | null + /** + * True when this repo pins a different launch agent than the global default — + * the silent-override case we surface instead of auto-launching blindly. + */ + overridesGlobalAgent: boolean +} + +export function resolveSourceControlLaunchAgentScope(input: { + settings: + | Pick + | null + | undefined + repo?: Pick | null + actionId: SourceControlActionId +}): SourceControlLaunchAgentScope { + const effectiveAgentId = readSourceControlLaunchRecipeAgentId( + resolveSourceControlActionRecipe({ + settings: input.settings, + repo: input.repo, + actionId: input.actionId + }) + ) + const globalRecipeAgentId = readSourceControlLaunchRecipeAgentId( + resolveSourceControlActionRecipe({ + settings: input.settings, + repo: null, + actionId: input.actionId + }) + ) + // Why: the note compares against what would run with no override, so fall back + // to the global default agent when no global recipe agent is set. + const defaultTuiAgent = input.settings?.defaultTuiAgent + const globalAgentId = + globalRecipeAgentId ?? (defaultTuiAgent && defaultTuiAgent !== 'blank' ? defaultTuiAgent : null) + const hasRepoAgentOverride = + normalizeRepoSourceControlAiOverrides(input.repo?.sourceControlAi)?.actionOverrides?.[ + input.actionId + ]?.agentId !== undefined + return { + effectiveAgentId, + globalAgentId, + overridesGlobalAgent: + hasRepoAgentOverride && effectiveAgentId !== null && effectiveAgentId !== globalAgentId + } +} + +export type SourceControlActionRecipeOverrideField = 'agent' | 'commandTemplate' | 'agentArgs' + +export type SourceControlActionRecipeOverride = { + repoId: string + repoName: string + fields: SourceControlActionRecipeOverrideField[] +} + +export type SourceControlActionRecipeOverrideSummary = { + count: number + overrides: SourceControlActionRecipeOverride[] +} + +type NormalizedRepoSourceControlAiOverrides = NonNullable< + ReturnType +> + +function hasActionOverride( + overrides: NormalizedRepoSourceControlAiOverrides['actionOverrides'] | undefined, + actionId: SourceControlActionId +): boolean { + return Object.prototype.hasOwnProperty.call(overrides ?? {}, actionId) +} + +function readRecipeOverrideFields( + recipe: + | NonNullable[SourceControlActionId] + | null + | undefined +): SourceControlActionRecipeOverrideField[] { + const fields: SourceControlActionRecipeOverrideField[] = [] + if (Object.prototype.hasOwnProperty.call(recipe ?? {}, 'agentId')) { + fields.push('agent') + } + if (Object.prototype.hasOwnProperty.call(recipe ?? {}, 'commandInputTemplate')) { + fields.push('commandTemplate') + } + if (Object.prototype.hasOwnProperty.call(recipe ?? {}, 'agentArgs')) { + fields.push('agentArgs') + } + return fields +} + +/** + * Which repos own any recipe setting for an action — lets the global recipe + * editor warn that saving globally will not update those repo-specific recipes. + */ +export function summarizeReposOverridingActionRecipe(input: { + repos: readonly Pick[] + actionId: SourceControlActionId +}): SourceControlActionRecipeOverrideSummary { + const overrides: SourceControlActionRecipeOverride[] = [] + for (const repo of input.repos) { + const actionOverrides = normalizeRepoSourceControlAiOverrides( + repo.sourceControlAi + )?.actionOverrides + if (!hasActionOverride(actionOverrides, input.actionId)) { + continue + } + overrides.push({ + repoId: repo.id, + repoName: repo.displayName, + fields: readRecipeOverrideFields(actionOverrides?.[input.actionId]) + }) + } + return { count: overrides.length, overrides } +} From 7e1f9e55f2b8bf9e70a36345adf9e4610700c74c Mon Sep 17 00:00:00 2001 From: Rod Boev Date: Sun, 5 Jul 2026 00:31:51 -0400 Subject: [PATCH 41/45] fix(worktrees): fall back from stale default base refs (#7398) * fix(worktrees): fall back from stale default base refs (#7312) * docs(worktrees): explain base fallback policy * fix(worktrees): harden stale base fallback edge cases --------- Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com> --- src/main/git/repo.test.ts | 68 ++ src/main/git/repo.ts | 74 ++- src/main/git/worktree-base-ref-probe.ts | 13 +- .../ssh-worktree-create-root-registration.ts | 50 ++ src/main/ipc/worktree-remote.ts | 225 +++++-- src/main/ipc/worktrees.test.ts | 583 ++++++++++++++++-- src/main/runtime/orca-runtime.test.ts | 185 +++++- src/main/runtime/orca-runtime.ts | 134 +++- src/main/worktree-create-base-prefetch.ts | 55 +- src/main/worktree-create-base.test.ts | 53 ++ src/main/worktree-create-base.ts | 29 + ...poserState-host-context-boundaries.test.ts | 4 +- src/renderer/src/hooks/useComposerState.ts | 7 +- .../src/runtime/worktree-create-base.test.ts | 32 +- .../src/runtime/worktree-create-base.ts | 13 +- 15 files changed, 1295 insertions(+), 230 deletions(-) create mode 100644 src/main/ipc/ssh-worktree-create-root-registration.ts create mode 100644 src/main/worktree-create-base.test.ts create mode 100644 src/main/worktree-create-base.ts diff --git a/src/main/git/repo.test.ts b/src/main/git/repo.test.ts index 8392b5434a0..d4abbd2501e 100644 --- a/src/main/git/repo.test.ts +++ b/src/main/git/repo.test.ts @@ -10,6 +10,7 @@ import { getBranchConflictKind, getRemoteCount, parseAndFilterSearchRefDetails, + resolveDefaultBaseRefViaExec, searchBaseRefDetails, searchBaseRefs } from './repo' @@ -493,6 +494,26 @@ describe('getDefaultBaseRef (regression — unchanged behavior)', () => { expect(result).toBe('origin/main') }) + it('falls through from a stale origin/HEAD target to an existing primary ref', () => { + const sha = getHeadSha(tmpDir) + createRemoteRef(tmpDir, 'origin/main', sha) + git(tmpDir, ['symbolic-ref', 'refs/remotes/origin/HEAD', 'refs/remotes/origin/master']) + + const result = getDefaultBaseRef(tmpDir) + + expect(result).toBe('origin/main') + }) + + it('falls through from a stale origin/HEAD primary target to another existing default ref', () => { + const sha = getHeadSha(tmpDir) + createRemoteRef(tmpDir, 'origin/master', sha) + git(tmpDir, ['symbolic-ref', 'refs/remotes/origin/HEAD', 'refs/remotes/origin/main']) + + const result = getDefaultBaseRef(tmpDir) + + expect(result).toBe('origin/master') + }) + it('does NOT fall through to upstream/main when origin/* is absent', () => { // Why: this is the explicit design decision — the default probe order // is origin-only. upstream-aware defaulting is deferred work. @@ -508,6 +529,53 @@ describe('getDefaultBaseRef (regression — unchanged behavior)', () => { }) }) +describe('resolveDefaultBaseRefViaExec', () => { + it('falls through from a stale origin/HEAD target to the probe list', async () => { + const calls: string[][] = [] + const exec = async (argv: string[]): Promise<{ stdout: string }> => { + calls.push(argv) + if (argv[0] === 'symbolic-ref') { + return { stdout: 'refs/remotes/origin/master\n' } + } + if (argv[0] === 'rev-parse' && argv.at(-1) === 'refs/remotes/origin/main') { + return { stdout: 'main-sha\n' } + } + throw new Error('missing ref') + } + + await expect(resolveDefaultBaseRefViaExec(exec)).resolves.toBe('origin/main') + + expect(calls).toEqual([ + ['symbolic-ref', '--quiet', 'refs/remotes/origin/HEAD'], + ['rev-parse', '--verify', '--quiet', 'refs/remotes/origin/master'], + ['rev-parse', '--verify', '--quiet', 'refs/remotes/origin/main'] + ]) + }) + + it('verifies origin/HEAD even when it points at origin/main', async () => { + const calls: string[][] = [] + const exec = async (argv: string[]): Promise<{ stdout: string }> => { + calls.push(argv) + if (argv[0] === 'symbolic-ref') { + return { stdout: 'refs/remotes/origin/main\n' } + } + if (argv[0] === 'rev-parse' && argv.at(-1) === 'refs/remotes/origin/master') { + return { stdout: 'master-sha\n' } + } + throw new Error('missing ref') + } + + await expect(resolveDefaultBaseRefViaExec(exec)).resolves.toBe('origin/master') + + expect(calls).toEqual([ + ['symbolic-ref', '--quiet', 'refs/remotes/origin/HEAD'], + ['rev-parse', '--verify', '--quiet', 'refs/remotes/origin/main'], + ['rev-parse', '--verify', '--quiet', 'refs/remotes/origin/main'], + ['rev-parse', '--verify', '--quiet', 'refs/remotes/origin/master'] + ]) + }) +}) + describe('getRemoteCount', () => { let tmpDir: string diff --git a/src/main/git/repo.ts b/src/main/git/repo.ts index d743c8ab5e4..68900f810bf 100644 --- a/src/main/git/repo.ts +++ b/src/main/git/repo.ts @@ -467,6 +467,24 @@ function hasGitRef(path: string, ref: string): boolean { } } +function gitRefToDefaultBaseRef(ref: string): string { + return ref.replace(/^refs\/remotes\//, '') +} + +function getVerifiedOriginHeadBaseRef(path: string): string | null { + try { + const ref = gitExecFileSync(['symbolic-ref', '--quiet', 'refs/remotes/origin/HEAD'], { + cwd: path + }).trim() + + // Why: origin/HEAD may survive a default-branch rename while pointing at a + // deleted ref; verify before trusting it over the probe list. + return ref && hasGitRef(path, ref) ? gitRefToDefaultBaseRef(ref) : null + } catch { + return null + } +} + /** * Resolve the default base ref for new worktrees. * Prefer the remote primary branch over a potentially stale local branch. @@ -478,16 +496,9 @@ function hasGitRef(path: string, ref: string): boolean { * degrade gracefully for non-creation uses (e.g. hosted URL building). */ export function getDefaultBaseRef(path: string): string | null { - try { - const ref = gitExecFileSync(['symbolic-ref', '--quiet', 'refs/remotes/origin/HEAD'], { - cwd: path - }).trim() - - if (ref) { - return ref.replace(/^refs\/remotes\//, '') - } - } catch { - // Fall through to explicit remote branch probes. + const originHeadBaseRef = getVerifiedOriginHeadBaseRef(path) + if (originHeadBaseRef) { + return originHeadBaseRef } // Why: walk the shared DEFAULT_BASE_REF_PROBES list so the sync path and the @@ -595,6 +606,28 @@ export async function getRemoteCount(path: string): Promise { /** Callback shape for a git exec function that yields stdout. */ export type GitExec = (argv: string[]) => Promise<{ stdout: string }> +async function hasGitRefViaExec(exec: GitExec, ref: string): Promise { + try { + await exec(['rev-parse', '--verify', '--quiet', ref]) + return true + } catch { + return false + } +} + +async function resolveVerifiedOriginHeadBaseRefViaExec(exec: GitExec): Promise { + try { + const { stdout } = await exec(['symbolic-ref', '--quiet', 'refs/remotes/origin/HEAD']) + const ref = stdout.trim() + if (!ref || !(await hasGitRefViaExec(exec, ref))) { + return null + } + return gitRefToDefaultBaseRef(ref) + } catch { + return null + } +} + /** * Resolve the default base ref given a git exec callback. Prefers * origin/HEAD's symbolic-ref target; falls back to DEFAULT_BASE_REF_PROBES. @@ -609,24 +642,11 @@ export type GitExec = (argv: string[]) => Promise<{ stdout: string }> * from a genuine transport failure. */ export async function resolveDefaultBaseRefViaExec(exec: GitExec): Promise { - try { - const { stdout } = await exec(['symbolic-ref', '--quiet', 'refs/remotes/origin/HEAD']) - const ref = stdout.trim() - if (ref) { - return ref.replace(/^refs\/remotes\//, '') - } - } catch { - // symbolic-ref returns non-zero when origin/HEAD is unset — expected. - // Fall through to probes. + const originHeadBaseRef = await resolveVerifiedOriginHeadBaseRefViaExec(exec) + if (originHeadBaseRef) { + return originHeadBaseRef } - return resolveDefaultBaseRefFromProbes(async (ref) => { - try { - await exec(['rev-parse', '--verify', '--quiet', ref]) - return true - } catch { - return false - } - }) + return resolveDefaultBaseRefFromProbes((ref) => hasGitRefViaExec(exec, ref)) } async function getDefaultBaseRefAsync( diff --git a/src/main/git/worktree-base-ref-probe.ts b/src/main/git/worktree-base-ref-probe.ts index 0d8e132df40..c7144159dfe 100644 --- a/src/main/git/worktree-base-ref-probe.ts +++ b/src/main/git/worktree-base-ref-probe.ts @@ -10,11 +10,14 @@ export async function hasWorktreeBaseCommitRef( options: GitExecOptions = {} ): Promise { try { - await gitExecFileAsync(['rev-parse', '--verify', '--quiet', `${qualifiedRef}^{commit}`], { - cwd: repoPath, - ...options - }) - return true + const { stdout } = await gitExecFileAsync( + ['rev-parse', '--verify', '--quiet', `${qualifiedRef}^{commit}`], + { + cwd: repoPath, + ...options + } + ) + return stdout.trim().length > 0 } catch { return false } diff --git a/src/main/ipc/ssh-worktree-create-root-registration.ts b/src/main/ipc/ssh-worktree-create-root-registration.ts new file mode 100644 index 00000000000..e364239cdbe --- /dev/null +++ b/src/main/ipc/ssh-worktree-create-root-registration.ts @@ -0,0 +1,50 @@ +import { getActiveMultiplexer } from './ssh' + +const SSH_CONNECTION_UNAVAILABLE_MESSAGE = + 'SSH connection is not available. Please reconnect and try again.' + +type SshRootRegistrationMux = { + request: (method: 'session.registerRoot', payload: { rootPath: string }) => Promise + notify: (method: 'session.registerRoot', payload: { rootPath: string }) => void +} + +async function registerSshWorktreeCreateRoots( + mux: SshRootRegistrationMux, + rootPaths: string[] +): Promise { + try { + await Promise.all( + rootPaths.map((rootPath) => mux.request('session.registerRoot', { rootPath })) + ) + } catch (err) { + if (err instanceof Error && err.message.includes('Method not found')) { + for (const rootPath of rootPaths) { + mux.notify('session.registerRoot', { rootPath }) + } + return + } + throw err + } +} + +export async function registerOptionalSshWorktreeCreateRoots( + connectionId: string, + rootPaths: string[] +): Promise { + const mux = getActiveMultiplexer(connectionId) + if (!mux) { + return + } + await registerSshWorktreeCreateRoots(mux, rootPaths) +} + +export async function registerRequiredSshWorktreeCreateRoots( + connectionId: string, + rootPaths: string[] +): Promise { + const mux = getActiveMultiplexer(connectionId) + if (!mux) { + throw new Error(SSH_CONNECTION_UNAVAILABLE_MESSAGE) + } + await registerSshWorktreeCreateRoots(mux, rootPaths) +} diff --git a/src/main/ipc/worktree-remote.ts b/src/main/ipc/worktree-remote.ts index 8193d6d74c1..e0a2bbf5db1 100644 --- a/src/main/ipc/worktree-remote.ts +++ b/src/main/ipc/worktree-remote.ts @@ -30,6 +30,8 @@ import type { AddWorktreeOptions, AddWorktreeResult } from '../git/worktree' import { getBranchConflictKind, resolveDefaultBaseRefViaExec } from '../git/repo' import { resolveLocalGitUsername } from '../git/git-username' import { hasCommitObjectViaGitExec } from '../git/commit-object-ref' +import { resolveWorktreeCreateBase } from '../worktree-create-base' +import { resolveWorktreeAddBaseRef } from '../../shared/worktree-base-ref' import { getHostedReviewForBranch } from '../source-control/hosted-review' import type { ForgeProviderId } from '../source-control/forge-provider' import { validateGitPushTarget } from '../git/push-target-validation' @@ -53,12 +55,15 @@ import { } from '../hooks' import { requireSshGitProvider } from '../providers/ssh-git-dispatch' import { getSshFilesystemProvider } from '../providers/ssh-filesystem-dispatch' -import { getActiveMultiplexer } from './ssh' import type { SshGitProvider } from '../providers/ssh-git-provider' import { TUI_AGENT_CONFIG, isTuiAgent } from '../../shared/tui-agent-config' import { isWindowsAbsolutePathLike } from '../../shared/cross-platform-path' import { getSshGitUsername } from '../git/git-username' import { runWorktreeChangeInvalidators } from './worktree-change-invalidators' +import { + registerOptionalSshWorktreeCreateRoots, + registerRequiredSshWorktreeCreateRoots +} from './ssh-worktree-create-root-registration' type CreateWorktreeArgsWithSystemProvenance = CreateWorktreeArgs & { automationProvenance?: AutomationWorkspaceProvenance @@ -608,6 +613,35 @@ function hasLocalCommitObjectWithOptions( ) } +async function hasLocalWorktreeBaseRefWithOptions( + repoPath: string, + baseRef: string, + gitOptions: { wslDistro?: string } +): Promise { + const refExists = async (qualifiedRef: string) => { + try { + const { stdout } = await gitExecFileAsync( + ['rev-parse', '--verify', '--quiet', `${qualifiedRef}^{commit}`], + { + cwd: repoPath, + ...gitOptions + } + ) + return stdout.trim().length > 0 + } catch { + return false + } + } + const resolvedBaseRef = await resolveWorktreeAddBaseRef(baseRef, refExists) + if (resolvedBaseRef !== baseRef) { + return true + } + if (baseRef.startsWith('refs/')) { + return refExists(baseRef) + } + return hasLocalCommitObjectWithOptions(repoPath, baseRef, gitOptions) +} + function getLocalGitHubPrForBranch( repoPath: string, branchName: string, @@ -626,6 +660,23 @@ function hasRemoteCommitObject( return hasCommitObjectViaGitExec((gitArgs) => provider.exec(gitArgs, repoPath), ref) } +async function hasRemoteWorktreeBaseRef( + provider: SshGitProvider, + repoPath: string, + baseRef: string +): Promise { + const refExists = (qualifiedRef: string) => + hasRemoteTrackingRefSsh(provider, repoPath, qualifiedRef) + const resolvedBaseRef = await resolveWorktreeAddBaseRef(baseRef, refExists) + if (resolvedBaseRef !== baseRef) { + return true + } + if (baseRef.startsWith('refs/')) { + return refExists(baseRef) + } + return hasRemoteCommitObject(provider, repoPath, baseRef) +} + // Why: hasRemoteCommitObject only resolves full SHAs; a remote-tracking base is // a symbolic ref (refs/remotes/origin/main), so detect its presence directly so // SSH creates can fall back to an existing local base ref when the refresh @@ -1218,33 +1269,31 @@ async function resolveRemoteTrackingBaseSsh( } } -async function resolveRemoteWorktreeCreateBase( - provider: SshGitProvider, - repo: Repo, - requestedBaseBranch: string | undefined -): Promise { - let baseBranch = requestedBaseBranch || repo.worktreeBaseRef - if (baseBranch) { - return baseBranch - } - try { - const { stdout } = await provider.exec( - ['symbolic-ref', 'refs/remotes/origin/HEAD', '--short'], - repo.path - ) - baseBranch = stdout.trim() - } catch { - return null - } - return baseBranch || null -} - async function resolveRemoteWorktreeCreateBasePlan( provider: SshGitProvider, repo: Repo, requestedBaseBranch: string | undefined ): Promise { - const baseBranch = await resolveRemoteWorktreeCreateBase(provider, repo, requestedBaseBranch) + const baseBranch = await resolveWorktreeCreateBase({ + requestedBaseBranch, + repoWorktreeBaseRef: repo.worktreeBaseRef, + resolveDefaultBaseRef: () => + resolveDefaultBaseRefViaExec((argv) => provider.exec(argv, repo.path)), + isBaseUsable: async (baseBranchCandidate) => { + const remoteTrackingBase = await resolveRemoteTrackingBaseSsh( + provider, + repo.path, + baseBranchCandidate + ) + if (remoteTrackingBase) { + if (await hasRemoteTrackingRefSsh(provider, repo.path, remoteTrackingBase.ref)) { + return true + } + return hasRemoteWorktreeBaseRef(provider, repo.path, baseBranchCandidate) + } + return hasRemoteWorktreeBaseRef(provider, repo.path, baseBranchCandidate) + } + }) if (!baseBranch) { return null } @@ -1280,15 +1329,23 @@ export async function prefetchRemoteWorktreeCreateBase( repo: Repo, args: { baseBranch?: string } ): Promise { + // Why: the shared base-plan probes use generic git.exec, and some relays + // require the repo root to be registered before those probes can see refs. + await registerOptionalSshWorktreeCreateRoots(repo.connectionId!, [repo.path]) const basePlan = await getOrStartRemoteWorktreeCreateBasePlan(provider, repo, args.baseBranch) if (!basePlan) { return } if (basePlan.remoteTrackingBase) { - await refreshRemoteTrackingBaseForWorktreeCreate(provider, repo, basePlan.remoteTrackingBase) - return + if ( + (await hasRemoteTrackingRefSsh(provider, repo.path, basePlan.remoteTrackingBase.ref)) || + !(await hasRemoteWorktreeBaseRef(provider, repo.path, basePlan.baseBranch)) + ) { + await refreshRemoteTrackingBaseForWorktreeCreate(provider, repo, basePlan.remoteTrackingBase) + return + } } - if (await hasRemoteCommitObject(provider, repo.path, basePlan.baseBranch)) { + if (await hasRemoteWorktreeBaseRef(provider, repo.path, basePlan.baseBranch)) { // Why: PR/MR resolvers already fetched verified SHA start points. A broad // remote fetch only updates unrelated refs when the commit object exists. return @@ -1471,6 +1528,10 @@ export async function createRemoteWorktree( ? sanitizeWorktreeDisplayName(args.displayName) : undefined + // Why: resolving the create base can probe repo refs through generic git.exec. + // Register the repo root first so relays do not report a valid base as stale. + await registerRequiredSshWorktreeCreateRoots(repo.connectionId!, [repo.path]) + // Why: SSH targets cannot use the local `gh` account, and git email/name are // commit author identity rather than hosted-account usernames. const username = await getSshGitUsername(provider, repo.path) @@ -1488,7 +1549,8 @@ export async function createRemoteWorktree( 'Could not resolve a default base ref for this repo. Pick a base branch explicitly and try again.' ) } - const { baseBranch, remoteTrackingBase } = basePlan + const { baseBranch } = basePlan + let { remoteTrackingBase } = basePlan let branchName = '' let checkoutExistingBranch = false @@ -1594,25 +1656,20 @@ export async function createRemoteWorktree( } } - const mux = getActiveMultiplexer(repo.connectionId!) - if (!mux) { - throw new Error('SSH connection is not available. Please reconnect and try again.') - } - // Why: register before any generic git.exec probe (base-ref existence, the - // local-base advisory) as well as addWorktree. Fresh/older relays may gate - // generic git.exec on registered roots; probing first would falsely report a - // ref missing (and defeat the offline fallback below) even though create works. - try { - await Promise.all([ - mux.request('session.registerRoot', { rootPath: repo.path }), - mux.request('session.registerRoot', { rootPath: remotePath }) - ]) - } catch (err) { - if (err instanceof Error && err.message.includes('Method not found')) { - mux.notify('session.registerRoot', { rootPath: repo.path }) - mux.notify('session.registerRoot', { rootPath: remotePath }) - } else { - throw err + // Why: addWorktree and setup probes run inside the new worktree path; older + // relays need that root registered before accepting git/fs operations there. + await registerRequiredSshWorktreeCreateRoots(repo.connectionId!, [remotePath]) + + if (remoteTrackingBase) { + const hasRemoteTrackingBaseRef = await hasRemoteTrackingRefSsh( + provider, + repo.path, + remoteTrackingBase.ref + ) + const hasLocalBaseRef = + hasRemoteTrackingBaseRef || (await hasRemoteWorktreeBaseRef(provider, repo.path, baseBranch)) + if (!hasRemoteTrackingBaseRef && hasLocalBaseRef) { + remoteTrackingBase = null } } @@ -1633,7 +1690,7 @@ export async function createRemoteWorktree( ) } } - } else if (!(await hasRemoteCommitObject(provider, repo.path, baseBranch))) { + } else if (!(await hasRemoteWorktreeBaseRef(provider, repo.path, baseBranch))) { // Why: local or otherwise non-remote-tracking bases preserve legacy // best-effort fetch behavior. Verified PR/MR SHA bases already have the // commit object locally, so a broad remote fetch only updates unrelated refs. @@ -1914,15 +1971,40 @@ export async function createLocalWorktree( ? sanitizeWorktreeDisplayName(args.displayName) : undefined - // Why: resolve the base before branch/path selection so remote-tracking bases - // can be refreshed before `git worktree add`. Creating first and repairing - // later races setup scripts, agents, and user edits. - const baseBranch = - args.baseBranch || - repo.worktreeBaseRef || - (await resolveDefaultBaseRefViaExec((argv) => gitExecFileAsync(argv, localGitExecOptions))) + const baseBranch = await resolveWorktreeCreateBase({ + requestedBaseBranch: args.baseBranch, + repoWorktreeBaseRef: repo.worktreeBaseRef, + resolveDefaultBaseRef: () => + resolveDefaultBaseRefViaExec((argv) => gitExecFileAsync(argv, localGitExecOptions)), + isBaseUsable: async (baseBranchCandidate) => { + if (runtime) { + const remoteTrackingBase = await runtime.resolveRemoteTrackingBase( + repo.path, + baseBranchCandidate, + ...localWorktreeGitOptionArgs + ) + if (remoteTrackingBase) { + if ( + await runtime.hasRemoteTrackingRef( + repo.path, + remoteTrackingBase, + ...localWorktreeGitOptionArgs + ) + ) { + return true + } + return hasLocalWorktreeBaseRefWithOptions( + repo.path, + baseBranchCandidate, + localGitExecOptions + ) + } + } + return hasLocalWorktreeBaseRefWithOptions(repo.path, baseBranchCandidate, localGitExecOptions) + } + }) if (!baseBranch) { - // Why: getDefaultBaseRef may return null when none of origin/HEAD, + // Why: resolveDefaultBaseRefViaExec may return null when none of origin/HEAD, // origin/main, origin/master, local main, or local master exist. Don't // fall back to a hardcoded 'origin/main' — passing a non-existent ref to // `git worktree add` produces an opaque error. Fail here with a clear @@ -1947,23 +2029,30 @@ export async function createLocalWorktree( ...localWorktreeGitOptionArgs ) if (remoteTrackingBase) { - const hasLocalBaseRef = await runtime.hasRemoteTrackingRef( + const hasRemoteTrackingBaseRef = await runtime.hasRemoteTrackingRef( repo.path, remoteTrackingBase, ...localWorktreeGitOptionArgs ) - emitCreateWorktreeProgress(mainWindow, 'fetching', args.creationId) - remoteTrackingRefresh = { - base: remoteTrackingBase, - hadLocalBaseRef: hasLocalBaseRef, - promise: runtime.getOrStartRemoteTrackingBaseRefresh( - repo.path, - remoteTrackingBase, - ...localWorktreeGitOptionArgs - ) + const hasLocalBaseRef = + hasRemoteTrackingBaseRef || + (await hasLocalWorktreeBaseRefWithOptions(repo.path, baseBranch, localGitExecOptions)) + if (!hasRemoteTrackingBaseRef && hasLocalBaseRef) { + remoteTrackingBase = null + } else { + emitCreateWorktreeProgress(mainWindow, 'fetching', args.creationId) + remoteTrackingRefresh = { + base: remoteTrackingBase, + hadLocalBaseRef: hasRemoteTrackingBaseRef, + promise: runtime.getOrStartRemoteTrackingBaseRefresh( + repo.path, + remoteTrackingBase, + ...localWorktreeGitOptionArgs + ) + } } } else if ( - !(await hasLocalCommitObjectWithOptions(repo.path, baseBranch, localWorktreeGitOptions)) + !(await hasLocalWorktreeBaseRefWithOptions(repo.path, baseBranch, localWorktreeGitOptions)) ) { // Why: when the base branch does not match a configured remote prefix // (e.g. plain `main`, `master`, or any local branch), the legacy path @@ -1976,7 +2065,9 @@ export async function createLocalWorktree( emitCreateWorktreeProgress(mainWindow, 'fetching', args.creationId) } } else { - if (!(await hasLocalCommitObjectWithOptions(repo.path, baseBranch, localWorktreeGitOptions))) { + if ( + !(await hasLocalWorktreeBaseRefWithOptions(repo.path, baseBranch, localWorktreeGitOptions)) + ) { legacyFetchPromise = gitExecFileAsync(['fetch', 'origin'], { ...localGitExecOptions, timeout: CREATE_BASE_FALLBACK_FETCH_TIMEOUT_MS diff --git a/src/main/ipc/worktrees.test.ts b/src/main/ipc/worktrees.test.ts index f619d078630..e9f9edfcdae 100644 --- a/src/main/ipc/worktrees.test.ts +++ b/src/main/ipc/worktrees.test.ts @@ -493,16 +493,32 @@ describe('registerWorktreeHandlers', () => { }) it('prefetches the local default create base through the runtime refresh cache', async () => { + const repo = { + id: 'repo-1', + path: '/workspace/repo', + displayName: 'repo', + badgeColor: '#000', + addedAt: 0, + worktreeBaseRef: 'origin/master' + } const remoteBase = { remote: 'origin', branch: 'main', ref: 'refs/remotes/origin/main', base: 'origin/main' } - runtimeStub.resolveRemoteTrackingBase.mockResolvedValue(remoteBase) + store.getRepo.mockReturnValue(repo) + runtimeStub.resolveRemoteTrackingBase.mockImplementation(async (_repoPath, baseBranch) => + baseBranch === 'origin/main' ? remoteBase : null + ) + runtimeStub.hasRemoteTrackingRef.mockResolvedValue(true) await handlers['worktrees:prefetchCreateBase'](null, { repoId: 'repo-1' }) + expect(runtimeStub.resolveRemoteTrackingBase).toHaveBeenCalledWith( + '/workspace/repo', + 'origin/master' + ) expect(runtimeStub.resolveRemoteTrackingBase).toHaveBeenCalledWith( '/workspace/repo', 'origin/main' @@ -3668,7 +3684,7 @@ describe('registerWorktreeHandlers', () => { }) }) - it('does not create an SSH worktree when the refresh fails and no local base ref exists', async () => { + it('keeps an explicit SSH base strict when refresh fails and no local base ref exists', async () => { const repo = { id: 'repo-ssh', path: '/remote/repo', @@ -3679,7 +3695,6 @@ describe('registerWorktreeHandlers', () => { worktreeBaseRef: 'origin/main' } const provider = { - // Empty rev-parse stdout -> no local remote-tracking base ref to fall back on. exec: vi.fn().mockImplementation(async (args: string[]) => { if (args[0] === 'remote') { return { stdout: 'origin\n', stderr: '' } @@ -3705,24 +3720,26 @@ describe('registerWorktreeHandlers', () => { await expect( handlers['worktrees:create'](null, { repoId: 'repo-ssh', - name: 'improve-dashboard' + name: 'improve-dashboard', + baseBranch: 'origin/master' }) ).rejects.toThrow( - 'Could not refresh base ref "origin/main" from "origin". Check your network and try again.' + 'Could not refresh base ref "origin/master" from "origin". Check your network and try again.' ) expect(provider.addWorktree).not.toHaveBeenCalled() + expect(resolveDefaultBaseRefViaExecMock).not.toHaveBeenCalled() expect(provider.fetchRemoteTrackingRef).toHaveBeenCalledWith( '/remote/repo', 'origin', - 'main', - 'refs/remotes/origin/main' + 'master', + 'refs/remotes/origin/master' ) }) - it('creates an SSH worktree from an existing local base ref when the refresh fails', async () => { - // Regression: a failed SSH refresh must fall back to an existing local - // remote-tracking base ref instead of blocking creation. + it('creates an SSH worktree from the detected default base when the persisted base is stale', async () => { + // Regression: a stale persisted repo base must fall back to the detected + // primary default instead of blocking creation. const repo = { id: 'repo-ssh', path: '/remote/repo', @@ -3730,20 +3747,25 @@ describe('registerWorktreeHandlers', () => { badgeColor: '#000', addedAt: 0, connectionId: 'conn-1', - worktreeBaseRef: 'origin/main' + worktreeBaseRef: 'origin/master' } const provider = { exec: vi.fn().mockImplementation(async (args: string[]) => { if (args[0] === 'remote') { return { stdout: 'origin\n', stderr: '' } } - // Local remote-tracking base ref resolves -> usable fallback exists. + if (args[0] === 'symbolic-ref') { + return { stdout: 'refs/remotes/origin/main\n', stderr: '' } + } + if (args[0] === 'rev-parse' && args.includes('refs/remotes/origin/master^{commit}')) { + return { stdout: '', stderr: '' } + } if (args[0] === 'rev-parse' && args.includes('refs/remotes/origin/main^{commit}')) { return { stdout: `${'a'.repeat(40)}\n`, stderr: '' } } return { stdout: '', stderr: '' } }), - fetchRemoteTrackingRef: vi.fn().mockRejectedValue(new Error('network unavailable')), + fetchRemoteTrackingRef: vi.fn().mockResolvedValue(undefined), addWorktree: vi.fn().mockResolvedValue(undefined), listWorktrees: vi .fn() @@ -3773,15 +3795,175 @@ describe('registerWorktreeHandlers', () => { name: 'improve-dashboard' }) - // The refresh must be ATTEMPTED (and fail) before falling back — guards - // against a regression that skips the refresh whenever a local ref exists. + expect(resolveDefaultBaseRefViaExecMock).toHaveBeenCalled() expect(provider.fetchRemoteTrackingRef).toHaveBeenCalledWith( '/remote/repo', 'origin', 'main', 'refs/remotes/origin/main' ) - expect(provider.addWorktree).toHaveBeenCalled() + expect(provider.addWorktree).toHaveBeenCalledWith( + '/remote/repo', + 'improve-dashboard', + '/remote/improve-dashboard', + { + base: 'origin/main' + } + ) + }) + + it('keeps a usable SSH persisted local branch base after registering the repo root', async () => { + const repo = { + id: 'repo-ssh', + path: '/remote/repo', + displayName: 'ssh', + badgeColor: '#000', + addedAt: 0, + connectionId: 'conn-1', + worktreeBaseRef: 'develop' + } + let repoRootRegistered = false + const provider = { + exec: vi.fn().mockImplementation(async (args: string[]) => { + if (args[0] === 'config') { + return { stdout: '', stderr: '' } + } + if (args[0] === 'remote') { + return { stdout: 'origin\n', stderr: '' } + } + if (args[0] === 'symbolic-ref') { + return { stdout: 'refs/remotes/origin/main\n', stderr: '' } + } + if (args[0] === 'rev-parse' && args.includes('refs/heads/develop^{commit}')) { + return { stdout: repoRootRegistered ? 'develop-sha\n' : '', stderr: '' } + } + return { stdout: '', stderr: '' } + }), + fetchRemoteTrackingRef: vi.fn().mockResolvedValue(undefined), + addWorktree: vi.fn().mockResolvedValue(undefined), + listWorktrees: vi.fn().mockResolvedValue([ + { + path: '/remote/local-branch-base', + head: 'develop-sha', + branch: 'refs/heads/local-branch-base', + isBare: false, + isMainWorktree: false + } + ]) + } + const mux = { + request: vi + .fn() + .mockImplementation(async (_method: string, payload: { rootPath: string }) => { + if (payload.rootPath === repo.path) { + repoRootRegistered = true + } + }), + notify: vi.fn() + } + store.getRepos.mockReturnValue([repo]) + store.getRepo.mockReturnValue(repo) + getSshGitProviderMock.mockReturnValue(provider) + getActiveMultiplexerMock.mockReturnValue(mux) + store.setWorktreeMeta.mockImplementation((_worktreeId, meta) => meta) + + await handlers['worktrees:create'](null, { + repoId: 'repo-ssh', + name: 'local-branch-base' + }) + + expect(mux.request).toHaveBeenCalledWith('session.registerRoot', { rootPath: repo.path }) + expect(provider.fetchRemoteTrackingRef).not.toHaveBeenCalled() + expect(provider.addWorktree).toHaveBeenCalledWith( + '/remote/repo', + 'local-branch-base', + '/remote/local-branch-base', + { + base: 'develop' + } + ) + }) + + it('keeps a usable SSH slash-named local branch base that matches a remote prefix', async () => { + const repo = { + id: 'repo-ssh', + path: '/remote/repo', + displayName: 'ssh', + badgeColor: '#000', + addedAt: 0, + connectionId: 'conn-1', + worktreeBaseRef: 'team/feature' + } + let repoRootRegistered = false + const provider = { + exec: vi.fn().mockImplementation(async (args: string[]) => { + if (args[0] === 'config') { + return { stdout: '', stderr: '' } + } + if (args[0] === 'remote') { + return { stdout: 'team\norigin\n', stderr: '' } + } + if (args[0] === 'symbolic-ref') { + return { stdout: 'refs/remotes/origin/main\n', stderr: '' } + } + if (args[0] === 'rev-parse' && args.includes('refs/remotes/origin/main')) { + return { stdout: 'main-sha\n', stderr: '' } + } + if (args[0] === 'rev-parse' && args.includes('refs/remotes/team/feature^{commit}')) { + return { stdout: '', stderr: '' } + } + if (args[0] === 'rev-parse' && args.includes('refs/heads/team/feature^{commit}')) { + return { stdout: repoRootRegistered ? 'team-feature-sha\n' : '', stderr: '' } + } + return { stdout: '', stderr: '' } + }), + fetchRemoteTrackingRef: vi.fn().mockResolvedValue(undefined), + addWorktree: vi.fn().mockResolvedValue(undefined), + listWorktrees: vi.fn().mockResolvedValue([ + { + path: '/remote/slash-local-base', + head: 'team-feature-sha', + branch: 'refs/heads/slash-local-base', + isBare: false, + isMainWorktree: false + } + ]) + } + const mux = { + request: vi + .fn() + .mockImplementation(async (_method: string, payload: { rootPath: string }) => { + if (payload.rootPath === repo.path) { + repoRootRegistered = true + } + }), + notify: vi.fn() + } + store.getRepos.mockReturnValue([repo]) + store.getRepo.mockReturnValue(repo) + getSshGitProviderMock.mockReturnValue(provider) + getActiveMultiplexerMock.mockReturnValue(mux) + store.setWorktreeMeta.mockImplementation((_worktreeId, meta) => meta) + + await handlers['worktrees:create'](null, { + repoId: 'repo-ssh', + name: 'slash-local-base' + }) + + expect(provider.fetchRemoteTrackingRef).not.toHaveBeenCalledWith( + '/remote/repo', + 'team', + 'feature', + 'refs/remotes/team/feature' + ) + expect(provider.addWorktree).toHaveBeenCalledWith( + '/remote/repo', + 'slash-local-base', + '/remote/slash-local-base', + { + base: 'team/feature' + } + ) }) it('reuses a fresh SSH remote-tracking base refresh for repeated creates', async () => { @@ -3986,6 +4168,160 @@ describe('registerWorktreeHandlers', () => { expect(provider.addWorktree).toHaveBeenCalledTimes(1) }) + it('registers the SSH repo root before create-base prefetch probes refs', async () => { + const repo = { + id: 'repo-ssh', + path: '/remote/repo', + displayName: 'ssh', + badgeColor: '#000', + addedAt: 0, + connectionId: 'conn-1', + worktreeBaseRef: 'origin/master' + } + const registeredRoots = new Set() + const events: string[] = [] + const provider = { + exec: vi.fn().mockImplementation(async (args: string[]) => { + events.push(`exec:${args[0]}:${registeredRoots.has('/remote/repo')}`) + if (!registeredRoots.has('/remote/repo')) { + throw new Error('root not registered') + } + if (args[0] === 'symbolic-ref') { + return { stdout: 'refs/remotes/origin/main\n', stderr: '' } + } + if (args[0] === 'remote') { + return { stdout: 'origin\n', stderr: '' } + } + if (args[0] === 'rev-parse' && args.includes('refs/remotes/origin/master^{commit}')) { + throw new Error('missing stale base') + } + if (args[0] === 'rev-parse' && args.includes('refs/remotes/origin/main^{commit}')) { + return { stdout: 'main-sha\n', stderr: '' } + } + return { stdout: '', stderr: '' } + }), + fetchRemoteTrackingRef: vi.fn().mockResolvedValue(undefined), + addWorktree: vi.fn().mockResolvedValue(undefined), + listWorktrees: vi.fn().mockResolvedValue([ + { + path: '/remote/prefetched-worktree', + head: 'abc123', + branch: 'refs/heads/prefetched-worktree', + isBare: false, + isMainWorktree: false + } + ]) + } + const mux = { + request: vi + .fn() + .mockImplementation(async (_method: string, payload: { rootPath: string }) => { + events.push(`register:${payload.rootPath}`) + registeredRoots.add(payload.rootPath) + }), + notify: vi.fn() + } + store.getRepos.mockReturnValue([repo]) + store.getRepo.mockReturnValue(repo) + getSshGitProviderMock.mockReturnValue(provider) + getActiveMultiplexerMock.mockReturnValue(mux) + store.setWorktreeMeta.mockImplementation((_worktreeId, meta) => meta) + + await handlers['worktrees:prefetchCreateBase'](null, { + repoId: 'repo-ssh' + }) + await handlers['worktrees:create'](null, { + repoId: 'repo-ssh', + name: 'prefetched-worktree' + }) + + expect(events[0]).toBe('register:/remote/repo') + expect(events).not.toContain('exec:symbolic-ref:false') + expect(provider.addWorktree).toHaveBeenCalledWith( + '/remote/repo', + 'prefetched-worktree', + '/remote/prefetched-worktree', + { + base: 'origin/main' + } + ) + }) + + it('does not let SSH prefetch turn a persisted slash-named local branch into a remote-tracking base', async () => { + const repo = { + id: 'repo-ssh', + path: '/remote/repo', + displayName: 'ssh', + badgeColor: '#000', + addedAt: 0, + connectionId: 'conn-1', + worktreeBaseRef: 'team/feature' + } + const provider = { + exec: vi.fn().mockImplementation(async (args: string[]) => { + if (args[0] === 'symbolic-ref') { + return { stdout: 'refs/remotes/origin/main\n', stderr: '' } + } + if (args[0] === 'remote') { + return { stdout: 'team\norigin\n', stderr: '' } + } + if (args[0] === 'rev-parse' && args.includes('refs/remotes/origin/main')) { + return { stdout: 'main-sha\n', stderr: '' } + } + if (args[0] === 'rev-parse' && args.includes('refs/remotes/team/feature^{commit}')) { + throw new Error('missing remote-tracking ref') + } + if (args[0] === 'rev-parse' && args.includes('refs/heads/team/feature^{commit}')) { + return { stdout: 'team-feature-sha\n', stderr: '' } + } + return { stdout: '', stderr: '' } + }), + fetchRemoteTrackingRef: vi.fn().mockResolvedValue(undefined), + addWorktree: vi.fn().mockResolvedValue(undefined), + listWorktrees: vi.fn().mockResolvedValue([ + { + path: '/remote/slash-local-base', + head: 'team-feature-sha', + branch: 'refs/heads/slash-local-base', + isBare: false, + isMainWorktree: false + } + ]) + } + const mux = { + request: vi.fn().mockResolvedValue(undefined), + notify: vi.fn() + } + store.getRepos.mockReturnValue([repo]) + store.getRepo.mockReturnValue(repo) + getSshGitProviderMock.mockReturnValue(provider) + getActiveMultiplexerMock.mockReturnValue(mux) + store.setWorktreeMeta.mockImplementation((_worktreeId, meta) => meta) + + await handlers['worktrees:prefetchCreateBase'](null, { + repoId: 'repo-ssh' + }) + await handlers['worktrees:create'](null, { + repoId: 'repo-ssh', + name: 'slash-local-base' + }) + + expect(provider.fetchRemoteTrackingRef).not.toHaveBeenCalledWith( + '/remote/repo', + 'team', + 'feature', + 'refs/remotes/team/feature' + ) + expect(provider.addWorktree).toHaveBeenCalledWith( + '/remote/repo', + 'slash-local-base', + '/remote/slash-local-base', + { + base: 'team/feature' + } + ) + }) + it('shares in-flight SSH create-base resolution with create', async () => { const repo = { id: 'repo-ssh', @@ -4241,7 +4577,16 @@ describe('registerWorktreeHandlers', () => { isMainWorktree: false } ]) - gitExecFileAsyncMock.mockResolvedValue({ stdout: 'created-sha\n', stderr: '' }) + gitExecFileAsyncMock.mockImplementation(async (args: string[]) => { + if ( + args[0] === 'rev-parse' && + (args.includes('refs/remotes/origin/master^{commit}') || + args.includes('refs/heads/origin/master^{commit}')) + ) { + throw new Error('missing ref') + } + return { stdout: 'created-sha\n', stderr: '' } + }) const createPromise = handlers['worktrees:create'](null, { repoId: 'repo-1', @@ -4270,17 +4615,82 @@ describe('registerWorktreeHandlers', () => { ) }) - it('creates from an existing local base ref when the pre-create refresh fails', async () => { - // Regression: a failed refresh must not block creation when a usable local - // remote-tracking base ref already exists. + it('creates from the detected default base when the persisted base is stale', async () => { + // Regression: a stale persisted repo base must fall back to the detected + // primary default instead of blocking creation. const remoteBase = { remote: 'origin', branch: 'main', ref: 'refs/remotes/origin/main', base: 'origin/main' } - runtimeStub.resolveRemoteTrackingBase.mockResolvedValue(remoteBase) + store.getRepo.mockReturnValue({ + id: 'repo-1', + path: '/workspace/repo', + displayName: 'repo', + badgeColor: '#000', + addedAt: 0, + worktreeBaseRef: 'origin/master' + }) + runtimeStub.resolveRemoteTrackingBase.mockImplementation(async (_repoPath, baseBranch) => + baseBranch === 'origin/main' ? remoteBase : null + ) runtimeStub.hasRemoteTrackingRef.mockResolvedValue(true) + runtimeStub.getOrStartRemoteTrackingBaseRefresh.mockResolvedValue({ + ok: true, + errorKind: 'git_error' + }) + listWorktreesMock.mockResolvedValue([ + { + path: '/workspace/improve-dashboard', + head: 'created-sha', + branch: 'improve-dashboard', + isBare: false, + isMainWorktree: false + } + ]) + gitExecFileAsyncMock.mockImplementation(async (args: string[]) => { + if ( + args[0] === 'rev-parse' && + (args.includes('refs/remotes/origin/master^{commit}') || + args.includes('refs/heads/origin/master^{commit}')) + ) { + throw new Error('missing ref') + } + return { stdout: 'created-sha\n', stderr: '' } + }) + + const result = (await handlers['worktrees:create'](null, { + repoId: 'repo-1', + name: 'improve-dashboard' + })) as CreateWorktreeResult + + expect(addWorktreeMock).toHaveBeenCalled() + expect(runtimeStub.resolveRemoteTrackingBase).toHaveBeenCalledWith( + '/workspace/repo', + 'origin/master' + ) + expect(runtimeStub.resolveRemoteTrackingBase).toHaveBeenCalledWith( + '/workspace/repo', + 'origin/main' + ) + expect(runtimeStub.getOrStartRemoteTrackingBaseRefresh).toHaveBeenCalledWith( + '/workspace/repo', + remoteBase + ) + expect(result.worktree.id).toBe('repo-1::/workspace/improve-dashboard') + }) + + it('keeps a usable persisted local branch base when a detected default exists', async () => { + store.getRepo.mockReturnValue({ + id: 'repo-1', + path: '/workspace/repo', + displayName: 'repo', + badgeColor: '#000', + addedAt: 0, + worktreeBaseRef: 'develop' + }) + runtimeStub.resolveRemoteTrackingBase.mockResolvedValue(null) runtimeStub.getOrStartRemoteTrackingBaseRefresh.mockResolvedValue({ ok: false, errorKind: 'git_error' @@ -4294,29 +4704,14 @@ describe('registerWorktreeHandlers', () => { isMainWorktree: false } ]) - gitExecFileAsyncMock.mockResolvedValue({ stdout: 'created-sha\n', stderr: '' }) - - const result = (await handlers['worktrees:create'](null, { - repoId: 'repo-1', - name: 'improve-dashboard' - })) as CreateWorktreeResult - - expect(addWorktreeMock).toHaveBeenCalled() - expect(result.worktree.id).toBe('repo-1::/workspace/improve-dashboard') - }) - - it('does not create when the pre-create refresh fails and no local base ref exists', async () => { - const remoteBase = { - remote: 'origin', - branch: 'main', - ref: 'refs/remotes/origin/main', - base: 'origin/main' - } - runtimeStub.resolveRemoteTrackingBase.mockResolvedValue(remoteBase) - runtimeStub.hasRemoteTrackingRef.mockResolvedValue(false) - runtimeStub.getOrStartRemoteTrackingBaseRefresh.mockResolvedValue({ - ok: false, - errorKind: 'git_error' + gitExecFileAsyncMock.mockImplementation(async (args: string[]) => { + if (args[0] === 'rev-parse' && args.includes('refs/heads/develop^{commit}')) { + return { stdout: 'develop-sha\n', stderr: '' } + } + if (args[0] === 'fetch') { + throw new Error('network unavailable') + } + return { stdout: 'created-sha\n', stderr: '' } }) await expect( @@ -4324,11 +4719,109 @@ describe('registerWorktreeHandlers', () => { repoId: 'repo-1', name: 'improve-dashboard' }) + ).resolves.toEqual(expect.objectContaining({ worktree: expect.any(Object) })) + + expect(addWorktreeMock).toHaveBeenCalledWith( + '/workspace/repo', + '/workspace/improve-dashboard', + 'improve-dashboard', + 'develop', + false + ) + }) + + it('keeps a usable persisted slash-named local branch base that matches a remote prefix', async () => { + const remoteBase = { + remote: 'team', + branch: 'feature', + ref: 'refs/remotes/team/feature', + base: 'team/feature' + } + store.getRepo.mockReturnValue({ + id: 'repo-1', + path: '/workspace/repo', + displayName: 'repo', + badgeColor: '#000', + addedAt: 0, + worktreeBaseRef: 'team/feature' + }) + runtimeStub.resolveRemoteTrackingBase.mockImplementation(async (_repoPath, baseBranch) => + baseBranch === 'team/feature' ? remoteBase : null + ) + runtimeStub.hasRemoteTrackingRef.mockResolvedValue(false) + listWorktreesMock.mockResolvedValue([ + { + path: '/workspace/slash-local-base', + head: 'created-sha', + branch: 'slash-local-base', + isBare: false, + isMainWorktree: false + } + ]) + gitExecFileAsyncMock.mockImplementation(async (args: string[]) => { + if (args[0] === 'rev-parse' && args.includes('refs/remotes/team/feature^{commit}')) { + throw new Error('missing remote-tracking ref') + } + if (args[0] === 'rev-parse' && args.includes('refs/heads/team/feature^{commit}')) { + return { stdout: 'team-feature-sha\n', stderr: '' } + } + if (args[0] === 'fetch') { + throw new Error('network unavailable') + } + return { stdout: 'created-sha\n', stderr: '' } + }) + + await expect( + handlers['worktrees:create'](null, { + repoId: 'repo-1', + name: 'slash-local-base' + }) + ).resolves.toEqual(expect.objectContaining({ worktree: expect.any(Object) })) + + expect(runtimeStub.getOrStartRemoteTrackingBaseRefresh).not.toHaveBeenCalled() + expect(addWorktreeMock).toHaveBeenCalledWith( + '/workspace/repo', + '/workspace/slash-local-base', + 'slash-local-base', + 'team/feature', + false + ) + }) + + it('keeps an explicit base strict when the pre-create refresh fails', async () => { + const remoteBase = { + remote: 'origin', + branch: 'master', + ref: 'refs/remotes/origin/master', + base: 'origin/master' + } + runtimeStub.resolveRemoteTrackingBase.mockResolvedValue(remoteBase) + runtimeStub.hasRemoteTrackingRef.mockResolvedValue(false) + runtimeStub.getOrStartRemoteTrackingBaseRefresh.mockResolvedValue({ + ok: false, + errorKind: 'git_error' + }) + store.getRepo.mockReturnValue({ + id: 'repo-1', + path: '/workspace/repo', + displayName: 'repo', + badgeColor: '#000', + addedAt: 0, + worktreeBaseRef: 'origin/main' + }) + + await expect( + handlers['worktrees:create'](null, { + repoId: 'repo-1', + name: 'improve-dashboard', + baseBranch: 'origin/master' + }) ).rejects.toThrow( - 'Could not refresh base ref "origin/main" from "origin". Check your network and try again.' + 'Could not refresh base ref "origin/master" from "origin". Check your network and try again.' ) expect(addWorktreeMock).not.toHaveBeenCalled() + expect(resolveDefaultBaseRefViaExecMock).not.toHaveBeenCalled() }) it('delegates remote-tracking base freshness to the runtime before create', async () => { diff --git a/src/main/runtime/orca-runtime.test.ts b/src/main/runtime/orca-runtime.test.ts index a8ef74053a3..5bc855ee7c4 100644 --- a/src/main/runtime/orca-runtime.test.ts +++ b/src/main/runtime/orca-runtime.test.ts @@ -811,6 +811,16 @@ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0- const HEADLESS_LEAF_ID = '11111111-1111-4111-8111-111111111111' const HEADLESS_SECOND_LEAF_ID = '22222222-2222-4222-8222-222222222222' +function isOriginMainBaseRefProbe(args: string[]): boolean { + return ( + args[0] === 'rev-parse' && + args[1] === '--verify' && + (args.includes('origin/main') || + args.includes('refs/remotes/origin/main') || + args.includes('refs/remotes/origin/main^{commit}')) + ) +} + function antigravityReadyScreen(model = 'Gemini 3.5 Flash (High)'): string { return [ 'Antigravity CLI 1.0.3', @@ -2370,9 +2380,8 @@ describe('OrcaRuntimeService', () => { } }) - it('creates a runtime local worktree from an existing local base ref when the refresh fails', async () => { - // Regression: an offline/auth failure while refreshing the remote-tracking - // base must not block creation when a usable local base ref already exists. + it('creates a runtime local worktree from the detected default when the persisted base is stale', async () => { + // Regression: a stale persisted repo base must fall back to the detected default. const runtime = new OrcaRuntimeService(store) const createdWorktree = { path: '/tmp/workspaces/cli-refresh-fails', @@ -2381,6 +2390,8 @@ describe('OrcaRuntimeService', () => { isBare: false, isMainWorktree: false } + const repo = { ...store.getRepos()[0], worktreeBaseRef: 'origin/master' } + const getReposSpy = vi.spyOn(store, 'getRepos').mockReturnValue([repo] as never) computeWorktreePathMock.mockReturnValue(createdWorktree.path) ensurePathWithinWorkspaceMock.mockReturnValue(createdWorktree.path) vi.mocked(addWorktree).mockResolvedValueOnce({}) @@ -2392,8 +2403,10 @@ describe('OrcaRuntimeService', () => { if (args[0] === 'rev-parse' && args.includes('--git-common-dir')) { return { stdout: '/tmp/repo/.git\n', stderr: '' } } - // Local remote-tracking base ref resolves -> usable fallback exists. - if (args[0] === 'rev-parse' && args[1] === '--verify') { + if (args[0] === 'rev-parse' && args.includes('refs/remotes/origin/master^{commit}')) { + throw new Error('missing ref') + } + if (args[0] === 'rev-parse' && args.includes('refs/remotes/origin/main^{commit}')) { return { stdout: 'base-sha\n', stderr: '' } } if (args[0] === 'fetch') { @@ -2409,8 +2422,135 @@ describe('OrcaRuntimeService', () => { }) ).resolves.toBeDefined() - expect(addWorktree).toHaveBeenCalled() + expect(addWorktree).toHaveBeenCalledWith( + TEST_REPO_PATH, + createdWorktree.path, + 'cli-refresh-fails', + 'origin/main', + false, + false, + { + remoteTrackingBase: { + remote: 'origin', + branch: 'main', + ref: 'refs/remotes/origin/main', + base: 'origin/main' + }, + suggestLocalBaseRefUpdate: true + } + ) + expect(getDefaultBaseRef).toHaveBeenCalled() } finally { + getReposSpy.mockRestore() + gitSpy.mockRestore() + } + }) + + it('creates a runtime local worktree from a usable persisted local branch base', async () => { + const runtime = new OrcaRuntimeService(store) + const createdWorktree = { + path: '/tmp/workspaces/local-branch-base', + head: 'develop-sha', + branch: 'local-branch-base', + isBare: false, + isMainWorktree: false + } + const repo = { ...store.getRepos()[0], worktreeBaseRef: 'develop' } + const getReposSpy = vi.spyOn(store, 'getRepos').mockReturnValue([repo] as never) + computeWorktreePathMock.mockReturnValue(createdWorktree.path) + ensurePathWithinWorkspaceMock.mockReturnValue(createdWorktree.path) + vi.mocked(addWorktree).mockResolvedValueOnce({}) + vi.mocked(listWorktrees).mockResolvedValue([createdWorktree]) + const gitSpy = vi.spyOn(gitRunner, 'gitExecFileAsync').mockImplementation(async (args) => { + if (args[0] === 'remote') { + return { stdout: 'origin\n', stderr: '' } + } + if (args[0] === 'rev-parse' && args.includes('--git-common-dir')) { + return { stdout: '/tmp/repo/.git\n', stderr: '' } + } + if (args[0] === 'rev-parse' && args.includes('refs/heads/develop^{commit}')) { + return { stdout: 'develop-sha\n', stderr: '' } + } + if (args[0] === 'fetch') { + throw new Error('network unavailable') + } + return { stdout: '', stderr: '' } + }) + try { + await expect( + runtime.createManagedWorktree({ + repoSelector: 'id:repo-1', + name: 'local-branch-base' + }) + ).resolves.toBeDefined() + + expect(addWorktree).toHaveBeenCalledWith( + TEST_REPO_PATH, + createdWorktree.path, + 'local-branch-base', + 'develop', + false + ) + } finally { + getReposSpy.mockRestore() + gitSpy.mockRestore() + } + }) + + it('creates a runtime local worktree from a slash-named local branch matching a remote prefix', async () => { + const runtime = new OrcaRuntimeService(store) + const createdWorktree = { + path: '/tmp/workspaces/slash-local-base', + head: 'team-feature-sha', + branch: 'slash-local-base', + isBare: false, + isMainWorktree: false + } + const repo = { ...store.getRepos()[0], worktreeBaseRef: 'team/feature' } + const getReposSpy = vi.spyOn(store, 'getRepos').mockReturnValue([repo] as never) + computeWorktreePathMock.mockReturnValue(createdWorktree.path) + ensurePathWithinWorkspaceMock.mockReturnValue(createdWorktree.path) + vi.mocked(addWorktree).mockResolvedValueOnce({}) + vi.mocked(listWorktrees).mockResolvedValue([createdWorktree]) + const gitSpy = vi.spyOn(gitRunner, 'gitExecFileAsync').mockImplementation(async (args) => { + if (args[0] === 'remote') { + return { stdout: 'team\norigin\n', stderr: '' } + } + if (args[0] === 'rev-parse' && args.includes('--git-common-dir')) { + return { stdout: '/tmp/repo/.git\n', stderr: '' } + } + if (args[0] === 'rev-parse' && args.includes('refs/remotes/team/feature^{commit}')) { + throw new Error('missing remote-tracking ref') + } + if (args[0] === 'rev-parse' && args.includes('refs/heads/team/feature^{commit}')) { + return { stdout: 'team-feature-sha\n', stderr: '' } + } + if (args[0] === 'fetch') { + throw new Error('network unavailable') + } + return { stdout: '', stderr: '' } + }) + try { + await expect( + runtime.createManagedWorktree({ + repoSelector: 'id:repo-1', + name: 'slash-local-base' + }) + ).resolves.toBeDefined() + + expect(addWorktree).toHaveBeenCalledWith( + TEST_REPO_PATH, + createdWorktree.path, + 'slash-local-base', + 'team/feature', + false + ) + expect(gitSpy).not.toHaveBeenCalledWith( + ['fetch', '--no-tags', 'team', '+refs/heads/feature:refs/remotes/team/feature'], + expect.any(Object) + ) + } finally { + getReposSpy.mockRestore() gitSpy.mockRestore() } }) @@ -3140,6 +3280,9 @@ describe('OrcaRuntimeService', () => { if (args[0] === 'symbolic-ref') { return { stdout: 'origin/main\n', stderr: '' } } + if (isOriginMainBaseRefProbe(args)) { + return { stdout: 'main-sha\n', stderr: '' } + } if (args[0] === 'fetch') { return { stdout: '', stderr: '' } } @@ -3244,6 +3387,9 @@ describe('OrcaRuntimeService', () => { if (args[0] === 'symbolic-ref') { return { stdout: 'origin/main\n', stderr: '' } } + if (isOriginMainBaseRefProbe(args)) { + return { stdout: 'main-sha\n', stderr: '' } + } if (args[0] === 'fetch') { return { stdout: '', stderr: '' } } @@ -3397,6 +3543,9 @@ describe('OrcaRuntimeService', () => { if (args[0] === 'symbolic-ref') { return { stdout: 'origin/main\n', stderr: '' } } + if (isOriginMainBaseRefProbe(args)) { + return { stdout: 'main-sha\n', stderr: '' } + } if (args[0] === 'fetch') { return { stdout: '', stderr: '' } } @@ -3507,6 +3656,9 @@ describe('OrcaRuntimeService', () => { if (args[0] === 'symbolic-ref') { return { stdout: 'origin/main\n', stderr: '' } } + if (isOriginMainBaseRefProbe(args)) { + return { stdout: 'main-sha\n', stderr: '' } + } if (args[0] === 'fetch') { return { stdout: '', stderr: '' } } @@ -3609,6 +3761,9 @@ describe('OrcaRuntimeService', () => { if (args[0] === 'symbolic-ref') { return { stdout: 'origin/main\n', stderr: '' } } + if (isOriginMainBaseRefProbe(args)) { + return { stdout: 'main-sha\n', stderr: '' } + } if (args[0] === 'fetch') { return { stdout: '', stderr: '' } } @@ -3765,6 +3920,9 @@ describe('OrcaRuntimeService', () => { if (args[0] === 'symbolic-ref') { return { stdout: 'origin/main\n', stderr: '' } } + if (isOriginMainBaseRefProbe(args)) { + return { stdout: 'main-sha\n', stderr: '' } + } if (args[0] === 'fetch') { return { stdout: '', stderr: '' } } @@ -4840,6 +4998,9 @@ describe('OrcaRuntimeService', () => { if (args[0] === 'symbolic-ref') { return { stdout: 'refs/remotes/origin/main\n', stderr: '' } } + if (isOriginMainBaseRefProbe(args)) { + return { stdout: 'main-sha\n', stderr: '' } + } if (args[0] === 'remote') { return { stdout: 'origin\n', stderr: '' } } @@ -22332,6 +22493,9 @@ describe('OrcaRuntimeService', () => { if (args[0] === 'symbolic-ref') { return { stdout: 'origin/main\n', stderr: '' } } + if (isOriginMainBaseRefProbe(args)) { + return { stdout: 'main-sha\n', stderr: '' } + } if (args[0] === 'fetch') { return { stdout: '', stderr: '' } } @@ -22425,6 +22589,9 @@ describe('OrcaRuntimeService', () => { if (args[0] === 'symbolic-ref') { return { stdout: 'origin/main\n', stderr: '' } } + if (isOriginMainBaseRefProbe(args)) { + return { stdout: 'main-sha\n', stderr: '' } + } if (args[0] === 'fetch') { return { stdout: '', stderr: '' } } @@ -22535,6 +22702,9 @@ describe('OrcaRuntimeService', () => { if (args[0] === 'symbolic-ref') { return { stdout: 'origin/main\n', stderr: '' } } + if (isOriginMainBaseRefProbe(args)) { + return { stdout: 'main-sha\n', stderr: '' } + } if (args[0] === 'fetch') { return { stdout: '', stderr: '' } } @@ -23216,6 +23386,9 @@ describe('OrcaRuntimeService', () => { if (args[0] === 'symbolic-ref') { return { stdout: 'refs/remotes/origin/main\n', stderr: '' } } + if (isOriginMainBaseRefProbe(args)) { + return { stdout: 'main-sha\n', stderr: '' } + } if (args[0] === 'config') { return { stdout: 'origin\n', stderr: '' } } diff --git a/src/main/runtime/orca-runtime.ts b/src/main/runtime/orca-runtime.ts index 8c53bbd9964..12d6c3e9f37 100644 --- a/src/main/runtime/orca-runtime.ts +++ b/src/main/runtime/orca-runtime.ts @@ -46,6 +46,8 @@ import { createHash, randomUUID } from 'node:crypto' import { homedir } from 'node:os' import { isAbsolute, join, resolve } from 'node:path' import { mkdir, readFile, readdir, rm, stat } from 'node:fs/promises' +import { resolveWorktreeCreateBase } from '../worktree-create-base' +import { resolveWorktreeAddBaseRef } from '../../shared/worktree-base-ref' import { OrchestrationDb } from './orchestration/db' import { formatMessagesForInjection } from './orchestration/formatter' import type { @@ -581,7 +583,8 @@ import { getRemoteDrift, getRecentDriftSubjects } from '../git/repo' -import { hasLocalCommitObject } from '../git/commit-object-ref' +import { hasCommitObjectViaGitExec } from '../git/commit-object-ref' +import { hasWorktreeBaseCommitRef } from '../git/worktree-base-ref-probe' import { resolveLocalGitUsername } from '../git/git-username' import { listWorktrees, @@ -1963,6 +1966,26 @@ type LayoutQueueEntry = { }[] } +async function hasLocalWorktreeBaseRef( + repoPath: string, + baseRef: string, + options: { wslDistro?: string } = {} +): Promise { + const refExists = (qualifiedRef: string) => + hasWorktreeBaseCommitRef(repoPath, qualifiedRef, options) + const resolvedBaseRef = await resolveWorktreeAddBaseRef(baseRef, refExists) + if (resolvedBaseRef !== baseRef) { + return true + } + if (baseRef.startsWith('refs/')) { + return refExists(baseRef) + } + return hasCommitObjectViaGitExec( + (gitArgs) => gitExecFileAsync(gitArgs, { cwd: repoPath, ...options }), + baseRef + ) +} + export class OrcaRuntimeService { private readonly runtimeId = randomUUID() private readonly startedAt = Date.now() @@ -12992,12 +13015,42 @@ export class OrcaRuntimeService { let effectiveSanitizedName = sanitizedName const username = await resolveLocalGitUsername(repo.path) - const baseBranch = - args.baseBranch || - repo.worktreeBaseRef || - (hasLocalWorktreeGitOptions - ? await resolveDefaultBaseRefViaExec((argv) => gitExecFileAsync(argv, localGitExecOptions)) - : getDefaultBaseRef(repo.path)) + const baseBranch = await resolveWorktreeCreateBase({ + requestedBaseBranch: args.baseBranch, + repoWorktreeBaseRef: repo.worktreeBaseRef, + resolveDefaultBaseRef: () => + hasLocalWorktreeGitOptions + ? resolveDefaultBaseRefViaExec((argv) => gitExecFileAsync(argv, localGitExecOptions)) + : Promise.resolve(getDefaultBaseRef(repo.path)), + isBaseUsable: async (baseBranchCandidate) => { + const remoteTrackingBase = await this.resolveRemoteTrackingBase( + repo.path, + baseBranchCandidate, + ...localWorktreeGitOptionArgs + ) + if (remoteTrackingBase) { + if ( + await this.hasRemoteTrackingRef( + repo.path, + remoteTrackingBase, + ...localWorktreeGitOptionArgs + ) + ) { + return true + } + return hasLocalWorktreeBaseRef( + repo.path, + baseBranchCandidate, + hasLocalWorktreeGitOptions ? localWorktreeGitOptions : {} + ) + } + return hasLocalWorktreeBaseRef( + repo.path, + baseBranchCandidate, + hasLocalWorktreeGitOptions ? localWorktreeGitOptions : {} + ) + } + }) if (!baseBranch) { // Why: getDefaultBaseRef returns null when no suitable ref exists. // Don't fabricate 'origin/main' — passing it to addWorktree would @@ -13128,44 +13181,61 @@ export class OrcaRuntimeService { `Could not find an available worktree path for "${sanitizedName}". Pick a different worktree name.` ) } - const remoteTrackingBase = await this.resolveRemoteTrackingBase( + let remoteTrackingBase = await this.resolveRemoteTrackingBase( repo.path, baseBranch, ...localWorktreeGitOptionArgs ) if (remoteTrackingBase) { - const hadLocalBaseRef = await this.hasRemoteTrackingRef( + const hadRemoteTrackingBaseRef = await this.hasRemoteTrackingRef( repo.path, remoteTrackingBase, ...localWorktreeGitOptionArgs ) - const refreshResult = await this.getOrStartRemoteTrackingBaseRefresh( - repo.path, - remoteTrackingBase, - ...localWorktreeGitOptionArgs - ) - if (!refreshResult.ok && !hadLocalBaseRef) { - // Why: only block creation when the refresh failed AND there is no - // usable local base ref to fall back on. If a local remote-tracking ref - // already exists, `git worktree add` can create from it — a possibly - // stale but valid base — so a transient offline/auth failure must not - // make the workspace uncreatable. The compare-to-base view reflects any - // drift once the remote is reachable again. - throw new Error( - `Could not refresh base ref "${baseBranch}" from "${remoteTrackingBase.remote}". Check your network and try again.` - ) - } - if ( - !hadLocalBaseRef && - !(await this.hasRemoteTrackingRef( + const hasLocalBaseRef = + hadRemoteTrackingBaseRef || + (await hasLocalWorktreeBaseRef( + repo.path, + baseBranch, + hasLocalWorktreeGitOptions ? localWorktreeGitOptions : {} + )) + if (!hadRemoteTrackingBaseRef && hasLocalBaseRef) { + remoteTrackingBase = null + } else { + const refreshResult = await this.getOrStartRemoteTrackingBaseRefresh( repo.path, remoteTrackingBase, ...localWorktreeGitOptionArgs - )) - ) { - throw new Error(`Base ref "${baseBranch}" was not found after fetching.`) + ) + if (!refreshResult.ok && !hadRemoteTrackingBaseRef) { + // Why: only block creation when the refresh failed AND there is no + // usable local base ref to fall back on. If a local remote-tracking ref + // already exists, `git worktree add` can create from it — a possibly + // stale but valid base — so a transient offline/auth failure must not + // make the workspace uncreatable. The compare-to-base view reflects any + // drift once the remote is reachable again. + throw new Error( + `Could not refresh base ref "${baseBranch}" from "${remoteTrackingBase.remote}". Check your network and try again.` + ) + } + if ( + !hadRemoteTrackingBaseRef && + !(await this.hasRemoteTrackingRef( + repo.path, + remoteTrackingBase, + ...localWorktreeGitOptionArgs + )) + ) { + throw new Error(`Base ref "${baseBranch}" was not found after fetching.`) + } } - } else if (!(await hasLocalCommitObject(repo.path, baseBranch))) { + } else if ( + !(await hasLocalWorktreeBaseRef( + repo.path, + baseBranch, + hasLocalWorktreeGitOptions ? localWorktreeGitOptions : {} + )) + ) { // Why: local bases keep legacy best-effort fetch behavior. Verified PR // SHA bases already have the commit object needed by `git worktree add`. try { diff --git a/src/main/worktree-create-base-prefetch.ts b/src/main/worktree-create-base-prefetch.ts index 24c22837c22..42add57be66 100644 --- a/src/main/worktree-create-base-prefetch.ts +++ b/src/main/worktree-create-base-prefetch.ts @@ -1,9 +1,12 @@ import { isFolderRepo } from '../shared/repo-kind' import type { Repo } from '../shared/types' -import { hasLocalCommitObject } from './git/commit-object-ref' +import { hasLocalCommitObject, isFullGitObjectId } from './git/commit-object-ref' +import { hasWorktreeBaseCommitRef } from './git/worktree-base-ref-probe' import { getDefaultBaseRef } from './git/repo' import { getSshGitProvider } from './providers/ssh-git-dispatch' import { prefetchRemoteWorktreeCreateBase } from './ipc/worktree-remote' +import { resolveWorktreeCreateBase } from './worktree-create-base' +import { resolveWorktreeAddBaseRef } from '../shared/worktree-base-ref' type RemoteTrackingBaseForPrefetch = { remote: string @@ -17,6 +20,7 @@ type WorktreeCreateBasePrefetchRuntime = { repoPath: string, baseBranch: string ) => Promise + hasRemoteTrackingRef: (repoPath: string, base: RemoteTrackingBaseForPrefetch) => Promise getOrStartRemoteTrackingBaseRefresh: ( repoPath: string, base: RemoteTrackingBaseForPrefetch @@ -24,23 +28,62 @@ type WorktreeCreateBasePrefetchRuntime = { fetchRemoteWithCache: (repoPath: string, remote: string) => Promise } +async function hasLocalWorktreeBaseRef(repoPath: string, baseRef: string): Promise { + const refExists = (qualifiedRef: string) => hasWorktreeBaseCommitRef(repoPath, qualifiedRef) + const resolvedBaseRef = await resolveWorktreeAddBaseRef(baseRef, refExists) + if (resolvedBaseRef !== baseRef) { + return true + } + if (baseRef.startsWith('refs/')) { + return refExists(baseRef) + } + return hasLocalCommitObject(repoPath, baseRef) +} + async function prefetchLocalWorktreeCreateBase( repo: Repo, baseBranch: string | undefined, runtime: WorktreeCreateBasePrefetchRuntime ): Promise { - const resolvedBaseBranch = baseBranch || repo.worktreeBaseRef || getDefaultBaseRef(repo.path) + const resolvedBaseBranch = await resolveWorktreeCreateBase({ + requestedBaseBranch: baseBranch, + repoWorktreeBaseRef: repo.worktreeBaseRef, + resolveDefaultBaseRef: async () => getDefaultBaseRef(repo.path), + isBaseUsable: async (baseBranchCandidate) => { + const remoteTrackingBase = await runtime.resolveRemoteTrackingBase( + repo.path, + baseBranchCandidate + ) + if (remoteTrackingBase) { + if (await runtime.hasRemoteTrackingRef(repo.path, remoteTrackingBase)) { + return true + } + return hasLocalWorktreeBaseRef(repo.path, baseBranchCandidate) + } + return hasLocalWorktreeBaseRef(repo.path, baseBranchCandidate) + } + }) if (!resolvedBaseBranch) { return } - if (await hasLocalCommitObject(repo.path, resolvedBaseBranch)) { - // Why: hosted-review start points can be verified commit SHAs; a broad - // remote fetch cannot make an already-local object fresher. + if ( + isFullGitObjectId(resolvedBaseBranch) && + (await hasLocalWorktreeBaseRef(repo.path, resolvedBaseBranch)) + ) { return } const remoteTrackingBase = await runtime.resolveRemoteTrackingBase(repo.path, resolvedBaseBranch) if (remoteTrackingBase) { - await runtime.getOrStartRemoteTrackingBaseRefresh(repo.path, remoteTrackingBase) + if ( + (await runtime.hasRemoteTrackingRef(repo.path, remoteTrackingBase)) || + !(await hasLocalWorktreeBaseRef(repo.path, resolvedBaseBranch)) + ) { + await runtime.getOrStartRemoteTrackingBaseRefresh(repo.path, remoteTrackingBase) + return + } + } + if (await hasLocalWorktreeBaseRef(repo.path, resolvedBaseBranch)) { + // Why: hosted-review start points and local branch bases are already local; a broad remote fetch cannot make them fresher. return } diff --git a/src/main/worktree-create-base.test.ts b/src/main/worktree-create-base.test.ts new file mode 100644 index 00000000000..168cae3441c --- /dev/null +++ b/src/main/worktree-create-base.test.ts @@ -0,0 +1,53 @@ +import { describe, expect, it, vi } from 'vitest' +import { resolveWorktreeCreateBase } from './worktree-create-base' + +describe('resolveWorktreeCreateBase', () => { + it('falls back from a stale persisted base to the detected default', async () => { + const resolveDefaultBaseRef = vi.fn().mockResolvedValue('origin/main') + const isBaseUsable = vi.fn().mockResolvedValue(false) + + await expect( + resolveWorktreeCreateBase({ + repoWorktreeBaseRef: 'origin/master', + resolveDefaultBaseRef, + isBaseUsable + }) + ).resolves.toBe('origin/main') + + expect(resolveDefaultBaseRef).toHaveBeenCalledTimes(1) + expect(isBaseUsable).toHaveBeenCalledWith('origin/master') + }) + + it('returns an explicit base without probing defaults or usability', async () => { + const resolveDefaultBaseRef = vi.fn() + const isBaseUsable = vi.fn() + + await expect( + resolveWorktreeCreateBase({ + requestedBaseBranch: 'origin/master', + repoWorktreeBaseRef: 'origin/main', + resolveDefaultBaseRef, + isBaseUsable + }) + ).resolves.toBe('origin/master') + + expect(resolveDefaultBaseRef).not.toHaveBeenCalled() + expect(isBaseUsable).not.toHaveBeenCalled() + }) + + it('keeps a usable persisted base when it is still valid', async () => { + const resolveDefaultBaseRef = vi.fn().mockResolvedValue('origin/main') + const isBaseUsable = vi.fn().mockResolvedValue(true) + + await expect( + resolveWorktreeCreateBase({ + repoWorktreeBaseRef: 'origin/master', + resolveDefaultBaseRef, + isBaseUsable + }) + ).resolves.toBe('origin/master') + + expect(resolveDefaultBaseRef).toHaveBeenCalledTimes(1) + expect(isBaseUsable).toHaveBeenCalledWith('origin/master') + }) +}) diff --git a/src/main/worktree-create-base.ts b/src/main/worktree-create-base.ts new file mode 100644 index 00000000000..67f0427b392 --- /dev/null +++ b/src/main/worktree-create-base.ts @@ -0,0 +1,29 @@ +type ResolveWorktreeCreateBaseArgs = { + requestedBaseBranch?: string + repoWorktreeBaseRef?: string | null + resolveDefaultBaseRef: () => Promise + isBaseUsable: (baseBranch: string) => Promise +} + +export async function resolveWorktreeCreateBase( + args: ResolveWorktreeCreateBaseArgs +): Promise { + if (args.requestedBaseBranch) { + return args.requestedBaseBranch + } + + const defaultBaseRef = await args.resolveDefaultBaseRef() + const repoWorktreeBaseRef = args.repoWorktreeBaseRef + if (!repoWorktreeBaseRef) { + return defaultBaseRef + } + if (!defaultBaseRef) { + return (await args.isBaseUsable(repoWorktreeBaseRef)) ? repoWorktreeBaseRef : null + } + // Resolving the default already proved matching persisted refs exist. + if (repoWorktreeBaseRef === defaultBaseRef) { + return repoWorktreeBaseRef + } + // Stale persisted refs fall back to the detected default; usable custom refs stay authoritative. + return (await args.isBaseUsable(repoWorktreeBaseRef)) ? repoWorktreeBaseRef : defaultBaseRef +} diff --git a/src/renderer/src/hooks/useComposerState-host-context-boundaries.test.ts b/src/renderer/src/hooks/useComposerState-host-context-boundaries.test.ts index c43a051c8ab..00aa3fe4fea 100644 --- a/src/renderer/src/hooks/useComposerState-host-context-boundaries.test.ts +++ b/src/renderer/src/hooks/useComposerState-host-context-boundaries.test.ts @@ -512,8 +512,8 @@ describe('useComposerState host-context boundaries', () => { expect(section).toContain('resolveWorktreeCreateBaseBranch') expect(section).toContain('explicitBaseBranch: smartSubmitBaseBranch') - expect(section).toContain('repoWorktreeBaseRef: selectedRepo.worktreeBaseRef') - expect(section).toContain('getRuntimeRepoBaseRefDefault') + expect(section).not.toContain('repoWorktreeBaseRef: selectedRepo.worktreeBaseRef') + expect(section).not.toContain('getRuntimeRepoBaseRefDefault') }) it('plans new workspace agent startup from the selected repo runtime', () => { diff --git a/src/renderer/src/hooks/useComposerState.ts b/src/renderer/src/hooks/useComposerState.ts index dc6de85382b..33049241411 100644 --- a/src/renderer/src/hooks/useComposerState.ts +++ b/src/renderer/src/hooks/useComposerState.ts @@ -29,7 +29,6 @@ import { import { tuiAgentToAgentKind } from '@/lib/telemetry' import { isGitRepoKind } from '../../../shared/repo-kind' import { callRuntimeRpc, getActiveRuntimeTarget } from '@/runtime/runtime-rpc-client' -import { getRuntimeRepoBaseRefDefault } from '@/runtime/runtime-repo-client' import { resolveWorktreeCreateBaseBranch } from '@/runtime/worktree-create-base' import { buildTaskSourceContextFromRepo, @@ -3971,11 +3970,7 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS }) const submitBaseBranch = selectedRepoIsGit ? await resolveWorktreeCreateBaseBranch({ - explicitBaseBranch: smartSubmitBaseBranch, - repoWorktreeBaseRef: selectedRepo.worktreeBaseRef, - loadDefaultBaseRef: async () => - (await getRuntimeRepoBaseRefDefault(selectedRepoSettings, repoId).catch(() => null)) - ?.defaultBaseRef + explicitBaseBranch: smartSubmitBaseBranch }) : undefined const createDisplayName = diff --git a/src/renderer/src/runtime/worktree-create-base.test.ts b/src/renderer/src/runtime/worktree-create-base.test.ts index 67f74558b3b..2ece667beef 100644 --- a/src/renderer/src/runtime/worktree-create-base.test.ts +++ b/src/renderer/src/runtime/worktree-create-base.test.ts @@ -1,42 +1,28 @@ -import { describe, expect, it, vi } from 'vitest' +import { describe, expect, it } from 'vitest' import { resolveWorktreeCreateBaseBranch } from './worktree-create-base' describe('resolveWorktreeCreateBaseBranch', () => { - it('uses an explicit Start-from selection before repo defaults', async () => { - const loadDefaultBaseRef = vi.fn().mockResolvedValue('origin/main') - + it('uses an explicit Start-from selection', async () => { await expect( resolveWorktreeCreateBaseBranch({ - explicitBaseBranch: 'origin/feature', - repoWorktreeBaseRef: 'dev', - loadDefaultBaseRef + explicitBaseBranch: ' origin/feature ' }) ).resolves.toBe('origin/feature') - - expect(loadDefaultBaseRef).not.toHaveBeenCalled() }) - it('uses the pinned repo worktree base before resolving the git primary', async () => { - const loadDefaultBaseRef = vi.fn().mockResolvedValue('origin/main') - + it('omits repo defaults so backend create owns base selection', async () => { await expect( resolveWorktreeCreateBaseBranch({ - explicitBaseBranch: undefined, - repoWorktreeBaseRef: ' dev ', - loadDefaultBaseRef + explicitBaseBranch: undefined }) - ).resolves.toBe('dev') - - expect(loadDefaultBaseRef).not.toHaveBeenCalled() + ).resolves.toBeUndefined() }) - it('falls back to the git primary when no explicit or pinned base exists', async () => { + it('omits blank explicit selections', async () => { await expect( resolveWorktreeCreateBaseBranch({ - explicitBaseBranch: undefined, - repoWorktreeBaseRef: undefined, - loadDefaultBaseRef: vi.fn().mockResolvedValue('origin/main') + explicitBaseBranch: ' ' }) - ).resolves.toBe('origin/main') + ).resolves.toBeUndefined() }) }) diff --git a/src/renderer/src/runtime/worktree-create-base.ts b/src/renderer/src/runtime/worktree-create-base.ts index cb3f2fc62f8..d182e1c90a3 100644 --- a/src/renderer/src/runtime/worktree-create-base.ts +++ b/src/renderer/src/runtime/worktree-create-base.ts @@ -1,15 +1,6 @@ +// Backend base resolution owns stale-ref fallback, so the renderer only forwards explicit selections. export async function resolveWorktreeCreateBaseBranch(args: { explicitBaseBranch: string | undefined - repoWorktreeBaseRef: string | undefined - loadDefaultBaseRef: () => Promise }): Promise { - if (args.explicitBaseBranch) { - return args.explicitBaseBranch - } - const pinnedBaseRef = args.repoWorktreeBaseRef?.trim() - if (pinnedBaseRef) { - return pinnedBaseRef - } - const defaultBaseRef = (await args.loadDefaultBaseRef())?.trim() - return defaultBaseRef || undefined + return args.explicitBaseBranch?.trim() || undefined } From 863d94167c84cd403d423b86f9e7c9471214aca0 Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Sat, 4 Jul 2026 22:19:28 -0700 Subject: [PATCH 42/45] Use activeWorkspaceKey to reveal active workspace in sidebar (#7406) Older folder-based workspaces are tracked by `activeWorkspaceKey` rather than the legacy `activeWorktreeId`. Deriving the active sidebar workspace ID from the workspace key enables the "Reveal active workspace" action to work correctly for both types of workspaces. --- .../src/components/sidebar/WorktreeList.tsx | 34 ++- tests/e2e/worktree-scroll-to-current.spec.ts | 264 +++++++++++++++++- 2 files changed, 282 insertions(+), 16 deletions(-) diff --git a/src/renderer/src/components/sidebar/WorktreeList.tsx b/src/renderer/src/components/sidebar/WorktreeList.tsx index 2320889a3e3..dbf77ecf0f9 100644 --- a/src/renderer/src/components/sidebar/WorktreeList.tsx +++ b/src/renderer/src/components/sidebar/WorktreeList.tsx @@ -268,7 +268,7 @@ import { buildSidebarHostOptions } from './sidebar-host-options' import { HostSectionHeaderMenu } from './HostSectionHeaderMenu' import { ProjectHeaderActions } from './ProjectHeaderActions' import { translate } from '@/i18n/i18n' -import { folderWorkspaceKey } from '../../../../shared/workspace-scope' +import { folderWorkspaceKey, parseWorkspaceKey } from '../../../../shared/workspace-scope' import { getHostDisplayLabelOverrides } from '../../../../shared/host-setting-overrides' import { isConfirmedStaleFolderPathStatus, @@ -405,6 +405,22 @@ function getWorktreeOptionId(rowKey: string): string { return `worktree-list-option-${encodeURIComponent(rowKey)}` } +// Why: folder workspaces are tracked by the scoped active key, while older +// worktree-only paths still read activeWorktreeId. +function getActiveSidebarWorkspaceId( + activeWorkspaceKey: string | null, + activeWorktreeId: string | null +): string | null { + const scope = activeWorkspaceKey ? parseWorkspaceKey(activeWorkspaceKey) : null + if (scope?.type === 'folder') { + return folderWorkspaceKey(scope.folderWorkspaceId) + } + if (scope?.type === 'worktree') { + return scope.worktreeId + } + return activeWorktreeId +} + function getMountedWorktreeOptions(worktreeId: string, root?: ParentNode | null): HTMLElement[] { const scope = root ?? document const result: HTMLElement[] = [] @@ -5151,7 +5167,11 @@ const WorktreeList = React.memo(function WorktreeList({ const worktreesByRepo = useAppStore((s) => s.worktreesByRepo) const detectedWorktreesByRepo = useAppStore((s) => s.detectedWorktreesByRepo) const activeWorktreeId = useAppStore((s) => s.activeWorktreeId) - const currentSidebarWorktreeId = activeWorktreeId + const activeWorkspaceKey = useAppStore((s) => s.activeWorkspaceKey) + const currentSidebarWorktreeId = useMemo( + () => getActiveSidebarWorkspaceId(activeWorkspaceKey, activeWorktreeId), + [activeWorkspaceKey, activeWorktreeId] + ) const groupBy = useAppStore((s) => s.groupBy) const setGroupBy = useAppStore((s) => s.setGroupBy) const workspaceHostScope = useAppStore((s) => s.workspaceHostScope) @@ -6642,31 +6662,31 @@ const WorktreeList = React.memo(function WorktreeList({ }) return } - if (!activeWorktreeId) { + if (!currentSidebarWorktreeId) { return } const activeWorktree = getKnownSidebarWorktreeById( - activeWorktreeId, + currentSidebarWorktreeId, worktreeMap, folderWorkspaces ) if (!activeWorktree || activeWorktree.isArchived) { return } - if (!renderedWorktreeIds.includes(activeWorktreeId)) { + if (!renderedWorktreeIds.includes(currentSidebarWorktreeId)) { // Why: the toolbar action promises to reveal the current workspace; when // sidebar filters hide it, relax those filters before queuing the reveal. clearFilters() } - revealWorktreeInSidebar(activeWorktreeId, { + revealWorktreeInSidebar(currentSidebarWorktreeId, { behavior: 'smooth', highlight: true, beginRename: (detail as { beginRename?: boolean } | undefined)?.beginRename === true }) }, [ - activeWorktreeId, clearFilters, + currentSidebarWorktreeId, folderWorkspaces, revealSidebarRow, renderedWorktreeIds, diff --git a/tests/e2e/worktree-scroll-to-current.spec.ts b/tests/e2e/worktree-scroll-to-current.spec.ts index 2cbd0df4a08..91ae5e88142 100644 --- a/tests/e2e/worktree-scroll-to-current.spec.ts +++ b/tests/e2e/worktree-scroll-to-current.spec.ts @@ -1,11 +1,6 @@ import type { Page } from '@stablyai/playwright-test' import { test, expect } from './helpers/orca-app' import { waitForActiveWorktree, waitForSessionReady } from './helpers/store' -import { worktreeRow } from './worktree-row-locators' - -function worktreeOption(page: Page, worktreeId: string) { - return worktreeRow(page, worktreeId) -} async function prepareSidebarForScrollTest(page: Page): Promise { await page.evaluate(() => { @@ -117,7 +112,9 @@ test.describe('Reveal active workspace button', () => { throw new Error('Bottom workspace row did not expose a data-worktree-id') } - const targetRow = worktreeOption(orcaPage, targetId) + const targetRow = orcaPage + .locator(`[data-worktree-sidebar] [data-worktree-id=${JSON.stringify(targetId)}]`) + .first() const revealButton = orcaPage.getByRole('button', { name: 'Reveal active workspace' }) await renderedOptions.last().click() @@ -175,10 +172,31 @@ test.describe('Reveal active workspace button', () => { throw new Error('Bottom workspace row did not expose a data-worktree-id') } - const targetRow = worktreeOption(orcaPage, targetId) + const targetRows = orcaPage.locator( + `[data-worktree-sidebar] [data-worktree-id=${JSON.stringify(targetId)}]` + ) + const targetRow = targetRows.first() const revealButton = orcaPage.getByRole('button', { name: 'Reveal active workspace' }) - await renderedOptions.last().click() + await orcaPage.evaluate((targetId) => { + const store = window.__store + if (!store) { + throw new Error('window.__store is not available') + } + const state = store.getState() + const target = Object.values(state.worktreesByRepo) + .flat() + .find((worktree) => worktree.id === targetId) + if (!target) { + throw new Error(`Target workspace ${targetId} not found`) + } + store.setState({ + activeRepoId: target.repoId, + activeWorktreeId: target.id, + activeWorkspaceKey: `worktree:${target.id}`, + pendingRevealWorktree: null + }) + }, targetId) await expect(targetRow).toHaveAttribute('aria-current', 'page') await orcaPage.evaluate(() => { @@ -190,7 +208,7 @@ test.describe('Reveal active workspace button', () => { }) await expect(renderedOptions).toHaveCount(0) - await expect(orcaPage.getByText('No workspaces found')).toBeVisible() + await expect(targetRows).toHaveCount(0) await revealButton.click() @@ -213,4 +231,232 @@ test.describe('Reveal active workspace button', () => { ) .toEqual([]) }) + + test('reveals the current workspace when it starts outside the virtualized window', async ({ + orcaPage + }) => { + await prepareSidebarForScrollTest(orcaPage) + + const targetId = await orcaPage.evaluate(() => { + const store = window.__store + if (!store) { + throw new Error('window.__store is not available') + } + + const state = store.getState() + const repo = state.repos[0] + if (!repo) { + throw new Error('Expected a seeded e2e repo') + } + + const now = Date.now() + const seededWorktrees = state.worktreesByRepo[repo.id] ?? [] + const syntheticWorktrees = Array.from({ length: 60 }, (_, index) => { + const suffix = String(index).padStart(2, '0') + return { + id: `${repo.id}::/virtual-reveal-${suffix}`, + instanceId: `virtual-reveal-${suffix}`, + repoId: repo.id, + path: `${repo.path}/../virtual-reveal-${suffix}`, + displayName: `Virtual reveal ${suffix}`, + comment: '', + linkedIssue: null, + linkedPR: null, + linkedLinearIssue: null, + isArchived: false, + isUnread: false, + isPinned: false, + sortOrder: 10_000 - index, + lastActivityAt: now - index - 100, + head: '0000000000000000000000000000000000000000', + branch: `virtual-reveal-${suffix}`, + isBare: false, + isMainWorktree: false + } + }) + const target = syntheticWorktrees.at(-1) + if (!target) { + throw new Error('Expected a synthetic target worktree') + } + + store.setState({ + activeRepoId: repo.id, + activeWorktreeId: target.id, + activeWorkspaceKey: `worktree:${target.id}`, + pendingRevealWorktree: null, + sortBy: 'manual', + worktreesByRepo: { + ...state.worktreesByRepo, + [repo.id]: [...seededWorktrees, ...syntheticWorktrees] + } + }) + return target.id + }) + + const scroller = orcaPage.locator('[data-worktree-sidebar]') + await expect + .poll( + () => + orcaPage.evaluate(() => { + const scroller = document.querySelector('[data-worktree-sidebar]') + return scroller?.scrollTop ?? null + }), + { timeout: 10_000, message: 'sidebar scroller did not mount' } + ) + .not.toBeNull() + + await scroller.evaluate((element) => { + element.scrollTop = 0 + element.dispatchEvent(new Event('scroll', { bubbles: true })) + }) + await expect + .poll( + () => + orcaPage.evaluate((targetId) => { + const scroller = document.querySelector('[data-worktree-sidebar]') + const target = [...document.querySelectorAll('[data-worktree-id]')].find( + (candidate) => candidate.dataset.worktreeId === targetId + ) + if (!scroller || !target) { + return false + } + const scrollerBounds = scroller.getBoundingClientRect() + const targetBounds = target.getBoundingClientRect() + return ( + targetBounds.top >= scrollerBounds.top - 1 && + targetBounds.bottom <= scrollerBounds.bottom + 1 + ) + }, targetId), + { timeout: 10_000, message: 'target workspace should start outside the sidebar viewport' } + ) + .toBe(false) + + const revealButton = orcaPage.getByRole('button', { name: 'Reveal active workspace' }) + await expect(revealButton).toBeVisible() + await expect(revealButton).toBeEnabled() + + await revealButton.click() + const targetRow = orcaPage + .locator(`[data-worktree-sidebar] [data-worktree-id=${JSON.stringify(targetId)}]`) + .first() + await expect(targetRow).toBeVisible() + await expect(targetRow).toHaveAttribute('data-scroll-reveal-highlight', 'true') + }) + + test('uses the active workspace key when the legacy active worktree id is not set', async ({ + orcaPage + }) => { + await prepareSidebarForScrollTest(orcaPage) + + const folderWorktreeId = await orcaPage.evaluate(() => { + const store = window.__store + if (!store) { + throw new Error('window.__store is not available') + } + + const now = Date.now() + const folderWorkspaces = Array.from({ length: 36 }, (_, index) => { + const suffix = String(index).padStart(2, '0') + return { + id: `reveal-folder-workspace-${suffix}`, + projectGroupId: 'reveal-folder-group', + name: `Reveal folder workspace ${suffix}`, + folderPath: `/tmp/reveal-folder-workspace-${suffix}`, + linkedTask: null, + comment: '', + isArchived: false, + isUnread: false, + isPinned: false, + sortOrder: 1_000 - index, + lastActivityAt: now - index, + createdAt: now, + updatedAt: now + } + }) + const targetFolder = folderWorkspaces.at(-1) + if (!targetFolder) { + throw new Error('Expected a target folder workspace') + } + const folderWorktreeId = `folder:${targetFolder.id}` + store.setState({ + activeRepoId: null, + activeWorktreeId: null, + activeWorkspaceKey: folderWorktreeId, + projectGroups: [ + { + id: 'reveal-folder-group', + name: 'Reveal folder group', + parentPath: '/tmp/reveal-folder-group', + parentGroupId: null, + createdFrom: 'manual', + tabOrder: 1, + isCollapsed: false, + color: null, + createdAt: now, + updatedAt: now + } + ], + folderWorkspaces + }) + store.getState().setGroupBy('repo') + return folderWorktreeId + }) + + const scroller = orcaPage.locator('[data-worktree-sidebar]') + await scroller.evaluate((element) => { + element.scrollTop = 0 + element.dispatchEvent(new Event('scroll', { bubbles: true })) + }) + await expect + .poll( + () => + orcaPage.evaluate((targetId) => { + const scroller = document.querySelector('[data-worktree-sidebar]') + const target = [...document.querySelectorAll('[data-worktree-id]')].find( + (candidate) => candidate.dataset.worktreeId === targetId + ) + if (!scroller || !target) { + return false + } + const scrollerBounds = scroller.getBoundingClientRect() + const targetBounds = target.getBoundingClientRect() + return ( + targetBounds.top >= scrollerBounds.top - 1 && + targetBounds.bottom <= scrollerBounds.bottom + 1 + ) + }, folderWorktreeId), + { timeout: 10_000, message: 'target folder workspace should start outside the viewport' } + ) + .toBe(false) + + const revealButton = orcaPage.getByRole('button', { name: 'Reveal active workspace' }) + await expect(revealButton).toBeVisible() + await expect(revealButton).toBeEnabled() + await revealButton.click() + + await expect + .poll( + () => + orcaPage.evaluate((targetId) => { + const scroller = document.querySelector('[data-worktree-sidebar]') + const target = [...document.querySelectorAll('[data-worktree-id]')].find( + (candidate) => candidate.dataset.worktreeId === targetId + ) + if (!scroller || !target) { + return false + } + const scrollerBounds = scroller.getBoundingClientRect() + const targetBounds = target.getBoundingClientRect() + return ( + targetBounds.top >= scrollerBounds.top - 1 && + targetBounds.bottom <= scrollerBounds.bottom + 1 + ) + }, folderWorktreeId), + { + timeout: 10_000, + message: 'Reveal button should scroll to the active folder workspace' + } + ) + .toBe(true) + }) }) From 331a30b4542bf355e3c5ab717239623f8c14d7c0 Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Sat, 4 Jul 2026 22:50:43 -0700 Subject: [PATCH 43/45] Harden rate-limit PTY working directory and abort background usage probes on stop (#7403) * Harden rate-limit PTY cwd and abort background usage probes on stop Resolve a runaway CPU incident where background Claude rate-limit usage probes could inherit a root working directory (such as `/` or `C:\`) and index the entire disk. - Force hidden Claude and Codex usage PTYs to spawn inside a small, dedicated directory under user data or temp. - Thread AbortSignal through rate-limit fetchers to immediately kill and reap in-flight PTY processes when RateLimitService stops. - Harden daemon and local PTY defaults to reject root-like working directories for automated agent startups. * Consolidate PTY path safety checks to prevent runaway CPU Centralize `isRootLikePath` into a dedicated PTY path safety module to prevent terminal launches and hidden usage probes from executing in filesystem or drive roots, avoiding unbounded file discovery. - Centralize path validation supporting POSIX, Windows, and UNC roots. - Fail loudly if no safe default working directory is available. - Enforce bounded directories for hidden usage PTYs and WSL probes. - Check for aborted signals during rate-limit CLI repairs. * Gate agent startup on the effective PTY cwd after default fallback Co-authored-by: Orca * test(repos): verify origin/HEAD target in SSH base-ref tests Co-authored-by: Orca * Pin HOME to assert exact default PTY cwd in spawn test Pin `process.env.HOME` in the fallback test for LocalPtyProvider to assert the exact resolved candidate rather than just checking that it is non-root-like. This catches potential regressions where an unintended home directory is picked, and uses a try/finally block to safely restore the environment afterwards. --------- Co-authored-by: Orca --- src/main/daemon/pty-subprocess.test.ts | 97 ++++++ src/main/daemon/pty-subprocess.ts | 42 +-- src/main/ipc/repos-remote.test.ts | 12 + src/main/providers/local-pty-provider.test.ts | 47 +++ src/main/providers/local-pty-provider.ts | 29 +- src/main/providers/pty-default-cwd.test.ts | 77 +++++ src/main/providers/pty-default-cwd.ts | 43 +++ src/main/providers/pty-path-safety.ts | 17 + src/main/rate-limits/claude-fetcher.ts | 116 ++++++- src/main/rate-limits/claude-pty.test.ts | 41 +++ src/main/rate-limits/claude-pty.ts | 72 +++- src/main/rate-limits/codex-fetcher.test.ts | 139 ++++++-- src/main/rate-limits/codex-fetcher.ts | 131 ++++++- .../rate-limits/hidden-pty-cleanup.test.ts | 36 +- src/main/rate-limits/hidden-pty-cleanup.ts | 15 + .../rate-limits/hidden-rate-limit-pty-cwd.ts | 37 ++ src/main/rate-limits/service.test.ts | 200 +++++++++-- src/main/rate-limits/service.ts | 321 +++++++++++++----- 18 files changed, 1262 insertions(+), 210 deletions(-) create mode 100644 src/main/providers/pty-default-cwd.test.ts create mode 100644 src/main/providers/pty-default-cwd.ts create mode 100644 src/main/providers/pty-path-safety.ts create mode 100644 src/main/rate-limits/hidden-rate-limit-pty-cwd.ts diff --git a/src/main/daemon/pty-subprocess.test.ts b/src/main/daemon/pty-subprocess.test.ts index 85366a1377b..fccd92e9d53 100644 --- a/src/main/daemon/pty-subprocess.test.ts +++ b/src/main/daemon/pty-subprocess.test.ts @@ -527,6 +527,7 @@ describe('createPtySubprocess', () => { sessionId: 'test', cols: 80, rows: 24, + cwd: 'C:\\repo\\orca', command: 'codex' }) @@ -1038,6 +1039,97 @@ describe('createPtySubprocess', () => { expect(shellArg.length).toBeGreaterThan(0) }) + it('allows an explicitly requested plain daemon shell at POSIX root', () => { + const proc = mockPtyProcess() + spawnMock.mockReturnValue(proc) + const platform = Object.getOwnPropertyDescriptor(process, 'platform') + Object.defineProperty(process, 'platform', { value: 'linux' }) + + try { + createPtySubprocess({ sessionId: 'test', cols: 80, rows: 24, cwd: '/' }) + } finally { + if (platform) { + Object.defineProperty(process, 'platform', platform) + } + } + + expect(spawnMock).toHaveBeenCalledWith( + expect.any(String), + expect.any(Array), + expect.objectContaining({ cwd: '/' }) + ) + }) + + it('rejects daemon automatic agent startup without an explicit cwd', () => { + const platform = Object.getOwnPropertyDescriptor(process, 'platform') + Object.defineProperty(process, 'platform', { value: 'linux' }) + spawnMock.mockClear() + + try { + expect(() => + createPtySubprocess({ + sessionId: 'test', + cols: 80, + rows: 24, + command: 'codex' + }) + ).toThrow(/requires a non-root workspace/) + } finally { + if (platform) { + Object.defineProperty(process, 'platform', platform) + } + } + + expect(spawnMock).not.toHaveBeenCalled() + }) + + it('rejects daemon automatic agent startup at POSIX root', () => { + const platform = Object.getOwnPropertyDescriptor(process, 'platform') + Object.defineProperty(process, 'platform', { value: 'linux' }) + spawnMock.mockClear() + + try { + expect(() => + createPtySubprocess({ + sessionId: 'test', + cols: 80, + rows: 24, + cwd: '/', + command: 'claude' + }) + ).toThrow(/requires a non-root workspace/) + } finally { + if (platform) { + Object.defineProperty(process, 'platform', platform) + } + } + + expect(spawnMock).not.toHaveBeenCalled() + }) + + it('rejects a missing explicit POSIX cwd before node-pty spawn', () => { + const platform = Object.getOwnPropertyDescriptor(process, 'platform') + Object.defineProperty(process, 'platform', { value: 'linux' }) + spawnMock.mockClear() + + try { + expect(() => + createPtySubprocess({ + sessionId: 'test', + cols: 80, + rows: 24, + cwd: '/definitely-missing-orca-cwd' + }) + ).toThrow(/definitely-missing-orca-cwd/) + } finally { + if (platform) { + Object.defineProperty(process, 'platform', platform) + } + } + + expect(spawnMock).not.toHaveBeenCalled() + }) + it('passes custom env to spawned process', () => { const proc = mockPtyProcess() spawnMock.mockReturnValue(proc) @@ -1238,6 +1330,7 @@ describe('createPtySubprocess', () => { sessionId: 'test', cols: 80, rows: 24, + cwd: '/repo', command: 'codex', env: { SHELL: '/bin/zsh' } }) @@ -1264,6 +1357,7 @@ describe('createPtySubprocess', () => { sessionId: 'test', cols: 80, rows: 24, + cwd: '/repo', command: "codex 'linked issue context'", startupCommandDelivery: 'shell-ready', env: { SHELL: '/bin/zsh' } @@ -1291,6 +1385,7 @@ describe('createPtySubprocess', () => { sessionId: 'test', cols: 80, rows: 24, + cwd: '/repo', command: "codex --prefill 'linked issue context'", env: { SHELL: '/bin/zsh' } }) @@ -1560,6 +1655,7 @@ describe('createPtySubprocess', () => { sessionId: 'test', cols: 80, rows: 24, + cwd: 'C:\\repo\\orca', shellOverride: 'powershell.exe', command: "& 'codex' '--no-alt-screen'" }) @@ -1589,6 +1685,7 @@ describe('createPtySubprocess', () => { sessionId: 'test', cols: 80, rows: 24, + cwd: 'C:\\repo\\orca', shellOverride: 'cmd.exe', command: `codex ${'x'.repeat(7000)}` }) diff --git a/src/main/daemon/pty-subprocess.ts b/src/main/daemon/pty-subprocess.ts index 6dd0554bb87..35e92157ba4 100644 --- a/src/main/daemon/pty-subprocess.ts +++ b/src/main/daemon/pty-subprocess.ts @@ -54,6 +54,7 @@ import { import { isShellProcess } from '../../shared/shell-process-detection' import { parsePtySessionId } from './pty-session-id' import { getAgentForegroundContextPaths } from '../providers/agent-foreground-context-paths' +import { assertSafeAgentStartupCwd, resolveSafePtyDefaultCwd } from '../providers/pty-default-cwd' const PANE_IDENTITY_ENV_KEYS = [ 'ORCA_PANE_KEY', @@ -93,20 +94,7 @@ export type PtySubprocessOptions = { * Returns a stable default working directory for daemon-spawned PTYs. */ function getDefaultCwd(): string { - if (process.platform !== 'win32') { - return process.env.HOME || '/' - } - - // Why: HOMEPATH alone is drive-relative (`\\Users\\name`). Pair it with - // HOMEDRIVE when USERPROFILE is unavailable so daemon-spawned Windows PTYs - // still start in a valid absolute home directory. - if (process.env.USERPROFILE) { - return process.env.USERPROFILE - } - if (process.env.HOMEDRIVE && process.env.HOMEPATH) { - return `${process.env.HOMEDRIVE}${process.env.HOMEPATH}` - } - return 'C:\\' + return resolveSafePtyDefaultCwd() } /** @@ -206,11 +194,13 @@ function isExistingDirectory(path: string | undefined): path is string { * Moves the daemon process to a stable cwd after its original cwd disappears. */ function repairDaemonCwd(): string | null { - const candidates = [ - process.env.ORCA_USER_DATA_PATH, - getDefaultCwd(), - process.platform === 'win32' ? 'C:\\' : '/' - ] + const candidates = [process.env.ORCA_USER_DATA_PATH] + try { + candidates.push(getDefaultCwd()) + } catch { + // Keep daemon cwd repair best-effort even when no user terminal cwd is safe. + } + candidates.push(process.platform === 'win32' ? 'C:\\' : '/') for (const candidate of candidates) { if (isExistingDirectory(candidate)) { try { @@ -314,6 +304,16 @@ function preflightWindowsPtySpawnEnvironment(args: { validateWorkingDirectory(args.validationCwd) } +/** + * Validates POSIX spawn cwd before node-pty can fail with an opaque ENOENT. + */ +function preflightPosixPtySpawnEnvironment(validationCwd: string): void { + if (process.platform === 'win32') { + return + } + validateWorkingDirectory(validationCwd) +} + /** * Wraps native PTY spawn failures with shell and cwd context. */ @@ -589,6 +589,9 @@ export function createPtySubprocess(opts: PtySubprocessOptions): SubprocessHandl let windowsFallbackAttempts: WindowsShellSpawnAttempt[] = [] const startupAgentRecognition = recognizeAgentProcessFromCommandLine(opts.command) const isCodexStartupCommand = startupAgentRecognition?.agent === 'codex' + if (opts.command && startupAgentRecognition) { + assertSafeAgentStartupCwd(opts.cwd, opts.command) + } const requestedCwd = opts.cwd || getDefaultCwd() let spawnCwd = requestedCwd let validationCwd = spawnCwd @@ -772,6 +775,7 @@ export function createPtySubprocess(opts: PtySubprocessOptions): SubprocessHandl // runs in a separate forked process with its own code path. ensureNodePtySpawnHelperExecutable() preflightUnixPtySpawnEnvironment() + preflightPosixPtySpawnEnvironment(validationCwd) preflightWindowsPtySpawnEnvironment({ validationCwd, cwdWasExplicit: opts.cwd !== undefined diff --git a/src/main/ipc/repos-remote.test.ts b/src/main/ipc/repos-remote.test.ts index 0b29082bf1e..2a9a66df94d 100644 --- a/src/main/ipc/repos-remote.test.ts +++ b/src/main/ipc/repos-remote.test.ts @@ -2441,6 +2441,12 @@ describe('repos:getBaseRefDefault envelope', () => { match: isSymbolicRef, respond: () => Promise.resolve({ stdout: 'refs/remotes/origin/main\n', stderr: '' }) }, + // The origin/HEAD target is verified before it is trusted, so the + // symbolic-ref result must also resolve via rev-parse. + { + match: isRevParseFor('refs/remotes/origin/main'), + respond: () => Promise.resolve({ stdout: '', stderr: '' }) + }, { match: isRemoteList, respond: () => Promise.resolve({ stdout: 'origin\nupstream\n', stderr: '' }) @@ -2471,6 +2477,12 @@ describe('repos:getBaseRefDefault envelope', () => { match: isSymbolicRef, respond: () => Promise.resolve({ stdout: 'refs/remotes/origin/main\n', stderr: '' }) }, + // The origin/HEAD target is verified before it is trusted, so the + // symbolic-ref result must also resolve via rev-parse. + { + match: isRevParseFor('refs/remotes/origin/main'), + respond: () => Promise.resolve({ stdout: '', stderr: '' }) + }, { match: isRemoteList, respond: () => Promise.reject(new Error('relay exec failed')) diff --git a/src/main/providers/local-pty-provider.test.ts b/src/main/providers/local-pty-provider.test.ts index d37faa0ac57..3fc96c897bf 100644 --- a/src/main/providers/local-pty-provider.test.ts +++ b/src/main/providers/local-pty-provider.test.ts @@ -82,6 +82,7 @@ vi.mock('../wsl', () => ({ })) import { LocalPtyProvider } from './local-pty-provider' +import { isRootLikePath } from './pty-path-safety' import { POWERLEVEL10K_WIZARD_DISABLE_ENV } from '../pty/powerlevel10k-wizard-env' describe('LocalPtyProvider', () => { @@ -218,6 +219,52 @@ describe('LocalPtyProvider', () => { ) }) + it('allows an explicitly requested plain shell at POSIX root', async () => { + await provider.spawn({ cols: 80, rows: 24, cwd: '/' }) + + expect(spawnMock).toHaveBeenCalledWith( + expect.any(String), + expect.any(Array), + expect.objectContaining({ cwd: '/' }) + ) + }) + + it('falls back to the safe default cwd for automatic agent startup without an explicit cwd', async () => { + spawnMock.mockClear() + const origHome = process.env.HOME + // Pin HOME so we assert the exact resolved candidate, not just non-root-ness — + // catches regressions where resolveSafePtyDefaultCwd picks an unintended home. + process.env.HOME = '/home/testuser' + + try { + // Why: an omitted cwd resolves to a guaranteed-safe default home (the + // guard only rejects root-like paths), so the agent must still launch. + await expect( + provider.spawn({ cols: 80, rows: 24, command: 'codex' }) + ).resolves.toBeDefined() + + const spawnCall = spawnMock.mock.calls.at(-1)! + expect(spawnCall[2].cwd).toBe('/home/testuser') + expect(isRootLikePath(spawnCall[2].cwd)).toBe(false) + } finally { + if (origHome === undefined) { + delete process.env.HOME + } else { + process.env.HOME = origHome + } + } + }) + + it('rejects automatic agent startup at POSIX root', async () => { + spawnMock.mockClear() + + await expect( + provider.spawn({ cols: 80, rows: 24, cwd: '/', command: 'claude' }) + ).rejects.toThrow(/requires a non-root workspace/) + + expect(spawnMock).not.toHaveBeenCalled() + }) + it('invokes onSpawned callback', async () => { const onSpawned = vi.fn() provider.configure({ onSpawned }) diff --git a/src/main/providers/local-pty-provider.ts b/src/main/providers/local-pty-provider.ts index 7e8588a6b64..0d2eb19f1a6 100644 --- a/src/main/providers/local-pty-provider.ts +++ b/src/main/providers/local-pty-provider.ts @@ -55,6 +55,7 @@ import { resolveAgentForegroundProcess } from './agent-foreground-process' import { getAgentForegroundContextPaths } from './agent-foreground-context-paths' import { recognizeAgentProcessFromCommandLine } from '../../shared/agent-process-recognition' import { shouldUseShellReadyStartupDelivery } from '../../shared/codex-startup-delivery' +import { assertSafeAgentStartupCwd, resolveSafePtyDefaultCwd } from './pty-default-cwd' const PANE_IDENTITY_ENV_KEYS = [ 'ORCA_PANE_KEY', @@ -88,20 +89,7 @@ const exitListeners = new Set() * Returns a stable default cwd for locally spawned PTYs. */ function getDefaultCwd(): string { - if (process.platform !== 'win32') { - return process.env.HOME || '/' - } - - // Why: USERPROFILE is not guaranteed in all Windows launch contexts. - // Falling back to bare HOMEPATH yields a drive-relative path, so combine - // HOMEDRIVE + HOMEPATH to keep spawned PTYs anchored to the intended home. - if (process.env.USERPROFILE) { - return process.env.USERPROFILE - } - if (process.env.HOMEDRIVE && process.env.HOMEPATH) { - return `${process.env.HOMEDRIVE}${process.env.HOMEPATH}` - } - return 'C:\\' + return resolveSafePtyDefaultCwd() } /** @@ -352,8 +340,18 @@ export class LocalPtyProvider implements IPtyProvider { } const id = allocatePtyId(reattachId ?? undefined) + const startupAgentRecognition = args.command + ? recognizeAgentProcessFromCommandLine(args.command) + : null + const defaultCwd = getDefaultCwd() const cwd = args.cwd || defaultCwd + // Why: gate on the effective cwd (post default-cwd fallback), not the raw + // args.cwd — an omitted cwd resolves to a safe default and must not be + // rejected as if it were a root-like path. + if (args.command && startupAgentRecognition) { + assertSafeAgentStartupCwd(cwd, args.command) + } const wslInfo = process.platform === 'win32' ? parseWslPath(cwd) : null const worktreeWslContext = process.platform === 'win32' ? getWslContextFromWorktreeId(args.worktreeId) : undefined @@ -594,8 +592,7 @@ export class LocalPtyProvider implements IPtyProvider { finalEnv.ORCA_OMP_STATUS_EXTENSION || finalEnv.ORCA_CODEX_HOME || finalEnv.ORCA_AGENT_TEAMS_SHIM_DIR - const isCodexStartupCommand = - recognizeAgentProcessFromCommandLine(args.command)?.agent === 'codex' + const isCodexStartupCommand = startupAgentRecognition?.agent === 'codex' let shellLaunch: ReturnType | null = null if (args.command && isCodexStartupCommand) { const shouldWaitForShellReady = shouldUseShellReadyStartupDelivery({ diff --git a/src/main/providers/pty-default-cwd.test.ts b/src/main/providers/pty-default-cwd.test.ts new file mode 100644 index 00000000000..2bb0ef3a79a --- /dev/null +++ b/src/main/providers/pty-default-cwd.test.ts @@ -0,0 +1,77 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { homedirMock } = vi.hoisted(() => ({ + homedirMock: vi.fn() +})) + +vi.mock('node:os', () => ({ + homedir: homedirMock +})) + +import { + assertSafeAgentStartupCwd, + isSafeImplicitPtyCwd, + resolveSafePtyDefaultCwd +} from './pty-default-cwd' + +function setPlatform(platform: NodeJS.Platform): void { + Object.defineProperty(process, 'platform', { + configurable: true, + value: platform + }) +} + +describe('pty default cwd safety', () => { + const originalPlatform = process.platform + + beforeEach(() => { + setPlatform(originalPlatform) + homedirMock.mockReset() + homedirMock.mockReturnValue('/home/orca') + }) + + it('rejects root-like implicit cwd values', () => { + expect(isSafeImplicitPtyCwd(undefined)).toBe(false) + expect(isSafeImplicitPtyCwd('')).toBe(false) + expect(isSafeImplicitPtyCwd('/')).toBe(false) + expect(isSafeImplicitPtyCwd('C:\\')).toBe(false) + expect(isSafeImplicitPtyCwd('\\\\server\\share')).toBe(false) + }) + + it('uses a non-root POSIX home and skips root env values', () => { + setPlatform('linux') + + expect(resolveSafePtyDefaultCwd({ HOME: '/home/alice' })).toBe('/home/alice') + expect(resolveSafePtyDefaultCwd({ HOME: '/' })).toBe('/home/orca') + }) + + it('throws instead of falling back to POSIX root', () => { + setPlatform('darwin') + homedirMock.mockReturnValue('/') + + expect(() => resolveSafePtyDefaultCwd({ HOME: '/' })).toThrow( + /No safe default working directory/ + ) + }) + + it('uses Windows home candidates without accepting drive roots', () => { + setPlatform('win32') + homedirMock.mockReturnValue('C:\\Users\\os-home') + + expect( + resolveSafePtyDefaultCwd({ + USERPROFILE: 'C:\\', + HOMEDRIVE: 'D:', + HOMEPATH: '\\Users\\alice' + }) + ).toBe('D:\\Users\\alice') + }) + + it('requires automatic agent startup commands to provide a non-root cwd', () => { + expect(() => assertSafeAgentStartupCwd(undefined, 'codex')).toThrow( + /requires a non-root workspace/ + ) + expect(() => assertSafeAgentStartupCwd('/', 'claude')).toThrow(/requires a non-root workspace/) + expect(() => assertSafeAgentStartupCwd('/repo', 'codex')).not.toThrow() + }) +}) diff --git a/src/main/providers/pty-default-cwd.ts b/src/main/providers/pty-default-cwd.ts new file mode 100644 index 00000000000..e9a7b7a5a3b --- /dev/null +++ b/src/main/providers/pty-default-cwd.ts @@ -0,0 +1,43 @@ +import { homedir } from 'node:os' + +import { isRootLikePath } from './pty-path-safety' + +function homeDrivePath(env: NodeJS.ProcessEnv): string | null { + if (!env.HOMEDRIVE || !env.HOMEPATH) { + return null + } + return `${env.HOMEDRIVE}${env.HOMEPATH}` +} + +export function isSafeImplicitPtyCwd(path: string | null | undefined): path is string { + return !isRootLikePath(path) +} + +export function resolveSafePtyDefaultCwd(env: NodeJS.ProcessEnv = process.env): string { + const candidates = + process.platform === 'win32' + ? [env.USERPROFILE, homeDrivePath(env), homedir()] + : [env.HOME, homedir()] + const selected = candidates.find(isSafeImplicitPtyCwd) + // Why: silently falling back to "/" or a drive root is the exact runaway-CPU + // bug this module prevents (see runaway-cpu-hidden-usage-pty-design.md) — fail loud. + if (!selected) { + // Log the rejected candidates so the rare "no safe home at all" environment + // (e.g. HOME=/ or unset in a minimal container) is diagnosable, not opaque. + console.error( + '[pty-default-cwd] No safe default working directory for terminal launch; every home candidate resolved to a root-like path.', + { platform: process.platform, candidates } + ) + throw new Error('No safe default working directory is available for terminal launch.') + } + return selected +} + +export function assertSafeAgentStartupCwd(cwd: string | undefined, command: string): void { + if (isSafeImplicitPtyCwd(cwd)) { + return + } + throw new Error( + `Automatic agent startup command "${command}" requires a non-root workspace working directory.` + ) +} diff --git a/src/main/providers/pty-path-safety.ts b/src/main/providers/pty-path-safety.ts new file mode 100644 index 00000000000..71360e14ce1 --- /dev/null +++ b/src/main/providers/pty-path-safety.ts @@ -0,0 +1,17 @@ +import { posix, win32 } from 'node:path' + +// Why: both PTY spawn paths (default terminal cwd + hidden usage probes) gate a +// security boundary here — an empty, "/", drive-root, or network-share-root cwd +// triggers the unbounded file discovery this guard exists to prevent. Keep the +// detection in one place so a fix to one enforcement path can't skip the other. +export function isRootLikePath(path: string | null | undefined): boolean { + const trimmed = path?.trim() ?? '' + if (!trimmed) { + return true + } + // A filesystem root is its own parent. Check both POSIX and Windows semantics + // so a Windows drive/UNC root is still rejected when running on POSIX (and vice + // versa) — path.dirname alone only understands the host platform's separators. + const normalized = posix.normalize(trimmed) + return posix.dirname(normalized) === normalized || win32.dirname(trimmed) === trimmed +} diff --git a/src/main/rate-limits/claude-fetcher.ts b/src/main/rate-limits/claude-fetcher.ts index e302c90b6d1..d4dbad55fd8 100644 --- a/src/main/rate-limits/claude-fetcher.ts +++ b/src/main/rate-limits/claude-fetcher.ts @@ -312,6 +312,17 @@ type ClaudeUsageAttemptState = { attemptedSources: UsageRateLimitSource[] } +function abortedClaudeRateLimitResult(): ProviderRateLimits { + return { + provider: 'claude', + session: null, + weekly: null, + updatedAt: Date.now(), + error: 'Rate-limit fetch aborted', + status: 'error' + } +} + function parseResetTimestamp(value: string | number | undefined): number | null { if (typeof value === 'number') { if (!Number.isFinite(value)) { @@ -389,11 +400,21 @@ function mapFableWeeklyWindow(data: OAuthUsageResponse): RateLimitWindow | null ) } -async function fetchViaOAuth(token: string): Promise { +async function fetchViaOAuth(token: string, signal?: AbortSignal): Promise { + if (signal?.aborted) { + return abortedClaudeRateLimitResult() + } await ensureProxyFromEnv() + if (signal?.aborted) { + return abortedClaudeRateLimitResult() + } const controller = new AbortController() const timeout = setTimeout(() => controller.abort(), API_TIMEOUT_MS) + const abortFromExternalSignal = (): void => { + controller.abort() + } + signal?.addEventListener('abort', abortFromExternalSignal, { once: true }) try { // Why: net.fetch uses Chromium's networking stack which respects OS proxy @@ -414,6 +435,9 @@ async function fetchViaOAuth(token: string): Promise { } const data = (await res.json()) as OAuthUsageResponse + if (signal?.aborted) { + return abortedClaudeRateLimitResult() + } return { provider: 'claude', @@ -424,8 +448,14 @@ async function fetchViaOAuth(token: string): Promise { error: null, status: 'ok' } + } catch (err) { + if (signal?.aborted) { + return abortedClaudeRateLimitResult() + } + throw err } finally { clearTimeout(timeout) + signal?.removeEventListener('abort', abortFromExternalSignal) } } @@ -507,11 +537,13 @@ async function fetchClaudeUsageViaCli(input: { oauthCredentials: OAuthCredentialReadResult attempts: ClaudeUsageAttemptState networkProxySettings?: NetworkProxySettings + signal?: AbortSignal }): Promise { recordAttempt(input.attempts, 'cli') const limits = await fetchViaPty({ authPreparation: input.authPreparation, - networkProxySettings: input.networkProxySettings + networkProxySettings: input.networkProxySettings, + signal: input.signal }) return withClaudeUsageMetadata( limits, @@ -567,8 +599,9 @@ async function supplementOAuthUsageFromCli(input: { attempts: ClaudeUsageAttemptState allowUsagePanelSupplement: boolean networkProxySettings?: NetworkProxySettings + signal?: AbortSignal }): Promise { - if (!canSupplementOAuthUsageFromCli(input)) { + if (input.signal?.aborted || !canSupplementOAuthUsageFromCli(input)) { return input.oauthLimits } try { @@ -576,7 +609,8 @@ async function supplementOAuthUsageFromCli(input: { authPreparation: input.authPreparation, oauthCredentials: input.oauthCredentials, attempts: input.attempts, - networkProxySettings: input.networkProxySettings + networkProxySettings: input.networkProxySettings, + signal: input.signal }) return mergeClaudeUsageWindows(input.oauthLimits, cliLimits) } catch (err) { @@ -637,25 +671,40 @@ async function attemptCliRepairThenRetryOAuth(input: { attempts: ClaudeUsageAttemptState oauthCredentials: OAuthCredentialReadResult }): Promise { + if (input.options?.signal?.aborted) { + return abortedClaudeRateLimitResult() + } let cliResult: ProviderRateLimits | null = null try { cliResult = await fetchClaudeUsageViaCli({ authPreparation: input.options?.authPreparation, oauthCredentials: input.oauthCredentials, attempts: input.attempts, - networkProxySettings: input.options?.networkProxySettings + networkProxySettings: input.options?.networkProxySettings, + signal: input.options?.signal }) } catch (err) { warnClaudeUsageFetchFailure(input.options?.authPreparation, input.oauthCredentials, err) } + // Why: bail before credential I/O if the fetch cycle was stopped mid-CLI-repair. + if (input.options?.signal?.aborted) { + return abortedClaudeRateLimitResult() + } + const refreshedCredentials = await readOAuthCredentials( resolveOAuthCredentialReadOptions(input.options?.authPreparation) ) + if (input.options?.signal?.aborted) { + return abortedClaudeRateLimitResult() + } if (refreshedCredentials.token) { recordAttempt(input.attempts, 'oauth') try { - const oauthRetry = await fetchViaOAuth(refreshedCredentials.token) + const oauthRetry = await fetchViaOAuth(refreshedCredentials.token, input.options?.signal) + if (input.options?.signal?.aborted) { + return abortedClaudeRateLimitResult() + } const supplemented = mergeClaudeUsageWindows(oauthRetry, cliResult) return withClaudeUsageMetadata( supplemented, @@ -683,16 +732,21 @@ export type FetchClaudeRateLimitsOptions = { allowPtyFallback?: boolean allowUsagePanelSupplement?: boolean networkProxySettings?: NetworkProxySettings + signal?: AbortSignal } export type FetchManagedAccountUsageOptions = { allowUsagePanelSupplement?: boolean networkProxySettings?: NetworkProxySettings + signal?: AbortSignal } export async function fetchClaudeRateLimits( options?: FetchClaudeRateLimitsOptions ): Promise { + if (options?.signal?.aborted) { + return abortedClaudeRateLimitResult() + } const attempts: ClaudeUsageAttemptState = { attemptedSources: [] } const allowCliFallback = options?.allowPtyFallback !== false const plan = resolveClaudeUsageRefreshPlan({ @@ -715,11 +769,17 @@ export async function fetchClaudeRateLimits( const oauthCredentials = await readOAuthCredentials( resolveOAuthCredentialReadOptions(options?.authPreparation) ) + if (options?.signal?.aborted) { + return abortedClaudeRateLimitResult() + } if (plan.steps.some((step) => step.source === 'oauth') && oauthCredentials.token) { recordAttempt(attempts, 'oauth') try { - const oauthLimits = await fetchViaOAuth(oauthCredentials.token) + const oauthLimits = await fetchViaOAuth(oauthCredentials.token, options?.signal) + if (options?.signal?.aborted) { + return abortedClaudeRateLimitResult() + } const limits = await supplementOAuthUsageFromCli({ oauthLimits, authPreparation: options?.authPreparation, @@ -727,8 +787,12 @@ export async function fetchClaudeRateLimits( attempts, networkProxySettings: options?.networkProxySettings, allowUsagePanelSupplement: - options?.allowUsagePanelSupplement ?? isManagedClaudeAuth(options?.authPreparation) + options?.allowUsagePanelSupplement ?? isManagedClaudeAuth(options?.authPreparation), + signal: options?.signal }) + if (options?.signal?.aborted) { + return abortedClaudeRateLimitResult() + } return withClaudeUsageMetadata( limits, metadataForAttempt({ @@ -767,7 +831,8 @@ export async function fetchClaudeRateLimits( authPreparation: options?.authPreparation, oauthCredentials, attempts, - networkProxySettings: options?.networkProxySettings + networkProxySettings: options?.networkProxySettings, + signal: options?.signal }) } catch (ptyError) { warnClaudeUsageFetchFailure(options?.authPreparation, oauthCredentials, ptyError) @@ -825,7 +890,8 @@ export async function fetchClaudeRateLimits( authPreparation: options?.authPreparation, oauthCredentials, attempts, - networkProxySettings: options?.networkProxySettings + networkProxySettings: options?.networkProxySettings, + signal: options?.signal }) } catch (err) { warnClaudeUsageFetchFailure(options?.authPreparation, oauthCredentials, err) @@ -871,7 +937,8 @@ export async function fetchClaudeRateLimits( authPreparation: options?.authPreparation, oauthCredentials, attempts, - networkProxySettings: options?.networkProxySettings + networkProxySettings: options?.networkProxySettings, + signal: options?.signal }) } catch (err) { warnClaudeUsageFetchFailure(options?.authPreparation, oauthCredentials, err) @@ -1079,7 +1146,11 @@ async function fetchManagedUsagePanelSupplement(input: { credentialsJson: string oauthLimits: ProviderRateLimits networkProxySettings?: NetworkProxySettings + signal?: AbortSignal }): Promise { + if (input.signal?.aborted) { + return null + } const authPreparation = getManagedUsagePanelAuthPreparation(input.account, input.location) if (!authPreparation) { return null @@ -1087,8 +1158,12 @@ async function fetchManagedUsagePanelSupplement(input: { return withManagedPreviewKeychainCredentials(input.location, input.credentialsJson, async () => { const cliLimits = await fetchViaPty({ authPreparation, - networkProxySettings: input.networkProxySettings + networkProxySettings: input.networkProxySettings, + signal: input.signal }) + if (input.signal?.aborted) { + return null + } if ( !canTrustManagedUsagePanelSupplement(input.oauthLimits, cliLimits, { requireMatchingOAuthWindow: input.location.kind === 'keychain' @@ -1108,8 +1183,14 @@ export async function fetchManagedAccountUsage( account: InactiveClaudeAccountInfo, options: FetchManagedAccountUsageOptions = {} ): Promise { + if (options.signal?.aborted) { + return abortedClaudeRateLimitResult() + } const location = resolveManagedCredentialsLocation(account) let credentialsJson = location ? await readManagedCredentialsJson(location) : null + if (options.signal?.aborted) { + return abortedClaudeRateLimitResult() + } if (!location || !credentialsJson) { return { provider: 'claude', @@ -1129,6 +1210,9 @@ export async function fetchManagedAccountUsage( let token = parseOAuthCredentialsJson(credentialsJson, 'credentials-file').token if (isOauthTokenExpiring(credentialsJson)) { const refreshed = await refreshClaudeOauthCredentials(credentialsJson) + if (options.signal?.aborted) { + return abortedClaudeRateLimitResult() + } if (refreshed) { try { await writeManagedCredentialsJson(location, refreshed) @@ -1155,7 +1239,10 @@ export async function fetchManagedAccountUsage( // Why: PTY fallback is intentionally omitted for inactive accounts. The PTY // path is used only as a supplement after OAuth succeeds, and it points // directly at the managed account's isolated config so selection is unchanged. - const oauthLimits = await fetchViaOAuth(token) + const oauthLimits = await fetchViaOAuth(token, options.signal) + if (options.signal?.aborted) { + return abortedClaudeRateLimitResult() + } if ( !canSupplementOAuthUsageFromCli({ oauthLimits, @@ -1171,7 +1258,8 @@ export async function fetchManagedAccountUsage( location, credentialsJson, oauthLimits, - networkProxySettings: options.networkProxySettings + networkProxySettings: options.networkProxySettings, + signal: options.signal }) return mergeClaudeUsageWindows(oauthLimits, cliLimits) } catch (err) { diff --git a/src/main/rate-limits/claude-pty.test.ts b/src/main/rate-limits/claude-pty.test.ts index f6f4d22e6a0..f2fd976821b 100644 --- a/src/main/rate-limits/claude-pty.test.ts +++ b/src/main/rate-limits/claude-pty.test.ts @@ -14,6 +14,7 @@ vi.mock('node-pty', () => ({ })) import { fetchViaPty } from './claude-pty' +import { getActiveHiddenRateLimitPtyCount } from './hidden-pty-cleanup' function makeDisposable() { return { dispose: vi.fn() } @@ -97,6 +98,22 @@ describe('fetchViaPty', () => { await resultPromise }) + it('spawns the hidden usage PTY in a bounded non-root cwd', async () => { + const term = makeMockTerm() + spawnMock.mockReturnValue(term) + + const resultPromise = fetchViaPty() + await vi.advanceTimersByTimeAsync(0) + + const spawnCwd = spawnMock.mock.calls[0]?.[2]?.cwd as string + expect(spawnCwd).toContain('rate-limit-pty-cwd') + expect(spawnCwd).not.toBe('/') + expect(spawnCwd).not.toMatch(/^[A-Za-z]:(?:[\\/])?$/) + + term.emitExit() + await resultPromise + }) + it('leaves the spawn env proxy untouched when no proxy is configured', async () => { const term = makeMockTerm() spawnMock.mockReturnValue(term) @@ -135,6 +152,8 @@ describe('fetchViaPty', () => { const [spawnFile, spawnArgs] = spawnMock.mock.calls[0] as [string, string[]] expect(spawnFile).toBe('wsl.exe') const bashCommand = spawnArgs.at(-1) as string + expect(bashCommand).toContain('mkdir -p "$orca_rate_limit_cwd"') + expect(bashCommand).toContain('cd "$orca_rate_limit_cwd"') expect(bashCommand).toContain("export HTTPS_PROXY='http://127.0.0.1:7890'") expect(bashCommand).toContain('exec claude') @@ -142,6 +161,28 @@ describe('fetchViaPty', () => { await resultPromise }) + it('kills and unregisters the hidden PTY when the fetch signal aborts', async () => { + const term = makeMockTerm() + spawnMock.mockReturnValue(term) + const controller = new AbortController() + const killMock = term.kill + + const resultPromise = fetchViaPty({ signal: controller.signal }) + await vi.advanceTimersByTimeAsync(0) + + expect(getActiveHiddenRateLimitPtyCount()).toBe(1) + + controller.abort() + + await expect(resultPromise).resolves.toMatchObject({ + provider: 'claude', + status: 'error', + error: 'Rate-limit fetch aborted' + }) + expect(killMock).toHaveBeenCalledTimes(1) + expect(getActiveHiddenRateLimitPtyCount()).toBe(0) + }) + it('clears the startup delay timer when the hidden PTY exits early', async () => { const term = makeMockTerm() spawnMock.mockReturnValue(term) diff --git a/src/main/rate-limits/claude-pty.ts b/src/main/rate-limits/claude-pty.ts index f2000489275..88301b9c9dd 100644 --- a/src/main/rate-limits/claude-pty.ts +++ b/src/main/rate-limits/claude-pty.ts @@ -7,8 +7,12 @@ import { resolveClaudeCommand } from '../codex-cli/command' import type { ClaudeRuntimeAuthPreparation } from '../claude-accounts/runtime-auth-service' import { applyClaudeEnvPatch } from '../claude-accounts/environment' import { withMacTailscaleDnsHint } from '../network/macos-tailscale-dns-diagnostic' -import { cleanupHiddenRateLimitPty } from './hidden-pty-cleanup' +import { cleanupHiddenRateLimitPty, registerHiddenRateLimitPty } from './hidden-pty-cleanup' import { extractClaudePtyResetMetadata } from './claude-pty-reset-parser' +import { + getHiddenRateLimitWslCwdSetupCommands, + resolveHiddenRateLimitPtyCwd +} from './hidden-rate-limit-pty-cwd' const PTY_TIMEOUT_MS = 25_000 const MAX_OUTPUT_LENGTH = 100_000 // 100KB buffer limit @@ -197,11 +201,29 @@ function describeClaudeUsageFailure(output: string): string { return 'Claude usage is unavailable right now.' } +function abortedClaudeUsageResult(): ProviderRateLimits { + return { + provider: 'claude', + session: null, + weekly: null, + updatedAt: Date.now(), + error: 'Rate-limit fetch aborted', + status: 'error' + } +} + export async function fetchViaPty(options?: { authPreparation?: ClaudeRuntimeAuthPreparation networkProxySettings?: NetworkProxySettings + signal?: AbortSignal }): Promise { + if (options?.signal?.aborted) { + return abortedClaudeUsageResult() + } const pty = await import('node-pty') + if (options?.signal?.aborted) { + return abortedClaudeUsageResult() + } return new Promise((resolve) => { let output = '' @@ -251,10 +273,13 @@ export async function fetchViaPty(options?: { // Why: Windows-side env does not cross into the distro without WSLENV, // so export the configured proxy inside the command for the inner claude. [ + // Why: hidden usage probes must not inherit a root-like WSL cwd; + // keep Claude discovery bounded to a tiny temp directory. + ...getHiddenRateLimitWslCwdSetupCommands(), `export CLAUDE_CONFIG_DIR=${shellQuote(wslConfig.linuxConfigDir)}`, ...Object.entries(proxyEnv).map(([key, value]) => `export ${key}=${shellQuote(value)}`), 'exec claude' - ].join('; ') + ].join(' && ') ] : isWin32 ? ['/c', `"${claudeCommand}"`] @@ -264,10 +289,14 @@ export async function fetchViaPty(options?: { name: 'xterm-256color', cols: 120, rows: 40, + // Why: hidden usage PTYs must not inherit the process cwd (e.g. / or a + // drive root), which can trigger unbounded file discovery. + cwd: resolveHiddenRateLimitPtyCwd(), env: spawnEnv }) - const termDisposables: { dispose: () => void }[] = [] + const termDisposables: { dispose: () => void }[] = [registerHiddenRateLimitPty(term)] let enterInterval: ReturnType | null = null + let timeout: ReturnType | null = null function clearFollowupTimers(): void { if (startupDelayTimer) { @@ -288,7 +317,32 @@ export async function fetchViaPty(options?: { } } - const timeout = setTimeout(() => { + function settleAborted(): void { + if (resolved) { + return + } + resolved = true + if (timeout) { + clearTimeout(timeout) + timeout = null + } + clearFollowupTimers() + cleanupHiddenRateLimitPty(term, termDisposables, { kill: true }) + resolve(abortedClaudeUsageResult()) + } + + if (options?.signal) { + if (options.signal.aborted) { + settleAborted() + return + } + options.signal.addEventListener('abort', settleAborted, { once: true }) + termDisposables.push({ + dispose: () => options.signal?.removeEventListener('abort', settleAborted) + }) + } + + timeout = setTimeout(() => { if (!resolved) { resolved = true clearFollowupTimers() @@ -342,7 +396,10 @@ export async function fetchViaPty(options?: { return } resolved = true - clearTimeout(timeout) + if (timeout) { + clearTimeout(timeout) + timeout = null + } clearFollowupTimers() cleanupHiddenRateLimitPty(term, termDisposables, { kill: true }) @@ -443,7 +500,10 @@ export async function fetchViaPty(options?: { clearFollowupTimers() if (!resolved) { resolved = true - clearTimeout(timeout) + if (timeout) { + clearTimeout(timeout) + timeout = null + } const clean = stripTerminalControlSequences(output) const { session, weekly, fableWeekly } = parsePtyUsage(clean) resolve({ diff --git a/src/main/rate-limits/codex-fetcher.test.ts b/src/main/rate-limits/codex-fetcher.test.ts index 4c7221e9e96..daaec0ce7fd 100644 --- a/src/main/rate-limits/codex-fetcher.test.ts +++ b/src/main/rate-limits/codex-fetcher.test.ts @@ -33,6 +33,7 @@ vi.mock('./codex-auth-presence', () => ({ import { fetchCodexRateLimits } from './codex-fetcher' import { codexAuthExists } from './codex-auth-presence' +import { getActiveHiddenRateLimitPtyCount } from './hidden-pty-cleanup' function makeDisposable() { return { dispose: vi.fn() } @@ -52,6 +53,25 @@ function makeRpcChild() { return child } +function makePtyTerm() { + let dataHandler: ((data: string) => void) | null = null + let exitHandler: (() => void) | null = null + return { + onData: vi.fn((callback: (data: string) => void) => { + dataHandler = callback + return makeDisposable() + }), + onExit: vi.fn((callback: () => void) => { + exitHandler = callback + return makeDisposable() + }), + write: vi.fn(), + kill: vi.fn(), + emitData: (data: string) => dataHandler?.(data), + emitExit: () => exitHandler?.() + } +} + describe('fetchCodexRateLimits', () => { beforeEach(() => { vi.useFakeTimers() @@ -108,6 +128,83 @@ describe('fetchCodexRateLimits', () => { ) }) + it('spawns the RPC rate-limit reader in a bounded non-root cwd', async () => { + const rpcChild = makeRpcChild() + childSpawnMock.mockReturnValue(rpcChild) + + const resultPromise = fetchCodexRateLimits({ allowPtyFallback: false }) + + const spawnCwd = childSpawnMock.mock.calls[0]?.[2]?.cwd as string + expect(spawnCwd).toContain('rate-limit-pty-cwd') + expect(spawnCwd).not.toBe('/') + expect(spawnCwd).not.toMatch(/^[A-Za-z]:\\?$/) + + rpcChild.emit('close') + await resultPromise + }) + + it('spawns the PTY fallback in a bounded non-root cwd', async () => { + const term = makePtyTerm() + childSpawnMock.mockImplementation(() => { + throw new Error('rpc unavailable') + }) + ptySpawnMock.mockReturnValue(term) + + const resultPromise = fetchCodexRateLimits() + await vi.advanceTimersByTimeAsync(0) + + const spawnCwd = ptySpawnMock.mock.calls[0]?.[2]?.cwd as string + expect(spawnCwd).toContain('rate-limit-pty-cwd') + expect(spawnCwd).not.toBe('/') + expect(spawnCwd).not.toMatch(/^[A-Za-z]:\\?$/) + + term.emitExit() + await resultPromise + }) + + it('kills the RPC child and skips PTY fallback when the fetch signal aborts', async () => { + const rpcChild = makeRpcChild() + childSpawnMock.mockReturnValue(rpcChild) + const controller = new AbortController() + + const resultPromise = fetchCodexRateLimits({ signal: controller.signal }) + + controller.abort() + + await expect(resultPromise).resolves.toMatchObject({ + provider: 'codex', + status: 'error', + error: 'Rate-limit fetch aborted' + }) + expect(rpcChild.kill).toHaveBeenCalledTimes(1) + expect(ptySpawnMock).not.toHaveBeenCalled() + }) + + it('kills and unregisters the PTY fallback when the fetch signal aborts', async () => { + const term = makePtyTerm() + childSpawnMock.mockImplementation(() => { + throw new Error('rpc unavailable') + }) + ptySpawnMock.mockReturnValue(term) + const controller = new AbortController() + const killMock = term.kill + + const resultPromise = fetchCodexRateLimits({ signal: controller.signal }) + await vi.advanceTimersByTimeAsync(0) + + expect(getActiveHiddenRateLimitPtyCount()).toBe(1) + + controller.abort() + + await expect(resultPromise).resolves.toMatchObject({ + provider: 'codex', + status: 'error', + error: 'Rate-limit fetch aborted' + }) + expect(killMock).toHaveBeenCalledTimes(1) + expect(getActiveHiddenRateLimitPtyCount()).toBe(0) + }) + it('falls back to the PTY status reader when RPC exits before returning usage', async () => { const rpcChild = makeRpcChild() const ptyHandlers: { onData?: (data: string) => void } = {} @@ -437,17 +534,18 @@ describe('fetchCodexRateLimits', () => { await vi.advanceTimersByTimeAsync(1) await resultPromise - expect(childSpawnMock).toHaveBeenCalledWith( - 'wsl.exe', - [ - '-d', - 'Ubuntu', - '--', - 'bash', - '-lc', - "export CODEX_HOME='/home/alice/.local/share/orca/account/home'; exec codex '-s' 'read-only' '-a' 'untrusted' 'app-server'" - ], + const [spawnFile, spawnArgs, spawnOptions] = childSpawnMock.mock.calls[0] + expect(spawnFile).toBe('wsl.exe') + const bashCommand = spawnArgs.at(-1) as string + expect(bashCommand).toContain('mkdir -p "$orca_rate_limit_cwd"') + expect(bashCommand).toContain('cd "$orca_rate_limit_cwd"') + expect(bashCommand).toContain( + "export CODEX_HOME='/home/alice/.local/share/orca/account/home'" + ) + expect(bashCommand).toContain("exec codex '-s' 'read-only' '-a' 'untrusted' 'app-server'") + expect(spawnOptions).toEqual( expect.objectContaining({ + cwd: expect.stringContaining('rate-limit-pty-cwd'), env: expect.not.objectContaining({ CODEX_HOME: expect.anything() }) }) ) @@ -488,17 +586,18 @@ describe('fetchCodexRateLimits', () => { rpcChild.emit('close') await vi.advanceTimersByTimeAsync(0) - expect(ptySpawnMock).toHaveBeenCalledWith( - 'wsl.exe', - [ - '-d', - 'Ubuntu', - '--', - 'bash', - '-lc', - "export CODEX_HOME='/home/alice/.local/share/orca/account/home'; exec codex " - ], + const [spawnFile, spawnArgs, spawnOptions] = ptySpawnMock.mock.calls[0] + expect(spawnFile).toBe('wsl.exe') + const bashCommand = spawnArgs.at(-1) as string + expect(bashCommand).toContain('mkdir -p "$orca_rate_limit_cwd"') + expect(bashCommand).toContain('cd "$orca_rate_limit_cwd"') + expect(bashCommand).toContain( + "export CODEX_HOME='/home/alice/.local/share/orca/account/home'" + ) + expect(bashCommand).toContain('exec codex ') + expect(spawnOptions).toEqual( expect.objectContaining({ + cwd: expect.stringContaining('rate-limit-pty-cwd'), env: expect.not.objectContaining({ CODEX_HOME: expect.anything() }) }) ) diff --git a/src/main/rate-limits/codex-fetcher.ts b/src/main/rate-limits/codex-fetcher.ts index 439c5438d2f..8ffbd9c8d95 100644 --- a/src/main/rate-limits/codex-fetcher.ts +++ b/src/main/rate-limits/codex-fetcher.ts @@ -14,9 +14,13 @@ import { codexAuthExists } from './codex-auth-presence' import { resolveCodexCommand } from '../codex-cli/command' import { withMacTailscaleDnsHint } from '../network/macos-tailscale-dns-diagnostic' import { getCmdExePath, getSpawnArgsForWindows } from '../win32-utils' -import { cleanupHiddenRateLimitPty } from './hidden-pty-cleanup' +import { cleanupHiddenRateLimitPty, registerHiddenRateLimitPty } from './hidden-pty-cleanup' import { parseWslUncPath } from '../../shared/wsl-paths' import { extractCodexAuthError, isCodexAuthError } from '../../shared/codex-auth-errors' +import { + getHiddenRateLimitWslCwdSetupCommands, + resolveHiddenRateLimitPtyCwd +} from './hidden-rate-limit-pty-cwd' const RPC_TIMEOUT_MS = 10_000 const WSL_RPC_TIMEOUT_MS = 25_000 @@ -26,6 +30,7 @@ const MAX_DIAGNOSTIC_OUTPUT_LENGTH = 100_000 export type FetchCodexRateLimitsOptions = { codexHomePath?: string | null allowPtyFallback?: boolean + signal?: AbortSignal } // --------------------------------------------------------------------------- @@ -117,9 +122,10 @@ function buildWslCodexCommand( return null } const script = [ + ...getHiddenRateLimitWslCwdSetupCommands(), `export CODEX_HOME=${shellQuote(wslInfo.linuxPath)}`, `exec codex ${args.map(shellQuote).join(' ')}` - ].join('; ') + ].join(' && ') return { command: 'wsl.exe', args: ['-d', wslInfo.distro, '--', 'bash', '-lc', script] @@ -161,6 +167,17 @@ function normalizeCreditStatus(status: string | undefined): string { return status?.toLowerCase() ?? 'unknown' } +function abortedCodexRateLimitResult(): ProviderRateLimits { + return { + provider: 'codex', + session: null, + weekly: null, + updatedAt: Date.now(), + error: 'Rate-limit fetch aborted', + status: 'error' + } +} + function getNextAvailableCreditExpiry( credits: RateLimitResetCredits['credits'] | undefined ): number | null { @@ -228,6 +245,9 @@ function mapBackendRateLimitResetCredits( async function getCodexBackendAuthHeaders( options?: FetchCodexRateLimitsOptions ): Promise { + if (options?.signal?.aborted) { + return null + } const authPath = join(getCodexHomePath(options?.codexHomePath), 'auth.json') const auth = JSON.parse(await readFile(authPath, 'utf8')) as CodexAuthFile const accessToken = auth.tokens?.access_token @@ -250,16 +270,22 @@ async function getCodexBackendAuthHeaders( async function fetchBackendRateLimitResetCredits( options?: FetchCodexRateLimitsOptions ): Promise { + if (options?.signal?.aborted) { + return null + } const auth = await getCodexBackendAuthHeaders(options) if (!auth) { return null } + if (options?.signal?.aborted) { + return null + } // Why: published Codex 0.140 can read windows through app-server but strips // reset-credit metadata that the backend already returns. - const response = await fetch( - 'https://chatgpt.com/backend-api/wham/rate-limit-reset-credits', - auth - ) + const response = await fetch('https://chatgpt.com/backend-api/wham/rate-limit-reset-credits', { + ...auth, + signal: options?.signal + }) if (!response.ok) { return null } @@ -272,6 +298,7 @@ async function withBackendRateLimitResetCredits( options?: FetchCodexRateLimitsOptions ): Promise { if ( + options?.signal?.aborted || limits.provider !== 'codex' || (limits.rateLimitResetCredits?.nextExpiresAt !== undefined && limits.rateLimitResetCredits.nextExpiresAt !== null) @@ -373,6 +400,9 @@ function mapRpcWindow( // --------------------------------------------------------------------------- async function fetchViaRpc(options?: FetchCodexRateLimitsOptions): Promise { + if (options?.signal?.aborted) { + return abortedCodexRateLimitResult() + } return new Promise((resolve) => { let buffer = '' let stderr = '' @@ -396,6 +426,7 @@ async function fetchViaRpc(options?: FetchCodexRateLimitsOptions): Promise { settle( { @@ -632,7 +676,13 @@ function parsePtyStatus(output: string): { } async function fetchViaPty(options?: FetchCodexRateLimitsOptions): Promise { + if (options?.signal?.aborted) { + return abortedCodexRateLimitResult() + } const pty = await import('node-pty') + if (options?.signal?.aborted) { + return abortedCodexRateLimitResult() + } const wslCodex = options?.codexHomePath ? buildWslCodexCommand(options.codexHomePath, []) : null const codexCommand = wslCodex ? 'codex' : resolveCodexCommand() @@ -652,20 +702,50 @@ async function fetchViaPty(options?: FetchCodexRateLimitsOptions): Promise | null = null + let timeout: ReturnType | null = null const term = pty.spawn(spawnFile, spawnArgs, { name: 'xterm-256color', cols: 120, rows: 40, + cwd: resolveHiddenRateLimitPtyCwd(), env: { ...(wslCodex ? cloneProcessEnvWithoutCodexHome() : process.env), TERM: 'xterm-256color', ...(options?.codexHomePath && !wslCodex ? { CODEX_HOME: options.codexHomePath } : {}) } }) - const termDisposables: { dispose: () => void }[] = [] + const termDisposables: { dispose: () => void }[] = [registerHiddenRateLimitPty(term)] - const timeout = setTimeout(() => { + function settleAborted(): void { + if (resolved) { + return + } + resolved = true + if (timeout) { + clearTimeout(timeout) + timeout = null + } + if (settleTimer) { + clearTimeout(settleTimer) + settleTimer = null + } + cleanupHiddenRateLimitPty(term, termDisposables, { kill: true }) + resolve(abortedCodexRateLimitResult()) + } + + if (options?.signal) { + if (options.signal.aborted) { + settleAborted() + return + } + options.signal.addEventListener('abort', settleAborted, { once: true }) + termDisposables.push({ + dispose: () => options.signal?.removeEventListener('abort', settleAborted) + }) + } + + timeout = setTimeout(() => { if (!resolved) { resolved = true if (settleTimer) { @@ -709,7 +789,10 @@ async function fetchViaPty(options?: FetchCodexRateLimitsOptions): Promise { + if (options?.signal?.aborted) { + return abortedCodexRateLimitResult() + } // Why: never spawn the `codex` binary unless the user has signed in. Without // auth the RPC/PTY paths can only error, and spawning them shows up as an // unexpected background Codex process for users who don't use Codex. @@ -790,8 +879,12 @@ export async function fetchCodexRateLimits( // Path A: try RPC first try { const rpcResult = await fetchViaRpc(options) + if (options?.signal?.aborted) { + return abortedCodexRateLimitResult() + } if (rpcResult.status === 'ok' || rpcResult.status === 'unavailable') { - return await withBackendRateLimitResetCredits(rpcResult, options) + const withResetCredits = await withBackendRateLimitResetCredits(rpcResult, options) + return options?.signal?.aborted ? abortedCodexRateLimitResult() : withResetCredits } if (isCodexAuthError(rpcResult.error)) { return rpcResult @@ -802,6 +895,9 @@ export async function fetchCodexRateLimits( // Why: app-server can fail independently of the interactive CLI. Keep the // status-bar useful by trying the older /status PTY reader on RPC errors. } catch { + if (options?.signal?.aborted) { + return abortedCodexRateLimitResult() + } if (options?.allowPtyFallback === false) { return { provider: 'codex', @@ -817,8 +913,19 @@ export async function fetchCodexRateLimits( // Path B: PTY fallback try { - return await withBackendRateLimitResetCredits(await fetchViaPty(options), options) + if (options?.signal?.aborted) { + return abortedCodexRateLimitResult() + } + const ptyResult = await fetchViaPty(options) + if (options?.signal?.aborted) { + return abortedCodexRateLimitResult() + } + const withResetCredits = await withBackendRateLimitResetCredits(ptyResult, options) + return options?.signal?.aborted ? abortedCodexRateLimitResult() : withResetCredits } catch (err) { + if (options?.signal?.aborted) { + return abortedCodexRateLimitResult() + } const message = err instanceof Error ? err.message : 'Unknown error' const isNotInstalled = message.includes('ENOENT') return { diff --git a/src/main/rate-limits/hidden-pty-cleanup.test.ts b/src/main/rate-limits/hidden-pty-cleanup.test.ts index 094931a97b0..43e8453fa0f 100644 --- a/src/main/rate-limits/hidden-pty-cleanup.test.ts +++ b/src/main/rate-limits/hidden-pty-cleanup.test.ts @@ -1,5 +1,9 @@ import { afterEach, describe, expect, it, vi } from 'vitest' -import { cleanupHiddenRateLimitPty } from './hidden-pty-cleanup' +import { + cleanupHiddenRateLimitPty, + getActiveHiddenRateLimitPtyCount, + registerHiddenRateLimitPty +} from './hidden-pty-cleanup' function setPlatform(platform: NodeJS.Platform): void { Object.defineProperty(process, 'platform', { @@ -98,4 +102,34 @@ describe('cleanupHiddenRateLimitPty', () => { expect(term.kill).toHaveBeenCalledTimes(1) expect(term.destroy).toHaveBeenCalledTimes(1) }) + + it('removes registered hidden PTYs when cleanup kills them', () => { + setPlatform('darwin') + const term = { + kill: vi.fn(), + destroy: vi.fn() + } + const registration = registerHiddenRateLimitPty(term) + + expect(getActiveHiddenRateLimitPtyCount()).toBe(1) + + cleanupHiddenRateLimitPty(term, [registration], { kill: true }) + + expect(getActiveHiddenRateLimitPtyCount()).toBe(0) + }) + + it('removes registered hidden PTYs after natural exit cleanup', () => { + setPlatform('darwin') + const term = { + kill: vi.fn(), + destroy: vi.fn() + } + const registration = registerHiddenRateLimitPty(term) + + expect(getActiveHiddenRateLimitPtyCount()).toBe(1) + + cleanupHiddenRateLimitPty(term, [registration], { kill: false }) + + expect(getActiveHiddenRateLimitPtyCount()).toBe(0) + }) }) diff --git a/src/main/rate-limits/hidden-pty-cleanup.ts b/src/main/rate-limits/hidden-pty-cleanup.ts index 6d85421203c..068b937ee84 100644 --- a/src/main/rate-limits/hidden-pty-cleanup.ts +++ b/src/main/rate-limits/hidden-pty-cleanup.ts @@ -7,6 +7,21 @@ type Disposable = { dispose: () => void } +const activeHiddenRateLimitPtys = new Set() + +export function registerHiddenRateLimitPty(term: HiddenPty): Disposable { + activeHiddenRateLimitPtys.add(term) + return { + dispose: () => { + activeHiddenRateLimitPtys.delete(term) + } + } +} + +export function getActiveHiddenRateLimitPtyCount(): number { + return activeHiddenRateLimitPtys.size +} + export function cleanupHiddenRateLimitPty( term: HiddenPty, disposables: Disposable[], diff --git a/src/main/rate-limits/hidden-rate-limit-pty-cwd.ts b/src/main/rate-limits/hidden-rate-limit-pty-cwd.ts new file mode 100644 index 00000000000..bb78f041bd5 --- /dev/null +++ b/src/main/rate-limits/hidden-rate-limit-pty-cwd.ts @@ -0,0 +1,37 @@ +import { mkdirSync, realpathSync, statSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' + +import { isRootLikePath } from '../providers/pty-path-safety' + +const HIDDEN_RATE_LIMIT_PTY_CWD_DIR = 'rate-limit-pty-cwd' +const WSL_RATE_LIMIT_PTY_CWD_DIR = 'orca-rate-limit-pty-cwd' + +// Why: the hidden usage PTY must run in a bounded, never-root directory so +// Claude's discovery cannot walk a whole filesystem — reject a root-like user +// data path and scope to tmpdir instead (see runaway-cpu-hidden-usage-pty-design.md). +function resolveUserDataRoot(userDataPath?: string | null): string { + const root = userDataPath?.trim() || process.env.ORCA_USER_DATA_PATH?.trim() + if (root && !isRootLikePath(root)) { + return root + } + return join(tmpdir(), 'orca-rate-limit-pty') +} + +export function resolveHiddenRateLimitPtyCwd(options?: { userDataPath?: string | null }): string { + const cwd = join(resolveUserDataRoot(options?.userDataPath), HIDDEN_RATE_LIMIT_PTY_CWD_DIR) + mkdirSync(cwd, { recursive: true }) + const realCwd = realpathSync(cwd) + if (isRootLikePath(realCwd) || !statSync(realCwd).isDirectory()) { + throw new Error(`Hidden rate-limit PTY cwd is not a safe directory: ${realCwd}`) + } + return realCwd +} + +export function getHiddenRateLimitWslCwdSetupCommands(): string[] { + return [ + `orca_rate_limit_cwd="\${TMPDIR:-/tmp}/${WSL_RATE_LIMIT_PTY_CWD_DIR}"`, + 'mkdir -p "$orca_rate_limit_cwd"', + 'cd "$orca_rate_limit_cwd"' + ] +} diff --git a/src/main/rate-limits/service.test.ts b/src/main/rate-limits/service.test.ts index 5af9abca04f..1b810859ce7 100644 --- a/src/main/rate-limits/service.test.ts +++ b/src/main/rate-limits/service.test.ts @@ -333,6 +333,110 @@ describe('RateLimitService', () => { expect(fetchCodexRateLimits).toHaveBeenCalledTimes(2) }) + it('aborts the active fetch cycle and clears queued refreshes on stop', async () => { + const service = new RateLimitService() + const capturedSignals: { claude?: AbortSignal; codex?: AbortSignal } = {} + + vi.mocked(fetchClaudeRateLimits).mockImplementation( + (options) => + new Promise((resolve) => { + capturedSignals.claude = options?.signal + options?.signal?.addEventListener( + 'abort', + () => resolve(errorProvider('claude', 'aborted')), + { once: true } + ) + }) + ) + vi.mocked(fetchCodexRateLimits).mockImplementation( + (options) => + new Promise((resolve) => { + capturedSignals.codex = options?.signal + options?.signal?.addEventListener( + 'abort', + () => resolve(errorProvider('codex', 'aborted')), + { once: true } + ) + }) + ) + + const activeFetch = serviceInternals(service).fetchAll() + await Promise.resolve() + await Promise.resolve() + + const queuedRefresh = service.refresh() + await Promise.resolve() + + service.stop() + + expect(capturedSignals.claude?.aborted).toBe(true) + expect(capturedSignals.codex?.aborted).toBe(true) + + await queuedRefresh + await activeFetch + + expect(fetchClaudeRateLimits).toHaveBeenCalledTimes(1) + expect(fetchCodexRateLimits).toHaveBeenCalledTimes(1) + }) + + it('aborts inactive Claude preview fetches on stop', async () => { + const service = new RateLimitService() + const account = { id: 'account-1', managedAuthPath: '/tmp/account-1/auth' } + const capturedSignals: { claude?: AbortSignal } = {} + service.setInactiveClaudeAccountsResolver(() => [account]) + vi.mocked(fetchManagedAccountUsage).mockImplementation( + (_account, options) => + new Promise((resolve) => { + capturedSignals.claude = options?.signal + options?.signal?.addEventListener( + 'abort', + () => resolve(errorProvider('claude', 'aborted')), + { once: true } + ) + }) + ) + + const previewFetch = service.fetchInactiveClaudeAccountsOnOpen() + await Promise.resolve() + + service.stop() + + expect(capturedSignals.claude?.aborted).toBe(true) + + await previewFetch + + expect(service.getState().inactiveClaudeAccounts).toEqual([]) + }) + + it('aborts inactive Codex preview fetches on stop', async () => { + const service = new RateLimitService() + const account = { id: 'account-1', managedHomePath: '/tmp/account-1/home' } + const capturedSignals: { codex?: AbortSignal } = {} + service.setInactiveCodexAccountsResolver(() => [account]) + vi.mocked(fetchCodexRateLimits).mockImplementation( + (options) => + new Promise((resolve) => { + capturedSignals.codex = options?.signal + options?.signal?.addEventListener( + 'abort', + () => resolve(errorProvider('codex', 'aborted')), + { once: true } + ) + }) + ) + + const previewFetch = service.fetchInactiveCodexAccountsOnOpen() + await Promise.resolve() + + service.stop() + + expect(capturedSignals.codex?.aborted).toBe(true) + + await previewFetch + + expect(service.getState().inactiveCodexAccounts).toEqual([]) + }) + it('fetches Gemini and OpenCode Go alongside Claude and Codex', async () => { const service = new RateLimitService() service.setOpenCodeGoConfigResolver(() => ({ @@ -351,11 +455,14 @@ describe('RateLimitService', () => { await service.refresh() expect(fetchClaudeRateLimits).toHaveBeenCalledTimes(1) - expect(fetchClaudeRateLimits).toHaveBeenCalledWith({ - authPreparation: undefined, - allowPtyFallback: false, - allowUsagePanelSupplement: true - }) + expect(fetchClaudeRateLimits).toHaveBeenCalledWith( + expect.objectContaining({ + authPreparation: undefined, + allowPtyFallback: false, + allowUsagePanelSupplement: true, + signal: expect.any(AbortSignal) + }) + ) expect(fetchCodexRateLimits).toHaveBeenCalledTimes(1) expect(fetchGeminiRateLimits).toHaveBeenCalledTimes(1) expect(fetchGeminiRateLimits).toHaveBeenCalledWith(true) @@ -453,16 +560,19 @@ describe('RateLimitService', () => { await service.refresh() expect(resolver).toHaveBeenCalledWith({ runtime: 'wsl', wslDistro: 'Ubuntu' }) - expect(fetchClaudeRateLimits).toHaveBeenCalledWith({ - authPreparation: expect.objectContaining({ - runtime: 'wsl', - wslDistro: 'Ubuntu', - wslLinuxConfigDir: '/home/jin/.claude', - stripAuthEnv: true - }), - allowPtyFallback: true, - allowUsagePanelSupplement: true - }) + expect(fetchClaudeRateLimits).toHaveBeenCalledWith( + expect.objectContaining({ + authPreparation: expect.objectContaining({ + runtime: 'wsl', + wslDistro: 'Ubuntu', + wslLinuxConfigDir: '/home/jin/.claude', + stripAuthEnv: true + }), + allowPtyFallback: true, + allowUsagePanelSupplement: true, + signal: expect.any(AbortSignal) + }) + ) expect(service.getState().claudeTarget).toEqual({ runtime: 'wsl', wslDistro: 'Ubuntu' }) }) @@ -483,11 +593,14 @@ describe('RateLimitService', () => { await service.refresh() - expect(fetchClaudeRateLimits).toHaveBeenCalledWith({ - authPreparation: expect.objectContaining({ provenance: 'system' }), - allowPtyFallback: false, - allowUsagePanelSupplement: true - }) + expect(fetchClaudeRateLimits).toHaveBeenCalledWith( + expect.objectContaining({ + authPreparation: expect.objectContaining({ provenance: 'system' }), + allowPtyFallback: false, + allowUsagePanelSupplement: true, + signal: expect.any(AbortSignal) + }) + ) }) it('does not use Claude PTY fallback when Claude auth preparation is unavailable', async () => { @@ -498,11 +611,14 @@ describe('RateLimitService', () => { await service.refresh() - expect(fetchClaudeRateLimits).toHaveBeenCalledWith({ - authPreparation: undefined, - allowPtyFallback: false, - allowUsagePanelSupplement: true - }) + expect(fetchClaudeRateLimits).toHaveBeenCalledWith( + expect.objectContaining({ + authPreparation: undefined, + allowPtyFallback: false, + allowUsagePanelSupplement: true, + signal: expect.any(AbortSignal) + }) + ) }) it('does not use Claude PTY fallback for WSL system-default usage refreshes', async () => { @@ -523,11 +639,14 @@ describe('RateLimitService', () => { await service.refresh() - expect(fetchClaudeRateLimits).toHaveBeenCalledWith({ - authPreparation: expect.objectContaining({ provenance: 'wsl:Ubuntu:system' }), - allowPtyFallback: false, - allowUsagePanelSupplement: true - }) + expect(fetchClaudeRateLimits).toHaveBeenCalledWith( + expect.objectContaining({ + authPreparation: expect.objectContaining({ provenance: 'wsl:Ubuntu:system' }), + allowPtyFallback: false, + allowUsagePanelSupplement: true, + signal: expect.any(AbortSignal) + }) + ) }) it('does not cache host Codex usage under an outgoing WSL account', async () => { @@ -604,10 +723,13 @@ describe('RateLimitService', () => { await service.fetchInactiveCodexAccountsOnOpen() - expect(fetchCodexRateLimits).toHaveBeenCalledWith({ - codexHomePath: wslCodexHome, - allowPtyFallback: false - }) + expect(fetchCodexRateLimits).toHaveBeenCalledWith( + expect.objectContaining({ + codexHomePath: wslCodexHome, + allowPtyFallback: false, + signal: expect.any(AbortSignal) + }) + ) expect(service.getState().inactiveCodexAccounts).toEqual([ { accountId: 'account-1', @@ -629,9 +751,13 @@ describe('RateLimitService', () => { await service.fetchInactiveClaudeAccountsOnOpen() - expect(fetchManagedAccountUsage).toHaveBeenCalledWith(account, { - allowUsagePanelSupplement: true - }) + expect(fetchManagedAccountUsage).toHaveBeenCalledWith( + account, + expect.objectContaining({ + allowUsagePanelSupplement: true, + signal: expect.any(AbortSignal) + }) + ) }) it('does not start overlapping inactive Codex preview fetches', async () => { diff --git a/src/main/rate-limits/service.ts b/src/main/rate-limits/service.ts index e3befa61404..840f4e4a788 100644 --- a/src/main/rate-limits/service.ts +++ b/src/main/rate-limits/service.ts @@ -103,6 +103,7 @@ export class RateLimitService { private fullFetchQueued = false private codexOnlyFetchQueued = false private claudeOnlyFetchQueued = false + private activeFetchAbortControllers = new Set() private fetchIdleResolvers: (() => void)[] = [] private codexFetchGeneration = 0 private claudeFetchGeneration = 0 @@ -221,6 +222,11 @@ export class RateLimitService { } stop(): void { + this.abortActiveFetchCycle() + this.clearQueuedFetches() + this.inactiveClaudeFetching.clear() + this.inactiveCodexFetching.clear() + this.resolveAndClearFetchIdleWaiters() this.stopTimer() this.clearDeferredStartupRefresh() this.detachWindowListeners?.() @@ -369,30 +375,18 @@ export class RateLimitService { return } const fetchGeneration = this.inactiveClaudeAccountsGeneration + const controller = this.beginFetchCycle() + const signal = controller.signal for (const account of accounts) { this.inactiveClaudeFetching.add(account.id) } this.pushToRenderer() - for (const account of accounts) { - if ( - fetchGeneration !== this.inactiveClaudeAccountsGeneration || - !this.isCurrentInactiveClaudeAccount(account.id) - ) { - this.inactiveClaudeFetching.delete(account.id) - if (!this.isCurrentInactiveClaudeAccount(account.id)) { - this.inactiveClaudeCache.delete(account.id) - } - this.pushToRenderer() - continue - } - try { - const fresh = await fetchManagedAccountUsage(account, { - allowUsagePanelSupplement: this.shouldAllowClaudeUsagePanelSupplement(), - networkProxySettings: this.networkProxySettingsResolver?.() - }) + try { + for (const account of accounts) { if ( + signal.aborted || fetchGeneration !== this.inactiveClaudeAccountsGeneration || !this.isCurrentInactiveClaudeAccount(account.id) ) { @@ -403,24 +397,46 @@ export class RateLimitService { this.pushToRenderer() continue } - const cached = this.inactiveClaudeCache.get(account.id) ?? null - this.inactiveClaudeCache.set(account.id, this.applyStalePolicy(fresh, cached)) - } catch { - // Why: per-account try/catch prevents one Keychain rejection or - // network error from aborting the remaining accounts in the batch. - if ( - fetchGeneration !== this.inactiveClaudeAccountsGeneration || - !this.isCurrentInactiveClaudeAccount(account.id) - ) { - this.inactiveClaudeCache.delete(account.id) + try { + const fresh = await fetchManagedAccountUsage(account, { + allowUsagePanelSupplement: this.shouldAllowClaudeUsagePanelSupplement(), + networkProxySettings: this.networkProxySettingsResolver?.(), + signal + }) + if ( + signal.aborted || + fetchGeneration !== this.inactiveClaudeAccountsGeneration || + !this.isCurrentInactiveClaudeAccount(account.id) + ) { + this.inactiveClaudeFetching.delete(account.id) + if (!this.isCurrentInactiveClaudeAccount(account.id)) { + this.inactiveClaudeCache.delete(account.id) + } + this.pushToRenderer() + continue + } + const cached = this.inactiveClaudeCache.get(account.id) ?? null + this.inactiveClaudeCache.set(account.id, this.applyStalePolicy(fresh, cached)) + } catch { + // Why: per-account try/catch prevents one Keychain rejection or + // network error from aborting the remaining accounts in the batch. + if ( + signal.aborted || + fetchGeneration !== this.inactiveClaudeAccountsGeneration || + !this.isCurrentInactiveClaudeAccount(account.id) + ) { + this.inactiveClaudeCache.delete(account.id) + } } + this.inactiveClaudeFetching.delete(account.id) + this.pushToRenderer() } - this.inactiveClaudeFetching.delete(account.id) - this.pushToRenderer() - } - if (fetchGeneration === this.inactiveClaudeAccountsGeneration) { - this.lastInactiveClaudeFetchAt = Date.now() + if (!signal.aborted && fetchGeneration === this.inactiveClaudeAccountsGeneration) { + this.lastInactiveClaudeFetchAt = Date.now() + } + } finally { + this.finishFetchCycle(controller) } } @@ -439,36 +455,18 @@ export class RateLimitService { // Why: account switching can make a previewed account active while its // RPC-only usage fetch is still in flight; stale results must be ignored. const fetchGeneration = this.inactiveCodexAccountsGeneration + const controller = this.beginFetchCycle() + const signal = controller.signal for (const account of accounts) { this.inactiveCodexFetching.add(account.id) } this.pushToRenderer() - for (const account of accounts) { - if ( - fetchGeneration !== this.inactiveCodexAccountsGeneration || - !this.isCurrentInactiveCodexAccount(account.id) - ) { - this.inactiveCodexFetching.delete(account.id) - if (!this.isCurrentInactiveCodexAccount(account.id)) { - this.inactiveCodexCache.delete(account.id) - } - this.pushToRenderer() - continue - } - try { - // Why: fetchCodexRateLimits already accepts codexHomePath, so we can - // point it at the managed account's home directory directly without - // materializing credentials into the shared runtime location. - // Why: opening the account switcher should never start hidden PTYs for - // every inactive account. On Windows that fallback can crash inside - // ConPTY; RPC-only is enough for this non-critical preview surface. - const fresh = await fetchCodexRateLimits({ - codexHomePath: account.managedHomePath, - allowPtyFallback: false - }) + try { + for (const account of accounts) { if ( + signal.aborted || fetchGeneration !== this.inactiveCodexAccountsGeneration || !this.isCurrentInactiveCodexAccount(account.id) ) { @@ -479,23 +477,51 @@ export class RateLimitService { this.pushToRenderer() continue } - const cached = this.inactiveCodexCache.get(account.id) ?? null - this.inactiveCodexCache.set(account.id, this.applyStalePolicy(fresh, cached)) - } catch { - // Why: per-account try/catch prevents one failure from aborting the batch. - if ( - fetchGeneration !== this.inactiveCodexAccountsGeneration || - !this.isCurrentInactiveCodexAccount(account.id) - ) { - this.inactiveCodexCache.delete(account.id) + try { + // Why: fetchCodexRateLimits already accepts codexHomePath, so we can + // point it at the managed account's home directory directly without + // materializing credentials into the shared runtime location. + // Why: opening the account switcher should never start hidden PTYs for + // every inactive account. On Windows that fallback can crash inside + // ConPTY; RPC-only is enough for this non-critical preview surface. + const fresh = await fetchCodexRateLimits({ + codexHomePath: account.managedHomePath, + allowPtyFallback: false, + signal + }) + if ( + signal.aborted || + fetchGeneration !== this.inactiveCodexAccountsGeneration || + !this.isCurrentInactiveCodexAccount(account.id) + ) { + this.inactiveCodexFetching.delete(account.id) + if (!this.isCurrentInactiveCodexAccount(account.id)) { + this.inactiveCodexCache.delete(account.id) + } + this.pushToRenderer() + continue + } + const cached = this.inactiveCodexCache.get(account.id) ?? null + this.inactiveCodexCache.set(account.id, this.applyStalePolicy(fresh, cached)) + } catch { + // Why: per-account try/catch prevents one failure from aborting the batch. + if ( + signal.aborted || + fetchGeneration !== this.inactiveCodexAccountsGeneration || + !this.isCurrentInactiveCodexAccount(account.id) + ) { + this.inactiveCodexCache.delete(account.id) + } } + this.inactiveCodexFetching.delete(account.id) + this.pushToRenderer() } - this.inactiveCodexFetching.delete(account.id) - this.pushToRenderer() - } - if (fetchGeneration === this.inactiveCodexAccountsGeneration) { - this.lastInactiveCodexFetchAt = Date.now() + if (!signal.aborted && fetchGeneration === this.inactiveCodexAccountsGeneration) { + this.lastInactiveCodexFetchAt = Date.now() + } + } finally { + this.finishFetchCycle(controller) } } @@ -644,8 +670,13 @@ export class RateLimitService { try { let shouldContinue = true while (shouldContinue) { - await this.runFetchAllCycle() + const signal = await this.runWithFetchAbortSignal((fetchSignal) => + this.runFetchAllCycle(fetchSignal) + ) shouldContinue = false + if (signal.aborted) { + break + } if (this.fullFetchQueued) { this.fullFetchQueued = false shouldContinue = true @@ -653,11 +684,21 @@ export class RateLimitService { } if (this.codexOnlyFetchQueued) { this.codexOnlyFetchQueued = false - await this.runFetchCodexOnlyCycle() + const codexSignal = await this.runWithFetchAbortSignal((fetchSignal) => + this.runFetchCodexOnlyCycle(fetchSignal) + ) + if (codexSignal.aborted) { + break + } } if (this.claudeOnlyFetchQueued) { this.claudeOnlyFetchQueued = false - await this.runFetchClaudeOnlyCycle() + const claudeSignal = await this.runWithFetchAbortSignal((fetchSignal) => + this.runFetchClaudeOnlyCycle(fetchSignal) + ) + if (claudeSignal.aborted) { + break + } } } } finally { @@ -679,11 +720,21 @@ export class RateLimitService { try { let shouldContinue = true while (shouldContinue) { - await this.runFetchCodexOnlyCycle() + const signal = await this.runWithFetchAbortSignal((fetchSignal) => + this.runFetchCodexOnlyCycle(fetchSignal) + ) shouldContinue = false + if (signal.aborted) { + break + } if (this.fullFetchQueued) { this.fullFetchQueued = false - await this.runFetchAllCycle() + const fullSignal = await this.runWithFetchAbortSignal((fetchSignal) => + this.runFetchAllCycle(fetchSignal) + ) + if (fullSignal.aborted) { + break + } continue } if (this.codexOnlyFetchQueued) { @@ -692,7 +743,12 @@ export class RateLimitService { } if (this.claudeOnlyFetchQueued) { this.claudeOnlyFetchQueued = false - await this.runFetchClaudeOnlyCycle() + const claudeSignal = await this.runWithFetchAbortSignal((fetchSignal) => + this.runFetchClaudeOnlyCycle(fetchSignal) + ) + if (claudeSignal.aborted) { + break + } } } } finally { @@ -714,11 +770,21 @@ export class RateLimitService { try { let shouldContinue = true while (shouldContinue) { - await this.runFetchClaudeOnlyCycle() + const signal = await this.runWithFetchAbortSignal((fetchSignal) => + this.runFetchClaudeOnlyCycle(fetchSignal) + ) shouldContinue = false + if (signal.aborted) { + break + } if (this.fullFetchQueued) { this.fullFetchQueued = false - await this.runFetchAllCycle() + const fullSignal = await this.runWithFetchAbortSignal((fetchSignal) => + this.runFetchAllCycle(fetchSignal) + ) + if (fullSignal.aborted) { + break + } continue } if (this.claudeOnlyFetchQueued) { @@ -727,7 +793,12 @@ export class RateLimitService { } if (this.codexOnlyFetchQueued) { this.codexOnlyFetchQueued = false - await this.runFetchCodexOnlyCycle() + const codexSignal = await this.runWithFetchAbortSignal((fetchSignal) => + this.runFetchCodexOnlyCycle(fetchSignal) + ) + if (codexSignal.aborted) { + break + } } } } finally { @@ -769,6 +840,49 @@ export class RateLimitService { } } + private beginFetchCycle(): AbortController { + const controller = new AbortController() + this.activeFetchAbortControllers.add(controller) + return controller + } + + private finishFetchCycle(controller: AbortController): void { + this.activeFetchAbortControllers.delete(controller) + } + + private async runWithFetchAbortSignal( + fn: (signal: AbortSignal) => Promise + ): Promise { + const controller = this.beginFetchCycle() + try { + await fn(controller.signal) + return controller.signal + } finally { + this.finishFetchCycle(controller) + } + } + + private abortActiveFetchCycle(): void { + for (const controller of this.activeFetchAbortControllers) { + controller.abort() + } + this.activeFetchAbortControllers.clear() + } + + private clearQueuedFetches(): void { + this.fullFetchQueued = false + this.codexOnlyFetchQueued = false + this.claudeOnlyFetchQueued = false + } + + private resolveAndClearFetchIdleWaiters(): void { + const resolvers = this.fetchIdleResolvers + this.fetchIdleResolvers = [] + for (const resolve of resolvers) { + resolve() + } + } + private isSameCodexTarget( left: NormalizedCodexAccountSelectionTarget, right: NormalizedCodexAccountSelectionTarget @@ -850,9 +964,15 @@ export class RateLimitService { return { ...current, status: 'fetching' } } - private async runFetchAllCycle(): Promise { + private async runFetchAllCycle(signal: AbortSignal): Promise { + if (signal.aborted) { + return + } const claudeTarget = this.claudeFetchTarget const claudeAuthPreparation = await this.claudeAuthPreparationResolver?.(claudeTarget) + if (signal.aborted) { + return + } const claudeProvenance = claudeAuthPreparation?.provenance ?? 'system' const claudeGeneration = this.claudeFetchGeneration const codexTarget = this.codexFetchTarget @@ -898,18 +1018,24 @@ export class RateLimitService { authPreparation: claudeAuthPreparation, allowPtyFallback: this.shouldAllowClaudePtyFallback(claudeAuthPreparation), allowUsagePanelSupplement: this.shouldAllowClaudeUsagePanelSupplement(), - networkProxySettings: this.networkProxySettingsResolver?.() + networkProxySettings: this.networkProxySettingsResolver?.(), + signal }), missingWslCodexHome ?? fetchCodexRateLimits({ codexHomePath, - allowPtyFallback: this.shouldAllowCodexPtyFallback() + allowPtyFallback: this.shouldAllowCodexPtyFallback(), + signal }), fetchGeminiRateLimits(geminiCliOAuthEnabled), fetchOpenCodeGoRateLimits(cookie, workspaceIdOverride || undefined), fetchKimiRateLimits() ]) + if (signal.aborted) { + return + } + const claude = claudeResult.status === 'fulfilled' ? claudeResult.value @@ -979,6 +1105,9 @@ export class RateLimitService { const latestCodexHomePath = this.codexHomePathResolver?.(codexTarget) ?? null const latestClaudeAuthPreparation = await this.claudeAuthPreparationResolver?.(claudeTarget) + if (signal.aborted) { + return + } const latestClaudeProvenance = latestClaudeAuthPreparation?.provenance ?? 'system' const latestCodexProvenance = this.getCodexProvenance(codexTarget, latestCodexHomePath) const shouldApplyCodex = @@ -1013,7 +1142,10 @@ export class RateLimitService { this.lastFetchAt = Date.now() } - private async runFetchCodexOnlyCycle(): Promise { + private async runFetchCodexOnlyCycle(signal: AbortSignal): Promise { + if (signal.aborted) { + return + } const codexTarget = this.codexFetchTarget const codexHomePath = this.codexHomePathResolver?.(codexTarget) ?? null const codexProvenance = this.getCodexProvenance(codexTarget, codexHomePath) @@ -1033,7 +1165,8 @@ export class RateLimitService { ? Promise.resolve(missingWslCodexHome) : fetchCodexRateLimits({ codexHomePath, - allowPtyFallback: this.shouldAllowCodexPtyFallback() + allowPtyFallback: this.shouldAllowCodexPtyFallback(), + signal }) ).catch( (err): ProviderRateLimits => ({ @@ -1046,6 +1179,10 @@ export class RateLimitService { }) ) + if (signal.aborted) { + return + } + const latestCodexHomePath = this.codexHomePathResolver?.(codexTarget) ?? null const latestCodexProvenance = this.getCodexProvenance(codexTarget, latestCodexHomePath) const shouldApplyCodex = @@ -1059,9 +1196,15 @@ export class RateLimitService { this.lastFetchAt = Date.now() } - private async runFetchClaudeOnlyCycle(): Promise { + private async runFetchClaudeOnlyCycle(signal: AbortSignal): Promise { + if (signal.aborted) { + return + } const claudeTarget = this.claudeFetchTarget const claudeAuthPreparation = await this.claudeAuthPreparationResolver?.(claudeTarget) + if (signal.aborted) { + return + } const claudeProvenance = claudeAuthPreparation?.provenance ?? 'system' const claudeGeneration = this.claudeFetchGeneration const previousState = this.state @@ -1075,7 +1218,8 @@ export class RateLimitService { authPreparation: claudeAuthPreparation, allowPtyFallback: this.shouldAllowClaudePtyFallback(claudeAuthPreparation), allowUsagePanelSupplement: this.shouldAllowClaudeUsagePanelSupplement(), - networkProxySettings: this.networkProxySettingsResolver?.() + networkProxySettings: this.networkProxySettingsResolver?.(), + signal }).catch( (err): ProviderRateLimits => ({ provider: 'claude', @@ -1087,7 +1231,14 @@ export class RateLimitService { }) ) + if (signal.aborted) { + return + } + const latestClaudeAuthPreparation = await this.claudeAuthPreparationResolver?.(claudeTarget) + if (signal.aborted) { + return + } const latestClaudeProvenance = latestClaudeAuthPreparation?.provenance ?? 'system' const shouldApplyClaude = claudeGeneration === this.claudeFetchGeneration && From e94c83d164bc448b415e5cc165f9efdd723c28e7 Mon Sep 17 00:00:00 2001 From: Avichal Dwivedi <150160561+AvichalDwivedi2205@users.noreply.github.com> Date: Sun, 5 Jul 2026 11:26:55 +0530 Subject: [PATCH 44/45] fix(ai-vault): make SSH session history host-aware (#7367) * fix(ai-vault): scan sessions by execution host * fix(ai-vault): route history resume by host * test(e2e): cover SSH AI Vault history * Generalize remote session scanning for all AI Vault agents Replace the Codex-only remote SSH session history scanner with a unified scanner supporting all registered agents. This ensures remote transcripts for Claude, Gemini, Devin, Droid, and others are scanned and listed alongside local history. - Propagate host metadata (host ID and platform) to scanned sessions - Scope remote actions by host, disabling local OS path actions on remote session logs - Resolve ambiguous project/worktree matching for overlapping paths by verifying matching host setup IDs - Update tests and E2E specs to validate multi-agent remote scanning --------- Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com> --- .../remote-session-scanner-codex-index.ts | 51 +++ .../remote-session-scanner-sources.ts | 221 +++++++++++ .../ai-vault/remote-session-scanner-types.ts | 34 ++ .../ai-vault/remote-session-scanner.test.ts | 344 ++++++++++++++++++ src/main/ai-vault/remote-session-scanner.ts | 296 +++++++++++++++ .../ai-vault/session-scanner-accumulator.ts | 15 +- .../ai-vault/session-scanner-agent-parser.ts | 7 +- .../ai-vault/session-scanner-codex-parser.ts | 67 +++- .../ai-vault/session-scanner-devin-parser.ts | 19 +- .../ai-vault/session-scanner-droid-parser.ts | 120 ++++++ .../ai-vault/session-scanner-graph-parsers.ts | 112 ++---- .../session-scanner-primary-parsers.ts | 101 ++++- .../session-scanner-secondary-parsers.ts | 93 ++++- src/main/ai-vault/session-scanner-types.ts | 2 + src/main/ai-vault/session-scanner.ts | 36 +- src/main/ipc/ai-vault.test.ts | 163 +++++++++ src/main/ipc/ai-vault.ts | 174 ++++++++- src/main/ipc/ssh.ts | 19 +- src/main/ssh/ssh-relay-session.ts | 24 ++ .../components/right-sidebar/AiVaultPanel.tsx | 17 +- .../right-sidebar/AiVaultPanelControls.tsx | 62 +++- .../right-sidebar/AiVaultPanelHeader.tsx | 14 +- .../AiVaultSessionActionMenuItems.tsx | 108 ++++++ .../right-sidebar/AiVaultSessionDetails.tsx | 32 +- .../right-sidebar/AiVaultSessionRow.tsx | 111 +----- .../AiVaultSessionVirtualList.tsx | 11 +- .../SessionRowTrailingActions.tsx | 6 +- .../right-sidebar/ai-vault-host-scope.test.ts | 120 ++++++ .../right-sidebar/ai-vault-host-scope.ts | 64 ++++ .../ai-vault-original-pane.test.ts | 1 + .../ai-vault-session-display.test.ts | 1 + .../ai-vault-session-filters.test.ts | 1 + .../ai-vault-session-launch-actions.ts | 26 +- .../ai-vault-session-path-actions.test.ts | 14 + .../ai-vault-session-path-actions.ts | 13 + .../ai-vault-session-projects.test.ts | 30 +- .../ai-vault-session-projects.ts | 57 ++- .../ai-vault-session-refresh.test.ts | 80 +++- .../right-sidebar/ai-vault-session-refresh.ts | 156 ++++---- .../right-sidebar/ai-vault-session-resume.ts | 21 +- .../ai-vault-session-worktree.test.ts | 65 +++- .../ai-vault-session-worktree.ts | 33 +- .../tab-group/AiVaultSessionDropLayer.tsx | 13 +- src/renderer/src/i18n/locales/en.json | 6 +- src/renderer/src/i18n/locales/es.json | 10 +- src/renderer/src/i18n/locales/ja.json | 10 +- src/renderer/src/i18n/locales/ko.json | 10 +- src/renderer/src/i18n/locales/zh.json | 10 +- .../src/lib/ai-vault-resume-command.test.ts | 107 ++++++ .../src/lib/ai-vault-resume-command.ts | 23 +- .../src/lib/ai-vault-resume-target.test.ts | 74 ++++ .../src/lib/ai-vault-resume-target.ts | 82 +++++ src/renderer/src/lib/ai-vault-session-drag.ts | 16 +- src/shared/ai-vault-types.ts | 5 + .../e2e/ssh-ai-vault-session-history.spec.ts | 293 +++++++++++++++ 55 files changed, 3197 insertions(+), 403 deletions(-) create mode 100644 src/main/ai-vault/remote-session-scanner-codex-index.ts create mode 100644 src/main/ai-vault/remote-session-scanner-sources.ts create mode 100644 src/main/ai-vault/remote-session-scanner-types.ts create mode 100644 src/main/ai-vault/remote-session-scanner.test.ts create mode 100644 src/main/ai-vault/remote-session-scanner.ts create mode 100644 src/main/ai-vault/session-scanner-droid-parser.ts create mode 100644 src/main/ipc/ai-vault.test.ts create mode 100644 src/renderer/src/components/right-sidebar/AiVaultSessionActionMenuItems.tsx create mode 100644 src/renderer/src/components/right-sidebar/ai-vault-host-scope.test.ts create mode 100644 src/renderer/src/components/right-sidebar/ai-vault-host-scope.ts create mode 100644 src/renderer/src/components/right-sidebar/ai-vault-session-path-actions.test.ts create mode 100644 src/renderer/src/components/right-sidebar/ai-vault-session-path-actions.ts create mode 100644 tests/e2e/ssh-ai-vault-session-history.spec.ts diff --git a/src/main/ai-vault/remote-session-scanner-codex-index.ts b/src/main/ai-vault/remote-session-scanner-codex-index.ts new file mode 100644 index 00000000000..d6a305ba4cf --- /dev/null +++ b/src/main/ai-vault/remote-session-scanner-codex-index.ts @@ -0,0 +1,51 @@ +import type { IFilesystemProvider } from '../providers/types' +import type { RemoteHostPlatform } from '../ssh/ssh-remote-platform' +import { joinRemotePath } from '../ssh/ssh-remote-platform' +import { extractString, normalizeTitleText, parseJsonObject } from './session-scanner-values' + +const CODEX_SESSION_INDEX_FILE = 'session_index.jsonl' + +export async function remoteCodexIndexTitles(args: { + provider: IFilesystemProvider + codexHome: string + hostPlatform: RemoteHostPlatform + titleCaches: Map>> +}): Promise> { + const cached = args.titleCaches.get(args.codexHome) + if (cached) { + return cached + } + const pending = readRemoteCodexIndexTitles(args.provider, args.codexHome, args.hostPlatform) + args.titleCaches.set(args.codexHome, pending) + return pending +} + +async function readRemoteCodexIndexTitles( + provider: IFilesystemProvider, + codexHome: string, + hostPlatform: RemoteHostPlatform +): Promise> { + const titleBySessionId = new Map() + try { + const { content, isBinary } = await provider.readFile( + joinRemotePath(hostPlatform, codexHome, CODEX_SESSION_INDEX_FILE) + ) + if (isBinary) { + return titleBySessionId + } + for (const line of content.split(/\r?\n/)) { + const record = parseJsonObject(line) + if (!record) { + continue + } + const sessionId = extractString(record.id) + const title = normalizeTitleText(extractString(record.thread_name) ?? '') + if (sessionId && title) { + titleBySessionId.set(sessionId, title) + } + } + } catch { + // Codex indexes are opportunistic; raw transcripts remain sufficient. + } + return titleBySessionId +} diff --git a/src/main/ai-vault/remote-session-scanner-sources.ts b/src/main/ai-vault/remote-session-scanner-sources.ts new file mode 100644 index 00000000000..f232b0adf81 --- /dev/null +++ b/src/main/ai-vault/remote-session-scanner-sources.ts @@ -0,0 +1,221 @@ +import type { AiVaultAgent, AiVaultSession } from '../../shared/ai-vault-types' +import type { RemoteHostPlatform } from '../ssh/ssh-remote-platform' +import { joinRemotePath } from '../ssh/ssh-remote-platform' +import { parseCodexSessionContent } from './session-scanner-codex-parser' +import { parseDevinSessionContent } from './session-scanner-devin-parser' +import { parseDroidSessionContent } from './session-scanner-droid-parser' +import { parseMessageGraphSessionContent } from './session-scanner-graph-parsers' +import { + parseClaudeSessionContent, + parseGeminiSessionContent +} from './session-scanner-primary-parsers' +import { + parseCopilotSessionContent, + parseCursorSessionContent, + parseHermesSessionContent +} from './session-scanner-secondary-parsers' +import type { FileWithMtime } from './session-scanner-types' +import { normalizePiSessionsDir } from './session-scanner-values' +import { remoteCodexIndexTitles } from './remote-session-scanner-codex-index' +import type { + RemoteParserOptions, + RemoteScannerContext, + RemoteSessionSource +} from './remote-session-scanner-types' + +type RemoteContentParser = ( + file: FileWithMtime, + content: string, + platform: NodeJS.Platform, + options: RemoteParserOptions +) => Promise | AiVaultSession | null + +export function remoteSessionSources( + remoteHome: string, + hostPlatform: RemoteHostPlatform +): RemoteSessionSource[] { + return [ + ...remoteCodexSources(remoteHome, hostPlatform), + jsonlSource( + 'claude', + remoteHome, + hostPlatform, + ['.claude', 'projects'], + parseClaudeSessionContent + ), + source( + 'gemini', + remoteHome, + hostPlatform, + ['.gemini', 'tmp'], + ['.json', '.jsonl'], + parseGeminiSessionContent + ), + jsonlSource( + 'copilot', + remoteHome, + hostPlatform, + ['.copilot', 'session-state'], + parseCopilotSessionContent + ), + jsonlSource( + 'cursor', + remoteHome, + hostPlatform, + ['.cursor', 'projects'], + parseCursorSessionContent, + (path) => remotePathSegments(path).includes('agent-transcripts') + ), + source( + 'hermes', + remoteHome, + hostPlatform, + ['.hermes', 'sessions'], + ['.json'], + parseHermesSessionContent + ), + source( + 'devin', + remoteHome, + hostPlatform, + ['.local', 'share', 'devin', 'cli', 'transcripts'], + ['.json'], + parseDevinSessionContent + ), + jsonlSource('pi', remoteHome, hostPlatform, remotePiSessionsSegments(), piParser), + jsonlSource( + 'droid', + remoteHome, + hostPlatform, + ['.factory', 'sessions'], + parseDroidSessionContent + ), + jsonlSource( + 'droid', + remoteHome, + hostPlatform, + ['.factory', 'projects'], + parseDroidSessionContent + ), + ...remoteOpenClawSources(remoteHome, hostPlatform) + ] +} + +function source( + agent: AiVaultAgent, + remoteHome: string, + hostPlatform: RemoteHostPlatform, + segments: readonly string[], + extensions: readonly string[], + parseContent: RemoteContentParser, + filePredicate?: (path: string) => boolean +): RemoteSessionSource { + return { + agent, + rootDir: joinRemotePath(hostPlatform, remoteHome, ...segments), + extensions, + filePredicate, + parse: (file, content, context) => + Promise.resolve(parseContent(file, content, context.hostPlatform.os, parserOptions(context))) + } +} + +function jsonlSource( + agent: AiVaultAgent, + remoteHome: string, + hostPlatform: RemoteHostPlatform, + segments: readonly string[], + parseContent: RemoteContentParser, + filePredicate?: (path: string) => boolean +): RemoteSessionSource { + return source(agent, remoteHome, hostPlatform, segments, ['.jsonl'], parseContent, filePredicate) +} + +function remoteCodexSources( + remoteHome: string, + hostPlatform: RemoteHostPlatform +): RemoteSessionSource[] { + return [ + joinRemotePath(hostPlatform, remoteHome, '.codex'), + joinRemotePath( + hostPlatform, + remoteHome, + '.local', + 'share', + 'orca', + 'codex-runtime-home', + 'home' + ) + ].map((codexHome) => ({ + agent: 'codex', + rootDir: joinRemotePath(hostPlatform, codexHome, 'sessions'), + extensions: ['.jsonl'], + parse: (file, content, context) => + parseCodexSessionContent({ + file, + content, + platform: context.hostPlatform.os, + codexHome, + executionHostId: context.executionHostId, + executionHostPlatform: context.hostPlatform.os, + readIndexedTitle: async (sessionId) => + ( + await remoteCodexIndexTitles({ + provider: context.provider, + codexHome, + hostPlatform, + titleCaches: context.titleCaches + }) + ).get(sessionId) ?? null + }) + })) +} + +function remoteOpenClawSources( + remoteHome: string, + hostPlatform: RemoteHostPlatform +): RemoteSessionSource[] { + return ['.openclaw', '.clawdbot'].map((rootName) => + jsonlSource( + 'openclaw', + remoteHome, + hostPlatform, + [rootName, 'agents'], + openClawParser, + (path) => remotePathSegments(path).includes('sessions') + ) + ) +} + +function parserOptions(context: RemoteScannerContext): RemoteParserOptions { + return { + executionHostId: context.executionHostId, + executionHostPlatform: context.hostPlatform.os + } +} + +function piParser( + file: FileWithMtime, + content: string, + platform: NodeJS.Platform, + options: RemoteParserOptions +): Promise { + return parseMessageGraphSessionContent('pi', file, content, platform, options) +} + +function openClawParser( + file: FileWithMtime, + content: string, + platform: NodeJS.Platform, + options: RemoteParserOptions +): Promise { + return parseMessageGraphSessionContent('openclaw', file, content, platform, options) +} + +function remotePathSegments(path: string): string[] { + return path.replace(/\\/g, '/').split('/').filter(Boolean) +} + +function remotePiSessionsSegments(): string[] { + return normalizePiSessionsDir('/.pi/agent/sessions').split('/').filter(Boolean) +} diff --git a/src/main/ai-vault/remote-session-scanner-types.ts b/src/main/ai-vault/remote-session-scanner-types.ts new file mode 100644 index 00000000000..d5a4b2f1acf --- /dev/null +++ b/src/main/ai-vault/remote-session-scanner-types.ts @@ -0,0 +1,34 @@ +import type { AiVaultAgent, AiVaultSession } from '../../shared/ai-vault-types' +import type { ExecutionHostId } from '../../shared/execution-host' +import type { IFilesystemProvider } from '../providers/types' +import type { RemoteHostPlatform } from '../ssh/ssh-remote-platform' +import type { FileWithMtime } from './session-scanner-types' + +export type RemoteScannerContext = { + provider: IFilesystemProvider + executionHostId: ExecutionHostId + hostPlatform: RemoteHostPlatform + titleCaches: Map>> +} + +export type RemoteParserOptions = { + executionHostId: ExecutionHostId + executionHostPlatform: NodeJS.Platform +} + +export type RemoteSessionSource = { + agent: AiVaultAgent + rootDir: string + extensions: readonly string[] + filePredicate?: (path: string) => boolean + parse: ( + file: FileWithMtime, + content: string, + context: RemoteScannerContext + ) => Promise +} + +export type RemoteSessionCandidate = { + source: RemoteSessionSource + file: FileWithMtime +} diff --git a/src/main/ai-vault/remote-session-scanner.test.ts b/src/main/ai-vault/remote-session-scanner.test.ts new file mode 100644 index 00000000000..16173f7b736 --- /dev/null +++ b/src/main/ai-vault/remote-session-scanner.test.ts @@ -0,0 +1,344 @@ +import { describe, expect, it } from 'vitest' +import type { DirEntry } from '../../shared/types' +import type { FileReadResult, FileStat, IFilesystemProvider } from '../providers/types' +import { getRemoteHostPlatform } from '../ssh/ssh-remote-platform' +import { scanRemoteAiVaultSessions } from './remote-session-scanner' + +class MemoryRemoteProvider implements IFilesystemProvider { + private readonly files = new Map() + + addFile(path: string, content: string, mtimeMs: number): void { + this.files.set(normalize(path), { content, mtimeMs }) + } + + async readDir(dirPath: string): Promise { + const dir = normalize(dirPath) + const prefix = dir.endsWith('/') ? dir : `${dir}/` + const entries = new Map() + for (const path of this.files.keys()) { + if (!path.startsWith(prefix)) { + continue + } + const relative = path.slice(prefix.length) + if (!relative) { + continue + } + const [name, ...rest] = relative.split('/') + if (!name) { + continue + } + entries.set(name, { + name, + isDirectory: rest.length > 0, + isSymlink: false + }) + } + return [...entries.values()].sort((left, right) => left.name.localeCompare(right.name)) + } + + async readFile(filePath: string): Promise { + const file = this.files.get(normalize(filePath)) + if (!file) { + throw new Error(`ENOENT: ${filePath}`) + } + return { content: file.content, isBinary: false } + } + + async stat(filePath: string): Promise { + const file = this.files.get(normalize(filePath)) + if (!file) { + throw new Error(`ENOENT: ${filePath}`) + } + return { size: file.content.length, type: 'file', mtime: file.mtimeMs, mtimeMs: file.mtimeMs } + } + + writeFile = unsupported + writeFileBase64 = unsupported + writeFileBase64Chunk = unsupported + deletePath = unsupported + createFile = unsupported + createDir = unsupported + createDirNoClobber = unsupported + rename = unsupported + renameNoClobber = unsupported + copy = unsupported + realpath = async (path: string): Promise => path + search = unsupported + listFiles = unsupported + watch = unsupported +} + +async function unsupported(): Promise { + throw new Error('unsupported') +} + +function normalize(path: string): string { + return path.replace(/\\/g, '/').replace(/\/+$/, '') +} + +function jsonLines(records: unknown[]): string { + return records.map((record) => JSON.stringify(record)).join('\n') +} + +describe('scanRemoteAiVaultSessions', () => { + it('parses remote default and Orca-managed Codex homes with SSH host ids', async () => { + const provider = new MemoryRemoteProvider() + provider.addFile( + '/home/ada/.codex/session_index.jsonl', + jsonLines([{ id: 'default-session', thread_name: 'Indexed remote title' }]), + 1 + ) + provider.addFile( + '/home/ada/.codex/sessions/2026/07/04/default.jsonl', + jsonLines([ + { + timestamp: '2026-07-04T01:00:00.000Z', + type: 'session_meta', + payload: { id: 'default-session', cwd: '/home/ada/repo' } + }, + { + timestamp: '2026-07-04T01:00:01.000Z', + type: 'response_item', + payload: { + type: 'message', + role: 'user', + content: [{ type: 'text', text: 'Fallback default title' }] + } + } + ]), + 10 + ) + provider.addFile( + '/home/ada/.local/share/orca/codex-runtime-home/home/sessions/runtime.jsonl', + jsonLines([ + { + timestamp: '2026-07-04T02:00:00.000Z', + type: 'session_meta', + payload: { id: 'runtime-session', cwd: '/home/ada/runtime-repo' } + }, + { + timestamp: '2026-07-04T02:00:01.000Z', + type: 'response_item', + payload: { + type: 'message', + role: 'user', + content: [{ type: 'text', text: 'Managed remote title' }] + } + } + ]), + 20 + ) + + const result = await scanRemoteAiVaultSessions({ + provider, + executionHostId: 'ssh:dev-box', + remoteHome: '/home/ada', + hostPlatform: getRemoteHostPlatform('linux-x64') + }) + + expect(result.issues).toEqual([]) + expect(result.sessions.map((session) => session.title)).toEqual([ + 'Managed remote title', + 'Indexed remote title' + ]) + expect(new Set(result.sessions.map((session) => session.id)).size).toBe(2) + expect(result.sessions.every((session) => session.executionHostId === 'ssh:dev-box')).toBe(true) + expect(result.sessions.every((session) => session.executionHostPlatform === 'linux')).toBe(true) + expect( + result.sessions.find((session) => session.sessionId === 'default-session') + ).toMatchObject({ + codexHome: '/home/ada/.codex', + resumeCommand: + "cd '/home/ada/repo' && CODEX_HOME='/home/ada/.codex' codex resume 'default-session'" + }) + expect( + result.sessions.find((session) => session.sessionId === 'runtime-session') + ).toMatchObject({ + codexHome: '/home/ada/.local/share/orca/codex-runtime-home/home', + resumeCommand: + "cd '/home/ada/runtime-repo' && CODEX_HOME='/home/ada/.local/share/orca/codex-runtime-home/home' codex resume 'runtime-session'" + }) + }) + + it('parses non-Codex transcripts through the same remote scanner', async () => { + const provider = new MemoryRemoteProvider() + provider.addFile( + '/home/ada/.claude/projects/repo/claude-session.jsonl', + jsonLines([ + { + sessionId: 'claude-session', + timestamp: '2026-07-04T04:00:00.000Z', + type: 'user', + message: { content: [{ type: 'text', text: 'Summarize the remote branch' }] } + }, + { + sessionId: 'claude-session', + timestamp: '2026-07-04T04:00:01.000Z', + type: 'assistant', + message: { model: 'claude-opus-4', content: 'Sure.' } + } + ]), + 40 + ) + + const result = await scanRemoteAiVaultSessions({ + provider, + executionHostId: 'ssh:dev-box', + remoteHome: '/home/ada', + hostPlatform: getRemoteHostPlatform('linux-x64') + }) + + expect(result.issues).toEqual([]) + expect(result.sessions).toHaveLength(1) + expect(result.sessions[0]).toMatchObject({ + executionHostId: 'ssh:dev-box', + executionHostPlatform: 'linux', + agent: 'claude', + sessionId: 'claude-session', + title: 'Summarize the remote branch', + model: 'claude-opus-4', + filePath: '/home/ada/.claude/projects/repo/claude-session.jsonl' + }) + }) + + it('builds resume commands with the remote host platform', async () => { + const provider = new MemoryRemoteProvider() + provider.addFile( + 'C:/Users/Ada/.codex/sessions/win.jsonl', + jsonLines([ + { + timestamp: '2026-07-04T03:00:00.000Z', + type: 'session_meta', + payload: { id: 'win-session', cwd: 'C:/repo/app' } + }, + { + timestamp: '2026-07-04T03:00:01.000Z', + type: 'response_item', + payload: { + type: 'message', + role: 'user', + content: [{ type: 'text', text: 'Windows remote title' }] + } + } + ]), + 30 + ) + + const result = await scanRemoteAiVaultSessions({ + provider, + executionHostId: 'ssh:win-box', + remoteHome: 'C:/Users/Ada', + hostPlatform: getRemoteHostPlatform('win32-x64') + }) + + expect(result.issues).toEqual([]) + expect(result.sessions[0]?.executionHostPlatform).toBe('win32') + expect(result.sessions[0]?.resumeCommand).toBe( + 'cmd /d /s /c "cd /d ""C:/repo/app"" && set ""CODEX_HOME=C:/Users/Ada/.codex"" && codex resume ""win-session"""' + ) + }) + + it('continues past skipped candidates to fill the remote scan limit', async () => { + const provider = new MemoryRemoteProvider() + provider.addFile( + '/home/ada/.codex/sessions/worker.jsonl', + codexTranscript({ + sessionId: 'worker-session', + title: 'Internal worker', + cwd: '/home/ada/repo', + timestamp: '2026-07-04T04:00:00.000Z', + threadSource: 'agent' + }), + 40 + ) + provider.addFile( + '/home/ada/.codex/sessions/user.jsonl', + codexTranscript({ + sessionId: 'user-session', + title: 'Visible user session', + cwd: '/home/ada/repo', + timestamp: '2026-07-04T03:00:00.000Z' + }), + 30 + ) + + const result = await scanRemoteAiVaultSessions({ + provider, + executionHostId: 'ssh:dev-box', + remoteHome: '/home/ada', + hostPlatform: getRemoteHostPlatform('linux-x64'), + limit: 1 + }) + + expect(result.issues).toEqual([]) + expect(result.sessions.map((session) => session.sessionId)).toEqual(['user-session']) + }) + + it('keeps scoped remote sessions even when they are older than the recency cap', async () => { + const provider = new MemoryRemoteProvider() + provider.addFile( + '/home/ada/.codex/sessions/other.jsonl', + codexTranscript({ + sessionId: 'other-session', + title: 'Other workspace', + cwd: '/home/ada/other', + timestamp: '2026-07-04T05:00:00.000Z' + }), + 50 + ) + provider.addFile( + '/home/ada/.codex/sessions/scoped.jsonl', + codexTranscript({ + sessionId: 'scoped-session', + title: 'Scoped workspace', + cwd: '/home/ada/repo/app', + timestamp: '2026-07-04T01:00:00.000Z' + }), + 10 + ) + + const result = await scanRemoteAiVaultSessions({ + provider, + executionHostId: 'ssh:dev-box', + remoteHome: '/home/ada', + hostPlatform: getRemoteHostPlatform('linux-x64'), + limit: 1, + scopePaths: ['/home/ada/repo'] + }) + + expect(result.issues).toEqual([]) + expect(result.sessions.map((session) => session.sessionId)).toEqual([ + 'other-session', + 'scoped-session' + ]) + }) +}) + +function codexTranscript(args: { + sessionId: string + title: string + cwd: string + timestamp: string + threadSource?: string +}): string { + return jsonLines([ + { + timestamp: args.timestamp, + type: 'session_meta', + payload: { + id: args.sessionId, + cwd: args.cwd, + ...(args.threadSource ? { thread_source: args.threadSource } : {}) + } + }, + { + timestamp: args.timestamp.replace(':00.000Z', ':01.000Z'), + type: 'response_item', + payload: { + type: 'message', + role: 'user', + content: [{ type: 'text', text: args.title }] + } + } + ]) +} diff --git a/src/main/ai-vault/remote-session-scanner.ts b/src/main/ai-vault/remote-session-scanner.ts new file mode 100644 index 00000000000..1b7d66cb7b0 --- /dev/null +++ b/src/main/ai-vault/remote-session-scanner.ts @@ -0,0 +1,296 @@ +import { extname } from 'node:path' +import type { + AiVaultAgent, + AiVaultListResult, + AiVaultScanIssue, + AiVaultSession +} from '../../shared/ai-vault-types' +import { isPathInsideOrEqual } from '../../shared/cross-platform-path' +import type { ExecutionHostId } from '../../shared/execution-host' +import type { FileStat, IFilesystemProvider } from '../providers/types' +import type { RemoteHostPlatform } from '../ssh/ssh-remote-platform' +import { joinRemotePath } from '../ssh/ssh-remote-platform' +import { sessionSortTime } from './session-scanner-accumulator' +import type { FileWithMtime } from './session-scanner-types' +import { errorMessage } from './session-scanner-values' +import { remoteSessionSources } from './remote-session-scanner-sources' +import type { + RemoteScannerContext, + RemoteSessionCandidate, + RemoteSessionSource +} from './remote-session-scanner-types' + +const DEFAULT_REMOTE_SCAN_LIMIT = 1000 +const REMOTE_SCAN_CONCURRENCY = 8 +const REMOTE_SCOPE_PARSE_LIMIT = 2000 + +export async function scanRemoteAiVaultSessions(args: { + provider: IFilesystemProvider + executionHostId: ExecutionHostId + remoteHome: string + hostPlatform: RemoteHostPlatform + limit?: number + scopePaths?: readonly string[] +}): Promise { + const limit = args.limit && args.limit > 0 ? Math.floor(args.limit) : DEFAULT_REMOTE_SCAN_LIMIT + const issues: AiVaultScanIssue[] = [] + const context: RemoteScannerContext = { + provider: args.provider, + executionHostId: args.executionHostId, + hostPlatform: args.hostPlatform, + titleCaches: new Map() + } + const candidates = ( + await mapRemoteScanConcurrently( + remoteSessionSources(args.remoteHome, args.hostPlatform), + (source) => discoverRemoteSourceCandidates({ source, context, issues }) + ) + ) + .flat() + .sort((left, right) => right.file.mtimeMs - left.file.mtimeMs) + + const parsed = await parseRemoteSessionCandidates({ candidates, context, issues, limit }) + const cappedSessions = parsed.sessions + .sort((left, right) => sessionSortTime(right) - sessionSortTime(left)) + .slice(0, limit) + const scopePaths = normalizeRemoteScopePaths(args.scopePaths ?? []) + const parsedScopeSessions = parsed.sessions.filter((session) => + isRemoteSessionInScope(session, scopePaths) + ) + const extraScopeSessions = await scanRemoteInScopeSessions({ + candidates, + context, + issues, + scopePaths, + alreadyParsedFilePaths: parsed.parsedFilePaths + }) + + return { + sessions: mergeRemoteSessions(cappedSessions, [...parsedScopeSessions, ...extraScopeSessions]), + issues, + scannedAt: new Date().toISOString() + } +} + +async function discoverRemoteSourceCandidates(args: { + source: RemoteSessionSource + context: RemoteScannerContext + issues: AiVaultScanIssue[] +}): Promise { + const paths = await walkRemoteSessionFiles( + args.source, + args.context.provider, + args.context.hostPlatform + ) + const files = await mapRemoteScanConcurrently(paths, (path) => + statRemoteFile( + args.context.provider, + path, + args.source.agent, + args.context.executionHostId, + args.issues + ) + ) + return files + .filter((file): file is FileWithMtime => Boolean(file)) + .map((file) => ({ source: args.source, file })) +} + +async function walkRemoteSessionFiles( + source: RemoteSessionSource, + provider: IFilesystemProvider, + hostPlatform: RemoteHostPlatform, + dirPath = source.rootDir +): Promise { + let entries + try { + entries = await provider.readDir(dirPath) + } catch { + return [] + } + + const extensions = new Set(source.extensions) + const files: string[] = [] + for (const entry of entries) { + const fullPath = joinRemotePath(hostPlatform, dirPath, entry.name) + if (entry.isDirectory && !entry.isSymlink) { + files.push(...(await walkRemoteSessionFiles(source, provider, hostPlatform, fullPath))) + continue + } + if ( + !entry.isSymlink && + extensions.has(extname(entry.name).toLowerCase()) && + (source.filePredicate?.(fullPath) ?? true) + ) { + files.push(fullPath) + } + } + return files +} + +async function parseRemoteSessionCandidates(args: { + candidates: readonly RemoteSessionCandidate[] + context: RemoteScannerContext + issues: AiVaultScanIssue[] + limit: number +}): Promise<{ sessions: AiVaultSession[]; parsedFilePaths: Set }> { + const sessions: AiVaultSession[] = [] + const parsedFilePaths = new Set() + let index = 0 + + while (index < args.candidates.length) { + if (canStopParsingRemoteSessions(sessions, args.limit, args.candidates[index]?.file.mtimeMs)) { + break + } + + const batch = args.candidates.slice(index, index + REMOTE_SCAN_CONCURRENCY) + for (const candidate of batch) { + parsedFilePaths.add(candidate.file.path) + } + const results = await Promise.all( + batch.map((candidate) => parseRemoteSessionCandidate(candidate, args.context, args.issues)) + ) + sessions.push(...results.filter(isAiVaultSession)) + index += batch.length + } + + return { sessions, parsedFilePaths } +} + +async function scanRemoteInScopeSessions(args: { + candidates: readonly RemoteSessionCandidate[] + context: RemoteScannerContext + issues: AiVaultScanIssue[] + scopePaths: readonly string[] + alreadyParsedFilePaths: ReadonlySet +}): Promise { + if (args.scopePaths.length === 0) { + return [] + } + + const candidates = args.candidates + .filter((candidate) => !args.alreadyParsedFilePaths.has(candidate.file.path)) + .slice(0, REMOTE_SCOPE_PARSE_LIMIT) + const sessions: AiVaultSession[] = [] + + for (let index = 0; index < candidates.length; index += REMOTE_SCAN_CONCURRENCY) { + const batch = candidates.slice(index, index + REMOTE_SCAN_CONCURRENCY) + const results = await Promise.all( + batch.map((candidate) => parseRemoteSessionCandidate(candidate, args.context, args.issues)) + ) + sessions.push( + ...results.filter( + (session): session is AiVaultSession => + isAiVaultSession(session) && isRemoteSessionInScope(session, args.scopePaths) + ) + ) + } + + return sessions +} + +async function parseRemoteSessionCandidate( + candidate: RemoteSessionCandidate, + context: RemoteScannerContext, + issues: AiVaultScanIssue[] +): Promise { + try { + const read = await context.provider.readFile(candidate.file.path) + if (read.isBinary) { + return null + } + return candidate.source.parse(candidate.file, read.content, context) + } catch (err) { + issues.push({ + executionHostId: context.executionHostId, + agent: candidate.source.agent, + path: candidate.file.path, + message: errorMessage(err) + }) + return null + } +} + +function mergeRemoteSessions( + cappedSessions: AiVaultSession[], + scopeSessions: AiVaultSession[] +): AiVaultSession[] { + if (scopeSessions.length === 0) { + return cappedSessions + } + const byId = new Map() + for (const session of cappedSessions) { + byId.set(session.id, session) + } + for (const session of scopeSessions) { + byId.set(session.id, session) + } + return [...byId.values()].sort((left, right) => sessionSortTime(right) - sessionSortTime(left)) +} + +function isRemoteSessionInScope(session: AiVaultSession, scopePaths: readonly string[]): boolean { + const cwd = session.cwd + return Boolean(cwd && scopePaths.some((scopePath) => isPathInsideOrEqual(scopePath, cwd))) +} + +function normalizeRemoteScopePaths(scopePaths: readonly string[]): string[] { + return scopePaths.map((scopePath) => scopePath.trim()).filter(Boolean) +} + +async function statRemoteFile( + provider: IFilesystemProvider, + path: string, + agent: AiVaultAgent, + executionHostId: ExecutionHostId, + issues: AiVaultScanIssue[] +): Promise { + try { + const stat = await provider.stat(path) + const mtimeMs = remoteStatMtimeMs(stat) + return { path, mtimeMs, modifiedAt: new Date(mtimeMs).toISOString() } + } catch (err) { + issues.push({ executionHostId, agent, path, message: errorMessage(err) }) + return null + } +} + +function remoteStatMtimeMs(stat: FileStat): number { + if (typeof stat.mtimeMs === 'number' && Number.isFinite(stat.mtimeMs)) { + return stat.mtimeMs + } + return stat.mtime > 10_000_000_000 ? stat.mtime : stat.mtime * 1000 +} + +function canStopParsingRemoteSessions( + sessions: AiVaultSession[], + limit: number, + nextCandidateMtimeMs: number | undefined +): boolean { + if (sessions.length < limit || typeof nextCandidateMtimeMs !== 'number') { + return false + } + const visibleCutoff = sessions + .map(sessionSortTime) + .sort((left, right) => right - left) + .at(limit - 1) + + // Transcript mtimes bound the remaining candidate order; once the visible + // cutoff is newer, older files cannot enter the unscoped top-N result. + return typeof visibleCutoff === 'number' && nextCandidateMtimeMs < visibleCutoff +} + +function isAiVaultSession(session: AiVaultSession | null): session is AiVaultSession { + return Boolean(session) +} + +async function mapRemoteScanConcurrently( + items: readonly T[], + mapper: (item: T) => Promise +): Promise { + const results: U[] = [] + for (let index = 0; index < items.length; index += REMOTE_SCAN_CONCURRENCY) { + const batch = items.slice(index, index + REMOTE_SCAN_CONCURRENCY) + results.push(...(await Promise.all(batch.map(mapper)))) + } + return results +} diff --git a/src/main/ai-vault/session-scanner-accumulator.ts b/src/main/ai-vault/session-scanner-accumulator.ts index a6e8cc293dd..85b33a962d6 100644 --- a/src/main/ai-vault/session-scanner-accumulator.ts +++ b/src/main/ai-vault/session-scanner-accumulator.ts @@ -6,6 +6,7 @@ import { type AiVaultSession, type AiVaultSessionPreviewMessage } from '../../shared/ai-vault-types' +import { LOCAL_EXECUTION_HOST_ID, type ExecutionHostId } from '../../shared/execution-host' import type { FileWithMtime, SessionAccumulator } from './session-scanner-types' import { extractPreviewContentText, @@ -43,7 +44,11 @@ export function createAccumulator(args: { export function finalizeSession( accumulator: SessionAccumulator, platform: NodeJS.Platform, - options: { codexHome?: string | null } = {} + options: { + codexHome?: string | null + executionHostId?: ExecutionHostId + executionHostPlatform?: NodeJS.Platform | null + } = {} ): AiVaultSession | null { const sessionId = accumulator.sessionId.trim() if (!sessionId) { @@ -54,8 +59,14 @@ export function finalizeSession( accumulator.fallbackTitle || `${aiVaultAgentLabel(accumulator.agent)} ${sessionId.slice(0, 8)}` + const executionHostId = options.executionHostId ?? LOCAL_EXECUTION_HOST_ID + return { - id: `${accumulator.agent}:${sessionId}:${accumulator.filePath}`, + id: `${executionHostId}:${accumulator.agent}:${sessionId}:${accumulator.filePath}`, + executionHostId, + ...(options.executionHostPlatform + ? { executionHostPlatform: options.executionHostPlatform } + : {}), agent: accumulator.agent, sessionId, title, diff --git a/src/main/ai-vault/session-scanner-agent-parser.ts b/src/main/ai-vault/session-scanner-agent-parser.ts index adfb2aa86db..1114c1b8bb2 100644 --- a/src/main/ai-vault/session-scanner-agent-parser.ts +++ b/src/main/ai-vault/session-scanner-agent-parser.ts @@ -1,11 +1,8 @@ import type { AiVaultSession } from '../../shared/ai-vault-types' import { parseDevinSessionFile } from './session-scanner-devin-parser' +import { parseDroidSessionFile } from './session-scanner-droid-parser' import { parseGrokSessionFile } from './session-scanner-grok-parser' -import { - parseDroidSessionFile, - parseMessageGraphSessionFile, - parseRovoSessionFile -} from './session-scanner-graph-parsers' +import { parseMessageGraphSessionFile, parseRovoSessionFile } from './session-scanner-graph-parsers' import { parseKimiSessionFile } from './session-scanner-kimi-parser' import { splitOpenCodeSqliteCandidate } from './session-scanner-opencode-sqlite-paths' import { parseOpenCodeSqliteSession } from './session-scanner-opencode-sqlite' diff --git a/src/main/ai-vault/session-scanner-codex-parser.ts b/src/main/ai-vault/session-scanner-codex-parser.ts index a42e0af7962..b18721a8296 100644 --- a/src/main/ai-vault/session-scanner-codex-parser.ts +++ b/src/main/ai-vault/session-scanner-codex-parser.ts @@ -3,6 +3,7 @@ import { stat } from 'node:fs/promises' import { basename, dirname, join } from 'node:path' import { createInterface } from 'node:readline' import type { AiVaultSession } from '../../shared/ai-vault-types' +import type { ExecutionHostId } from '../../shared/execution-host' import { addPreviewContent, createAccumulator, @@ -35,21 +36,61 @@ const codexSessionIndexTitleCache = new Map { - const accumulator = createAccumulator({ - agent: 'codex', - file, - sessionId: sessionIdFromFileName(file.path) - }) - let previousTotals: CodexUsageSnapshot | null = null - const lines = createInterface({ input: createReadStream(file.path, { encoding: 'utf-8' }), crlfDelay: Infinity }) - for await (const line of lines) { + return parseCodexSessionLines({ + file, + lines, + platform, + codexHome, + executionHostId, + titleReader: (sessionId) => readCodexSessionIndexTitle(file.path, codexHome, sessionId) + }) +} + +export async function parseCodexSessionContent(args: { + file: FileWithMtime + content: string + platform?: NodeJS.Platform + codexHome?: string | null + executionHostId?: ExecutionHostId + executionHostPlatform?: NodeJS.Platform | null + readIndexedTitle?: (sessionId: string) => Promise +}): Promise { + return parseCodexSessionLines({ + file: args.file, + lines: args.content.split(/\r?\n/), + platform: args.platform ?? process.platform, + codexHome: args.codexHome ?? null, + executionHostId: args.executionHostId, + executionHostPlatform: args.executionHostPlatform, + titleReader: args.readIndexedTitle + }) +} + +async function parseCodexSessionLines(args: { + file: FileWithMtime + lines: AsyncIterable | Iterable + platform: NodeJS.Platform + codexHome: string | null + executionHostId?: ExecutionHostId + executionHostPlatform?: NodeJS.Platform | null + titleReader?: (sessionId: string) => Promise +}): Promise { + const accumulator = createAccumulator({ + agent: 'codex', + file: args.file, + sessionId: sessionIdFromFileName(args.file.path) + }) + let previousTotals: CodexUsageSnapshot | null = null + + for await (const line of args.lines) { const record = parseJsonObject(line) if (!record) { continue @@ -70,7 +111,7 @@ export async function parseCodexSessionFile( } const indexedTitle = extractCodexSessionMetadataTitle(payload) ?? - (await readCodexSessionIndexTitle(file.path, codexHome, accumulator.sessionId)) + (await args.titleReader?.(accumulator.sessionId)) if (indexedTitle) { accumulator.title = indexedTitle } @@ -158,7 +199,11 @@ export async function parseCodexSessionFile( } } - return finalizeSession(accumulator, platform, { codexHome }) + return finalizeSession(accumulator, args.platform, { + codexHome: args.codexHome, + executionHostId: args.executionHostId, + executionHostPlatform: args.executionHostPlatform + }) } function addCodexUsage( diff --git a/src/main/ai-vault/session-scanner-devin-parser.ts b/src/main/ai-vault/session-scanner-devin-parser.ts index 4750dd788ef..4ad6fa45f78 100644 --- a/src/main/ai-vault/session-scanner-devin-parser.ts +++ b/src/main/ai-vault/session-scanner-devin-parser.ts @@ -1,5 +1,6 @@ import { readFile } from 'node:fs/promises' import type { AiVaultSession } from '../../shared/ai-vault-types' +import type { ExecutionHostId } from '../../shared/execution-host' import type { FileWithMtime } from './session-scanner-types' import { addPreviewContent, @@ -17,11 +18,25 @@ import { numberValue } from './session-scanner-values' +type ParserSessionOptions = { + executionHostId?: ExecutionHostId + executionHostPlatform?: NodeJS.Platform | null +} + export async function parseDevinSessionFile( file: FileWithMtime, platform: NodeJS.Platform = process.platform ): Promise { - const record = asRecord(JSON.parse(await readFile(file.path, 'utf-8')) as unknown) + return parseDevinSessionContent(file, await readFile(file.path, 'utf-8'), platform) +} + +export function parseDevinSessionContent( + file: FileWithMtime, + content: string, + platform: NodeJS.Platform = process.platform, + options: ParserSessionOptions = {} +): AiVaultSession | null { + const record = asRecord(JSON.parse(content) as unknown) if (!record) { return null } @@ -69,7 +84,7 @@ export async function parseDevinSessionFile( ) } } - return finalizeSession(accumulator, platform) + return finalizeSession(accumulator, platform, options) } function extractDevinStepText(step: Record): string | null { diff --git a/src/main/ai-vault/session-scanner-droid-parser.ts b/src/main/ai-vault/session-scanner-droid-parser.ts new file mode 100644 index 00000000000..d99795bbbd3 --- /dev/null +++ b/src/main/ai-vault/session-scanner-droid-parser.ts @@ -0,0 +1,120 @@ +import { createReadStream } from 'node:fs' +import { createInterface } from 'node:readline' +import type { AiVaultSession } from '../../shared/ai-vault-types' +import type { ExecutionHostId } from '../../shared/execution-host' +import type { FileWithMtime, SessionAccumulator } from './session-scanner-types' +import { + addPreviewMessage, + createAccumulator, + finalizeSession, + sessionIdFromFileName, + updateTimeline +} from './session-scanner-accumulator' +import { + asRecord, + extractMessageText, + extractPreviewContentText, + extractString, + normalizeTitleText, + parseJsonObject, + tokenTotal +} from './session-scanner-values' + +type ParserSessionOptions = { + executionHostId?: ExecutionHostId + executionHostPlatform?: NodeJS.Platform | null +} + +export async function parseDroidSessionFile( + file: FileWithMtime, + platform: NodeJS.Platform = process.platform +): Promise { + const lines = createInterface({ + input: createReadStream(file.path, { encoding: 'utf-8' }), + crlfDelay: Infinity + }) + return parseDroidSessionLines({ file, lines, platform }) +} + +export async function parseDroidSessionContent( + file: FileWithMtime, + content: string, + platform: NodeJS.Platform = process.platform, + options: ParserSessionOptions = {} +): Promise { + return parseDroidSessionLines({ + file, + lines: content.split(/\r?\n/), + platform, + options + }) +} + +async function parseDroidSessionLines(args: { + file: FileWithMtime + lines: AsyncIterable | Iterable + platform: NodeJS.Platform + options?: ParserSessionOptions +}): Promise { + const accumulator = createAccumulator({ + agent: 'droid', + file: args.file, + sessionId: sessionIdFromFileName(args.file.path) + }) + + for await (const line of args.lines) { + const record = parseJsonObject(line) + if (!record) { + continue + } + updateTimeline(accumulator, record.timestamp) + if (record.type === 'session_start') { + accumulator.sessionId = extractString(record.id) ?? accumulator.sessionId + accumulator.title = normalizeTitleText(extractString(record.title) ?? '') + accumulator.cwd = extractString(record.cwd) ?? accumulator.cwd + continue + } + if (record.type === 'system') { + accumulator.cwd = extractString(record.cwd) ?? accumulator.cwd + accumulator.model = extractString(record.model) ?? accumulator.model + } + const streamSessionId = extractString(record.session_id) ?? extractString(record.sessionId) + if (streamSessionId) { + accumulator.sessionId = streamSessionId + } + if (record.type === 'message') { + consumeDroidMessage(accumulator, record) + } else if (record.type === 'completion') { + accumulator.messageCount++ + accumulator.totalTokens += tokenTotal(record.usage) + addPreviewMessage(accumulator, { + role: 'assistant', + text: extractString(record.finalText), + timestamp: record.timestamp + }) + } + } + return finalizeSession(accumulator, args.platform, args.options) +} + +function consumeDroidMessage( + accumulator: SessionAccumulator, + record: Record +): void { + const role = extractString(record.role) ?? extractString(asRecord(record.message)?.role) + if (role !== 'user' && role !== 'assistant') { + return + } + accumulator.messageCount++ + if (role === 'user') { + accumulator.title ??= + normalizeTitleText(extractString(record.text) ?? '') || + extractMessageText(asRecord(record.message)) + } + addPreviewMessage(accumulator, { + role, + text: + extractString(record.text) ?? extractPreviewContentText(asRecord(record.message)?.content), + timestamp: record.timestamp + }) +} diff --git a/src/main/ai-vault/session-scanner-graph-parsers.ts b/src/main/ai-vault/session-scanner-graph-parsers.ts index 3013925a548..a52c83b3213 100644 --- a/src/main/ai-vault/session-scanner-graph-parsers.ts +++ b/src/main/ai-vault/session-scanner-graph-parsers.ts @@ -3,6 +3,7 @@ import { readFile } from 'node:fs/promises' import { basename, dirname, join } from 'node:path' import { createInterface } from 'node:readline' import type { AiVaultSession } from '../../shared/ai-vault-types' +import type { ExecutionHostId } from '../../shared/execution-host' import type { FileWithMtime, SessionAccumulator } from './session-scanner-types' import { addPreviewContent, @@ -17,15 +18,18 @@ import { asRecord, extractContentText, extractMessageText, - extractPreviewContentText, extractString, firstString, - normalizeTitleText, parseJsonObject, readJsonObjectIfExists, tokenTotal } from './session-scanner-values' +type ParserSessionOptions = { + executionHostId?: ExecutionHostId + executionHostPlatform?: NodeJS.Platform | null +} + export async function parseRovoSessionFile( file: FileWithMtime, platform: NodeJS.Platform = process.platform @@ -159,17 +163,43 @@ export async function parseMessageGraphSessionFile( file: FileWithMtime, platform: NodeJS.Platform = process.platform ): Promise { - const accumulator = createAccumulator({ - agent, - file, - sessionId: sessionIdFromFileName(file.path) - }) const lines = createInterface({ input: createReadStream(file.path, { encoding: 'utf-8' }), crlfDelay: Infinity }) + return parseMessageGraphSessionLines({ agent, file, lines, platform }) +} - for await (const line of lines) { +export async function parseMessageGraphSessionContent( + agent: 'openclaw' | 'pi', + file: FileWithMtime, + content: string, + platform: NodeJS.Platform = process.platform, + options: ParserSessionOptions = {} +): Promise { + return parseMessageGraphSessionLines({ + agent, + file, + lines: content.split(/\r?\n/), + platform, + options + }) +} + +async function parseMessageGraphSessionLines(args: { + agent: 'openclaw' | 'pi' + file: FileWithMtime + lines: AsyncIterable | Iterable + platform: NodeJS.Platform + options?: ParserSessionOptions +}): Promise { + const accumulator = createAccumulator({ + agent: args.agent, + file: args.file, + sessionId: sessionIdFromFileName(args.file.path) + }) + + for await (const line of args.lines) { const record = parseJsonObject(line) if (!record) { continue @@ -204,69 +234,5 @@ export async function parseMessageGraphSessionFile( } } - return finalizeSession(accumulator, platform) -} - -export async function parseDroidSessionFile( - file: FileWithMtime, - platform: NodeJS.Platform = process.platform -): Promise { - const accumulator = createAccumulator({ - agent: 'droid', - file, - sessionId: sessionIdFromFileName(file.path) - }) - const lines = createInterface({ - input: createReadStream(file.path, { encoding: 'utf-8' }), - crlfDelay: Infinity - }) - - for await (const line of lines) { - const record = parseJsonObject(line) - if (!record) { - continue - } - updateTimeline(accumulator, record.timestamp) - if (record.type === 'session_start') { - accumulator.sessionId = extractString(record.id) ?? accumulator.sessionId - accumulator.title = normalizeTitleText(extractString(record.title) ?? '') - accumulator.cwd = extractString(record.cwd) ?? accumulator.cwd - continue - } - if (record.type === 'system') { - accumulator.cwd = extractString(record.cwd) ?? accumulator.cwd - accumulator.model = extractString(record.model) ?? accumulator.model - } - const streamSessionId = extractString(record.session_id) ?? extractString(record.sessionId) - if (streamSessionId) { - accumulator.sessionId = streamSessionId - } - if (record.type === 'message') { - const role = extractString(record.role) ?? extractString(asRecord(record.message)?.role) - if (role === 'user' || role === 'assistant') { - accumulator.messageCount++ - if (role === 'user') { - accumulator.title ??= - normalizeTitleText(extractString(record.text) ?? '') || - extractMessageText(asRecord(record.message)) - } - addPreviewMessage(accumulator, { - role, - text: - extractString(record.text) ?? - extractPreviewContentText(asRecord(record.message)?.content), - timestamp: record.timestamp - }) - } - } else if (record.type === 'completion') { - accumulator.messageCount++ - accumulator.totalTokens += tokenTotal(record.usage) - addPreviewMessage(accumulator, { - role: 'assistant', - text: extractString(record.finalText), - timestamp: record.timestamp - }) - } - } - return finalizeSession(accumulator, platform) + return finalizeSession(accumulator, args.platform, args.options) } diff --git a/src/main/ai-vault/session-scanner-primary-parsers.ts b/src/main/ai-vault/session-scanner-primary-parsers.ts index 363bfe636a8..28a37602a6c 100644 --- a/src/main/ai-vault/session-scanner-primary-parsers.ts +++ b/src/main/ai-vault/session-scanner-primary-parsers.ts @@ -2,6 +2,7 @@ import { createReadStream } from 'node:fs' import { readFile } from 'node:fs/promises' import { createInterface } from 'node:readline' import type { AiVaultSession } from '../../shared/ai-vault-types' +import type { ExecutionHostId } from '../../shared/execution-host' import type { FileWithMtime, SessionAccumulator } from './session-scanner-types' import { addPreviewContent, @@ -23,24 +24,51 @@ import { tokenTotal } from './session-scanner-values' +type ParserSessionOptions = { + executionHostId?: ExecutionHostId + executionHostPlatform?: NodeJS.Platform | null +} + export async function parseClaudeSessionFile( file: FileWithMtime, platform: NodeJS.Platform = process.platform ): Promise { - const accumulator = createAccumulator({ - agent: 'claude', - file, - sessionId: sessionIdFromFileName(file.path) - }) - let metaTitle: string | null = null - let generatedTitle: string | null = null - const lines = createInterface({ input: createReadStream(file.path, { encoding: 'utf-8' }), crlfDelay: Infinity }) + return parseClaudeSessionLines({ file, lines, platform }) +} - for await (const line of lines) { +export async function parseClaudeSessionContent( + file: FileWithMtime, + content: string, + platform: NodeJS.Platform = process.platform, + options: ParserSessionOptions = {} +): Promise { + return parseClaudeSessionLines({ + file, + lines: content.split(/\r?\n/), + platform, + options + }) +} + +async function parseClaudeSessionLines(args: { + file: FileWithMtime + lines: AsyncIterable | Iterable + platform: NodeJS.Platform + options?: ParserSessionOptions +}): Promise { + const accumulator = createAccumulator({ + agent: 'claude', + file: args.file, + sessionId: sessionIdFromFileName(args.file.path) + }) + let metaTitle: string | null = null + let generatedTitle: string | null = null + + for await (const line of args.lines) { const record = parseJsonObject(line) if (!record) { continue @@ -92,7 +120,7 @@ export async function parseClaudeSessionFile( } accumulator.fallbackTitle = generatedTitle ?? metaTitle - return finalizeSession(accumulator, platform) + return finalizeSession(accumulator, args.platform, args.options) } export async function parseGeminiSessionFile( @@ -103,7 +131,33 @@ export async function parseGeminiSessionFile( return parseGeminiJsonlSessionFile(file, platform) } - const record = asRecord(JSON.parse(await readFile(file.path, 'utf-8')) as unknown) + return parseGeminiJsonSessionContent(file, await readFile(file.path, 'utf-8'), platform) +} + +export async function parseGeminiSessionContent( + file: FileWithMtime, + content: string, + platform: NodeJS.Platform = process.platform, + options: ParserSessionOptions = {} +): Promise { + if (file.path.endsWith('.jsonl')) { + return parseGeminiJsonlSessionLines({ + file, + lines: content.split(/\r?\n/), + platform, + options + }) + } + return parseGeminiJsonSessionContent(file, content, platform, options) +} + +function parseGeminiJsonSessionContent( + file: FileWithMtime, + content: string, + platform: NodeJS.Platform, + options: ParserSessionOptions = {} +): AiVaultSession | null { + const record = asRecord(JSON.parse(content) as unknown) if (!record) { return null } @@ -117,24 +171,33 @@ export async function parseGeminiSessionFile( for (const message of arrayValue(record.messages)) { consumeGeminiMessage(accumulator, asRecord(message)) } - return finalizeSession(accumulator, platform) + return finalizeSession(accumulator, platform, options) } export async function parseGeminiJsonlSessionFile( file: FileWithMtime, platform: NodeJS.Platform ): Promise { - const accumulator = createAccumulator({ - agent: 'gemini', - file, - sessionId: sessionIdFromFileName(file.path) - }) const lines = createInterface({ input: createReadStream(file.path, { encoding: 'utf-8' }), crlfDelay: Infinity }) + return parseGeminiJsonlSessionLines({ file, lines, platform }) +} - for await (const line of lines) { +async function parseGeminiJsonlSessionLines(args: { + file: FileWithMtime + lines: AsyncIterable | Iterable + platform: NodeJS.Platform + options?: ParserSessionOptions +}): Promise { + const accumulator = createAccumulator({ + agent: 'gemini', + file: args.file, + sessionId: sessionIdFromFileName(args.file.path) + }) + + for await (const line of args.lines) { const record = parseJsonObject(line) if (!record) { continue @@ -153,7 +216,7 @@ export async function parseGeminiJsonlSessionFile( consumeGeminiMessage(accumulator, record) } - return finalizeSession(accumulator, platform) + return finalizeSession(accumulator, args.platform, args.options) } export function consumeGeminiMessage( diff --git a/src/main/ai-vault/session-scanner-secondary-parsers.ts b/src/main/ai-vault/session-scanner-secondary-parsers.ts index edc03cf5815..1edcf6292f0 100644 --- a/src/main/ai-vault/session-scanner-secondary-parsers.ts +++ b/src/main/ai-vault/session-scanner-secondary-parsers.ts @@ -3,6 +3,7 @@ import { readFile, readdir } from 'node:fs/promises' import { join } from 'node:path' import { createInterface } from 'node:readline' import type { AiVaultSession } from '../../shared/ai-vault-types' +import type { ExecutionHostId } from '../../shared/execution-host' import type { FileWithMtime, SessionAccumulator } from './session-scanner-types' import { addPreviewContent, @@ -29,21 +30,49 @@ import { tokenTotal } from './session-scanner-values' +type ParserSessionOptions = { + executionHostId?: ExecutionHostId + executionHostPlatform?: NodeJS.Platform | null +} + export async function parseCopilotSessionFile( file: FileWithMtime, platform: NodeJS.Platform = process.platform ): Promise { - const accumulator = createAccumulator({ - agent: 'copilot', - file, - sessionId: sessionIdFromFileName(file.path) - }) const lines = createInterface({ input: createReadStream(file.path, { encoding: 'utf-8' }), crlfDelay: Infinity }) + return parseCopilotSessionLines({ file, lines, platform }) +} - for await (const line of lines) { +export async function parseCopilotSessionContent( + file: FileWithMtime, + content: string, + platform: NodeJS.Platform = process.platform, + options: ParserSessionOptions = {} +): Promise { + return parseCopilotSessionLines({ + file, + lines: content.split(/\r?\n/), + platform, + options + }) +} + +async function parseCopilotSessionLines(args: { + file: FileWithMtime + lines: AsyncIterable | Iterable + platform: NodeJS.Platform + options?: ParserSessionOptions +}): Promise { + const accumulator = createAccumulator({ + agent: 'copilot', + file: args.file, + sessionId: sessionIdFromFileName(args.file.path) + }) + + for await (const line of args.lines) { const record = parseJsonObject(line) if (!record) { continue @@ -94,24 +123,47 @@ export async function parseCopilotSessionFile( } } - return finalizeSession(accumulator, platform) + return finalizeSession(accumulator, args.platform, args.options) } export async function parseCursorSessionFile( file: FileWithMtime, platform: NodeJS.Platform = process.platform ): Promise { - const accumulator = createAccumulator({ - agent: 'cursor', - file, - sessionId: sessionIdFromFileName(file.path) - }) const lines = createInterface({ input: createReadStream(file.path, { encoding: 'utf-8' }), crlfDelay: Infinity }) + return parseCursorSessionLines({ file, lines, platform }) +} - for await (const line of lines) { +export async function parseCursorSessionContent( + file: FileWithMtime, + content: string, + platform: NodeJS.Platform = process.platform, + options: ParserSessionOptions = {} +): Promise { + return parseCursorSessionLines({ + file, + lines: content.split(/\r?\n/), + platform, + options + }) +} + +async function parseCursorSessionLines(args: { + file: FileWithMtime + lines: AsyncIterable | Iterable + platform: NodeJS.Platform + options?: ParserSessionOptions +}): Promise { + const accumulator = createAccumulator({ + agent: 'cursor', + file: args.file, + sessionId: sessionIdFromFileName(args.file.path) + }) + + for await (const line of args.lines) { const record = parseJsonObject(line) if (!record) { continue @@ -132,7 +184,7 @@ export async function parseCursorSessionFile( ) } } - return finalizeSession(accumulator, platform) + return finalizeSession(accumulator, args.platform, args.options) } export async function parseOpenCodeSessionFile( @@ -207,7 +259,16 @@ export async function parseHermesSessionFile( file: FileWithMtime, platform: NodeJS.Platform = process.platform ): Promise { - const record = asRecord(JSON.parse(await readFile(file.path, 'utf-8')) as unknown) + return parseHermesSessionContent(file, await readFile(file.path, 'utf-8'), platform) +} + +export async function parseHermesSessionContent( + file: FileWithMtime, + content: string, + platform: NodeJS.Platform = process.platform, + options: ParserSessionOptions = {} +): Promise { + const record = asRecord(JSON.parse(content) as unknown) if (!record) { return null } @@ -234,5 +295,5 @@ export async function parseHermesSessionFile( if (accumulator.messageCount === 0) { accumulator.messageCount = numberValue(record.message_count) } - return finalizeSession(accumulator, platform) + return finalizeSession(accumulator, platform, options) } diff --git a/src/main/ai-vault/session-scanner-types.ts b/src/main/ai-vault/session-scanner-types.ts index b6c6ae96412..5d2208b7b60 100644 --- a/src/main/ai-vault/session-scanner-types.ts +++ b/src/main/ai-vault/session-scanner-types.ts @@ -4,6 +4,7 @@ import type { AiVaultSession, AiVaultSessionPreviewMessage } from '../../shared/ai-vault-types' +import type { ExecutionHostId } from '../../shared/execution-host' export type AiVaultScanOptions = { claudeProjectsDir?: string @@ -33,6 +34,7 @@ export type AiVaultScanOptions = { // the recency cap (see discoverInScopeClaudeFiles). scopePaths?: readonly string[] platform?: NodeJS.Platform + executionHostId?: ExecutionHostId } export type FileWithMtime = { diff --git a/src/main/ai-vault/session-scanner.ts b/src/main/ai-vault/session-scanner.ts index 181bf1466ea..59f92dafc7a 100644 --- a/src/main/ai-vault/session-scanner.ts +++ b/src/main/ai-vault/session-scanner.ts @@ -3,6 +3,7 @@ import type { AiVaultScanIssue, AiVaultSession } from '../../shared/ai-vault-types' +import { LOCAL_EXECUTION_HOST_ID, type ExecutionHostId } from '../../shared/execution-host' import { sessionSortTime } from './session-scanner-accumulator' import { parseAgentSessionFile } from './session-scanner-agent-parser' import { codexHomeForSessionsDir } from './session-scanner-codex-paths' @@ -41,6 +42,7 @@ export async function scanAiVaultSessions( const limit = clampPositiveInteger(options.limit, DEFAULT_LIMIT) const limitPerAgent = clampPositiveInteger(options.limitPerAgent, DEFAULT_SCAN_LIMIT_PER_AGENT) const platform = options.platform ?? process.platform + const executionHostId = options.executionHostId ?? LOCAL_EXECUTION_HOST_ID const issues: AiVaultScanIssue[] = [] const discoveries = await discoverAiVaultSessionSources({ options, limitPerAgent, issues }) @@ -63,6 +65,7 @@ export async function scanAiVaultSessions( candidates, limit, platform, + executionHostId, issues }) @@ -75,12 +78,13 @@ export async function scanAiVaultSessions( scopePaths: options.scopePaths ?? [], alreadyParsedFilePaths: new Set(cappedSessions.map((session) => session.filePath)), platform, + executionHostId, issues }) return { sessions: mergeSessions(cappedSessions, scopeSessions), - issues, + issues: issues.map((issue) => ({ executionHostId, ...issue })), scannedAt: new Date().toISOString() } } @@ -110,6 +114,7 @@ async function scanInScopeSessions(args: { scopePaths: readonly string[] alreadyParsedFilePaths: ReadonlySet platform: NodeJS.Platform + executionHostId: ExecutionHostId issues: AiVaultScanIssue[] }): Promise { if (args.scopePaths.length === 0) { @@ -136,6 +141,7 @@ async function scanInScopeSessions(args: { candidates, limit: candidates.length, platform: args.platform, + executionHostId: args.executionHostId, issues: args.issues }) } @@ -144,6 +150,7 @@ async function parseSessionCandidates(args: { candidates: SessionFileCandidate[] limit: number platform: NodeJS.Platform + executionHostId: ExecutionHostId issues: AiVaultScanIssue[] }): Promise { const sessions: AiVaultSession[] = [] @@ -159,7 +166,9 @@ async function parseSessionCandidates(args: { const batchSize = Math.min(SESSION_PARSE_CONCURRENCY, needed, remaining) const batch = args.candidates.slice(index, index + batchSize) const results = await Promise.all( - batch.map((candidate) => parseSessionCandidate(candidate, args.platform)) + batch.map((candidate) => + parseSessionCandidate(candidate, args.platform, args.executionHostId) + ) ) for (const result of results) { @@ -179,15 +188,20 @@ async function parseSessionCandidates(args: { async function parseSessionCandidate( candidate: SessionFileCandidate, - platform: NodeJS.Platform + platform: NodeJS.Platform, + executionHostId: ExecutionHostId ): Promise { try { const session = await parseAgentSessionFile(candidate, platform) - return { session, issue: null } + return { + session: session ? withSessionExecutionHost(session, executionHostId) : null, + issue: null + } } catch (err) { return { session: null, issue: { + executionHostId, agent: candidate.agent, path: candidate.file.path, message: errorMessage(err) @@ -196,6 +210,20 @@ async function parseSessionCandidate( } } +function withSessionExecutionHost( + session: AiVaultSession, + executionHostId: ExecutionHostId +): AiVaultSession { + if (session.executionHostId === executionHostId) { + return session + } + return { + ...session, + executionHostId, + id: `${executionHostId}:${session.agent}:${session.sessionId}:${session.filePath}` + } +} + function canStopParsingSessions( sessions: AiVaultSession[], limit: number, diff --git a/src/main/ipc/ai-vault.test.ts b/src/main/ipc/ai-vault.test.ts new file mode 100644 index 00000000000..9cc8060eee2 --- /dev/null +++ b/src/main/ipc/ai-vault.test.ts @@ -0,0 +1,163 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { AiVaultListResult, AiVaultSession } from '../../shared/ai-vault-types' +import type { IFilesystemProvider } from '../providers/types' +import { getRemoteHostPlatform } from '../ssh/ssh-remote-platform' + +const mocks = vi.hoisted(() => ({ + scanAiVaultSessions: vi.fn(), + scanRemoteAiVaultSessions: vi.fn(), + getAiVaultWslHomeDirs: vi.fn(), + getSshFilesystemProvider: vi.fn(), + getActiveSshAiVaultHostInfo: vi.fn(), + getActiveSshAiVaultHostInfos: vi.fn() +})) + +vi.mock('electron', () => ({ + app: { on: vi.fn() }, + ipcMain: { handle: vi.fn() } +})) + +vi.mock('../ai-vault/session-scanner', () => ({ + scanAiVaultSessions: mocks.scanAiVaultSessions +})) + +vi.mock('../ai-vault/remote-session-scanner', () => ({ + scanRemoteAiVaultSessions: mocks.scanRemoteAiVaultSessions +})) + +vi.mock('../wsl', () => ({ + getWslHomeAsync: mocks.getAiVaultWslHomeDirs, + listWslDistrosAsync: vi.fn().mockResolvedValue([]) +})) + +vi.mock('../providers/ssh-filesystem-dispatch', () => ({ + SSH_FILESYSTEM_PROVIDER_UNAVAILABLE_MESSAGE: + 'Remote connection dropped. Click Reconnect on the SSH target before retrying.', + getSshFilesystemProvider: mocks.getSshFilesystemProvider +})) + +vi.mock('./ssh', () => ({ + getActiveSshAiVaultHostInfo: mocks.getActiveSshAiVaultHostInfo, + getActiveSshAiVaultHostInfos: mocks.getActiveSshAiVaultHostInfos +})) + +const { _internals } = await import('./ai-vault') + +const provider = {} as IFilesystemProvider + +beforeEach(() => { + vi.clearAllMocks() + _internals.resetAiVaultCacheForTests() + mocks.scanAiVaultSessions.mockResolvedValue(result([session('local', 'local-session')])) + mocks.scanRemoteAiVaultSessions.mockResolvedValue( + result([session('ssh:dev-box', 'remote-session')]) + ) + mocks.getSshFilesystemProvider.mockReturnValue(provider) + mocks.getActiveSshAiVaultHostInfo.mockReturnValue(hostInfo('dev-box')) + mocks.getActiveSshAiVaultHostInfos.mockReturnValue([hostInfo('dev-box')]) +}) + +describe('listAiVaultSessions host routing', () => { + it('routes local scope to the local scanner', async () => { + await _internals.listAiVaultSessions({ executionHostScope: 'local', scopePaths: ['/repo'] }) + + expect(mocks.scanAiVaultSessions).toHaveBeenCalledWith( + expect.objectContaining({ + scopePaths: ['/repo'], + executionHostId: 'local' + }) + ) + expect(mocks.scanRemoteAiVaultSessions).not.toHaveBeenCalled() + }) + + it('routes SSH scope to only that SSH target', async () => { + await _internals.listAiVaultSessions({ + executionHostScope: 'ssh:dev-box', + scopePaths: ['/home/ada/repo'] + }) + + expect(mocks.scanAiVaultSessions).not.toHaveBeenCalled() + expect(mocks.getActiveSshAiVaultHostInfo).toHaveBeenCalledWith('dev-box') + expect(mocks.scanRemoteAiVaultSessions).toHaveBeenCalledWith( + expect.objectContaining({ + provider, + executionHostId: 'ssh:dev-box', + remoteHome: '/home/ada', + scopePaths: ['/home/ada/repo'] + }) + ) + }) + + it('merges local plus connected SSH targets for all hosts', async () => { + const result = await _internals.listAiVaultSessions({ executionHostScope: 'all' }) + + expect(mocks.scanAiVaultSessions).toHaveBeenCalledTimes(1) + expect(mocks.scanRemoteAiVaultSessions).toHaveBeenCalledTimes(1) + expect(result.sessions.map((entry) => entry.executionHostId)).toEqual(['ssh:dev-box', 'local']) + }) + + it('returns a scan issue for a disconnected SSH target', async () => { + mocks.getActiveSshAiVaultHostInfo.mockReturnValue(null) + mocks.getSshFilesystemProvider.mockReturnValue(undefined) + + const result = await _internals.listAiVaultSessions({ + executionHostScope: 'ssh:disconnected' + }) + + expect(result.sessions).toEqual([]) + expect(result.issues).toMatchObject([ + { + executionHostId: 'ssh:disconnected', + agent: 'codex', + path: 'disconnected' + } + ]) + }) + + it('keeps host scope in the cache key', async () => { + await _internals.listAiVaultSessions({ executionHostScope: 'local' }) + await _internals.listAiVaultSessions({ executionHostScope: 'ssh:dev-box' }) + + expect(mocks.scanAiVaultSessions).toHaveBeenCalledTimes(1) + expect(mocks.scanRemoteAiVaultSessions).toHaveBeenCalledTimes(1) + }) +}) + +function hostInfo(targetId: string) { + return { + targetId, + executionHostId: `ssh:${targetId}` as const, + remoteHome: '/home/ada', + hostPlatform: getRemoteHostPlatform('linux-x64') + } +} + +function result(sessions: AiVaultSession[]): AiVaultListResult { + return { sessions, issues: [], scannedAt: new Date().toISOString() } +} + +function session( + executionHostId: AiVaultSession['executionHostId'], + sessionId: string +): AiVaultSession { + return { + id: `${executionHostId}:codex:${sessionId}:/tmp/${sessionId}.jsonl`, + executionHostId, + agent: 'codex', + sessionId, + title: sessionId, + cwd: '/repo', + branch: null, + model: null, + filePath: `/tmp/${sessionId}.jsonl`, + codexHome: null, + createdAt: null, + updatedAt: + sessionId === 'remote-session' ? '2026-07-04T02:00:00.000Z' : '2026-07-04T01:00:00.000Z', + modifiedAt: '2026-07-04T00:00:00.000Z', + messageCount: 1, + totalTokens: 0, + previewMessages: [], + resumeCommand: `codex resume ${sessionId}` + } +} diff --git a/src/main/ipc/ai-vault.ts b/src/main/ipc/ai-vault.ts index f96aa0b6d61..4551379afde 100644 --- a/src/main/ipc/ai-vault.ts +++ b/src/main/ipc/ai-vault.ts @@ -1,8 +1,26 @@ import { app, ipcMain } from 'electron' import { join } from 'node:path' +import { scanRemoteAiVaultSessions } from '../ai-vault/remote-session-scanner' import { scanAiVaultSessions } from '../ai-vault/session-scanner' +import { sessionSortTime } from '../ai-vault/session-scanner-accumulator' import { getWslHomeAsync, listWslDistrosAsync } from '../wsl' -import type { AiVaultListArgs, AiVaultListResult } from '../../shared/ai-vault-types' +import type { + AiVaultListArgs, + AiVaultListResult, + AiVaultScanIssue +} from '../../shared/ai-vault-types' +import { + LOCAL_EXECUTION_HOST_ID, + normalizeExecutionHostScope, + parseExecutionHostId, + toSshExecutionHostId, + type ExecutionHostScope +} from '../../shared/execution-host' +import { + getSshFilesystemProvider, + SSH_FILESYSTEM_PROVIDER_UNAVAILABLE_MESSAGE +} from '../providers/ssh-filesystem-dispatch' +import { getActiveSshAiVaultHostInfo, getActiveSshAiVaultHostInfos } from './ssh' const AI_VAULT_CACHE_TTL_MS = 15_000 @@ -22,10 +40,14 @@ let inflightKey: string | null = null let handlerOptions: AiVaultHandlerOptions = {} async function listAiVaultSessions(args?: AiVaultListArgs): Promise { + const executionHostScope = normalizeExecutionHostScope( + args?.executionHostScope ?? LOCAL_EXECUTION_HOST_ID + ) // Scope paths change the result set, so they must be part of the cache key. const key = JSON.stringify({ limit: args?.limit ?? 'default', - scopePaths: args?.scopePaths ?? [] + scopePaths: args?.scopePaths ?? [], + executionHostScope }) const now = Date.now() // Why: opening this panel repeatedly should not re-parse hundreds of JSONL @@ -38,16 +60,7 @@ async function listAiVaultSessions(args?: AiVaultListArgs): Promise join(homePath, 'sessions')) ?? - [] - inflightList = (async () => - scanAiVaultSessions({ - limit: args?.limit, - scopePaths: args?.scopePaths, - additionalCodexSessionsDirs, - wslHomeDirs: await getAiVaultWslHomeDirs() - }))() + inflightList = scanAiVaultSessionsByHostScope(args, executionHostScope) .then((result) => { cachedList = { key, @@ -57,12 +70,133 @@ async function listAiVaultSessions(args?: AiVaultListArgs): Promise { - inflightKey = null - inflightList = null + // Only clear tracking if it still refers to this request: a concurrent + // different-scope scan may have replaced it and must stay dedupable. + if (inflightKey === key) { + inflightKey = null + inflightList = null + } }) return inflightList } +async function scanAiVaultSessionsByHostScope( + args: AiVaultListArgs | undefined, + executionHostScope: ExecutionHostScope +): Promise { + if (executionHostScope === LOCAL_EXECUTION_HOST_ID) { + return scanLocalAiVaultSessions(args) + } + + if (executionHostScope === 'all') { + return mergeAiVaultListResults( + await Promise.all([ + scanLocalAiVaultSessions(args), + ...getActiveSshAiVaultHostInfos().map((hostInfo) => + scanSshAiVaultSessions(hostInfo.targetId, args) + ) + ]), + args?.limit + ) + } + + const parsed = parseExecutionHostId(executionHostScope) + if (parsed?.kind === 'ssh') { + return scanSshAiVaultSessions(parsed.targetId, args) + } + + return { + sessions: [], + issues: [ + { + executionHostId: executionHostScope, + agent: 'codex', + path: executionHostScope, + message: 'Agent Session History is not available for this execution host.' + } + ], + scannedAt: new Date().toISOString() + } +} + +async function scanLocalAiVaultSessions(args?: AiVaultListArgs): Promise { + const additionalCodexSessionsDirs = + handlerOptions.getAdditionalCodexHomePaths?.().map((homePath) => join(homePath, 'sessions')) ?? + [] + return scanAiVaultSessions({ + limit: args?.limit, + scopePaths: args?.scopePaths, + additionalCodexSessionsDirs, + wslHomeDirs: await getAiVaultWslHomeDirs(), + executionHostId: LOCAL_EXECUTION_HOST_ID + }) +} + +async function scanSshAiVaultSessions( + targetId: string, + args?: AiVaultListArgs +): Promise { + const executionHostId = toSshExecutionHostId(targetId) + const hostInfo = getActiveSshAiVaultHostInfo(targetId) + const provider = getSshFilesystemProvider(targetId) + if (!hostInfo || !provider) { + return sshScanIssueResult({ + executionHostId, + targetId, + message: SSH_FILESYSTEM_PROVIDER_UNAVAILABLE_MESSAGE + }) + } + return scanRemoteAiVaultSessions({ + provider, + executionHostId: hostInfo.executionHostId, + remoteHome: hostInfo.remoteHome, + hostPlatform: hostInfo.hostPlatform, + limit: args?.limit, + scopePaths: args?.scopePaths + }) +} + +function sshScanIssueResult(args: { + executionHostId: `ssh:${string}` + targetId: string + message: string +}): AiVaultListResult { + return { + sessions: [], + issues: [ + { + executionHostId: args.executionHostId, + agent: 'codex', + path: args.targetId, + message: args.message + } + ], + scannedAt: new Date().toISOString() + } +} + +function mergeAiVaultListResults( + results: readonly AiVaultListResult[], + rawLimit: number | undefined +): AiVaultListResult { + const limit = rawLimit && rawLimit > 0 ? Math.floor(rawLimit) : 1000 + const byId = new Map() + const issues: AiVaultScanIssue[] = [] + for (const result of results) { + for (const session of result.sessions) { + byId.set(session.id, session) + } + issues.push(...result.issues) + } + return { + sessions: [...byId.values()] + .sort((left, right) => sessionSortTime(right) - sessionSortTime(left)) + .slice(0, limit), + issues, + scannedAt: new Date().toISOString() + } +} + export function registerAiVaultHandlers(options: AiVaultHandlerOptions = {}): void { handlerOptions = options ipcMain.handle('aiVault:listSessions', (_event, args?: AiVaultListArgs) => @@ -77,6 +211,18 @@ export function registerAiVaultHandlers(options: AiVaultHandlerOptions = {}): vo }) } +function resetAiVaultCacheForTests(): void { + cachedList = null + inflightList = null + inflightKey = null + handlerOptions = {} +} + +export const _internals = { + listAiVaultSessions, + resetAiVaultCacheForTests +} + async function getAiVaultWslHomeDirs(): Promise { if (process.platform !== 'win32') { return [] diff --git a/src/main/ipc/ssh.ts b/src/main/ipc/ssh.ts index 15f95306c78..ff676112218 100644 --- a/src/main/ipc/ssh.ts +++ b/src/main/ipc/ssh.ts @@ -5,7 +5,7 @@ import type { Store } from '../persistence' import { SshConnectionStore } from '../ssh/ssh-connection-store' import { SshConnectionManager, type SshConnectionCallbacks } from '../ssh/ssh-connection' import type { SshChannelMultiplexer } from '../ssh/ssh-channel-multiplexer' -import { SshRelaySession } from '../ssh/ssh-relay-session' +import { SshRelaySession, type SshRelayAiVaultHostInfo } from '../ssh/ssh-relay-session' import { SshPortForwardManager } from '../ssh/ssh-port-forward' import type { DetectedPort, @@ -123,6 +123,23 @@ export async function removeRegisteredSshTarget(targetId: string): Promise // scattered Maps/Sets that previously tracked this state independently. const activeSessions = new Map() +export function getActiveSshAiVaultHostInfo(targetId: string): SshRelayAiVaultHostInfo | null { + if (isRuntimeOwnedSshTargetId(targetId)) { + return null + } + return activeSessions.get(targetId)?.getAiVaultHostInfo() ?? null +} + +export function getActiveSshAiVaultHostInfos(): SshRelayAiVaultHostInfo[] { + return [...activeSessions.values()].flatMap((session) => { + if (isRuntimeOwnedSshTargetId(session.targetId)) { + return [] + } + const info = session.getAiVaultHostInfo() + return info ? [info] : [] + }) +} + async function detachActiveSshSession(targetId: string): Promise { await teardownActiveSshSession(targetId, (session) => session.detach()) } diff --git a/src/main/ssh/ssh-relay-session.ts b/src/main/ssh/ssh-relay-session.ts index 58bf3fbf798..85761621d44 100644 --- a/src/main/ssh/ssh-relay-session.ts +++ b/src/main/ssh/ssh-relay-session.ts @@ -62,10 +62,12 @@ import { import type { Store } from '../persistence' import type { OrcaRuntimeService } from '../runtime/orca-runtime' import { runRemoteOrcaCli } from './ssh-remote-orca-cli' +import { toSshExecutionHostId, type ExecutionHostId } from '../../shared/execution-host' export type RelaySessionState = 'idle' | 'deploying' | 'ready' | 'reconnecting' | 'disposed' type RemoteCliBridgeEnv = { + remoteHome: string binDir: string relayDir: string nodePath: string @@ -79,6 +81,13 @@ type ForwardedReplayFingerprint = { deliveredAt: number } +export type SshRelayAiVaultHostInfo = { + targetId: string + executionHostId: ExecutionHostId + remoteHome: string + hostPlatform: RemoteHostPlatform +} + const RECONNECT_REPLAY_DUPLICATE_WINDOW_MS = 1000 const REPLAY_FINGERPRINT_EDGE_CHARS = 128 @@ -189,6 +198,19 @@ export class SshRelaySession { return this.remoteCliBridgeEnv?.hostPlatform ?? this.hostPlatform } + getAiVaultHostInfo(): SshRelayAiVaultHostInfo | null { + const env = this.remoteCliBridgeEnv + if (!env) { + return null + } + return { + targetId: this.targetId, + executionHostId: toSshExecutionHostId(this.targetId), + remoteHome: env.remoteHome, + hostPlatform: env.hostPlatform + } + } + getPortScanner(): PortScanner | null { return this.portScanner } @@ -218,6 +240,7 @@ export class SshRelaySession { this.remoteCliBridgeEnv = remoteHome && remoteRelayDir && nodePath && sockPath && hostPlatform ? { + remoteHome, binDir: joinRemotePath(hostPlatform, remoteHome, '.orca-relay', 'bin'), relayDir: remoteRelayDir, nodePath, @@ -345,6 +368,7 @@ export class SshRelaySession { this.remoteCliBridgeEnv = remoteHome && remoteRelayDir && nodePath && sockPath && hostPlatform ? { + remoteHome, binDir: joinRemotePath(hostPlatform, remoteHome, '.orca-relay', 'bin'), relayDir: remoteRelayDir, nodePath, diff --git a/src/renderer/src/components/right-sidebar/AiVaultPanel.tsx b/src/renderer/src/components/right-sidebar/AiVaultPanel.tsx index f67c7d82a3a..3fd7ae88809 100644 --- a/src/renderer/src/components/right-sidebar/AiVaultPanel.tsx +++ b/src/renderer/src/components/right-sidebar/AiVaultPanel.tsx @@ -41,6 +41,7 @@ import { translate } from '@/i18n/i18n' import { AiVaultPanelHeader } from './AiVaultPanelHeader' import { AiVaultSessionVirtualList } from './AiVaultSessionVirtualList' import { useAiVaultSessionRefresh } from './ai-vault-session-refresh' +import { useAiVaultExecutionHostScope } from './ai-vault-host-scope' export default function AiVaultPanel(): React.JSX.Element { const activeWorktreeId = useActiveWorktreeId() @@ -77,6 +78,11 @@ export default function AiVaultPanel(): React.JSX.Element { 'runtime', [activeWorktreeId, resumeTargetState] ) + const { executionHostScope, activeSshExecutionHostScope, onExecutionHostScopeChange } = + useAiVaultExecutionHostScope({ + activeWorktreeId: activeWorktreeId ?? null, + resumeTargetState + }) const activeWorktreePath = activeWorktree?.path ?? null // Why: AI Vault ownership is cwd-based, so we must consider live worktrees across all repos. const activeWorktreePaths = useMemo( @@ -106,7 +112,10 @@ export default function AiVaultPanel(): React.JSX.Element { }), [activeProjectKey, activeWorktree, allWorktrees, projectHostSetupProjection] ) - const { error, loading, refresh, scanResult, sessions } = useAiVaultSessionRefresh(scopePaths) + const { error, loading, refresh, scanResult, sessions } = useAiVaultSessionRefresh( + scopePaths, + executionHostScope + ) const sessionProjectById = useMemo( () => buildAiVaultProjectContext({ @@ -121,6 +130,7 @@ export default function AiVaultPanel(): React.JSX.Element { ) const sessionWorktreeById = useAiVaultSessionWorktreeMap({ sessions, + repos, worktrees: allWorktrees, activeWorktreeId: activeWorktreeId ?? activeWorktree?.id ?? null }) @@ -211,6 +221,7 @@ export default function AiVaultPanel(): React.JSX.Element { (session: AiVaultSession) => resolveAiVaultSessionResumeState({ sessionFilePath: session.filePath, + sessionExecutionHostId: session.executionHostId, worktreeInfo: sessionWorktreeById.get(session.id) ?? null, activeWorktreeId: activeWorktreeId ?? activeWorktree?.id ?? null, worktrees: allWorktrees, @@ -231,6 +242,7 @@ export default function AiVaultPanel(): React.JSX.Element { (session: AiVaultSession) => resolveAiVaultSessionResumeActions({ sessionFilePath: session.filePath, + sessionExecutionHostId: session.executionHostId, worktreeInfo: sessionWorktreeById.get(session.id) ?? null, activeWorktreeId: activeWorktreeId ?? activeWorktree?.id ?? null, worktrees: allWorktrees, @@ -293,6 +305,8 @@ export default function AiVaultPanel(): React.JSX.Element { activeWorktreePath={activeWorktreePath} activeProjectKey={activeProjectKey} scope={scope} + executionHostScope={executionHostScope} + activeSshExecutionHostScope={activeSshExecutionHostScope} agents={agents} sort={sort} group={group} @@ -300,6 +314,7 @@ export default function AiVaultPanel(): React.JSX.Element { adjustmentCount={viewAdjustmentCount} onQueryChange={setQuery} onScopeChange={handleScopeChange} + onExecutionHostScopeChange={onExecutionHostScopeChange} onAgentEnabledChange={setAgentEnabled} onSortChange={setSort} onGroupChange={setGroup} diff --git a/src/renderer/src/components/right-sidebar/AiVaultPanelControls.tsx b/src/renderer/src/components/right-sidebar/AiVaultPanelControls.tsx index 227e96ccca9..2b3cc2802b0 100644 --- a/src/renderer/src/components/right-sidebar/AiVaultPanelControls.tsx +++ b/src/renderer/src/components/right-sidebar/AiVaultPanelControls.tsx @@ -7,7 +7,8 @@ import { FolderOpen, ListFilter, LoaderCircle, - PanelsTopLeft + PanelsTopLeft, + Server } from 'lucide-react' import { Button } from '@/components/ui/button' import { @@ -31,6 +32,12 @@ import { type AiVaultScope, type AiVaultSort } from '../../../../shared/ai-vault-types' +import { + ALL_EXECUTION_HOSTS_SCOPE, + getExecutionHostLabel, + LOCAL_EXECUTION_HOST_ID, + type ExecutionHostScope +} from '../../../../shared/execution-host' import { agentLabel, type AiVaultSessionGroup } from './ai-vault-session-filters' import { translate } from '@/i18n/i18n' @@ -175,6 +182,59 @@ export function VaultScopeSwitch({ ) } +export function VaultHostScopeMenu({ + executionHostScope, + activeSshExecutionHostScope, + onExecutionHostScopeChange +}: { + executionHostScope: ExecutionHostScope + activeSshExecutionHostScope: ExecutionHostScope | null + onExecutionHostScopeChange: (scope: ExecutionHostScope) => void +}): React.JSX.Element { + const hostOptions = [ + LOCAL_EXECUTION_HOST_ID, + ...(activeSshExecutionHostScope ? [activeSshExecutionHostScope] : []), + ALL_EXECUTION_HOSTS_SCOPE + ] as const + const label = getExecutionHostLabel(executionHostScope) + + return ( + + + + + + + {translate('auto.components.right.sidebar.AiVaultPanelControls.host', 'Host')} + + onExecutionHostScopeChange(value as ExecutionHostScope)} + > + {hostOptions.map((scope) => ( + + {getExecutionHostLabel(scope)} + + ))} + + + + ) +} + export function VaultViewMenu({ agents, sort, diff --git a/src/renderer/src/components/right-sidebar/AiVaultPanelHeader.tsx b/src/renderer/src/components/right-sidebar/AiVaultPanelHeader.tsx index 6f40190617e..b463e188a7e 100644 --- a/src/renderer/src/components/right-sidebar/AiVaultPanelHeader.tsx +++ b/src/renderer/src/components/right-sidebar/AiVaultPanelHeader.tsx @@ -7,7 +7,8 @@ import type { AiVaultScope, AiVaultSort } from '../../../../shared/ai-vault-types' -import { VaultScopeSwitch, VaultViewMenu } from './AiVaultPanelControls' +import type { ExecutionHostScope } from '../../../../shared/execution-host' +import { VaultHostScopeMenu, VaultScopeSwitch, VaultViewMenu } from './AiVaultPanelControls' type AiVaultPanelHeaderProps = { query: string @@ -18,6 +19,8 @@ type AiVaultPanelHeaderProps = { activeWorktreePath: string | null activeProjectKey: string | null scope: AiVaultScope + executionHostScope: ExecutionHostScope + activeSshExecutionHostScope: ExecutionHostScope | null agents: readonly AiVaultAgent[] sort: AiVaultSort group: AiVaultGroup @@ -25,6 +28,7 @@ type AiVaultPanelHeaderProps = { adjustmentCount: number onQueryChange: (query: string) => void onScopeChange: (scope: AiVaultScope) => void + onExecutionHostScopeChange: (scope: ExecutionHostScope) => void onAgentEnabledChange: (agent: AiVaultAgent, enabled: boolean) => void onSortChange: (sort: AiVaultSort) => void onGroupChange: (group: AiVaultGroup) => void @@ -42,6 +46,8 @@ export function AiVaultPanelHeader({ activeWorktreePath, activeProjectKey, scope, + executionHostScope, + activeSshExecutionHostScope, agents, sort, group, @@ -49,6 +55,7 @@ export function AiVaultPanelHeader({ adjustmentCount, onQueryChange, onScopeChange, + onExecutionHostScopeChange, onAgentEnabledChange, onSortChange, onGroupChange, @@ -99,6 +106,11 @@ export function AiVaultPanelHeader({
+ void + onJumpToOriginalPane?: () => void + showJumpToWorktree: boolean + onJumpToWorktree?: () => void + onCopyResume: () => void + onCopyId: () => void + onCopyPath: () => void + onOpenLog?: () => void + onRevealLog?: () => void + onOpenCwd?: () => void +}) { + const Item = menuKind === 'context' ? ContextMenuItem : DropdownMenuItem + const Separator = menuKind === 'context' ? ContextMenuSeparator : DropdownMenuSeparator + const hasLocalPathActions = Boolean(onOpenLog || onRevealLog || onOpenCwd) + + return ( + <> + {onJumpToOriginalPane ? ( + + + {translate( + 'auto.components.right.sidebar.AiVaultSessionRow.jumpToOriginalPane', + 'Jump to Original Pane' + )} + + ) : null} + {showJumpToWorktree ? ( + + + {translate( + 'auto.components.right.sidebar.AiVaultSessionRow.jumpToWorktree', + 'Jump to Worktree' + )} + + ) : null} + + + {resumeLabel} + + + + {translate( + 'auto.components.right.sidebar.AiVaultSessionRow.copyResumeCommand', + 'Copy Resume Command' + )} + + {hasLocalPathActions ? ( + <> + + {onOpenLog ? ( + + + {translate('auto.components.right.sidebar.AiVaultSessionRow.openLog', 'Open Log')} + + ) : null} + {onRevealLog ? ( + + + {translate('auto.components.right.sidebar.AiVaultSessionRow.revealLog', 'Reveal Log')} + + ) : null} + {onOpenCwd ? ( + + + {translate( + 'auto.components.right.sidebar.AiVaultSessionRow.openWorkingDirectory', + 'Open Working Directory' + )} + + ) : null} + + ) : null} + + + {translate( + 'auto.components.right.sidebar.AiVaultSessionRow.copySessionId', + 'Copy Session ID' + )} + + + {translate('auto.components.right.sidebar.AiVaultSessionRow.copyLogPath', 'Copy Log Path')} + + + ) +} diff --git a/src/renderer/src/components/right-sidebar/AiVaultSessionDetails.tsx b/src/renderer/src/components/right-sidebar/AiVaultSessionDetails.tsx index 58923a53479..d5a538b44fd 100644 --- a/src/renderer/src/components/right-sidebar/AiVaultSessionDetails.tsx +++ b/src/renderer/src/components/right-sidebar/AiVaultSessionDetails.tsx @@ -31,7 +31,7 @@ export function SessionInlineDetails({ } onResumeInWorktree: () => void onResumeInNewTab: () => void - onOpenLog: () => void + onOpenLog?: () => void }): React.JSX.Element { const showResumeInWorktree = Boolean(resumeActions.worktree.worktreeId) const showResumeInNewTab = !showResumeInWorktree || Boolean(resumeActions.newTab.worktreeId) @@ -132,20 +132,22 @@ export function SessionInlineDetails({ )} ) : null} - + {onOpenLog ? ( + + ) : null}
) diff --git a/src/renderer/src/components/right-sidebar/AiVaultSessionRow.tsx b/src/renderer/src/components/right-sidebar/AiVaultSessionRow.tsx index 5ea6bd9d307..0826eed9509 100644 --- a/src/renderer/src/components/right-sidebar/AiVaultSessionRow.tsx +++ b/src/renderer/src/components/right-sidebar/AiVaultSessionRow.tsx @@ -1,14 +1,6 @@ import { useCallback } from 'react' import type React from 'react' -import { Copy, FileJson, FolderOpen, LocateFixed, PanelTopOpen, Play } from 'lucide-react' -import { DropdownMenuItem, DropdownMenuSeparator } from '@/components/ui/dropdown-menu' -import { - ContextMenu, - ContextMenuContent, - ContextMenuItem, - ContextMenuSeparator, - ContextMenuTrigger -} from '@/components/ui/context-menu' +import { ContextMenu, ContextMenuContent, ContextMenuTrigger } from '@/components/ui/context-menu' import { Badge } from '@/components/ui/badge' import RepoBadgeLabel from '@/components/repo/RepoBadgeLabel' import { AgentIcon } from '@/lib/agent-catalog' @@ -27,6 +19,7 @@ import { agentLabel } from './ai-vault-session-filters' import { translate } from '@/i18n/i18n' import { SessionInlineDetails, SessionTime } from './AiVaultSessionDetails' import { latestSessionConversationTurn } from './ai-vault-session-display' +import { SessionActionMenuItems } from './AiVaultSessionActionMenuItems' import { SessionRowTrailingActions } from './SessionRowTrailingActions' import type { AiVaultSessionResumeActions } from './ai-vault-session-resume' import { @@ -74,8 +67,8 @@ export function VaultSessionRow({ onCopyResume: () => void onCopyId: () => void onCopyPath: () => void - onOpenLog: () => void - onRevealLog: () => void + onOpenLog?: () => void + onRevealLog?: () => void onOpenCwd?: () => void }) { const updatedAt = session.updatedAt ?? session.modifiedAt @@ -102,6 +95,7 @@ export function VaultSessionRow({ title: session.title, command: resumeStartup.command, sessionFilePath: session.filePath, + sessionExecutionHostId: session.executionHostId, ...(resumeStartup.env ? { env: resumeStartup.env } : {}), ...(resumeStartup.launchConfig ? { launchConfig: resumeStartup.launchConfig } : {}) }) @@ -223,101 +217,6 @@ export function VaultSessionRow({ ) } -export function SessionActionMenuItems({ - menuKind = 'dropdown', - resumeDisabled, - resumeLabel, - onResume, - onJumpToOriginalPane, - showJumpToWorktree, - onJumpToWorktree, - onCopyResume, - onCopyId, - onCopyPath, - onOpenLog, - onRevealLog, - onOpenCwd -}: { - menuKind?: 'dropdown' | 'context' - resumeDisabled: boolean - resumeLabel: string - onResume: () => void - onJumpToOriginalPane?: () => void - showJumpToWorktree: boolean - onJumpToWorktree?: () => void - onCopyResume: () => void - onCopyId: () => void - onCopyPath: () => void - onOpenLog: () => void - onRevealLog: () => void - onOpenCwd?: () => void -}) { - const Item = menuKind === 'context' ? ContextMenuItem : DropdownMenuItem - const Separator = menuKind === 'context' ? ContextMenuSeparator : DropdownMenuSeparator - - return ( - <> - {onJumpToOriginalPane ? ( - - - {translate( - 'auto.components.right.sidebar.AiVaultSessionRow.jumpToOriginalPane', - 'Jump to Original Pane' - )} - - ) : null} - {showJumpToWorktree ? ( - - - {translate( - 'auto.components.right.sidebar.AiVaultSessionRow.jumpToWorktree', - 'Jump to Worktree' - )} - - ) : null} - - - {resumeLabel} - - - - {translate( - 'auto.components.right.sidebar.AiVaultSessionRow.copyResumeCommand', - 'Copy Resume Command' - )} - - - - - {translate('auto.components.right.sidebar.AiVaultSessionRow.openLog', 'Open Log')} - - - - {translate('auto.components.right.sidebar.AiVaultSessionRow.revealLog', 'Reveal Log')} - - {onOpenCwd ? ( - - - {translate( - 'auto.components.right.sidebar.AiVaultSessionRow.openWorkingDirectory', - 'Open Working Directory' - )} - - ) : null} - - - {translate( - 'auto.components.right.sidebar.AiVaultSessionRow.copySessionId', - 'Copy Session ID' - )} - - - {translate('auto.components.right.sidebar.AiVaultSessionRow.copyLogPath', 'Copy Log Path')} - - - ) -} - function getSessionDetailsId(sessionId: string): string { return `ai-vault-session-details-${sessionId.replace(/[^A-Za-z0-9_-]/g, '-')}` } diff --git a/src/renderer/src/components/right-sidebar/AiVaultSessionVirtualList.tsx b/src/renderer/src/components/right-sidebar/AiVaultSessionVirtualList.tsx index e7293f15de7..3d2bdde0384 100644 --- a/src/renderer/src/components/right-sidebar/AiVaultSessionVirtualList.tsx +++ b/src/renderer/src/components/right-sidebar/AiVaultSessionVirtualList.tsx @@ -19,6 +19,7 @@ import { isAiVaultSessionInCurrentWorktree, type AiVaultSessionWorktreeInfo } from './ai-vault-session-worktree' +import { canUseLocalAiVaultSessionPathActions } from './ai-vault-session-path-actions' import { extractVaultVirtualRowIndexes, getVaultStickyHeaderIndexes, @@ -275,6 +276,8 @@ function AiVaultVirtualRow({ const resumeState = row.type === 'session' ? getSessionResumeState(row.session) : null const resumeActions = row.type === 'session' ? getSessionResumeActions(row.session) : null const resumeLabel = resumeState ? aiVaultSessionResumeLabel(resumeState) : '' + const canOpenLocalSessionPaths = + row.type === 'session' && canUseLocalAiVaultSessionPathActions(row.session.executionHostId) return (
onCopyResume(row.session, resumeState?.worktreeId)} onCopyId={() => onCopyId(row.session)} onCopyPath={() => onCopyPath(row.session)} - onOpenLog={() => onOpenLog(row.session)} - onRevealLog={() => onRevealLog(row.session)} - onOpenCwd={row.session.cwd ? () => onOpenCwd(row.session) : undefined} + onOpenLog={canOpenLocalSessionPaths ? () => onOpenLog(row.session) : undefined} + onRevealLog={canOpenLocalSessionPaths ? () => onRevealLog(row.session) : undefined} + onOpenCwd={ + canOpenLocalSessionPaths && row.session.cwd ? () => onOpenCwd(row.session) : undefined + } /> )}
diff --git a/src/renderer/src/components/right-sidebar/SessionRowTrailingActions.tsx b/src/renderer/src/components/right-sidebar/SessionRowTrailingActions.tsx index 27a29bbf032..10167da346b 100644 --- a/src/renderer/src/components/right-sidebar/SessionRowTrailingActions.tsx +++ b/src/renderer/src/components/right-sidebar/SessionRowTrailingActions.tsx @@ -11,7 +11,7 @@ import { cn } from '@/lib/utils' import type { AiVaultSession } from '../../../../shared/ai-vault-types' import { agentLabel } from './ai-vault-session-filters' import { translate } from '@/i18n/i18n' -import { SessionActionMenuItems } from './AiVaultSessionRow' +import { SessionActionMenuItems } from './AiVaultSessionActionMenuItems' import { aiVaultWorktreeJumpTooltip, type AiVaultSessionWorktreeInfo @@ -67,8 +67,8 @@ export function SessionRowTrailingActions({ onCopyResume: () => void onCopyId: () => void onCopyPath: () => void - onOpenLog: () => void - onRevealLog: () => void + onOpenLog?: () => void + onRevealLog?: () => void onOpenCwd?: () => void }) { const jumpToWorktreeTooltip = aiVaultWorktreeJumpTooltip(worktreeInfo) diff --git a/src/renderer/src/components/right-sidebar/ai-vault-host-scope.test.ts b/src/renderer/src/components/right-sidebar/ai-vault-host-scope.test.ts new file mode 100644 index 00000000000..73af6ab915e --- /dev/null +++ b/src/renderer/src/components/right-sidebar/ai-vault-host-scope.test.ts @@ -0,0 +1,120 @@ +// @vitest-environment happy-dom + +import { act, createElement } from 'react' +import { createRoot, type Root } from 'react-dom/client' +import { afterEach, describe, expect, it } from 'vitest' +import type { AppState } from '@/store/types' +import type { AiVaultSessionResumeTargetState } from './ai-vault-session-resume' +import { useAiVaultExecutionHostScope } from './ai-vault-host-scope' + +type HostScopeResult = ReturnType + +let root: Root | null = null +let latest: HostScopeResult | null = null + +function HookProbe(props: { + activeWorktreeId: string | null + resumeTargetState: AiVaultSessionResumeTargetState +}): null { + latest = useAiVaultExecutionHostScope(props) + return null +} + +async function renderHook(props: { + activeWorktreeId: string | null + resumeTargetState: AiVaultSessionResumeTargetState +}): Promise { + if (!root) { + const container = document.createElement('div') + document.body.appendChild(container) + root = createRoot(container) + } + await act(async () => { + root?.render(createElement(HookProbe, props)) + }) +} + +function stateForWorktree(args: { + worktreeId: string + repoId: string + executionHostId?: string | null + hostId?: string | null +}): AiVaultSessionResumeTargetState { + return { + folderWorkspaces: [], + projectGroups: [], + repos: [ + { + id: args.repoId, + connectionId: null, + executionHostId: args.executionHostId ?? 'local' + } + ], + worktreesByRepo: { + [args.repoId]: [ + { + id: args.worktreeId, + repoId: args.repoId, + hostId: args.hostId ?? null + } + ] + } + } as unknown as Pick +} + +afterEach(() => { + if (root) { + act(() => root?.unmount()) + } + root = null + latest = null + document.body.replaceChildren() +}) + +describe('useAiVaultExecutionHostScope', () => { + it('defaults SSH worktrees to their SSH execution host', async () => { + await renderHook({ + activeWorktreeId: 'repo-1::/remote/repo', + resumeTargetState: stateForWorktree({ + worktreeId: 'repo-1::/remote/repo', + repoId: 'repo-1', + hostId: 'ssh:dev-box' + }) + }) + + expect(latest?.executionHostScope).toBe('ssh:dev-box') + expect(latest?.activeSshExecutionHostScope).toBe('ssh:dev-box') + }) + + it('defaults local worktrees to local history', async () => { + await renderHook({ + activeWorktreeId: 'repo-1::/local/repo', + resumeTargetState: stateForWorktree({ + worktreeId: 'repo-1::/local/repo', + repoId: 'repo-1' + }) + }) + + expect(latest?.executionHostScope).toBe('local') + expect(latest?.activeSshExecutionHostScope).toBeNull() + }) + + it('preserves manual host scope changes across unrelated rerenders', async () => { + const props = { + activeWorktreeId: 'repo-1::/remote/repo', + resumeTargetState: stateForWorktree({ + worktreeId: 'repo-1::/remote/repo', + repoId: 'repo-1', + hostId: 'ssh:dev-box' + }) + } + await renderHook(props) + + await act(async () => { + latest?.onExecutionHostScopeChange('all') + }) + await renderHook({ ...props, resumeTargetState: { ...props.resumeTargetState } }) + + expect(latest?.executionHostScope).toBe('all') + }) +}) diff --git a/src/renderer/src/components/right-sidebar/ai-vault-host-scope.ts b/src/renderer/src/components/right-sidebar/ai-vault-host-scope.ts new file mode 100644 index 00000000000..8993e8a00e6 --- /dev/null +++ b/src/renderer/src/components/right-sidebar/ai-vault-host-scope.ts @@ -0,0 +1,64 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { getAiVaultResumeWorkspaceExecutionHostId } from '@/lib/ai-vault-resume-target' +import { + ALL_EXECUTION_HOSTS_SCOPE, + LOCAL_EXECUTION_HOST_ID, + parseExecutionHostId, + type ExecutionHostScope +} from '../../../../shared/execution-host' +import type { AiVaultSessionResumeTargetState } from './ai-vault-session-resume' + +export function useAiVaultExecutionHostScope(args: { + activeWorktreeId: string | null + resumeTargetState: AiVaultSessionResumeTargetState +}): { + executionHostScope: ExecutionHostScope + activeSshExecutionHostScope: ExecutionHostScope | null + onExecutionHostScopeChange: (scope: ExecutionHostScope) => void +} { + const userChangedHostScopeRef = useRef(false) + const activeExecutionHostId = useMemo( + () => getAiVaultResumeWorkspaceExecutionHostId(args.resumeTargetState, args.activeWorktreeId), + [args.activeWorktreeId, args.resumeTargetState] + ) + const activeExecutionHost = parseExecutionHostId(activeExecutionHostId) + const defaultExecutionHostScope: ExecutionHostScope = + activeExecutionHost?.kind === 'ssh' ? activeExecutionHost.id : LOCAL_EXECUTION_HOST_ID + const activeSshExecutionHostScope: ExecutionHostScope | null = + activeExecutionHost?.kind === 'ssh' ? activeExecutionHost.id : null + const [executionHostScope, setExecutionHostScope] = + useState(defaultExecutionHostScope) + + useEffect(() => { + // Why: preserve an explicit user choice (e.g. "All") across incidental + // rerenders, but reset to the new default once that choice no longer + // applies to the active worktree's host. + const allowedScopes = new Set([ + LOCAL_EXECUTION_HOST_ID, + ALL_EXECUTION_HOSTS_SCOPE, + ...(activeSshExecutionHostScope ? [activeSshExecutionHostScope] : []) + ]) + if (!allowedScopes.has(executionHostScope)) { + setExecutionHostScope(defaultExecutionHostScope) + userChangedHostScopeRef.current = false + return + } + if (!userChangedHostScopeRef.current && executionHostScope !== defaultExecutionHostScope) { + setExecutionHostScope(defaultExecutionHostScope) + } + }, [activeSshExecutionHostScope, defaultExecutionHostScope, executionHostScope]) + + const handleExecutionHostScopeChange = useCallback( + (nextScope: ExecutionHostScope) => { + userChangedHostScopeRef.current = nextScope !== defaultExecutionHostScope + setExecutionHostScope(nextScope) + }, + [defaultExecutionHostScope] + ) + + return { + executionHostScope, + activeSshExecutionHostScope, + onExecutionHostScopeChange: handleExecutionHostScopeChange + } +} diff --git a/src/renderer/src/components/right-sidebar/ai-vault-original-pane.test.ts b/src/renderer/src/components/right-sidebar/ai-vault-original-pane.test.ts index cec8a30a270..c47f64b9720 100644 --- a/src/renderer/src/components/right-sidebar/ai-vault-original-pane.test.ts +++ b/src/renderer/src/components/right-sidebar/ai-vault-original-pane.test.ts @@ -10,6 +10,7 @@ const OTHER_LEAF_ID = '22222222-2222-4222-8222-222222222222' const baseSession: AiVaultSession = { id: 'codex:session-1', + executionHostId: 'local', agent: 'codex', sessionId: 'session-1', title: 'Fix the pane focus', diff --git a/src/renderer/src/components/right-sidebar/ai-vault-session-display.test.ts b/src/renderer/src/components/right-sidebar/ai-vault-session-display.test.ts index b341558c628..3da906781d9 100644 --- a/src/renderer/src/components/right-sidebar/ai-vault-session-display.test.ts +++ b/src/renderer/src/components/right-sidebar/ai-vault-session-display.test.ts @@ -9,6 +9,7 @@ import { const baseSession: AiVaultSession = { id: 'codex:1', + executionHostId: 'local', agent: 'codex', sessionId: 'session-1', title: 'Fix the flaky golden tests', diff --git a/src/renderer/src/components/right-sidebar/ai-vault-session-filters.test.ts b/src/renderer/src/components/right-sidebar/ai-vault-session-filters.test.ts index 3190221f389..acbe0b051a1 100644 --- a/src/renderer/src/components/right-sidebar/ai-vault-session-filters.test.ts +++ b/src/renderer/src/components/right-sidebar/ai-vault-session-filters.test.ts @@ -15,6 +15,7 @@ import { const baseSession: AiVaultSession = { id: 'claude:1', + executionHostId: 'local', agent: 'claude', sessionId: 'session-1', title: 'Implement vault filters', diff --git a/src/renderer/src/components/right-sidebar/ai-vault-session-launch-actions.ts b/src/renderer/src/components/right-sidebar/ai-vault-session-launch-actions.ts index dc2ed601f83..c55acb1e939 100644 --- a/src/renderer/src/components/right-sidebar/ai-vault-session-launch-actions.ts +++ b/src/renderer/src/components/right-sidebar/ai-vault-session-launch-actions.ts @@ -13,6 +13,7 @@ import { import { useAppStore } from '@/store' import { canResumeAiVaultSessionOnTarget, + getAiVaultResumeWorkspaceExecutionHostId, getAiVaultResumeWorkspaceTargetStatus } from '@/lib/ai-vault-resume-target' import type { AiVaultAgent, AiVaultSession } from '../../../../shared/ai-vault-types' @@ -79,6 +80,7 @@ export function useAiVaultSessionLaunchActions({ (session: AiVaultSession, targetWorktreeId?: string): void => { const targetId = resolveAiVaultSessionLaunchTarget({ sessionFilePath: session.filePath, + sessionExecutionHostId: session.executionHostId, activeWorktreeId: activeWorktreeId ?? activeWorktree?.id ?? null, targetWorktreeId, targetState @@ -130,6 +132,7 @@ export type AiVaultSessionLaunchTarget = export function resolveAiVaultSessionLaunchTarget(args: { sessionFilePath: string | null + sessionExecutionHostId?: AiVaultSession['executionHostId'] | null activeWorktreeId: string | null targetWorktreeId?: string targetState: AiVaultSessionResumeTargetState @@ -143,7 +146,18 @@ export function resolveAiVaultSessionLaunchTarget(args: { } const targetStatus = getAiVaultResumeWorkspaceTargetStatus(args.targetState, targetWorktreeId) - if (!canResumeAiVaultSessionOnTarget({ sessionFilePath: args.sessionFilePath, targetStatus })) { + const targetExecutionHostId = getAiVaultResumeWorkspaceExecutionHostId( + args.targetState, + targetWorktreeId + ) + if ( + !canResumeAiVaultSessionOnTarget({ + sessionFilePath: args.sessionFilePath, + sessionExecutionHostId: args.sessionExecutionHostId, + targetStatus, + targetExecutionHostId + }) + ) { return { status: 'unsupported', targetStatus } } @@ -159,12 +173,12 @@ function aiVaultResumeUnsupportedMessage( 'Resume from history is not available in runtime-hosted workspaces.' ) } - // Why: 'ssh' only reaches the unsupported branch when the session file lives - // on this machine, so the message explains the host mismatch, not SSH itself. - if (targetStatus === 'ssh') { + // Why: local and SSH targets can both be valid generally; this branch means + // the session's recorded host does not match the selected workspace. + if (targetStatus === 'ssh' || targetStatus === 'local') { return translate( - 'auto.components.right.sidebar.AiVaultPanel.localSessionSshWorkspaceUnsupported', - "This session's history is stored on this machine, so it can't resume in an SSH workspace. Open a local workspace instead." + 'auto.components.right.sidebar.AiVaultPanel.sessionHostMismatchUnsupported', + 'This session belongs to a different host. Open a workspace on the same host to resume it.' ) } return translate( diff --git a/src/renderer/src/components/right-sidebar/ai-vault-session-path-actions.test.ts b/src/renderer/src/components/right-sidebar/ai-vault-session-path-actions.test.ts new file mode 100644 index 00000000000..93fee6c94e9 --- /dev/null +++ b/src/renderer/src/components/right-sidebar/ai-vault-session-path-actions.test.ts @@ -0,0 +1,14 @@ +import { describe, expect, it } from 'vitest' +import { canUseLocalAiVaultSessionPathActions } from './ai-vault-session-path-actions' + +describe('canUseLocalAiVaultSessionPathActions', () => { + it('allows OS path actions for local session history', () => { + expect(canUseLocalAiVaultSessionPathActions('local')).toBe(true) + }) + + it('blocks OS path actions for non-local or unknown session history', () => { + expect(canUseLocalAiVaultSessionPathActions('ssh:dev-box')).toBe(false) + expect(canUseLocalAiVaultSessionPathActions('runtime:gpu-box')).toBe(false) + expect(canUseLocalAiVaultSessionPathActions(undefined)).toBe(false) + }) +}) diff --git a/src/renderer/src/components/right-sidebar/ai-vault-session-path-actions.ts b/src/renderer/src/components/right-sidebar/ai-vault-session-path-actions.ts new file mode 100644 index 00000000000..f723bbb1959 --- /dev/null +++ b/src/renderer/src/components/right-sidebar/ai-vault-session-path-actions.ts @@ -0,0 +1,13 @@ +import { + LOCAL_EXECUTION_HOST_ID, + normalizeExecutionHostId, + type ExecutionHostId +} from '../../../../shared/execution-host' + +export function canUseLocalAiVaultSessionPathActions( + executionHostId: ExecutionHostId | null | undefined +): boolean { + // Why: Electron shell open/reveal APIs only validate paths on this computer; + // SSH session history exposes paths that exist on the remote host instead. + return normalizeExecutionHostId(executionHostId) === LOCAL_EXECUTION_HOST_ID +} diff --git a/src/renderer/src/components/right-sidebar/ai-vault-session-projects.test.ts b/src/renderer/src/components/right-sidebar/ai-vault-session-projects.test.ts index fa53c685da8..d0e4c1a1c6e 100644 --- a/src/renderer/src/components/right-sidebar/ai-vault-session-projects.test.ts +++ b/src/renderer/src/components/right-sidebar/ai-vault-session-projects.test.ts @@ -6,6 +6,7 @@ import { buildAiVaultProjectContext, toAiVaultProjectKey } from './ai-vault-sess const baseSession: AiVaultSession = { id: 'claude:1', + executionHostId: 'local', agent: 'claude', sessionId: 'session-1', title: 'Implement project history', @@ -182,7 +183,11 @@ describe('buildAiVaultProjectContext', () => { repoId: repo.id, path: '/runtime/orca' }) - const session = makeSession({ id: 'claude:runtime-worktree', cwd: '/runtime/orca/src' }) + const session = makeSession({ + id: 'claude:runtime-worktree', + cwd: '/runtime/orca/src', + executionHostId: 'runtime:preview' + }) const context = buildAiVaultProjectContext({ repos: [repo], @@ -292,7 +297,7 @@ describe('buildAiVaultProjectContext', () => { expect(context.sessionProjectById.get(session.id)?.key).toBe('repo:repo-win') }) - it('falls back to folder when a hostless session matches multiple host buckets', () => { + it('uses the session host when matching overlapping local and SSH project paths', () => { const localRepo = makeRepo({ id: 'local', displayName: 'Local', path: '/srv/orca' }) const sshRepo = makeRepo({ id: 'ssh', @@ -300,7 +305,11 @@ describe('buildAiVaultProjectContext', () => { path: '/srv/orca', connectionId: 'target-1' }) - const session = makeSession({ id: 'claude:ambiguous', cwd: '/srv/orca/src' }) + const session = makeSession({ + id: 'claude:ssh-session', + cwd: '/srv/orca/src', + executionHostId: 'ssh:target-1' + }) const context = buildAiVaultProjectContext({ repos: [localRepo, sshRepo], @@ -324,16 +333,21 @@ describe('buildAiVaultProjectContext', () => { }) expect(context.sessionProjectById.get(session.id)).toMatchObject({ - kind: 'folder', - key: 'folder:/srv/orca/src', - label: 'orca/src' + kind: 'repo', + key: 'repo:ssh', + label: 'SSH', + hostKey: 'ssh:target-1' }) }) - it('uses ProjectHostSetup host ids when detecting ambiguous host buckets', () => { + it('falls back to folder when a legacy hostless session matches multiple host buckets', () => { const localRepo = makeRepo({ id: 'local', displayName: 'Local', path: '/srv/orca' }) const runtimeRepo = makeRepo({ id: 'runtime', displayName: 'Runtime', path: '/srv/orca' }) - const session = makeSession({ id: 'claude:runtime-ambiguous', cwd: '/srv/orca/src' }) + const session = makeSession({ + id: 'claude:runtime-ambiguous', + cwd: '/srv/orca/src', + executionHostId: undefined as unknown as AiVaultSession['executionHostId'] + }) const context = buildAiVaultProjectContext({ repos: [localRepo, runtimeRepo], diff --git a/src/renderer/src/components/right-sidebar/ai-vault-session-projects.ts b/src/renderer/src/components/right-sidebar/ai-vault-session-projects.ts index fdb78325025..c0734a11881 100644 --- a/src/renderer/src/components/right-sidebar/ai-vault-session-projects.ts +++ b/src/renderer/src/components/right-sidebar/ai-vault-session-projects.ts @@ -1,4 +1,9 @@ -import { getRepoExecutionHostId, LOCAL_EXECUTION_HOST_ID } from '../../../../shared/execution-host' +import { + getRepoExecutionHostId, + LOCAL_EXECUTION_HOST_ID, + normalizeExecutionHostId, + type ExecutionHostId +} from '../../../../shared/execution-host' import type { ProjectHostSetupProjection } from '../../../../shared/project-host-setup-projection' import type { AiVaultSession } from '../../../../shared/ai-vault-types' import type { ProjectHostSetup, Repo, Worktree } from '../../../../shared/types' @@ -14,7 +19,7 @@ export type AiVaultSessionProject = { label: string projectId?: string repoId?: string - hostKey?: string + hostKey?: ExecutionHostId } export type AiVaultProjectContext = { @@ -27,7 +32,7 @@ export type AiVaultProjectContext = { type SessionProjectCandidate = { source: 'worktree' | 'setup' normalizedPath: string - hostKey: string + hostKey: ExecutionHostId projectId: string | null repoId: string | null } @@ -63,7 +68,7 @@ export function buildAiVaultProjectContext({ for (const session of sessions) { sessionProjectById.set( session.id, - resolveSessionProject(session.cwd, candidates, projectLabelByKey) + resolveSessionProject(session, candidates, projectLabelByKey) ) } @@ -147,10 +152,11 @@ function buildProjectCandidates( candidates.push({ source: 'worktree', normalizedPath: normalizeRuntimePathForComparison(worktree.path), - hostKey: - worktree.hostId ?? - setup?.hostId ?? - (repo ? getRepoExecutionHostId(repo) : LOCAL_EXECUTION_HOST_ID), + hostKey: resolveCandidateHostId( + worktree.hostId, + setup?.hostId, + repo ? getRepoExecutionHostId(repo) : null + ), projectId: worktree.projectId ?? setup?.projectId ?? null, repoId: worktree.repoId }) @@ -168,7 +174,7 @@ function buildProjectCandidates( candidates.push({ source: 'setup', normalizedPath: normalizeRuntimePathForComparison(setup.path), - hostKey: setup.hostId || getRepoExecutionHostId(setup), + hostKey: resolveCandidateHostId(setup.hostId, getRepoExecutionHostId(setup)), projectId: setup.projectId, repoId: setup.repoId || null }) @@ -193,15 +199,28 @@ function buildProjectCandidates( return candidates } +function resolveCandidateHostId( + ...values: readonly (string | null | undefined)[] +): ExecutionHostId { + for (const value of values) { + const hostId = normalizeExecutionHostId(value) + if (hostId) { + return hostId + } + } + return LOCAL_EXECUTION_HOST_ID +} + function hasCandidatePath(pathValue: string): boolean { return pathValue.trim().length > 0 } function resolveSessionProject( - cwd: string | null, + session: AiVaultSession, candidates: readonly SessionProjectCandidate[], projectLabelByKey: ReadonlyMap ): AiVaultSessionProject { + const cwd = session.cwd if (!cwd) { return { kind: 'unknown', key: 'unknown', label: '' } } @@ -209,14 +228,22 @@ function resolveSessionProject( const matches = candidates.filter((candidate) => isPathInsideOrEqual(candidate.normalizedPath, cwd) ) - const hostBuckets = new Set(matches.map((candidate) => candidate.hostKey)) - if (hostBuckets.size > 1) { - // Why: session rows do not carry host ids yet, so overlapping local/SSH - // paths must stay visible without being attributed to the wrong project. + const sessionHostId = normalizeExecutionHostId(session.executionHostId) + const hostMatches = sessionHostId + ? matches.filter((candidate) => candidate.hostKey === sessionHostId) + : matches + if (sessionHostId && matches.length > 0 && hostMatches.length === 0) { + // Why: same-path projects can exist on several hosts; a tagged transcript + // should never be attributed to a project on a different machine. return folderProject(cwd) } - const bestCandidate = matches.sort(compareCandidates)[0] + const hostBuckets = new Set(hostMatches.map((candidate) => candidate.hostKey)) + if (!sessionHostId && hostBuckets.size > 1) { + return folderProject(cwd) + } + + const bestCandidate = hostMatches.sort(compareCandidates)[0] if (!bestCandidate) { return folderProject(cwd) } diff --git a/src/renderer/src/components/right-sidebar/ai-vault-session-refresh.test.ts b/src/renderer/src/components/right-sidebar/ai-vault-session-refresh.test.ts index cdff27604d9..2ea75f40f48 100644 --- a/src/renderer/src/components/right-sidebar/ai-vault-session-refresh.test.ts +++ b/src/renderer/src/components/right-sidebar/ai-vault-session-refresh.test.ts @@ -42,18 +42,37 @@ const initialAppState = useAppStore.getInitialState() const roots: Root[] = [] let latest: ReturnType | null = null -function HookProbe(props: { scopePaths: readonly string[] }): null { - latest = useAiVaultSessionRefresh(props.scopePaths) +function HookProbe(props: { + scopePaths: readonly string[] + executionHostScope?: 'local' | 'all' | `ssh:${string}` +}): null { + latest = useAiVaultSessionRefresh(props.scopePaths, props.executionHostScope ?? 'local') return null } -async function renderHook(scopePaths: readonly string[] = []): Promise { +async function renderHook( + scopePaths: readonly string[] = [], + executionHostScope: 'local' | 'all' | `ssh:${string}` = 'local' +): Promise { const container = document.createElement('div') document.body.appendChild(container) const root = createRoot(container) roots.push(root) await act(async () => { - root.render(createElement(HookProbe, { scopePaths })) + root.render(createElement(HookProbe, { scopePaths, executionHostScope })) + }) +} + +async function rerenderHook( + scopePaths: readonly string[] = [], + executionHostScope: 'local' | 'all' | `ssh:${string}` = 'local' +): Promise { + const root = roots.at(-1) + if (!root) { + throw new Error('renderHook must be called before rerenderHook') + } + await act(async () => { + root.render(createElement(HookProbe, { scopePaths, executionHostScope })) }) } @@ -126,7 +145,58 @@ describe('useAiVaultSessionRefresh refocus behavior', () => { await flushMicrotasks() expect(listSessionsMock).toHaveBeenCalledTimes(1) - expect(listSessionsMock.mock.calls[0]?.[0]).toMatchObject({ force: true }) + expect(listSessionsMock.mock.calls[0]?.[0]).toMatchObject({ + executionHostScope: 'local', + force: true + }) + }) + + it('passes the requested execution host scope to the scanner', async () => { + await renderHook(['/repo'], 'ssh:dev-box') + await flushMicrotasks() + + expect(listSessionsMock).toHaveBeenCalledTimes(1) + expect(lastCallArgs()).toMatchObject({ + executionHostScope: 'ssh:dev-box', + scopePaths: ['/repo'] + }) + }) + + it('does not apply stale results after the host scope changes mid-scan', async () => { + let resolveLocal: ((result: AiVaultListResult) => void) | null = null + let resolveSsh: ((result: AiVaultListResult) => void) | null = null + listSessionsMock + .mockImplementationOnce( + () => new Promise((resolve) => (resolveLocal = resolve)) + ) + .mockImplementationOnce( + () => new Promise((resolve) => (resolveSsh = resolve)) + ) + + await renderHook([], 'local') + expect(listSessionsMock).toHaveBeenCalledTimes(1) + + await rerenderHook(['/remote/repo'], 'ssh:dev-box') + expect(listSessionsMock).toHaveBeenCalledTimes(1) + + await act(async () => { + resolveLocal?.({ ...EMPTY_RESULT, scannedAt: '2026-07-01T00:00:01.000Z' }) + }) + await flushMicrotasks() + + expect(latest?.scanResult).toBeNull() + expect(listSessionsMock).toHaveBeenCalledTimes(2) + expect(lastCallArgs()).toMatchObject({ + executionHostScope: 'ssh:dev-box', + scopePaths: ['/remote/repo'] + }) + + await act(async () => { + resolveSsh?.({ ...EMPTY_RESULT, scannedAt: '2026-07-01T00:00:02.000Z' }) + }) + await flushMicrotasks() + + expect(latest?.scanResult?.scannedAt).toBe('2026-07-01T00:00:02.000Z') }) it('force re-scans on refocus once the throttle allows it', async () => { diff --git a/src/renderer/src/components/right-sidebar/ai-vault-session-refresh.ts b/src/renderer/src/components/right-sidebar/ai-vault-session-refresh.ts index 98539f858ce..88757297603 100644 --- a/src/renderer/src/components/right-sidebar/ai-vault-session-refresh.ts +++ b/src/renderer/src/components/right-sidebar/ai-vault-session-refresh.ts @@ -1,5 +1,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import type { AiVaultListResult, AiVaultSession } from '../../../../shared/ai-vault-types' +import type { ExecutionHostScope } from '../../../../shared/execution-host' import { useAppStore } from '@/store' const SESSION_LIMIT = 500 @@ -26,7 +27,10 @@ export function resetAiVaultForcedRescanThrottleForTest(): void { type AiVaultRefreshArgs = { force?: boolean; background?: boolean } -export function useAiVaultSessionRefresh(scopePaths: readonly string[]): { +export function useAiVaultSessionRefresh( + scopePaths: readonly string[], + executionHostScope: ExecutionHostScope +): { error: string | null loading: boolean refresh: (args?: AiVaultRefreshArgs) => Promise @@ -45,77 +49,99 @@ export function useAiVaultSessionRefresh(scopePaths: readonly string[]): { const lastAppliedScanRef = useRef<{ scopeKey: string; scannedAt: string } | null>(null) const mountedRef = useRef(true) const scopePathsKey = useMemo(() => scopePaths.join('\n'), [scopePaths]) + const scanScopeKey = `${executionHostScope}\n${scopePathsKey}` const scopePathsRef = useRef(scopePaths) scopePathsRef.current = scopePaths + const executionHostScopeRef = useRef(executionHostScope) + executionHostScopeRef.current = executionHostScope + const currentScanScopeKey = useCallback( + () => `${executionHostScopeRef.current}\n${scopePathsRef.current.join('\n')}`, + [] + ) - const refresh = useCallback(async (args: AiVaultRefreshArgs = {}): Promise => { - // A scope change during an in-flight scan must not be dropped; queue one more - // scan so the current scoped view is refreshed after the older scan settles. - if (refreshInFlightRef.current) { - pendingRefreshRef.current = true - pendingForceRef.current ||= args.force === true - pendingBackgroundRef.current &&= args.background === true - return - } - - refreshInFlightRef.current = true - const refreshId = refreshIdRef.current + 1 - refreshIdRef.current = refreshId - // A manual force scan counts against the throttle so an auto rescan right - // after the button press doesn't trigger a second full scan. - if (args.force === true) { - lastForcedRescanAt = Date.now() - } - // Background (refocus) refreshes usually resolve from the main-process - // cache; suppressing the loading flag avoids a spinner flash on every - // return to the app. - if (args.background !== true) { - setLoading(true) - } - setError(null) - const scopeKey = scopePathsRef.current.join('\n') - try { - const result = await window.api.aiVault.listSessions({ - limit: SESSION_LIMIT, - scopePaths: scopePathsRef.current, - force: args.force - }) - if (!mountedRef.current || refreshIdRef.current !== refreshId) { + const refresh = useCallback( + async (args: AiVaultRefreshArgs = {}): Promise => { + // A scope change during an in-flight scan must not be dropped; queue one more + // scan so the current scoped view is refreshed after the older scan settles. + if (refreshInFlightRef.current) { + pendingRefreshRef.current = true + pendingForceRef.current ||= args.force === true + pendingBackgroundRef.current &&= args.background === true return } - // A cache hit returns the snapshot already on screen; skip the state - // updates so refocus flips don't force pointless re-renders. - if ( - lastAppliedScanRef.current?.scopeKey === scopeKey && - lastAppliedScanRef.current.scannedAt === result.scannedAt - ) { - return + + refreshInFlightRef.current = true + const refreshId = refreshIdRef.current + 1 + refreshIdRef.current = refreshId + // A manual force scan counts against the throttle so an auto rescan right + // after the button press doesn't trigger a second full scan. + if (args.force === true) { + lastForcedRescanAt = Date.now() } - lastAppliedScanRef.current = { scopeKey, scannedAt: result.scannedAt } - setScanResult(result) - setSessions(result.sessions) - } catch (err) { - if (mountedRef.current && refreshIdRef.current === refreshId) { - setError(err instanceof Error ? err.message : String(err)) + // Background (refocus) refreshes usually resolve from the main-process + // cache; suppressing the loading flag avoids a spinner flash on every + // return to the app. + if (args.background !== true) { + setLoading(true) } - } finally { - refreshInFlightRef.current = false - if (mountedRef.current && refreshIdRef.current === refreshId) { - setLoading(false) + setError(null) + const scopeKey = scopePathsRef.current.join('\n') + const hostScope = executionHostScopeRef.current + const scanKey = `${hostScope}\n${scopeKey}` + try { + const result = await window.api.aiVault.listSessions({ + limit: SESSION_LIMIT, + scopePaths: scopePathsRef.current, + executionHostScope: hostScope, + force: args.force + }) + if (!mountedRef.current || refreshIdRef.current !== refreshId) { + return + } + // Why: host/scope changes queue a follow-up scan, but the older result + // may resolve first and must not briefly paint the wrong history list. + if (scanKey !== currentScanScopeKey()) { + return + } + // A cache hit returns the snapshot already on screen; skip the state + // updates so refocus flips don't force pointless re-renders. + if ( + lastAppliedScanRef.current?.scopeKey === scanKey && + lastAppliedScanRef.current.scannedAt === result.scannedAt + ) { + return + } + lastAppliedScanRef.current = { scopeKey: scanKey, scannedAt: result.scannedAt } + setScanResult(result) + setSessions(result.sessions) + } catch (err) { + if ( + mountedRef.current && + refreshIdRef.current === refreshId && + scanKey === currentScanScopeKey() + ) { + setError(err instanceof Error ? err.message : String(err)) + } + } finally { + refreshInFlightRef.current = false + if (mountedRef.current && refreshIdRef.current === refreshId) { + setLoading(false) + } + if (pendingRefreshRef.current && mountedRef.current) { + pendingRefreshRef.current = false + const force = pendingForceRef.current + // The queued refresh is background-only if every queued caller was. + const background = pendingBackgroundRef.current + pendingForceRef.current = false + pendingBackgroundRef.current = true + void refresh({ force, background }) + } } - if (pendingRefreshRef.current && mountedRef.current) { - pendingRefreshRef.current = false - const force = pendingForceRef.current - // The queued refresh is background-only if every queued caller was. - const background = pendingBackgroundRef.current - pendingForceRef.current = false - pendingBackgroundRef.current = true - void refresh({ force, background }) - } - } - // Deps are intentionally empty: refresh reads changing values through refs - // and recurses on itself, so its identity must stay stable. - }, []) + // Deps intentionally avoid changing scope values: refresh reads them + // through refs and recurses on itself, so its identity must stay stable. + }, + [currentScanScopeKey] + ) // Forced rescans triggered by events (refocus, agent-session starts) run // immediately when the throttle allows, otherwise once as soon as it frees @@ -162,7 +188,7 @@ export function useAiVaultSessionRefresh(scopePaths: readonly string[]): { if (!force) { requestForcedRescan() } - }, [refresh, requestForcedRescan, scopePathsKey]) + }, [refresh, requestForcedRescan, scanScopeKey]) // Sessions started while the app was backgrounded should appear when the // user returns, so refocus also bypasses the scan cache (throttled). OS diff --git a/src/renderer/src/components/right-sidebar/ai-vault-session-resume.ts b/src/renderer/src/components/right-sidebar/ai-vault-session-resume.ts index b8a428cd0c0..eb82d910cb9 100644 --- a/src/renderer/src/components/right-sidebar/ai-vault-session-resume.ts +++ b/src/renderer/src/components/right-sidebar/ai-vault-session-resume.ts @@ -1,8 +1,10 @@ import type { Repo, Worktree } from '../../../../shared/types' import { canResumeAiVaultSessionOnTarget, + getAiVaultResumeWorkspaceExecutionHostId, getAiVaultResumeWorkspaceTargetStatus } from '@/lib/ai-vault-resume-target' +import type { AiVaultSession } from '../../../../shared/ai-vault-types' import type { AppState } from '@/store/types' import { translate } from '@/i18n/i18n' import { parseWorkspaceKey } from '../../../../shared/workspace-scope' @@ -34,6 +36,7 @@ export type AiVaultSessionResumeActions = { export function resolveAiVaultSessionResumeState(args: { sessionFilePath: string | null + sessionExecutionHostId?: AiVaultSession['executionHostId'] | null worktreeInfo: AiVaultSessionWorktreeInfo | null activeWorktreeId: string | null worktrees: readonly Worktree[] @@ -56,6 +59,7 @@ export function resolveAiVaultSessionResumeState(args: { for (const worktreeId of candidateWorktreeIds) { const targetId = resolveSupportedResumeWorktreeId({ sessionFilePath: args.sessionFilePath, + sessionExecutionHostId: args.sessionExecutionHostId, worktreeId, targetState }) @@ -78,6 +82,7 @@ export function resolveAiVaultSessionResumeState(args: { export function resolveAiVaultSessionResumeActions(args: { sessionFilePath: string | null + sessionExecutionHostId?: AiVaultSession['executionHostId'] | null worktreeInfo: AiVaultSessionWorktreeInfo | null activeWorktreeId: string | null worktrees: readonly Worktree[] @@ -92,11 +97,13 @@ export function resolveAiVaultSessionResumeActions(args: { const sessionTargetId = resolveSupportedResumeWorktreeId({ sessionFilePath: args.sessionFilePath, + sessionExecutionHostId: args.sessionExecutionHostId, worktreeId: sessionWorktreeId, targetState }) const activeTargetId = resolveSupportedResumeWorktreeId({ sessionFilePath: args.sessionFilePath, + sessionExecutionHostId: args.sessionExecutionHostId, worktreeId: args.activeWorktreeId && args.activeWorktreeId !== sessionWorktreeId ? args.activeWorktreeId @@ -142,6 +149,7 @@ export function isKnownAiVaultResumeWorkspaceTarget( function resolveSupportedResumeWorktreeId(args: { sessionFilePath: string | null + sessionExecutionHostId?: AiVaultSession['executionHostId'] | null worktreeId: string | null targetState: AiVaultSessionResumeTargetState }): string | null { @@ -154,7 +162,18 @@ function resolveSupportedResumeWorktreeId(args: { } const targetStatus = getAiVaultResumeWorkspaceTargetStatus(args.targetState, args.worktreeId) - if (!canResumeAiVaultSessionOnTarget({ sessionFilePath: args.sessionFilePath, targetStatus })) { + const targetExecutionHostId = getAiVaultResumeWorkspaceExecutionHostId( + args.targetState, + args.worktreeId + ) + if ( + !canResumeAiVaultSessionOnTarget({ + sessionFilePath: args.sessionFilePath, + sessionExecutionHostId: args.sessionExecutionHostId, + targetStatus, + targetExecutionHostId + }) + ) { return null } diff --git a/src/renderer/src/components/right-sidebar/ai-vault-session-worktree.test.ts b/src/renderer/src/components/right-sidebar/ai-vault-session-worktree.test.ts index b378afe3d42..58687977a33 100644 --- a/src/renderer/src/components/right-sidebar/ai-vault-session-worktree.test.ts +++ b/src/renderer/src/components/right-sidebar/ai-vault-session-worktree.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import type { AiVaultSession } from '../../../../shared/ai-vault-types' -import type { Worktree } from '../../../../shared/types' +import type { Repo, Worktree } from '../../../../shared/types' import { aiVaultWorktreeCompactPath, aiVaultWorktreeJumpTooltip, @@ -15,6 +15,7 @@ import { const baseSession: AiVaultSession = { id: 'codex:session-1', + executionHostId: 'local', agent: 'codex', sessionId: 'session-1', title: 'Find the pane', @@ -55,6 +56,19 @@ function makeWorktree(overrides: Partial = {}): Worktree { return { ...worktree, ...overrides } } +function makeRepo(overrides: Partial = {}): Repo { + return { + id: 'repo-1', + path: '/repo/orca', + displayName: 'orca', + badgeColor: '#737373', + addedAt: 1, + connectionId: null, + executionHostId: 'local', + ...overrides + } +} + describe('resolveAiVaultSessionWorktreeInfo', () => { it('marks the selected owning worktree as current', () => { const worktree = makeWorktree() @@ -135,6 +149,55 @@ describe('resolveAiVaultSessionWorktreeInfo', () => { path: '\\\\wsl.localhost\\Ubuntu\\home\\ada\\orca' }) }) + + it('uses the session host when multiple worktrees share the same path', () => { + const localWorktree = makeWorktree({ + id: 'repo-local::/srv/orca', + repoId: 'repo-local', + displayName: 'local', + path: '/srv/orca', + hostId: 'local' + }) + const sshWorktree = makeWorktree({ + id: 'repo-ssh::/srv/orca', + repoId: 'repo-ssh', + displayName: 'ssh', + path: '/srv/orca', + hostId: 'ssh:target-1' + }) + + expect( + resolveAiVaultSessionWorktreeInfo({ + session: { ...baseSession, cwd: '/srv/orca/src', executionHostId: 'ssh:target-1' }, + worktrees: [localWorktree, sshWorktree], + activeWorktreeId: null + }) + ).toMatchObject({ + label: 'ssh', + worktreeId: sshWorktree.id + }) + }) + + it('uses repo host ownership when a legacy worktree lacks host metadata', () => { + const worktree = makeWorktree({ + id: 'repo-ssh::/srv/orca', + repoId: 'repo-ssh', + displayName: 'ssh', + path: '/srv/orca' + }) + + expect( + resolveAiVaultSessionWorktreeInfo({ + session: { ...baseSession, cwd: '/srv/orca/src', executionHostId: 'ssh:target-1' }, + repos: [makeRepo({ id: 'repo-ssh', connectionId: 'target-1', executionHostId: null })], + worktrees: [worktree], + activeWorktreeId: null + }) + ).toMatchObject({ + label: 'ssh', + worktreeId: worktree.id + }) + }) }) describe('canJumpToAiVaultSessionWorktree', () => { diff --git a/src/renderer/src/components/right-sidebar/ai-vault-session-worktree.ts b/src/renderer/src/components/right-sidebar/ai-vault-session-worktree.ts index bd7aeef1bea..31222bec1b5 100644 --- a/src/renderer/src/components/right-sidebar/ai-vault-session-worktree.ts +++ b/src/renderer/src/components/right-sidebar/ai-vault-session-worktree.ts @@ -1,6 +1,12 @@ import { useMemo } from 'react' import { parseWslUncPath } from '../../../../shared/wsl-paths' import { splitWorktreeIdForFilesystem } from '../../../../shared/worktree-id' +import { + getRepoExecutionHostId, + LOCAL_EXECUTION_HOST_ID, + normalizeExecutionHostId, + type ExecutionHostId +} from '../../../../shared/execution-host' import { isPathInsideOrEqual, isRuntimePathAbsolute, @@ -8,7 +14,7 @@ import { normalizeRuntimePathSeparators } from '../../../../shared/cross-platform-path' import type { AiVaultSession } from '../../../../shared/ai-vault-types' -import type { Worktree } from '../../../../shared/types' +import type { Repo, Worktree } from '../../../../shared/types' import { translate } from '@/i18n/i18n' export type AiVaultSessionWorktreeStatus = 'current' | 'active' | 'archived' | 'unavailable' @@ -23,16 +29,19 @@ export type AiVaultSessionWorktreeInfo = { type WorktreeCandidate = { worktree: Worktree path: string + hostId: ExecutionHostId status: Exclude source: 'current-path' | 'prior-path' } export function resolveAiVaultSessionWorktreeInfo({ session, + repos = [], worktrees, activeWorktreeId }: { session: AiVaultSession + repos?: readonly Pick[] worktrees: readonly Worktree[] activeWorktreeId: string | null }): AiVaultSessionWorktreeInfo | null { @@ -40,8 +49,10 @@ export function resolveAiVaultSessionWorktreeInfo({ return null } - const candidates = buildWorktreeCandidates(worktrees) + const sessionHostId = normalizeExecutionHostId(session.executionHostId) + const candidates = buildWorktreeCandidates(worktrees, repos) .filter((candidate) => isSessionInWorktreePath(candidate.path, session.cwd!)) + .filter((candidate) => !sessionHostId || candidate.hostId === sessionHostId) .sort(compareWorktreeCandidates) const best = candidates[0] @@ -85,6 +96,7 @@ export function extractWorktreePathFromSessionTitle(title: string): string | nul export function resolveAiVaultSessionWorktreeDisplay(args: { session: AiVaultSession + repos?: readonly Pick[] worktrees: readonly Worktree[] activeWorktreeId: string | null }): AiVaultSessionWorktreeInfo | null { @@ -117,10 +129,12 @@ export function resolveAiVaultSessionWorktreeDisplay(args: { export function useAiVaultSessionWorktreeMap({ sessions, + repos = [], worktrees, activeWorktreeId }: { sessions: readonly AiVaultSession[] + repos?: readonly Pick[] worktrees: readonly Worktree[] activeWorktreeId: string | null }): ReadonlyMap { @@ -130,13 +144,14 @@ export function useAiVaultSessionWorktreeMap({ sessions.flatMap((session) => { const worktreeInfo = resolveAiVaultSessionWorktreeDisplay({ session, + repos, worktrees, activeWorktreeId }) return worktreeInfo ? [[session.id, worktreeInfo] as const] : [] }) ), - [activeWorktreeId, sessions, worktrees] + [activeWorktreeId, repos, sessions, worktrees] ) } @@ -192,13 +207,22 @@ export function aiVaultWorktreeJumpTooltip( ) } -function buildWorktreeCandidates(worktrees: readonly Worktree[]): WorktreeCandidate[] { +function buildWorktreeCandidates( + worktrees: readonly Worktree[], + repos: readonly Pick[] +): WorktreeCandidate[] { const candidates: WorktreeCandidate[] = [] + const repoById = new Map(repos.map((repo) => [repo.id, repo])) for (const worktree of worktrees) { + const repo = repoById.get(worktree.repoId) + const hostId = + normalizeExecutionHostId(worktree.hostId) ?? + (repo ? getRepoExecutionHostId(repo) : LOCAL_EXECUTION_HOST_ID) if (hasUsablePath(worktree.path)) { candidates.push({ worktree, path: worktree.path, + hostId, status: worktree.isArchived ? 'archived' : 'active', source: 'current-path' }) @@ -211,6 +235,7 @@ function buildWorktreeCandidates(worktrees: readonly Worktree[]): WorktreeCandid candidates.push({ worktree, path: parsed.worktreePath, + hostId, status: worktree.isArchived ? 'archived' : 'active', source: 'prior-path' }) diff --git a/src/renderer/src/components/tab-group/AiVaultSessionDropLayer.tsx b/src/renderer/src/components/tab-group/AiVaultSessionDropLayer.tsx index ee41d5c6e81..af09c852cab 100644 --- a/src/renderer/src/components/tab-group/AiVaultSessionDropLayer.tsx +++ b/src/renderer/src/components/tab-group/AiVaultSessionDropLayer.tsx @@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef, useState, type CSSProperties } from 're import { toast } from 'sonner' import { canResumeAiVaultSessionOnTarget, + getAiVaultResumeWorkspaceExecutionHostId, getAiVaultResumeWorkspaceTargetStatus } from '@/lib/ai-vault-resume-target' import { @@ -169,7 +170,9 @@ export default function AiVaultSessionDropLayer({ return true } - const targetStatus = getAiVaultResumeWorkspaceTargetStatus(useAppStore.getState(), worktreeId) + const state = useAppStore.getState() + const targetStatus = getAiVaultResumeWorkspaceTargetStatus(state, worktreeId) + const targetExecutionHostId = getAiVaultResumeWorkspaceExecutionHostId(state, worktreeId) if (targetStatus === 'runtime') { toast.error( translate( @@ -191,13 +194,15 @@ export default function AiVaultSessionDropLayer({ if ( !canResumeAiVaultSessionOnTarget({ sessionFilePath: payload.sessionFilePath ?? null, - targetStatus + sessionExecutionHostId: payload.sessionExecutionHostId ?? null, + targetStatus, + targetExecutionHostId }) ) { toast.error( translate( - 'auto.components.tab.group.AiVaultSessionDropLayer.localSessionSshWorkspaceUnsupported', - "This session's history is stored on this machine, so it can't resume in an SSH workspace. Drop it onto a local workspace instead." + 'auto.components.tab.group.AiVaultSessionDropLayer.sessionHostMismatchUnsupported', + 'This session belongs to a different host. Drop it onto a workspace on the same host.' ) ) return true diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index ea81c2c094a..1080958f7c2 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -2589,6 +2589,7 @@ "sessionQueued": "Session queued", "runtimeWorkspacesUnsupported": "Resume from history is not available in runtime-hosted workspaces.", "openSupportedWorkspace": "Open a local or SSH workspace before resuming a session.", + "sessionHostMismatchUnsupported": "This session belongs to a different host. Drop it onto a workspace on the same host.", "localSessionSshWorkspaceUnsupported": "This session's history is stored on this machine, so it can't resume in an SSH workspace. Drop it onto a local workspace instead." }, "TabGroupDropOverlay": { @@ -9590,6 +9591,7 @@ "runtimeBrowseLocalHistory": "Runtime-hosted workspaces can browse local history. Resume actions are available in local and SSH workspaces.", "runtimeWorkspacesUnsupported": "Resume from history is not available in runtime-hosted workspaces.", "openSupportedWorkspace": "Open a local or SSH workspace before resuming a session.", + "sessionHostMismatchUnsupported": "This session belongs to a different host. Open a workspace on the same host to resume it.", "localSessionSshWorkspaceUnsupported": "This session's history is stored on this machine, so it can't resume in an SSH workspace. Open a local workspace instead." }, "AiVaultPanelControls": { @@ -9619,7 +9621,9 @@ "globalScope": "Global", "projectScope": "Project", "currentProjectLower": "current project", - "project": "Project" + "project": "Project", + "hostScopeAriaLabel": "Session History host: {{value0}}", + "host": "Host" }, "AiVaultSessionDetails": { "originalAsk": "Original ask", diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json index 97cbdff6f63..b7a29585352 100644 --- a/src/renderer/src/i18n/locales/es.json +++ b/src/renderer/src/i18n/locales/es.json @@ -2589,7 +2589,8 @@ "sessionQueued": "Sesión en cola", "runtimeWorkspacesUnsupported": "Reanudar desde el historial no está disponible en espacios de trabajo alojados en un host.", "openSupportedWorkspace": "Abre un espacio de trabajo local o SSH antes de reanudar una sesión.", - "localSessionSshWorkspaceUnsupported": "El historial de esta sesión solo existe en este equipo. Para reanudarla, arrástrala a un workspace local. Los workspaces SSH no tienen acceso a ese historial." + "localSessionSshWorkspaceUnsupported": "El historial de esta sesión solo existe en este equipo. Para reanudarla, arrástrala a un workspace local. Los workspaces SSH no tienen acceso a ese historial.", + "sessionHostMismatchUnsupported": "Esta sesión pertenece a un host diferente. Suéltala en un espacio de trabajo del mismo host." }, "TabGroupDropOverlay": { "paneColumnLabel": "Nueva división" @@ -9590,7 +9591,8 @@ "runtimeBrowseLocalHistory": "Los workspaces alojados en runtime pueden explorar el historial local. Las acciones de reanudación están disponibles en workspaces locales y SSH.", "runtimeWorkspacesUnsupported": "Reanudar desde el historial no está disponible en workspaces alojados en runtime.", "openSupportedWorkspace": "Abre un workspace local o SSH antes de reanudar una sesión.", - "localSessionSshWorkspaceUnsupported": "El historial de esta sesión solo existe en este equipo. Para reanudarla, abre un workspace local. Los workspaces SSH no tienen acceso a ese historial." + "localSessionSshWorkspaceUnsupported": "El historial de esta sesión solo existe en este equipo. Para reanudarla, abre un workspace local. Los workspaces SSH no tienen acceso a ese historial.", + "sessionHostMismatchUnsupported": "Esta sesión pertenece a un host diferente. Abre un espacio de trabajo en el mismo host para reanudarla." }, "AiVaultPanelControls": { "scanningSessions": "Buscando sesiones", @@ -9619,7 +9621,9 @@ "globalScope": "Global", "projectScope": "Proyecto", "currentProjectLower": "proyecto actual", - "project": "Proyecto" + "project": "Proyecto", + "hostScopeAriaLabel": "Session History host: {{value0}}", + "host": "Host" }, "AiVaultSessionDetails": { "updated": "Actualizado", diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index 590f6d505cf..e6ff4fc77f5 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -2589,7 +2589,8 @@ "sessionQueued": "セッションをキューに追加しました", "runtimeWorkspacesUnsupported": "Resume from history is not available in runtime-hosted workspaces.", "openSupportedWorkspace": "Open a local or SSH workspace before resuming a session.", - "localSessionSshWorkspaceUnsupported": "このセッションの履歴はこのマシンに保存されているため、SSH ワークスペースでは再開できません。代わりにローカルワークスペースにドロップしてください。" + "localSessionSshWorkspaceUnsupported": "このセッションの履歴はこのマシンに保存されているため、SSH ワークスペースでは再開できません。代わりにローカルワークスペースにドロップしてください。", + "sessionHostMismatchUnsupported": "このセッションは別のホストに属しています。同じホスト上のワークスペースにドロップしてください。" }, "TabGroupDropOverlay": { "paneColumnLabel": "新しい分割" @@ -9590,7 +9591,8 @@ "runtimeBrowseLocalHistory": "Runtime-hosted workspaces can browse local history. Resume actions are available in local and SSH workspaces.", "runtimeWorkspacesUnsupported": "Resume from history is not available in runtime-hosted workspaces.", "openSupportedWorkspace": "Open a local or SSH workspace before resuming a session.", - "localSessionSshWorkspaceUnsupported": "このセッションの履歴はこのマシンに保存されているため、SSH ワークスペースでは再開できません。代わりにローカルワークスペースを開いてください。" + "localSessionSshWorkspaceUnsupported": "このセッションの履歴はこのマシンに保存されているため、SSH ワークスペースでは再開できません。代わりにローカルワークスペースを開いてください。", + "sessionHostMismatchUnsupported": "このセッションは別のホストに属しています。再開するには同じホスト上のワークスペースを開いてください。" }, "AiVaultPanelControls": { "scanningSessions": "Scanning sessions", @@ -9619,7 +9621,9 @@ "globalScope": "グローバル", "projectScope": "プロジェクト", "currentProjectLower": "現在のプロジェクト", - "project": "プロジェクト" + "project": "プロジェクト", + "hostScopeAriaLabel": "Session History host: {{value0}}", + "host": "Host" }, "AiVaultSessionDetails": { "updated": "更新日時", diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index 8ae5f66134f..705815c7739 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -2589,7 +2589,8 @@ "sessionQueued": "세션이 대기열에 추가됨", "runtimeWorkspacesUnsupported": "Resume from history is not available in runtime-hosted workspaces.", "openSupportedWorkspace": "Open a local or SSH workspace before resuming a session.", - "localSessionSshWorkspaceUnsupported": "이 세션의 기록은 이 컴퓨터에 저장되어 있어 SSH 워크스페이스에서는 재개할 수 없습니다. 대신 로컬 워크스페이스에 놓으세요." + "localSessionSshWorkspaceUnsupported": "이 세션의 기록은 이 컴퓨터에 저장되어 있어 SSH 워크스페이스에서는 재개할 수 없습니다. 대신 로컬 워크스페이스에 놓으세요.", + "sessionHostMismatchUnsupported": "이 세션은 다른 호스트에 속합니다. 같은 호스트의 워크스페이스에 놓으세요." }, "TabGroupDropOverlay": { "paneColumnLabel": "새 분할" @@ -9590,7 +9591,8 @@ "runtimeBrowseLocalHistory": "Runtime-hosted workspaces can browse local history. Resume actions are available in local and SSH workspaces.", "runtimeWorkspacesUnsupported": "Resume from history is not available in runtime-hosted workspaces.", "openSupportedWorkspace": "Open a local or SSH workspace before resuming a session.", - "localSessionSshWorkspaceUnsupported": "이 세션의 기록은 이 컴퓨터에 저장되어 있어 SSH 워크스페이스에서는 재개할 수 없습니다. 대신 로컬 워크스페이스를 여세요." + "localSessionSshWorkspaceUnsupported": "이 세션의 기록은 이 컴퓨터에 저장되어 있어 SSH 워크스페이스에서는 재개할 수 없습니다. 대신 로컬 워크스페이스를 여세요.", + "sessionHostMismatchUnsupported": "이 세션은 다른 호스트에 속합니다. 재개하려면 같은 호스트의 워크스페이스를 여세요." }, "AiVaultPanelControls": { "scanningSessions": "세션 스캔 중", @@ -9619,7 +9621,9 @@ "globalScope": "전역", "projectScope": "프로젝트", "currentProjectLower": "현재 프로젝트", - "project": "프로젝트" + "project": "프로젝트", + "hostScopeAriaLabel": "Session History host: {{value0}}", + "host": "Host" }, "AiVaultSessionDetails": { "updated": "업데이트됨", diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index 4166c860372..db284162150 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -2589,7 +2589,8 @@ "sessionQueued": "会话已排队", "runtimeWorkspacesUnsupported": "在远程运行时托管的工作区中无法从历史记录恢复会话。", "openSupportedWorkspace": "在恢复会话之前,请先打开一个本地或 SSH 工作区。", - "localSessionSshWorkspaceUnsupported": "此会话的历史记录存储在本机上,因此无法在 SSH 工作区中恢复。请改为将其拖放到本地工作区。" + "localSessionSshWorkspaceUnsupported": "此会话的历史记录存储在本机上,因此无法在 SSH 工作区中恢复。请改为将其拖放到本地工作区。", + "sessionHostMismatchUnsupported": "此会话属于其他主机。请将其拖放到同一主机上的工作区。" }, "TabGroupDropOverlay": { "paneColumnLabel": "新建拆分" @@ -9590,7 +9591,8 @@ "runtimeBrowseLocalHistory": "远程运行时托管的工作区可以浏览本地历史。恢复操作在本地和 SSH 工作区中可用。", "runtimeWorkspacesUnsupported": "在远程运行时托管的工作区中无法从历史记录恢复会话。", "openSupportedWorkspace": "在恢复会话之前,请先打开一个本地或 SSH 工作区。", - "localSessionSshWorkspaceUnsupported": "此会话的历史记录存储在本机上,因此无法在 SSH 工作区中恢复。请改为打开一个本地工作区。" + "localSessionSshWorkspaceUnsupported": "此会话的历史记录存储在本机上,因此无法在 SSH 工作区中恢复。请改为打开一个本地工作区。", + "sessionHostMismatchUnsupported": "此会话属于其他主机。请打开同一主机上的工作区以恢复它。" }, "AiVaultPanelControls": { "scanningSessions": "正在扫描会话", @@ -9619,7 +9621,9 @@ "globalScope": "全局", "projectScope": "项目", "currentProjectLower": "当前项目", - "project": "项目" + "project": "项目", + "hostScopeAriaLabel": "Session History host: {{value0}}", + "host": "Host" }, "AiVaultSessionDetails": { "updated": "更新时间", diff --git a/src/renderer/src/lib/ai-vault-resume-command.test.ts b/src/renderer/src/lib/ai-vault-resume-command.test.ts index 1fa2559074d..dc0345491bf 100644 --- a/src/renderer/src/lib/ai-vault-resume-command.test.ts +++ b/src/renderer/src/lib/ai-vault-resume-command.test.ts @@ -294,4 +294,111 @@ describe('ai vault resume command runtime', () => { }) ).toBe("cd '/home/alice/repo' && CODEX_HOME='/home/alice/.codex' codex 'resume' 'session one'") }) + + it('returns the remote resume command verbatim for non-local host sessions', () => { + const state = makeState({ worktreePath: '/home/alice/repo' }) + state.repos = [{ id: 'repo-1', path: '/home/alice/repo', connectionId: 'ssh-1' }] as never + + expect( + buildAiVaultResumeStartupForWorktree({ + state, + worktreeId: 'repo-1::worktree-1', + session: { + agent: 'codex', + sessionId: 'session one', + cwd: '/home/alice/repo', + codexHome: null, + executionHostId: 'ssh:dev-box', + resumeCommand: "CODEX_HOME='/root/.codex' codex resume 'session one'" + } + }) + ).toEqual({ command: "CODEX_HOME='/root/.codex' codex resume 'session one'" }) + }) + + it('bypasses the resume pipeline even when the command override is blank', () => { + const state = makeState({ worktreePath: '/home/alice/repo' }) + state.repos = [{ id: 'repo-1', path: '/home/alice/repo', connectionId: 'ssh-1' }] as never + + expect( + buildAiVaultResumeCommandForWorktree({ + state, + worktreeId: 'repo-1::worktree-1', + commandOverride: ' ', + session: { + agent: 'codex', + sessionId: 'session one', + cwd: '/home/alice/repo', + codexHome: null, + executionHostId: 'ssh:dev-box', + resumeCommand: "CODEX_HOME='/root/.codex' codex resume 'session one'" + } + }) + ).toBe("CODEX_HOME='/root/.codex' codex resume 'session one'") + }) + + it('rebuilds the command when a non-blank override is supplied for a remote session', () => { + const state = makeState({ worktreePath: '/home/alice/repo' }) + state.repos = [{ id: 'repo-1', path: '/home/alice/repo', connectionId: 'ssh-1' }] as never + + expect( + buildAiVaultResumeCommandForWorktree({ + state, + worktreeId: 'repo-1::worktree-1', + commandOverride: 'my-codex', + session: { + agent: 'codex', + sessionId: 'session one', + cwd: '/home/alice/repo', + codexHome: null, + executionHostId: 'ssh:dev-box', + resumeCommand: "CODEX_HOME='/root/.codex' codex resume 'session one'" + } + }) + ).toBe("cd '/home/alice/repo' && my-codex 'resume' 'session one'") + }) + + it('rebuilds overridden remote commands with the recorded remote host platform', () => { + const state = makeState({ worktreePath: '/home/alice/repo' }) + state.repos = [{ id: 'repo-1', path: '/home/alice/repo', connectionId: 'ssh-1' }] as never + + expect( + buildAiVaultResumeCommandForWorktree({ + state, + worktreeId: 'repo-1::worktree-1', + commandOverride: 'my-codex', + session: { + agent: 'codex', + sessionId: 'session one', + cwd: 'C:/Users/alice/repo', + codexHome: 'C:/Users/alice/.codex', + executionHostId: 'ssh:win-box', + executionHostPlatform: 'win32', + resumeCommand: + 'cmd /d /s /c "cd /d ""C:/Users/alice/repo"" && set ""CODEX_HOME=C:/Users/alice/.codex"" && codex resume ""session one"""' + } + }) + ).toBe( + "Set-Location -LiteralPath 'C:/Users/alice/repo'; $env:CODEX_HOME='C:/Users/alice/.codex'; my-codex 'resume' 'session one'" + ) + }) + + it('ignores a stored resume command for local-host sessions', () => { + const state = makeState({ worktreePath: '/home/alice/repo' }) + state.repos = [{ id: 'repo-1', path: '/home/alice/repo', connectionId: 'ssh-1' }] as never + + expect( + buildAiVaultResumeCommandForWorktree({ + state, + worktreeId: 'repo-1::worktree-1', + session: { + agent: 'codex', + sessionId: 'session one', + cwd: '/home/alice/repo', + codexHome: null, + executionHostId: 'local', + resumeCommand: "CODEX_HOME='/root/.codex' codex resume 'session one'" + } + }) + ).toBe("cd '/home/alice/repo' && codex 'resume' 'session one'") + }) }) diff --git a/src/renderer/src/lib/ai-vault-resume-command.ts b/src/renderer/src/lib/ai-vault-resume-command.ts index 95da5ee49de..691d69efc09 100644 --- a/src/renderer/src/lib/ai-vault-resume-command.ts +++ b/src/renderer/src/lib/ai-vault-resume-command.ts @@ -19,10 +19,14 @@ import { getLocalProjectExecutionRuntimeContext } from '@/lib/local-preflight-co import { CLIENT_PLATFORM } from '@/lib/new-workspace' import { buildAgentResumeStartupPlan } from '@/lib/tui-agent-startup' import { getExecutionHostIdForWorktree } from '@/lib/worktree-runtime-owner' -import { parseExecutionHostId } from '../../../shared/execution-host' +import { LOCAL_EXECUTION_HOST_ID, parseExecutionHostId } from '../../../shared/execution-host' import { parseWorkspaceKey } from '../../../shared/workspace-scope' -type AiVaultResumeCommandSession = Pick +type AiVaultResumeCommandSession = Pick< + AiVaultSession, + 'agent' | 'sessionId' | 'cwd' | 'codexHome' +> & + Partial> export type AiVaultResumeStartup = { command: string @@ -65,7 +69,20 @@ export function buildAiVaultResumeStartupForWorktree(args: { session: AiVaultResumeCommandSession commandOverride?: string | null }): AiVaultResumeStartup { - const platform = getAiVaultResumePlatform(args.state, args.worktreeId) + if ( + args.session.executionHostId && + args.session.executionHostId !== LOCAL_EXECUTION_HOST_ID && + args.session.resumeCommand && + !args.commandOverride?.trim() + ) { + return { command: args.session.resumeCommand } + } + const platform = + args.session.executionHostId && + args.session.executionHostId !== LOCAL_EXECUTION_HOST_ID && + args.session.executionHostPlatform + ? args.session.executionHostPlatform + : getAiVaultResumePlatform(args.state, args.worktreeId) const codexHome = getAiVaultResumeCodexHome(args.session.codexHome, platform) // Why: the queued command is typed verbatim into the freshly spawned tab whose // live shell is the configured Windows shell (default PowerShell). Hardcoding diff --git a/src/renderer/src/lib/ai-vault-resume-target.test.ts b/src/renderer/src/lib/ai-vault-resume-target.test.ts index a2c827cf99b..207bc525a59 100644 --- a/src/renderer/src/lib/ai-vault-resume-target.test.ts +++ b/src/renderer/src/lib/ai-vault-resume-target.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest' import type { AppState } from '@/store/types' import { canResumeAiVaultSessionOnTarget, + getAiVaultResumeWorkspaceExecutionHostId, getAiVaultResumeRepoTargetStatus, getAiVaultResumeWorktreeTargetStatus, getAiVaultResumeWorkspaceTargetStatus, @@ -68,6 +69,51 @@ describe('ai vault session storage compatibility', () => { ).toBe(true) }) + it('allows host-tagged SSH sessions only on the matching SSH target', () => { + expect( + canResumeAiVaultSessionOnTarget({ + sessionFilePath: '/home/ada/.codex/sessions/remote.jsonl', + sessionExecutionHostId: 'ssh:dev-box', + targetStatus: 'ssh', + targetExecutionHostId: 'ssh:dev-box' + }) + ).toBe(true) + expect( + canResumeAiVaultSessionOnTarget({ + sessionFilePath: '/home/ada/.codex/sessions/remote.jsonl', + sessionExecutionHostId: 'ssh:dev-box', + targetStatus: 'ssh', + targetExecutionHostId: 'ssh:other-box' + }) + ).toBe(false) + expect( + canResumeAiVaultSessionOnTarget({ + sessionFilePath: '/home/ada/.codex/sessions/remote.jsonl', + sessionExecutionHostId: 'ssh:dev-box', + targetStatus: 'local', + targetExecutionHostId: 'local' + }) + ).toBe(false) + expect( + canResumeAiVaultSessionOnTarget({ + sessionFilePath: '/home/ada/.codex/sessions/remote.jsonl', + sessionExecutionHostId: 'ssh:dev-box', + targetStatus: 'local' + }) + ).toBe(false) + }) + + it('allows WSL-stored local sessions on any SSH target even with explicit host ids', () => { + expect( + canResumeAiVaultSessionOnTarget({ + sessionFilePath: wslSessionFile, + sessionExecutionHostId: 'local', + targetStatus: 'ssh', + targetExecutionHostId: 'ssh:dev-box' + }) + ).toBe(true) + }) + it('never allows runtime or unknown targets', () => { expect( canResumeAiVaultSessionOnTarget({ sessionFilePath: wslSessionFile, targetStatus: 'runtime' }) @@ -166,6 +212,34 @@ describe('ai vault resume target ownership', () => { ).toBe('runtime') }) + it('resolves exact execution host ids for active workspaces', () => { + const state = makeState({ + worktreesByRepo: { + 'repo-1': [{ id: 'repo-1::/repo/orca', repoId: 'repo-1', hostId: 'ssh:ssh-1' }] + }, + repos: [{ id: 'repo-1', connectionId: null, executionHostId: 'local' }] + }) + + expect(getAiVaultResumeWorkspaceExecutionHostId(state, 'repo-1::/repo/orca')).toBe('ssh:ssh-1') + expect(getAiVaultResumeWorkspaceExecutionHostId(state, 'worktree:repo-1::/repo/orca')).toBe( + 'ssh:ssh-1' + ) + }) + + it('resolves local execution host ids for local workspaces', () => { + expect( + getAiVaultResumeWorkspaceExecutionHostId( + makeState({ + worktreesByRepo: { + 'repo-1': [{ id: 'repo-1::/repo/orca', repoId: 'repo-1' }] + }, + repos: [{ id: 'repo-1', connectionId: null, executionHostId: 'local' }] + }), + 'repo-1::/repo/orca' + ) + ).toBe('local') + }) + it('supports folder workspaces owned by SSH project groups', () => { expect( getAiVaultResumeWorkspaceTargetStatus( diff --git a/src/renderer/src/lib/ai-vault-resume-target.ts b/src/renderer/src/lib/ai-vault-resume-target.ts index 74a1c8c48f2..823840ec85b 100644 --- a/src/renderer/src/lib/ai-vault-resume-target.ts +++ b/src/renderer/src/lib/ai-vault-resume-target.ts @@ -1,5 +1,6 @@ import { getRepoExecutionHostId, + LOCAL_EXECUTION_HOST_ID, normalizeExecutionHostId, parseExecutionHostId, toSshExecutionHostId, @@ -43,11 +44,33 @@ export function isWslStoredAiVaultSessionFile(sessionFilePath: string | null | u export function canResumeAiVaultSessionOnTarget(args: { sessionFilePath: string | null | undefined + sessionExecutionHostId?: ExecutionHostId | null targetStatus: AiVaultResumeTargetStatus + targetExecutionHostId?: ExecutionHostId | null }): boolean { if (!isSupportedAiVaultResumeTargetStatus(args.targetStatus)) { return false } + const sessionExecutionHostId = normalizeExecutionHostId(args.sessionExecutionHostId) + const targetExecutionHostId = normalizeExecutionHostId(args.targetExecutionHostId) + if (sessionExecutionHostId) { + if (targetExecutionHostId) { + if (sessionExecutionHostId === targetExecutionHostId) { + return true + } + // Why: SSH-to-local-WSL setups (#6270) tag the session 'local' but the + // file lives under a WSL UNC path reachable from any SSH shell into this + // machine, so we bypass the exact host-id match for that case. + return ( + sessionExecutionHostId === LOCAL_EXECUTION_HOST_ID && + args.targetStatus === 'ssh' && + isWslStoredAiVaultSessionFile(args.sessionFilePath) + ) + } + if (sessionExecutionHostId !== LOCAL_EXECUTION_HOST_ID) { + return false + } + } // Why: vault sessions are scanned from this machine's disk (host home dirs // plus local WSL homes). An SSH shell can only reach the WSL-stored ones // (SSH-to-local-WSL setups, #6270); host-stored session files do not exist @@ -85,6 +108,32 @@ export function getAiVaultResumeWorktreeTargetStatus(args: { ) } +export function getAiVaultResumeWorkspaceExecutionHostId( + state: Pick, + workspaceId: string | null +): ExecutionHostId | null { + if (!workspaceId) { + return null + } + + const workspaceKey = parseWorkspaceKey(workspaceId) + if (workspaceKey?.type === 'folder') { + return getAiVaultResumeFolderExecutionHostId(state, workspaceKey.folderWorkspaceId) + } + + const worktreeId = workspaceKey?.type === 'worktree' ? workspaceKey.worktreeId : workspaceId + const worktree = Object.values(state.worktreesByRepo ?? {}) + .flat() + .find((candidate) => candidate.id === worktreeId) + const worktreeHostId = normalizeExecutionHostId(worktree?.hostId) + if (worktreeHostId) { + return worktreeHostId + } + const repoId = worktree?.repoId ?? getRepoIdFromWorktreeId(worktreeId) + const repo = state.repos.find((candidate) => candidate.id === repoId) + return repo ? getRepoExecutionHostId(repo) : null +} + export function getAiVaultResumeWorkspaceTargetStatus( state: Pick, workspaceId: string | null @@ -136,6 +185,29 @@ function getAiVaultResumeFolderTargetStatus( ) } +function getAiVaultResumeFolderExecutionHostId( + state: Pick, + folderWorkspaceId: string +): ExecutionHostId | null { + const workspace = state.folderWorkspaces.find((entry) => entry.id === folderWorkspaceId) + if (!workspace) { + return null + } + + const group = state.projectGroups.find((entry) => entry.id === workspace.projectGroupId) + const groupHostId = normalizeExecutionHostId(group?.executionHostId) + if (groupHostId) { + return groupHostId + } + const explicitConnectionId = (workspace.connectionId ?? group?.connectionId ?? '').trim() + if (explicitConnectionId) { + return toSshExecutionHostId(explicitConnectionId) + } + return mergeAiVaultResumeExecutionHostIds( + getFolderWorkspaceCandidateRepos(state, folderWorkspaceId).map(getRepoExecutionHostId) + ) +} + function getAiVaultResumeExecutionHostTargetStatus( hostId: ExecutionHostId | null | undefined ): AiVaultResumeTargetStatus { @@ -162,3 +234,13 @@ function mergeAiVaultResumeExecutionHostTargetStatuses( } return new Set(hostIds).size === 1 ? (statuses[0] ?? 'unknown') : 'unknown' } + +function mergeAiVaultResumeExecutionHostIds( + hostIds: readonly ExecutionHostId[] +): ExecutionHostId | null { + if (hostIds.length === 0) { + return LOCAL_EXECUTION_HOST_ID + } + const uniqueHostIds = new Set(hostIds) + return uniqueHostIds.size === 1 ? (hostIds[0] ?? null) : null +} diff --git a/src/renderer/src/lib/ai-vault-session-drag.ts b/src/renderer/src/lib/ai-vault-session-drag.ts index 2b52359e534..7b5ec941dd7 100644 --- a/src/renderer/src/lib/ai-vault-session-drag.ts +++ b/src/renderer/src/lib/ai-vault-session-drag.ts @@ -1,6 +1,7 @@ import { AI_VAULT_AGENTS, type AiVaultAgent } from '../../../shared/ai-vault-types' import type { SleepingAgentLaunchConfig } from '../../../shared/agent-session-resume' import { measureClipboardTextByteLength } from '../../../shared/clipboard-text' +import { normalizeExecutionHostId, type ExecutionHostId } from '../../../shared/execution-host' export const AI_VAULT_SESSION_DRAG_TYPE = 'application/x-orca-ai-vault-session' export const AI_VAULT_SESSION_DRAG_START_EVENT = 'orca-ai-vault-session-drag-start' @@ -15,6 +16,7 @@ export type AiVaultSessionDragPayload = { // Why: drop targets must know where the session file lives (host vs local // WSL) to reject SSH panes that cannot reach it. sessionFilePath?: string + sessionExecutionHostId?: ExecutionHostId // Why: drag/drop resume must preserve planned env/default args, not just the shell command. env?: Record launchConfig?: SleepingAgentLaunchConfig @@ -67,6 +69,8 @@ function isSerializedPayload(value: unknown): value is SerializedAiVaultSessionD isNonEmptyString(payload.title) && isNonEmptyString(payload.command) && (payload.sessionFilePath === undefined || isNonEmptyString(payload.sessionFilePath)) && + (payload.sessionExecutionHostId === undefined || + Boolean(normalizeExecutionHostId(payload.sessionExecutionHostId))) && (payload.env === undefined || isStringRecord(payload.env)) && (payload.launchConfig === undefined || isLaunchConfig(payload.launchConfig)) ) @@ -114,13 +118,23 @@ export function readAiVaultSessionDragData( if (!isSerializedPayload(parsed)) { return null } - const { agent, sessionId, title, command, sessionFilePath, env, launchConfig } = parsed + const { + agent, + sessionId, + title, + command, + sessionFilePath, + sessionExecutionHostId, + env, + launchConfig + } = parsed return { agent, sessionId, title, command, ...(sessionFilePath ? { sessionFilePath } : {}), + ...(sessionExecutionHostId ? { sessionExecutionHostId } : {}), ...(env ? { env } : {}), ...(launchConfig ? { launchConfig } : {}) } diff --git a/src/shared/ai-vault-types.ts b/src/shared/ai-vault-types.ts index 70d2054d623..58bf82597ab 100644 --- a/src/shared/ai-vault-types.ts +++ b/src/shared/ai-vault-types.ts @@ -5,6 +5,7 @@ import { type AgentStartupShell } from './tui-agent-startup-shell' import type { TuiAgent } from './types' +import type { ExecutionHostId, ExecutionHostScope } from './execution-host' export const AI_VAULT_AGENTS = [ 'claude', @@ -53,6 +54,8 @@ export type AiVaultSessionPreviewMessage = { export type AiVaultSession = { id: string + executionHostId: ExecutionHostId + executionHostPlatform?: NodeJS.Platform | null agent: AiVaultAgent sessionId: string title: string @@ -71,6 +74,7 @@ export type AiVaultSession = { } export type AiVaultScanIssue = { + executionHostId?: ExecutionHostId agent: AiVaultAgent path: string message: string @@ -82,6 +86,7 @@ export type AiVaultListArgs = { // Active workspace/project paths. The global result is recency-capped, so these // guarantee a scoped view still surfaces its own (possibly older) sessions. scopePaths?: readonly string[] + executionHostScope?: ExecutionHostScope } export type AiVaultListResult = { diff --git a/tests/e2e/ssh-ai-vault-session-history.spec.ts b/tests/e2e/ssh-ai-vault-session-history.spec.ts new file mode 100644 index 00000000000..0cb6a485ca4 --- /dev/null +++ b/tests/e2e/ssh-ai-vault-session-history.spec.ts @@ -0,0 +1,293 @@ +import type { Page, TestInfo } from '@stablyai/playwright-test' +import { test, expect } from './helpers/orca-app' +import { + cleanupDockerSshRelayTarget, + DOCKER_SSH_RELAY_REMOTE_REPO_PATH, + startDockerSshRelayTarget, + type DockerSshRelayTarget +} from './helpers/docker-ssh-relay-target' +import { connectDockerRemote } from './ssh-codex-reconnect-replay-driver' +import { dockerExec, dockerWriteFile } from './ssh-codex-repro-remote-fixtures' +import { waitForActiveWorktree, waitForSessionReady } from './helpers/store' + +const RUN_DOCKER_SSH = process.env.ORCA_E2E_SSH_DOCKER === '1' + +test.describe('SSH Agent Session History', () => { + test.skip(!RUN_DOCKER_SSH, 'Set ORCA_E2E_SSH_DOCKER=1 to run Docker-backed SSH tests.') + test.skip(process.platform === 'win32', 'Docker SSH tests use POSIX ssh tooling.') + + test('shows remote session history only for the SSH host and resumes Codex on that worktree', async ({ + orcaPage + }, testInfo: TestInfo) => { + test.slow() + let target: DockerSshRelayTarget | null = null + const stamp = Date.now() + const defaultSessionId = `remote-ai-vault-${stamp}` + const runtimeSessionId = `remote-ai-vault-runtime-${stamp}` + const claudeSessionId = `remote-ai-vault-claude-${stamp}` + const defaultTitle = `Remote AI Vault ${stamp}` + const runtimeTitle = `Remote Runtime AI Vault ${stamp}` + const claudeTitle = `Remote Claude AI Vault ${stamp}` + + try { + target = startDockerSshRelayTarget(testInfo) + seedRemoteAiVaultHistory(target, { + defaultSessionId, + runtimeSessionId, + claudeSessionId, + defaultTitle, + runtimeTitle, + claudeTitle + }) + + await waitForSessionReady(orcaPage) + await waitForActiveWorktree(orcaPage) + const remote = await connectDockerRemote(orcaPage, target) + const sshScope = `ssh:${encodeURIComponent(remote.targetId)}` + + const scan = await orcaPage.evaluate( + async ({ sshScope, defaultTitle, runtimeTitle, claudeTitle }) => { + const local = await window.api.aiVault.listSessions({ + executionHostScope: 'local', + force: true + }) + const ssh = await window.api.aiVault.listSessions({ + executionHostScope: sshScope, + force: true + }) + const all = await window.api.aiVault.listSessions({ + executionHostScope: 'all', + force: true + }) + return { + localHasRemote: local.sessions.some((session) => session.title === defaultTitle), + sshTitles: ssh.sessions.map((session) => session.title), + allHasRuntime: all.sessions.some((session) => session.title === runtimeTitle), + allHasClaude: all.sessions.some((session) => session.title === claudeTitle), + remoteHostIds: ssh.sessions + .filter((session) => + [defaultTitle, runtimeTitle, claudeTitle].includes(session.title) + ) + .map((session) => session.executionHostId), + remoteCommands: ssh.sessions + .filter((session) => session.title === defaultTitle || session.title === runtimeTitle) + .map((session) => session.resumeCommand) + } + }, + { sshScope, defaultTitle, runtimeTitle, claudeTitle } + ) + expect(scan.localHasRemote).toBe(false) + expect(scan.sshTitles).toEqual( + expect.arrayContaining([defaultTitle, runtimeTitle, claudeTitle]) + ) + expect(scan.allHasRuntime).toBe(true) + expect(scan.allHasClaude).toBe(true) + expect(new Set(scan.remoteHostIds)).toEqual(new Set([sshScope])) + expect(scan.remoteCommands.join('\n')).toContain("CODEX_HOME='/root/.codex'") + expect(scan.remoteCommands.join('\n')).toContain( + "CODEX_HOME='/root/.local/share/orca/codex-runtime-home/home'" + ) + + const defaultSessionTitle = orcaPage.getByText(defaultTitle, { exact: true }) + const runtimeSessionTitle = orcaPage.getByText(runtimeTitle, { exact: true }) + + await openAiVaultSidebar(orcaPage) + await expect(defaultSessionTitle.first()).toBeVisible({ timeout: 30_000 }) + + const hostButton = orcaPage.getByRole('button', { name: /Session History host:/ }) + await hostButton.click() + await orcaPage.getByRole('menuitemradio', { name: /Local/ }).click() + await expect(defaultSessionTitle).toHaveCount(0, { timeout: 30_000 }) + + await hostButton.click() + await orcaPage.getByRole('menuitemradio', { name: 'All hosts' }).click() + await expect(runtimeSessionTitle.first()).toBeVisible({ timeout: 30_000 }) + + await hostButton.click() + await orcaPage + .getByRole('menuitemradio') + .filter({ hasNotText: /Local|All hosts/ }) + .click() + await expect(defaultSessionTitle.first()).toBeVisible({ timeout: 30_000 }) + + await installStartupQueueProbe(orcaPage) + await defaultSessionTitle.first().click() + await orcaPage.getByText('Resume in Worktree', { exact: true }).click() + + await expect + .poll(() => readLastQueuedStartupCommand(orcaPage), { timeout: 30_000 }) + .toContain(`CODEX_HOME='/root/.codex' codex resume '${defaultSessionId}'`) + const queuedWorktreeId = await readLastQueuedStartupWorktreeId(orcaPage) + expect(queuedWorktreeId).toBe(remote.worktreeId) + } finally { + cleanupDockerSshRelayTarget(target) + } + }) +}) + +function seedRemoteAiVaultHistory( + target: DockerSshRelayTarget, + args: { + defaultSessionId: string + runtimeSessionId: string + claudeSessionId: string + defaultTitle: string + runtimeTitle: string + claudeTitle: string + } +): void { + dockerExec( + target, + [ + 'mkdir -p /root/.codex/sessions/2026/07/04', + 'mkdir -p /root/.local/share/orca/codex-runtime-home/home/sessions/2026/07/04', + 'mkdir -p /root/.claude/projects/orca' + ].join(' && ') + ) + dockerWriteFile( + target, + '/root/.codex/session_index.jsonl', + jsonLines([{ id: args.defaultSessionId, thread_name: args.defaultTitle }]), + '600' + ) + dockerWriteFile( + target, + `/root/.codex/sessions/2026/07/04/${args.defaultSessionId}.jsonl`, + codexTranscript({ + sessionId: args.defaultSessionId, + title: args.defaultTitle, + cwd: DOCKER_SSH_RELAY_REMOTE_REPO_PATH, + timestamp: '2026-07-04T01:00:00.000Z' + }), + '600' + ) + dockerWriteFile( + target, + `/root/.local/share/orca/codex-runtime-home/home/sessions/2026/07/04/${args.runtimeSessionId}.jsonl`, + codexTranscript({ + sessionId: args.runtimeSessionId, + title: args.runtimeTitle, + cwd: DOCKER_SSH_RELAY_REMOTE_REPO_PATH, + timestamp: '2026-07-04T02:00:00.000Z' + }), + '600' + ) + dockerWriteFile( + target, + `/root/.claude/projects/orca/${args.claudeSessionId}.jsonl`, + claudeTranscript({ + sessionId: args.claudeSessionId, + title: args.claudeTitle, + timestamp: '2026-07-04T03:00:00.000Z' + }), + '600' + ) +} + +function codexTranscript(args: { + sessionId: string + title: string + cwd: string + timestamp: string +}): string { + return jsonLines([ + { + timestamp: args.timestamp, + type: 'session_meta', + payload: { id: args.sessionId, cwd: args.cwd } + }, + { + timestamp: args.timestamp.replace(':00.000Z', ':01.000Z'), + type: 'response_item', + payload: { + type: 'message', + role: 'user', + content: [{ type: 'text', text: args.title }] + } + } + ]) +} + +function jsonLines(records: unknown[]): string { + return records.map((record) => JSON.stringify(record)).join('\n') +} + +function claudeTranscript(args: { sessionId: string; title: string; timestamp: string }): string { + return jsonLines([ + { + sessionId: args.sessionId, + timestamp: args.timestamp, + type: 'user', + message: { content: [{ type: 'text', text: args.title }] } + }, + { + sessionId: args.sessionId, + timestamp: args.timestamp.replace(':00.000Z', ':01.000Z'), + type: 'assistant', + message: { model: 'claude-opus-4', content: 'Remote session acknowledged.' } + } + ]) +} + +async function openAiVaultSidebar(page: Page): Promise { + await page.evaluate(() => { + const store = window.__store + if (!store) { + throw new Error('Store unavailable') + } + store.getState().setRightSidebarOpen(true) + store.getState().setRightSidebarTab('vault') + }) +} + +async function installStartupQueueProbe(page: Page): Promise { + await page.evaluate(() => { + const store = window.__store + if (!store) { + throw new Error('Store unavailable') + } + const holder = window as unknown as { + __aiVaultQueuedStartups?: { tabId: string; startup: { command: string } }[] + } + holder.__aiVaultQueuedStartups = [] + const current = store.getState() + const original = current.queueTabStartupCommand + store.setState({ + queueTabStartupCommand: (tabId, startup) => { + holder.__aiVaultQueuedStartups?.push({ tabId, startup: { command: startup.command } }) + original(tabId, startup) + } + }) + }) +} + +async function readLastQueuedStartupCommand(page: Page): Promise { + return page.evaluate(() => { + const holder = window as unknown as { + __aiVaultQueuedStartups?: { startup: { command: string } }[] + } + return holder.__aiVaultQueuedStartups?.at(-1)?.startup.command ?? null + }) +} + +async function readLastQueuedStartupWorktreeId(page: Page): Promise { + return page.evaluate(() => { + const holder = window as unknown as { + __aiVaultQueuedStartups?: { tabId: string }[] + } + const tabId = holder.__aiVaultQueuedStartups?.at(-1)?.tabId + if (!tabId) { + return null + } + const state = window.__store?.getState() + if (!state) { + return null + } + for (const [worktreeId, tabs] of Object.entries(state.tabsByWorktree)) { + if (tabs.some((tab) => tab.id === tabId)) { + return worktreeId + } + } + return null + }) +} From 17d5eff5d69cd088311698a35c2f291aed53db48 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 5 Jul 2026 06:02:14 +0000 Subject: [PATCH 45/45] release: v1.4.123-rc.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 083696f8850..eb6139b6668 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "orca", - "version": "1.4.123-rc.0", + "version": "1.4.123-rc.1", "description": "Next-gen IDE for parallel agentic development", "homepage": "https://github.com/stablyai/orca", "author": "stablyai",